diff --git a/indiekit.config.js b/indiekit.config.js
index 483f836ed..8dd706663 100644
--- a/indiekit.config.js
+++ b/indiekit.config.js
@@ -26,6 +26,7 @@ const config = {
"@indiekit/post-type-video",
"@indiekit/preset-eleventy",
"@indiekit/store-github",
+ "@indiekit/syndicator-atproto",
"@indiekit/syndicator-internet-archive",
"@indiekit/syndicator-mastodon",
],
@@ -86,6 +87,12 @@ const config = {
endpoint: process.env.S3_ENDPOINT,
bucket: process.env.S3_BUCKET,
},
+ "@indiekit/syndicator-atproto": {
+ checked: true,
+ url: process.env.ATPROTO_URL,
+ user: process.env.ATPROTO_USER,
+ password: process.env.ATPROTO_PASSWORD,
+ },
"@indiekit/syndicator-internet-archive": {
checked: false,
accessKey: process.env.INTERNET_ARCHIVE_ACCESS_KEY,
diff --git a/package-lock.json b/package-lock.json
index 6a40dfa1e..7c1cd87c1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -347,6 +347,62 @@
"node": ">= 14.0.0"
}
},
+ "node_modules/@atproto/api": {
+ "version": "0.12.29",
+ "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.12.29.tgz",
+ "integrity": "sha512-PyzPLjGWR0qNOMrmj3Nt3N5NuuANSgOk/33Bu3j+rFjjPrHvk9CI6iQPU6zuDaDCoyOTRJRafw8X/aMQw+ilgw==",
+ "license": "MIT",
+ "dependencies": {
+ "@atproto/common-web": "^0.3.0",
+ "@atproto/lexicon": "^0.4.0",
+ "@atproto/syntax": "^0.3.0",
+ "@atproto/xrpc": "^0.5.0",
+ "await-lock": "^2.2.2",
+ "multiformats": "^9.9.0",
+ "tlds": "^1.234.0"
+ }
+ },
+ "node_modules/@atproto/common-web": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.3.1.tgz",
+ "integrity": "sha512-N7wiTnus5vAr+lT//0y8m/FaHHLJ9LpGuEwkwDAeV3LCiPif4m/FS8x/QOYrx1PdZQwKso95RAPzCGWQBH5j6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "graphemer": "^1.4.0",
+ "multiformats": "^9.9.0",
+ "uint8arrays": "3.0.0",
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@atproto/lexicon": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.3.tgz",
+ "integrity": "sha512-lFVZXe1S1pJP0dcxvJuHP3r/a+EAIBwwU7jUK+r8iLhIja+ml6NmYv8KeFHmIJATh03spEQ9s02duDmFVdCoXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@atproto/common-web": "^0.3.1",
+ "@atproto/syntax": "^0.3.1",
+ "iso-datestring-validator": "^2.2.2",
+ "multiformats": "^9.9.0",
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@atproto/syntax": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.3.1.tgz",
+ "integrity": "sha512-fzW0Mg1QUOVCWUD3RgEsDt6d1OZ6DdFmbKcDdbzUfh0t4rhtRAC05KbZYmxuMPWDAiJ4BbbQ5dkAc/mNypMXkw==",
+ "license": "MIT"
+ },
+ "node_modules/@atproto/xrpc": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.5.0.tgz",
+ "integrity": "sha512-swu+wyOLvYW4l3n+VAuJbHcPcES+tin2Lsrp8Bw5aIXIICiuFn1YMFlwK9JwVUzTH21Py1s1nHEjr4CJeElJog==",
+ "license": "MIT",
+ "dependencies": {
+ "@atproto/lexicon": "^0.4.0",
+ "zod": "^3.21.4"
+ }
+ },
"node_modules/@aws-crypto/crc32": {
"version": "5.2.0",
"license": "Apache-2.0",
@@ -1931,28 +1987,6 @@
"@img/sharp-libvips-darwin-arm64": "1.0.4"
}
},
- "node_modules/@img/sharp-darwin-x64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
- "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
- "cpu": [
- "x64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-darwin-x64": "1.0.4"
- }
- },
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.0.4",
"cpu": [
@@ -1967,307 +2001,6 @@
"url": "https://opencollective.com/libvips"
}
},
- "node_modules/@img/sharp-libvips-darwin-x64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
- "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
- "cpu": [
- "x64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "darwin"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-arm": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
- "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
- "cpu": [
- "arm"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-arm64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
- "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
- "cpu": [
- "arm64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-s390x": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
- "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
- "cpu": [
- "s390x"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-x64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
- "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
- "cpu": [
- "x64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
- "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
- "cpu": [
- "arm64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linuxmusl-x64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
- "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
- "cpu": [
- "x64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-linux-arm": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
- "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
- "cpu": [
- "arm"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-arm": "1.0.5"
- }
- },
- "node_modules/@img/sharp-linux-arm64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
- "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
- "cpu": [
- "arm64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-arm64": "1.0.4"
- }
- },
- "node_modules/@img/sharp-linux-s390x": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
- "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
- "cpu": [
- "s390x"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-s390x": "1.0.4"
- }
- },
- "node_modules/@img/sharp-linux-x64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
- "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
- "cpu": [
- "x64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-x64": "1.0.4"
- }
- },
- "node_modules/@img/sharp-linuxmusl-arm64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
- "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
- "cpu": [
- "arm64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
- }
- },
- "node_modules/@img/sharp-linuxmusl-x64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
- "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
- "cpu": [
- "x64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-x64": "1.0.4"
- }
- },
- "node_modules/@img/sharp-wasm32": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
- "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
- "cpu": [
- "wasm32"
- ],
- "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/runtime": "^1.2.0"
- },
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-win32-ia32": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
- "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
- "cpu": [
- "ia32"
- ],
- "license": "Apache-2.0 AND LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-win32-x64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
- "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
- "cpu": [
- "x64"
- ],
- "license": "Apache-2.0 AND LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
"node_modules/@indiekit-test/config": {
"resolved": "helpers/config",
"link": true
@@ -2452,6 +2185,10 @@
"resolved": "packages/store-s3",
"link": true
},
+ "node_modules/@indiekit/syndicator-atproto": {
+ "resolved": "packages/syndicator-atproto",
+ "link": true
+ },
"node_modules/@indiekit/syndicator-internet-archive": {
"resolved": "packages/syndicator-internet-archive",
"link": true
@@ -5450,6 +5187,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "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==",
+ "license": "MIT"
+ },
"node_modules/aws-sdk-client-mock": {
"version": "4.1.0",
"dev": true,
@@ -10080,6 +9823,12 @@
"version": "4.2.11",
"license": "ISC"
},
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "license": "MIT"
+ },
"node_modules/h3": {
"version": "1.13.0",
"license": "MIT",
@@ -11201,6 +10950,12 @@
"node": ">=6.0"
}
},
+ "node_modules/iso-datestring-validator": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz",
+ "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==",
+ "license": "MIT"
+ },
"node_modules/isobject": {
"version": "3.0.1",
"license": "MIT",
@@ -11820,8 +11575,6 @@
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.28.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.28.1.tgz",
- "integrity": "sha512-7vWDISaMUn+oo2TwRdf2hl/BLdPxvywv9JKEqNZB/0K7bXwV4XE9wN/C2sAp1gGuh6QBA8lpjF4JIPt3HNlCHA==",
"cpu": [
"x64"
],
@@ -13301,6 +13054,12 @@
"version": "2.1.3",
"license": "MIT"
},
+ "node_modules/multiformats": {
+ "version": "9.9.0",
+ "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz",
+ "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==",
+ "license": "(Apache-2.0 AND MIT)"
+ },
"node_modules/multimatch": {
"version": "5.0.0",
"license": "MIT",
@@ -15731,8 +15490,6 @@
},
"node_modules/sharp": {
"version": "0.33.5",
- "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
- "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -16889,6 +16646,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/tlds": {
+ "version": "1.255.0",
+ "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.255.0.tgz",
+ "integrity": "sha512-tcwMRIioTcF/FcxLev8MJWxCp+GUALRhFEqbDoZrnowmKSGqPrl5pqS+Sut2m8BgJ6S4FExCSSpGffZ0Tks6Aw==",
+ "license": "MIT",
+ "bin": {
+ "tlds": "bin.js"
+ }
+ },
"node_modules/tldts": {
"version": "6.1.61",
"dev": true,
@@ -17221,6 +16987,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "multiformats": "^9.4.2"
+ }
+ },
"node_modules/unbox-primitive": {
"version": "1.0.2",
"dev": true,
@@ -18151,6 +17926,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/zod": {
+ "version": "3.23.8",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
+ "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
"node_modules/zwitch": {
"version": "2.0.4",
"dev": true,
@@ -18796,6 +18580,21 @@
"node": ">=20"
}
},
+ "packages/syndicator-atproto": {
+ "name": "@indiekit/syndicator-atproto",
+ "version": "1.0.0-beta.16",
+ "license": "MIT",
+ "dependencies": {
+ "@atproto/api": "^0.12.9",
+ "@indiekit/error": "^1.0.0-beta.15",
+ "@indiekit/util": "^1.0.0-beta.16",
+ "brevity": "^0.2.9",
+ "html-to-text": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"packages/syndicator-internet-archive": {
"name": "@indiekit/syndicator-internet-archive",
"version": "1.0.0-beta.15",
diff --git a/packages/syndicator-atproto/README.md b/packages/syndicator-atproto/README.md
new file mode 100644
index 000000000..afb5ab657
--- /dev/null
+++ b/packages/syndicator-atproto/README.md
@@ -0,0 +1,33 @@
+# @indiekit/syndicator-atproto
+
+[AT Protocol](https://atproto.com) syndicator for Indiekit.
+
+## Installation
+
+`npm i @indiekit/syndicator-atproto`
+
+## Usage
+
+Add `@indiekit/syndicator-atproto` to your list of plug-ins, specifying options as required:
+
+```json
+{
+ "plugins": ["@indiekit/syndicator-atproto"],
+ "@indiekit/syndicator-atproto": {
+ "url": "https://bsky.social",
+ "user": "username.bsky.social",
+ "password": "password",
+ "checked": true
+ }
+}
+```
+
+## Options
+
+| Option | Type | Description |
+| :----------------- | :-------- | :------------------------------------------------------------------------------------------------------------ |
+| `password` | `string` | Your AT protocol password. _Required_, defaults to `process.env.ATPROTO_PASSWORD`. |
+| `url` | `string` | Your AT protocol service, i.e. `https://bsky.social`. _Required_. |
+| `user` | `string` | Your AT protocol identifier (without the `@`). _Required_. |
+| `checked` | `boolean` | Tell a Micropub client whether this syndicator should be enabled by default. _Optional_, defaults to `false`. |
+| `includePermalink` | `boolean` | Always include a link to the original post. _Optional_, defaults to `false`. |
diff --git a/packages/syndicator-atproto/assets/icon.svg b/packages/syndicator-atproto/assets/icon.svg
new file mode 100644
index 000000000..8ed5e1d70
--- /dev/null
+++ b/packages/syndicator-atproto/assets/icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/packages/syndicator-atproto/index.js b/packages/syndicator-atproto/index.js
new file mode 100644
index 000000000..b22eaa8a0
--- /dev/null
+++ b/packages/syndicator-atproto/index.js
@@ -0,0 +1,97 @@
+import process from "node:process";
+import { IndiekitError } from "@indiekit/error";
+import { atproto } from "./lib/atproto.js";
+
+const defaults = {
+ checked: false,
+ includePermalink: false,
+ password: process.env.ATPROTO_PASSWORD,
+ user: "",
+};
+
+export default class AtProtoSyndicator {
+ /**
+ * @param {object} [options] - Plug-in options
+ * @param {boolean} [options.includePermalink] - Include permalink in status
+ * @param {string} [options.password] - Password
+ * @param {string} [options.url] - Server URL
+ * @param {string} [options.user] - Username
+ * @param {boolean} [options.checked] - Check syndicator in UI
+ */
+ constructor(options = {}) {
+ this.name = "AT Protocol syndicator";
+ this.options = { ...defaults, ...options };
+ }
+
+ get #url() {
+ return this.options?.url ? new URL(this.options.url) : false;
+ }
+
+ get #user() {
+ return this.options?.user
+ ? `@${this.options.user.replace("@", "")}`
+ : false;
+ }
+
+ get environment() {
+ return ["ATPROTO_PASSWORD"];
+ }
+
+ get info() {
+ const service = {
+ name: "AT Protocol",
+ photo: "/assets/@indiekit-atproto/icon.svg",
+ };
+ const user = this.#user;
+ const url = this.#url;
+
+ if (!url) {
+ return {
+ error: "Service URL required",
+ service,
+ };
+ }
+
+ if (!user) {
+ return {
+ error: "User identifier required",
+ service,
+ };
+ }
+
+ const uid = `${url.protocol}//${this.options.user.replace("@", "")}`;
+ service.url = url.href;
+
+ return {
+ checked: this.options.checked,
+ name: user,
+ uid,
+ service,
+ user: {
+ name: user,
+ url: uid,
+ },
+ };
+ }
+
+ async syndicate(properties, publication) {
+ try {
+ return await atproto({
+ identifier: this.options.user,
+ password: this.options.password,
+ includePermalink: this.options.includePermalink,
+ service: `${this.#url.protocol}//${this.#url.hostname}`,
+ }).post(properties, publication.me);
+ } catch (error) {
+ throw new IndiekitError(error.message, {
+ cause: error,
+ plugin: this.name,
+ status: error.statusCode,
+ });
+ }
+ }
+
+ init(Indiekit) {
+ Indiekit.addSyndicator(this);
+ }
+}
diff --git a/packages/syndicator-atproto/lib/atproto.js b/packages/syndicator-atproto/lib/atproto.js
new file mode 100644
index 000000000..0c7aa926c
--- /dev/null
+++ b/packages/syndicator-atproto/lib/atproto.js
@@ -0,0 +1,49 @@
+import { BskyAgent, RichText } from "@atproto/api";
+import { getStatusText } from "./utils.js";
+
+/**
+ * Syndicate post to an AT Protocol service
+ * @param {object} options - Syndicator options
+ * @param {string} options.identifier - User identifier
+ * @param {string} options.password - Password
+ * @param {boolean} options.includePermalink - Include permalink in status
+ * @param {string} options.service - Service URL
+ * @returns {object} Post functions
+ */
+export const atproto = ({
+ identifier,
+ password,
+ includePermalink,
+ service,
+}) => ({
+ async client() {
+ const agent = new BskyAgent({ service });
+
+ await agent.login({ identifier, password });
+
+ return agent;
+ },
+
+ /**
+ * Post to AT Protocol
+ * @param {object} properties - JF2 properties
+ * @returns {Promise} URL of syndicated status
+ */
+ async post(properties) {
+ const client = await this.client();
+ const text = getStatusText(properties, {
+ includePermalink,
+ service,
+ });
+
+ const rt = new RichText({ text });
+ await rt.detectFacets(client);
+
+ return client.post({
+ $type: "app.bsky.feed.post",
+ text: rt.text,
+ facets: rt.facets,
+ createdAt: new Date().toISOString(),
+ });
+ },
+});
diff --git a/packages/syndicator-atproto/lib/utils.js b/packages/syndicator-atproto/lib/utils.js
new file mode 100644
index 000000000..f61ea45f2
--- /dev/null
+++ b/packages/syndicator-atproto/lib/utils.js
@@ -0,0 +1,79 @@
+import brevity from "brevity";
+import { htmlToText } from "html-to-text";
+
+/**
+ * Get status parameters from given JF2 properties
+ * @param {object} properties - JF2 properties
+ * @param {object} [options] - Options
+ * @param {boolean} [options.includePermalink] - Include permalink in status
+ * @param {string} [options.service] - Server URL
+ * @returns {string} Status text
+ */
+export const getStatusText = (properties, options = {}) => {
+ const { includePermalink, service } = options;
+
+ let text;
+ if (properties.content && properties.content.html) {
+ text = htmlToStatusText(properties.content.html, service);
+ }
+
+ // Truncate status if longer than 300 characters
+ text = brevity.shorten(
+ text,
+ properties.url,
+ includePermalink // https://indieweb.org/permashortlink
+ ? properties.url
+ : false,
+ false, // https://indieweb.org/permashortcitation
+ 300,
+ );
+
+ // Show permalink below status, not within brackets
+ text = text.replace(`(${properties.url})`, `\n\n${properties.url}`);
+
+ return text;
+};
+
+/**
+ * Convert HTML to plain text, appending last link href if present
+ * @param {string} html - HTML
+ * @param {string} serverUrl - Server URL, i.e. https://mastodon.social
+ * @returns {string} Text
+ */
+export const htmlToStatusText = (html, serverUrl) => {
+ // Get all the link references
+ let hrefs = [...html.matchAll(/href="(https?:\/\/.+?)"/g)];
+
+ // Remove any links to Mastodon server
+ // HTML may contain Mastodon usernames or hashtag links
+ hrefs = hrefs.filter((href) => {
+ const hrefHostname = new URL(href[1]).hostname;
+ const serverHostname = new URL(serverUrl).hostname;
+ return hrefHostname !== serverHostname;
+ });
+
+ // Get the last link mentioned, or return false
+ const lastHref = hrefs.length > 0 ? hrefs.at(-1)[1] : false;
+
+ // Convert HTML to plain text, removing any links
+ const text = htmlToText(html, {
+ selectors: [
+ {
+ selector: "a",
+ options: {
+ ignoreHref: true,
+ },
+ },
+ {
+ selector: "img",
+ format: "skip",
+ },
+ ],
+ wordwrap: false,
+ });
+
+ // Append the last link if present
+ const statusText = lastHref ? `${text} ${lastHref}` : text;
+
+ return statusText;
+};
diff --git a/packages/syndicator-atproto/package.json b/packages/syndicator-atproto/package.json
new file mode 100644
index 000000000..81e050c6e
--- /dev/null
+++ b/packages/syndicator-atproto/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "@indiekit/syndicator-atproto",
+ "version": "1.0.0-beta.16",
+ "description": "AT Protocol syndicator for Indiekit",
+ "keywords": [
+ "indiekit",
+ "indiekit-plugin",
+ "indieweb",
+ "syndication",
+ "at-protocol",
+ "atproto"
+ ],
+ "homepage": "https://getindiekit.com",
+ "author": {
+ "name": "Paul Robert Lloyd",
+ "url": "https://paulrobertlloyd.com"
+ },
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
+ "type": "module",
+ "main": "index.js",
+ "files": [
+ "assets",
+ "lib",
+ "index.js"
+ ],
+ "bugs": {
+ "url": "https://github.com/getindiekit/indiekit/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/getindiekit/indiekit.git",
+ "directory": "packages/syndicator-atproto"
+ },
+ "dependencies": {
+ "@atproto/api": "^0.12.9",
+ "@indiekit/error": "^1.0.0-beta.15",
+ "@indiekit/util": "^1.0.0-beta.16",
+ "brevity": "^0.2.9",
+ "html-to-text": "^9.0.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/syndicator-atproto/test/index.js b/packages/syndicator-atproto/test/index.js
new file mode 100644
index 000000000..cd9efd33c
--- /dev/null
+++ b/packages/syndicator-atproto/test/index.js
@@ -0,0 +1,56 @@
+import { strict as assert } from "node:assert";
+import { describe, it } from "node:test";
+import { Indiekit } from "@indiekit/indiekit";
+import AtProtoSyndicator from "../index.js";
+
+describe("syndicator-atproto", () => {
+ const atproto = new AtProtoSyndicator({
+ password: "password",
+ url: "https://butterfly.example",
+ user: "username.butterfly.example",
+ });
+
+ it("Gets plug-in environment", () => {
+ assert.deepEqual(atproto.environment, ["ATPROTO_PASSWORD"]);
+ });
+
+ it("Gets plug-in info", () => {
+ assert.equal(atproto.name, "AT Protocol syndicator");
+ assert.equal(atproto.info.checked, false);
+ assert.equal(atproto.info.name, "@username.butterfly.example");
+ assert.equal(atproto.info.uid, "https://username.butterfly.example");
+ assert.ok(atproto.info.service);
+ });
+
+ it("Returns error information if no server URL provided", async () => {
+ const result = new AtProtoSyndicator({
+ password: "password",
+ user: "username",
+ });
+
+ assert.equal(result.info.error, "Service URL required");
+ });
+
+ it("Returns error information if no username provided", () => {
+ const result = new AtProtoSyndicator({
+ password: "password",
+ url: "https://username.butterfly.example",
+ });
+
+ assert.equal(result.info.error, "User identifier required");
+ });
+
+ it("Initiates plug-in", async () => {
+ const indiekit = await Indiekit.initialize({ config: {} });
+ atproto.init(indiekit);
+
+ assert.equal(
+ indiekit.publication.syndicationTargets[0].info.name,
+ "@username.butterfly.example",
+ );
+ });
+
+ it.todo("Returns syndicated URL");
+
+ it.todo("Throws error getting syndicated URL if access token invalid");
+});