diff --git a/.gitignore b/.gitignore
index 34c5a32..09dd067 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,10 @@ target/
# root puts it here.
.vite/
+# Sequoia's record of what it has published. Rebuilt from the PDS when it is
+# missing, so it is a cache rather than state to keep.
+web/.sequoia-state.json
+
# Astro's generated types and its content cache. Written by every build and by
# `astro check`, so a checkout that has not built yet is missing them and that
# is fine - nothing reads them but the checker and the build.
diff --git a/README.md b/README.md
index 826b052..1be1ba8 100644
--- a/README.md
+++ b/README.md
@@ -120,6 +120,27 @@ CloudFront is invalidated; the script prints both steps. Pushing and serving
are separate on purpose — a push is safe from any laptop, and flipping the
path is an infra change with an infra review.
+### Publishing the blog to ATProto
+
+ ATP_APP_PASSWORD=... web/scripts/publish.sh
+
+The last step, once the release is confirmed serving — not part of the push.
+It writes the blog's posts to lance.blue's own repository as standard.site
+records with [sequoia](https://sequoia.pub), configured by `web/sequoia.json`.
+
+A document record carries the post's address, so the address has to answer
+first. The script refuses to publish a post the live site is not already
+serving, comparing the page's canonical link rather than its status — a
+missing page comes back as the app's own index.html with a 200. `--dry-run`
+runs that check and stops.
+
+The publication itself is made once, by hand: `sequoia init` creates the
+`site.standard.publication` record, writes its at:// URI into `sequoia.json`,
+and writes the same URI to `web/public/.well-known/site.standard.publication`,
+which is what proves the domain owns the publication. Both are committed, and
+nothing rewrites them. `sequoia.json` still holds a placeholder, so that step
+has not been done.
+
## Building the API image
services/api/push.sh
diff --git a/TODO.md b/TODO.md
index c09dacc..4ea832f 100644
--- a/TODO.md
+++ b/TODO.md
@@ -167,14 +167,15 @@ rediscovering them:
Revisit then, and write down what tipped it rather than reaching for the
default. Astro renders the pages around the app and does not route
inside it.
-- [ ] **Blog posts are pages, not records.** `/blog` is markdown in
- `web/src/content/blog`, prerendered. The point of it is to be an ATProto
- blog — a `site.standard.publication` and one `site.standard.document`
- per post in the lance.blue repository, plus the
- `/.well-known/site.standard.publication` that proves the domain and the
- `` on each page. None of that is
- built. The frontmatter field names already match what the publishing
- tools expect.
+- [ ] **No blog post has a record yet.** The machinery is here: a post with an
+ `atUri` in its frontmatter renders its ``,
+ `web/scripts/publish.sh` writes the records with sequoia and refuses to
+ publish a post the site is not already serving, and `push.sh` uploads the
+ well-known key as text. What is missing is the one manual step that
+ starts it — `sequoia init` against the lance.blue account, which creates
+ the `site.standard.publication` record and fills in the placeholder URI
+ in `web/sequoia.json`. See README's "Publishing the blog to ATProto".
+ Until then `/blog` is pages and nothing else.
- [ ] **The masthead's account menu sends you to Home, because there is no
profile page.** Signed in, the masthead shows the account's avatar and a
menu — the handle, Your account, Your camo, the theme control, Sign out
diff --git a/web/package-lock.json b/web/package-lock.json
index 663b611..9cc287a 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -18,6 +18,7 @@
"astro": "^7.2.0",
"png-to-ico": "^2.1.8",
"prettier-plugin-astro": "^0.14.1",
+ "sequoia-cli": "^0.5.7",
"sharp": "^0.34.2",
"typescript": "^5.9.0"
},
@@ -391,6 +392,39 @@
"node": ">=22"
}
},
+ "node_modules/@atproto-labs/fetch-node": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@atproto-labs/fetch-node/-/fetch-node-0.2.0.tgz",
+ "integrity": "sha512-Krq09nH/aeoiU2s9xdHA0FjTEFWG9B5FFenipv1iRixCcPc7V3DhTNDawxG9gI8Ny0k4dBVS9WTRN/IDzBx86Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto-labs/fetch": "0.2.3",
+ "@atproto-labs/pipe": "0.1.1",
+ "ipaddr.js": "^2.1.0",
+ "undici": "^6.14.1"
+ },
+ "engines": {
+ "node": ">=18.7.0"
+ }
+ },
+ "node_modules/@atproto-labs/fetch-node/node_modules/@atproto-labs/fetch": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@atproto-labs/fetch/-/fetch-0.2.3.tgz",
+ "integrity": "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto-labs/pipe": "0.1.1"
+ }
+ },
+ "node_modules/@atproto-labs/fetch-node/node_modules/@atproto-labs/pipe": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@atproto-labs/pipe/-/pipe-0.1.1.tgz",
+ "integrity": "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@atproto-labs/handle-resolver": {
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/@atproto-labs/handle-resolver/-/handle-resolver-0.4.8.tgz",
@@ -407,6 +441,62 @@
"node": ">=22"
}
},
+ "node_modules/@atproto-labs/handle-resolver-node": {
+ "version": "0.1.25",
+ "resolved": "https://registry.npmjs.org/@atproto-labs/handle-resolver-node/-/handle-resolver-node-0.1.25.tgz",
+ "integrity": "sha512-NY9WYM2VLd3IuMGRkkmvGBg8xqVEaK/fitv1vD8SMXqFTekdpjOLCCyv7EFtqVHouzmDcL83VOvWRfHVa8V9Yw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto-labs/fetch-node": "0.2.0",
+ "@atproto-labs/handle-resolver": "0.3.6",
+ "@atproto/did": "0.3.0"
+ },
+ "engines": {
+ "node": ">=18.7.0"
+ }
+ },
+ "node_modules/@atproto-labs/handle-resolver-node/node_modules/@atproto-labs/handle-resolver": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/@atproto-labs/handle-resolver/-/handle-resolver-0.3.6.tgz",
+ "integrity": "sha512-qnSTXvOBNj1EHhp2qTWSX8MS5q3AwYU5LKlt5fBvSbCjgmTr2j0URHCv+ydrwO55KvsojIkTMgeMOh4YuY4fCA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto-labs/simple-store": "0.3.0",
+ "@atproto-labs/simple-store-memory": "0.1.4",
+ "@atproto/did": "0.3.0",
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@atproto-labs/handle-resolver-node/node_modules/@atproto-labs/simple-store": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@atproto-labs/simple-store/-/simple-store-0.3.0.tgz",
+ "integrity": "sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@atproto-labs/handle-resolver-node/node_modules/@atproto-labs/simple-store-memory": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/@atproto-labs/simple-store-memory/-/simple-store-memory-0.1.4.tgz",
+ "integrity": "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto-labs/simple-store": "0.3.0",
+ "lru-cache": "^10.2.0"
+ }
+ },
+ "node_modules/@atproto-labs/handle-resolver-node/node_modules/@atproto/did": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@atproto/did/-/did-0.3.0.tgz",
+ "integrity": "sha512-raUPzUGegtW/6OxwCmM8bhZvuIMzxG5t9oWsth6Tp91Kb5fTnHV2h/KKNF1C82doeA4BdXCErTyg7ISwLbQkzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "zod": "^3.23.8"
+ }
+ },
"node_modules/@atproto-labs/identity-resolver": {
"version": "0.4.7",
"resolved": "https://registry.npmjs.org/@atproto-labs/identity-resolver/-/identity-resolver-0.4.7.tgz",
@@ -452,6 +542,132 @@
"node": ">=22"
}
},
+ "node_modules/@atproto/api": {
+ "version": "0.18.21",
+ "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.18.21.tgz",
+ "integrity": "sha512-s35MIJerGT/pKe2xJtKKswqlIr/ola2r2iURBKBL0Mk1OKe6jP4YvTMh1N2d2PEANFzNNTbKoDaLfJPo2Uvc/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto/common-web": "^0.4.16",
+ "@atproto/lexicon": "^0.6.1",
+ "@atproto/syntax": "^0.4.3",
+ "@atproto/xrpc": "^0.7.7",
+ "await-lock": "^2.2.2",
+ "multiformats": "^9.9.0",
+ "tlds": "^1.234.0",
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@atproto/api/node_modules/@atproto/common-web": {
+ "version": "0.4.21",
+ "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.21.tgz",
+ "integrity": "sha512-Odq+wdk3YNasGCjjlpl3bCIPvqYHige5DLfMkIffNv/2PI/iIj5ZvAvMvJlJ59OhReKSxtpI0invx5UQPc3+fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto/lex-data": "^0.0.15",
+ "@atproto/lex-json": "^0.0.16",
+ "@atproto/syntax": "^0.5.4",
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@atproto/api/node_modules/@atproto/common-web/node_modules/@atproto/syntax": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.5.4.tgz",
+ "integrity": "sha512-9XJOpMAgsGFxMEIp8nJ8AIWv+krrY1xQMj+wULbbXhQztQV+9aZ0TbG9Jtn3Op2or8Kr6OqyWR4ga9Z189kKDw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@atproto/api/node_modules/@atproto/lex-data": {
+ "version": "0.0.15",
+ "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.15.tgz",
+ "integrity": "sha512-ZsbGiaM5S3CnGrcTMbDGON3bLZzCi/Mx9UvcMREKSRujnF68eHgMiXxJqvykP7+QpOX6tYCK93axZkuJVhtSEw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "multiformats": "^9.9.0",
+ "tslib": "^2.8.1",
+ "uint8arrays": "3.0.0",
+ "unicode-segmenter": "^0.14.0"
+ }
+ },
+ "node_modules/@atproto/api/node_modules/@atproto/lex-json": {
+ "version": "0.0.16",
+ "resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.16.tgz",
+ "integrity": "sha512-IgLgQ0krshVlrIYZ+heTBDbCnM3LmAgWvsaYn5MxvKA3LcBot3PG3ptdO8VOweVZ+WgCLuo39cz9EbUmIbqdtg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto/lex-data": "^0.0.15",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@atproto/api/node_modules/@atproto/lexicon": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.6.2.tgz",
+ "integrity": "sha512-p3Ly6hinVZW0ETuAXZMeUGwuMm3g8HvQMQ41yyEE6AL0hAkfeKFaZKos6BdBrr6CjkpbrDZqE8M+5+QOceysMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto/common-web": "^0.4.18",
+ "@atproto/syntax": "^0.5.0",
+ "iso-datestring-validator": "^2.2.2",
+ "multiformats": "^9.9.0",
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@atproto/api/node_modules/@atproto/lexicon/node_modules/@atproto/syntax": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.5.4.tgz",
+ "integrity": "sha512-9XJOpMAgsGFxMEIp8nJ8AIWv+krrY1xQMj+wULbbXhQztQV+9aZ0TbG9Jtn3Op2or8Kr6OqyWR4ga9Z189kKDw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@atproto/api/node_modules/@atproto/syntax": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz",
+ "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@atproto/api/node_modules/@atproto/xrpc": {
+ "version": "0.7.7",
+ "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.7.tgz",
+ "integrity": "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto/lexicon": "^0.6.0",
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@atproto/api/node_modules/multiformats": {
+ "version": "9.9.0",
+ "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz",
+ "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==",
+ "dev": true,
+ "license": "(Apache-2.0 AND MIT)"
+ },
+ "node_modules/@atproto/api/node_modules/uint8arrays": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz",
+ "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "multiformats": "^9.4.2"
+ }
+ },
"node_modules/@atproto/common": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.7.5.tgz",
@@ -777,6 +993,268 @@
"node": ">=22"
}
},
+ "node_modules/@atproto/oauth-client-node": {
+ "version": "0.3.17",
+ "resolved": "https://registry.npmjs.org/@atproto/oauth-client-node/-/oauth-client-node-0.3.17.tgz",
+ "integrity": "sha512-67LNuKAlC35Exe7CB5S0QCAnEqr6fKV9Nvp64jAHFof1N+Vc9Ltt1K9oekE5Ctf7dvpGByrHRF0noUw9l9sWLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto-labs/did-resolver": "^0.2.6",
+ "@atproto-labs/handle-resolver-node": "^0.1.25",
+ "@atproto-labs/simple-store": "^0.3.0",
+ "@atproto/did": "^0.3.0",
+ "@atproto/jwk": "^0.6.0",
+ "@atproto/jwk-jose": "^0.1.11",
+ "@atproto/jwk-webcrypto": "^0.2.0",
+ "@atproto/oauth-client": "^0.6.0",
+ "@atproto/oauth-types": "^0.6.3"
+ },
+ "engines": {
+ "node": ">=18.7.0"
+ }
+ },
+ "node_modules/@atproto/oauth-client-node/node_modules/@atproto-labs/did-resolver": {
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/@atproto-labs/did-resolver/-/did-resolver-0.2.6.tgz",
+ "integrity": "sha512-2K1bC04nI2fmgNcvof+yA28IhGlpWn2JKYlPa7To9JTKI45FINCGkQSGiL2nyXlyzDJJ34fZ1aq6/IRFIOIiqg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto-labs/fetch": "0.2.3",
+ "@atproto-labs/pipe": "0.1.1",
+ "@atproto-labs/simple-store": "0.3.0",
+ "@atproto-labs/simple-store-memory": "0.1.4",
+ "@atproto/did": "0.3.0",
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@atproto/oauth-client-node/node_modules/@atproto-labs/fetch": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@atproto-labs/fetch/-/fetch-0.2.3.tgz",
+ "integrity": "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto-labs/pipe": "0.1.1"
+ }
+ },
+ "node_modules/@atproto/oauth-client-node/node_modules/@atproto-labs/handle-resolver": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/@atproto-labs/handle-resolver/-/handle-resolver-0.3.6.tgz",
+ "integrity": "sha512-qnSTXvOBNj1EHhp2qTWSX8MS5q3AwYU5LKlt5fBvSbCjgmTr2j0URHCv+ydrwO55KvsojIkTMgeMOh4YuY4fCA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto-labs/simple-store": "0.3.0",
+ "@atproto-labs/simple-store-memory": "0.1.4",
+ "@atproto/did": "0.3.0",
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@atproto/oauth-client-node/node_modules/@atproto-labs/identity-resolver": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/@atproto-labs/identity-resolver/-/identity-resolver-0.3.6.tgz",
+ "integrity": "sha512-qoWqBDRobln0NR8L8dQjSp79E0chGkBhibEgxQa2f9WD+JbJdjQ0YvwwO5yeQn05pJoJmAwmI2wyJ45zjU7aWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto-labs/did-resolver": "0.2.6",
+ "@atproto-labs/handle-resolver": "0.3.6"
+ }
+ },
+ "node_modules/@atproto/oauth-client-node/node_modules/@atproto-labs/pipe": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@atproto-labs/pipe/-/pipe-0.1.1.tgz",
+ "integrity": "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@atproto/oauth-client-node/node_modules/@atproto-labs/simple-store": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@atproto-labs/simple-store/-/simple-store-0.3.0.tgz",
+ "integrity": "sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@atproto/oauth-client-node/node_modules/@atproto-labs/simple-store-memory": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/@atproto-labs/simple-store-memory/-/simple-store-memory-0.1.4.tgz",
+ "integrity": "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto-labs/simple-store": "0.3.0",
+ "lru-cache": "^10.2.0"
+ }
+ },
+ "node_modules/@atproto/oauth-client-node/node_modules/@atproto/common-web": {
+ "version": "0.4.21",
+ "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.21.tgz",
+ "integrity": "sha512-Odq+wdk3YNasGCjjlpl3bCIPvqYHige5DLfMkIffNv/2PI/iIj5ZvAvMvJlJ59OhReKSxtpI0invx5UQPc3+fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto/lex-data": "^0.0.15",
+ "@atproto/lex-json": "^0.0.16",
+ "@atproto/syntax": "^0.5.4",
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@atproto/oauth-client-node/node_modules/@atproto/did": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@atproto/did/-/did-0.3.0.tgz",
+ "integrity": "sha512-raUPzUGegtW/6OxwCmM8bhZvuIMzxG5t9oWsth6Tp91Kb5fTnHV2h/KKNF1C82doeA4BdXCErTyg7ISwLbQkzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@atproto/oauth-client-node/node_modules/@atproto/jwk": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@atproto/jwk/-/jwk-0.6.0.tgz",
+ "integrity": "sha512-bDoJPvt7TrQVi/rBfBrSSpGykhtIriKxeYCYQTiPRKFfyRhbgpElF0wPXADjIswnbzZdOwbY63az4E/CFVT3Tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "multiformats": "^9.9.0",
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@atproto/oauth-client-node/node_modules/@atproto/jwk-jose": {
+ "version": "0.1.11",
+ "resolved": "https://registry.npmjs.org/@atproto/jwk-jose/-/jwk-jose-0.1.11.tgz",
+ "integrity": "sha512-i4Fnr2sTBYmMmHXl7NJh8GrCH+tDQEVWrcDMDnV5DjJfkgT17wIqvojIw9SNbSL4Uf0OtfEv6AgG0A+mgh8b5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto/jwk": "0.6.0",
+ "jose": "^5.2.0"
+ }
+ },
+ "node_modules/@atproto/oauth-client-node/node_modules/@atproto/jwk-webcrypto": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@atproto/jwk-webcrypto/-/jwk-webcrypto-0.2.0.tgz",
+ "integrity": "sha512-UmgRrrEAkWvxwhlwe30UmDOdTEFidlIzBC7C3cCbeJMcBN1x8B3KH+crXrsTqfWQBG58mXgt8wgSK3Kxs2LhFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto/jwk": "0.6.0",
+ "@atproto/jwk-jose": "0.1.11",
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@atproto/oauth-client-node/node_modules/@atproto/lex-data": {
+ "version": "0.0.15",
+ "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.15.tgz",
+ "integrity": "sha512-ZsbGiaM5S3CnGrcTMbDGON3bLZzCi/Mx9UvcMREKSRujnF68eHgMiXxJqvykP7+QpOX6tYCK93axZkuJVhtSEw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "multiformats": "^9.9.0",
+ "tslib": "^2.8.1",
+ "uint8arrays": "3.0.0",
+ "unicode-segmenter": "^0.14.0"
+ }
+ },
+ "node_modules/@atproto/oauth-client-node/node_modules/@atproto/lex-json": {
+ "version": "0.0.16",
+ "resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.16.tgz",
+ "integrity": "sha512-IgLgQ0krshVlrIYZ+heTBDbCnM3LmAgWvsaYn5MxvKA3LcBot3PG3ptdO8VOweVZ+WgCLuo39cz9EbUmIbqdtg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto/lex-data": "^0.0.15",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@atproto/oauth-client-node/node_modules/@atproto/lexicon": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.6.2.tgz",
+ "integrity": "sha512-p3Ly6hinVZW0ETuAXZMeUGwuMm3g8HvQMQ41yyEE6AL0hAkfeKFaZKos6BdBrr6CjkpbrDZqE8M+5+QOceysMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto/common-web": "^0.4.18",
+ "@atproto/syntax": "^0.5.0",
+ "iso-datestring-validator": "^2.2.2",
+ "multiformats": "^9.9.0",
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@atproto/oauth-client-node/node_modules/@atproto/oauth-client": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/@atproto/oauth-client/-/oauth-client-0.6.1.tgz",
+ "integrity": "sha512-QTLbEFyv7EJuwJf4A8IZnsylK5wwrzrSsxy0INcZf9zktPVQvgckWhSvbfK8alp60M+rwWfQQAlodcCF4WB78A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto-labs/did-resolver": "^0.2.6",
+ "@atproto-labs/fetch": "^0.2.3",
+ "@atproto-labs/handle-resolver": "^0.3.6",
+ "@atproto-labs/identity-resolver": "^0.3.6",
+ "@atproto-labs/simple-store": "^0.3.0",
+ "@atproto-labs/simple-store-memory": "^0.1.4",
+ "@atproto/did": "^0.3.0",
+ "@atproto/jwk": "^0.6.0",
+ "@atproto/oauth-types": "^0.6.3",
+ "@atproto/xrpc": "^0.7.7",
+ "core-js": "^3",
+ "multiformats": "^9.9.0",
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@atproto/oauth-client-node/node_modules/@atproto/oauth-types": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/@atproto/oauth-types/-/oauth-types-0.6.3.tgz",
+ "integrity": "sha512-jdKuoPknJuh/WjI+mYk7agSbx9mNVMbS6Dr3k1z2YMY2oRiCQjxYBuo4MLKATbxj05nMQaZRWlHRUazoAu5Cng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto/did": "^0.3.0",
+ "@atproto/jwk": "^0.6.0",
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@atproto/oauth-client-node/node_modules/@atproto/syntax": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.5.4.tgz",
+ "integrity": "sha512-9XJOpMAgsGFxMEIp8nJ8AIWv+krrY1xQMj+wULbbXhQztQV+9aZ0TbG9Jtn3Op2or8Kr6OqyWR4ga9Z189kKDw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@atproto/oauth-client-node/node_modules/@atproto/xrpc": {
+ "version": "0.7.7",
+ "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.7.tgz",
+ "integrity": "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@atproto/lexicon": "^0.6.0",
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@atproto/oauth-client-node/node_modules/multiformats": {
+ "version": "9.9.0",
+ "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz",
+ "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==",
+ "dev": true,
+ "license": "(Apache-2.0 AND MIT)"
+ },
+ "node_modules/@atproto/oauth-client-node/node_modules/uint8arrays": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz",
+ "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "multiformats": "^9.4.2"
+ }
+ },
"node_modules/@atproto/oauth-types": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@atproto/oauth-types/-/oauth-types-0.7.5.tgz",
@@ -3087,6 +3565,13 @@
"node": ">=8.0.0"
}
},
+ "node_modules/await-lock": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz",
+ "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -3136,6 +3621,22 @@
"node": "20 || >=22"
}
},
+ "node_modules/bundle-name": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
+ "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "run-applescript": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/cborg": {
"version": "4.5.8",
"resolved": "https://registry.npmjs.org/cborg/-/cborg-4.5.8.tgz",
@@ -3156,6 +3657,19 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
"node_modules/character-entities-html4": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
@@ -3251,6 +3765,19 @@
"node": ">=6"
}
},
+ "node_modules/cmd-ts": {
+ "version": "0.14.3",
+ "resolved": "https://registry.npmjs.org/cmd-ts/-/cmd-ts-0.14.3.tgz",
+ "integrity": "sha512-i9miaLBHGPn4T4vUFUdAdWoQ9HI8ato6usFrhubO21o/MQGqI6Nsqyd4ncZusT2chvPetNMmTt0JWkbaAOdWPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^5.4.1",
+ "debug": "^4.4.1",
+ "didyoumean": "^1.2.2",
+ "strip-ansi": "^7.1.0"
+ }
+ },
"node_modules/code-block-writer": {
"version": "13.0.3",
"resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz",
@@ -3413,6 +3940,67 @@
"dev": true,
"license": "CC0-1.0"
},
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/default-browser": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz",
+ "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bundle-name": "^4.1.0",
+ "default-browser-id": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser-id": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz",
+ "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-lazy-prop": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/defu": {
"version": "6.1.7",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
@@ -3468,6 +4056,13 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
"node_modules/diff": {
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz",
@@ -3828,6 +4423,24 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/glob": {
+ "version": "13.0.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "minimatch": "^10.2.2",
+ "minipass": "^7.1.3",
+ "path-scurry": "^2.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/h3": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz",
@@ -3981,6 +4594,16 @@
"dev": true,
"license": "BSD-2-Clause"
},
+ "node_modules/ipaddr.js": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz",
+ "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
"node_modules/iron-webcrypto": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
@@ -4007,6 +4630,54 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-in-ssh": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz",
+ "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-inside-container": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
+ "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^3.0.0"
+ },
+ "bin": {
+ "is-inside-container": "cli.js"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-inside-container/node_modules/is-docker": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
+ "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-plain-obj": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
@@ -4020,6 +4691,22 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-wsl": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz",
+ "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-inside-container": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/iso-datestring-validator": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz",
@@ -4507,6 +5194,29 @@
],
"license": "MIT"
},
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/minimatch": {
"version": "10.2.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
@@ -4532,6 +5242,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/minipass": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
@@ -4542,6 +5262,13 @@
"node": ">=10"
}
},
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/muggle-string": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
@@ -4696,6 +5423,27 @@
"regex-recursion": "^6.0.2"
}
},
+ "node_modules/open": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz",
+ "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "default-browser": "^5.4.0",
+ "define-lazy-prop": "^3.0.0",
+ "is-in-ssh": "^1.0.0",
+ "is-inside-container": "^1.0.0",
+ "powershell-utils": "^0.1.0",
+ "wsl-utils": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/p-limit": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.1.tgz",
@@ -4768,6 +5516,33 @@
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
"license": "MIT"
},
+ "node_modules/path-scurry": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+ "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "11.5.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
+ "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
"node_modules/piccolore": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz",
@@ -4895,6 +5670,19 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/powershell-utils": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz",
+ "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/prettier": {
"version": "3.9.6",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz",
@@ -5112,6 +5900,19 @@
"@rolldown/binding-win32-x64-msvc": "1.2.1"
}
},
+ "node_modules/run-applescript": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
+ "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/s.color": {
"version": "0.0.15",
"resolved": "https://registry.npmjs.org/s.color/-/s.color-0.0.15.tgz",
@@ -5185,6 +5986,30 @@
"node": ">=10"
}
},
+ "node_modules/sequoia-cli": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/sequoia-cli/-/sequoia-cli-0.5.7.tgz",
+ "integrity": "sha512-5gvVsS/cS9HNSujjLi+u0nDU3jtq4W6BtdtAwX0OpVaxxXsWJgJVEyFDYCbHm/RlddnQScGWFpcsD3EfguSi7g==",
+ "dev": true,
+ "dependencies": {
+ "@atproto/api": "^0.18.17",
+ "@atproto/oauth-client-node": "^0.3.16",
+ "@clack/prompts": "^1.0.0",
+ "cmd-ts": "^0.14.3",
+ "glob": "^13.0.0",
+ "js-yaml": "^4.1.1",
+ "mime-types": "^2.1.35",
+ "minimatch": "^10.1.1",
+ "open": "^11.0.0",
+ "smol-toml": "^1.6.0"
+ },
+ "bin": {
+ "sequoia": "dist/index.js"
+ },
+ "peerDependencies": {
+ "typescript": "^5"
+ }
+ },
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
@@ -5452,6 +6277,16 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "node_modules/tlds": {
+ "version": "1.261.0",
+ "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz",
+ "integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "tlds": "bin.js"
+ }
+ },
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@@ -5551,6 +6386,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/undici": {
+ "version": "6.28.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz",
+ "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.17"
+ }
+ },
"node_modules/unicode-segmenter": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/unicode-segmenter/-/unicode-segmenter-0.14.5.tgz",
@@ -6276,6 +7121,23 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/wsl-utils": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz",
+ "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-wsl": "^3.1.0",
+ "powershell-utils": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/xxhash-wasm": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz",
diff --git a/web/package.json b/web/package.json
index e73f64b..42b02e7 100644
--- a/web/package.json
+++ b/web/package.json
@@ -31,6 +31,7 @@
"astro": "^7.2.0",
"png-to-ico": "^2.1.8",
"prettier-plugin-astro": "^0.14.1",
+ "sequoia-cli": "^0.5.7",
"sharp": "^0.34.2",
"typescript": "^5.9.0"
},
diff --git a/web/scripts/publish.sh b/web/scripts/publish.sh
new file mode 100755
index 0000000..e9ca51b
--- /dev/null
+++ b/web/scripts/publish.sh
@@ -0,0 +1,96 @@
+#!/usr/bin/env bash
+# Publish the blog to ATProto as standard.site records: one
+# site.standard.document per post, in lance.blue's own repository.
+# Configuration is web/sequoia.json.
+#
+# ATP_APP_PASSWORD=... web/scripts/publish.sh [--dry-run]
+#
+# The last step of a deploy, not part of one. A document record says "this
+# post is at this address", so the address has to answer first - otherwise the
+# network is told about a page that is not there. This refuses to publish a
+# post the live site is not already serving.
+#
+# Order: web/scripts/push.sh, point infra's site_origin_path at the new
+# prefix, apply, invalidate, then run this. ATP_APP_PASSWORD is an app
+# password, never an account password; it is never written or printed here.
+set -euo pipefail
+
+cd "$(dirname "$0")/.."
+
+dry_run=""
+if [ $# -gt 0 ]; then
+ case "$1" in
+ --dry-run | -n) dry_run=1 ;;
+ *)
+ echo "usage: web/scripts/publish.sh [--dry-run]" >&2
+ exit 2
+ ;;
+ esac
+fi
+
+sequoia="./node_modules/.bin/sequoia"
+[ -x "$sequoia" ] || {
+ echo "$sequoia is missing. Run: npm --prefix web ci" >&2
+ exit 1
+}
+
+if grep -q PLACEHOLDER sequoia.json; then
+ echo "sequoia.json still has the placeholder publicationUri." >&2
+ echo "Create the publication once, then put its at:// URI there." >&2
+ exit 1
+fi
+
+: "${ATP_APP_PASSWORD:?set it to an app password for the publishing account}"
+export ATP_APP_PASSWORD
+export ATP_IDENTIFIER="${ATP_IDENTIFIER:-lance.blue}"
+
+# What publishing would do, and the only place the post URLs come from. A real
+# run may skip some of these after syncing state from the PDS, so this is a
+# superset of what gets written - the safe direction for a check.
+plan="$("$sequoia" publish --dry-run --verbose)"
+echo "$plan"
+
+# Read from the config rather than spelled again here: a second copy is a
+# second thing to get wrong.
+prefix="$(node -p 'const c = require("./sequoia.json"); c.siteUrl + c.pathPrefix')"
+urls="$(printf '%s\n' "$plan" | grep -oE "${prefix}/[^[:space:]]+" | sort -u || true)"
+if [ -z "$urls" ]; then
+ echo "Nothing to publish."
+ exit 0
+fi
+
+# A page that is not being served comes back as the app's own index.html with
+# a 200 - see the comment in push.sh - so the status proves nothing on its
+# own. The canonical link does: only the right page carries the right one.
+echo
+echo "Checking the live site..."
+not_live=0
+while IFS= read -r url; do
+ if ! body="$(curl -fsS --max-time 20 "$url")"; then
+ echo " not serving: $url" >&2
+ not_live=1
+ elif ! printf '%s' "$body" | grep -qF "rel=\"canonical\" href=\"$url\""; then
+ echo " wrong page at: $url" >&2
+ not_live=1
+ else
+ echo " live: $url"
+ fi
+done <<<"$urls"
+
+if [ "$not_live" -ne 0 ]; then
+ echo >&2
+ echo "Refusing to publish: deploy first, then run this again." >&2
+ exit 1
+fi
+
+if [ -n "$dry_run" ]; then
+ echo
+ echo "Dry run: every post is live, nothing published."
+ exit 0
+fi
+
+echo
+"$sequoia" publish
+
+echo
+echo "Published. Commit the atUri frontmatter sequoia wrote into the posts."
diff --git a/web/scripts/publish.test.mjs b/web/scripts/publish.test.mjs
new file mode 100644
index 0000000..ad52d72
--- /dev/null
+++ b/web/scripts/publish.test.mjs
@@ -0,0 +1,48 @@
+/**
+ * sequoia.json describes the same site Astro builds.
+ *
+ * A standard.site document record carries the post's address, and sequoia
+ * computes it as siteUrl + pathPrefix + "/" + slug. Astro computes the same
+ * address from its own `site`. Nothing connects the two files, so if either
+ * moves the records point somewhere that does not serve the post - and
+ * because the record is keyed by that path, the next publish does not correct
+ * the old one, it writes a second record beside it.
+ *
+ * Run with `npm test`, which builds first: this reads dist/.
+ */
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { readFile, readdir } from "node:fs/promises";
+
+const url = (path) => new URL(path, import.meta.url);
+
+const sequoia = JSON.parse(await readFile(url("../sequoia.json"), "utf8"));
+const astro = (await import("../astro.config.mjs")).default;
+
+test("sequoia.json and astro.config.mjs agree on the site", () => {
+ assert.equal(sequoia.siteUrl, astro.site);
+ // A trailing slash would make Astro's canonical /blog/x/ while the record
+ // still claims /blog/x, and the two would stop being the same address.
+ assert.equal(astro.trailingSlash, "never");
+});
+
+test("every built post is canonical at the address its record will claim", async () => {
+ // dist/ rather than the content directory: drafts are already excluded, and
+ // this is the page a reader following the record actually gets.
+ const slugs = await readdir(url("../dist/blog/"), { withFileTypes: true });
+ const posts = slugs.filter((e) => e.isDirectory());
+ assert.ok(posts.length > 0, "no posts were built");
+
+ for (const post of posts) {
+ const html = await readFile(
+ url(`../dist/blog/${post.name}/index.html`),
+ "utf8",
+ );
+ // The exact string web/scripts/publish.sh looks for on the live site.
+ const claimed = `${sequoia.siteUrl}${sequoia.pathPrefix}/${post.name}`;
+ assert.ok(
+ html.includes(`rel="canonical" href="${claimed}"`),
+ `dist/blog/${post.name} is not canonical at ${claimed}`,
+ );
+ }
+});
diff --git a/web/sequoia.json b/web/sequoia.json
new file mode 100644
index 0000000..803fa84
--- /dev/null
+++ b/web/sequoia.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://tangled.org/stevedylan.dev/sequoia/raw/main/sequoia.schema.json",
+ "siteUrl": "https://lance.blue",
+ "contentDir": "./src/content/blog",
+ "pathPrefix": "/blog",
+ "identity": "lance.blue",
+ "publicationUri": "at://did:plc:a2j2g42ai6v65qpbvb6hmubi/site.standard.publication/PLACEHOLDER-borborygmus"
+}