this repo has no description

CLI auth and `pub view` command

Was curious to see if I ran into any oauth snags but this seems to work
well! Neat.

seth.computer 75003b27 3db8b388

verified
+521 -17
+136
DEVELOPMENT.md
··· 1 + # Development Guide 2 + 3 + ## Prerequisites 4 + 5 + - [Bun](https://bun.sh/) v1.3+ 6 + 7 + ## Setup 8 + 9 + ```bash 10 + bun install 11 + ``` 12 + 13 + ## Project Structure 14 + 15 + ``` 16 + sitebase/ 17 + ├── packages/ 18 + │ ├── core/ # Export library (@sitebase/core) 19 + │ ├── cli/ # CLI tool (@sitebase/cli) 20 + │ └── web/ # Web management UI (@sitebase/web) 21 + ├── scripts/ # Utility scripts 22 + ├── biome.json # Linter/formatter config 23 + └── tsconfig.json # TypeScript config 24 + ``` 25 + 26 + ## Commands 27 + 28 + ### Verification 29 + 30 + Run these before committing: 31 + 32 + ```bash 33 + # Run all checks (typecheck + lint) 34 + bun run check 35 + 36 + # Individual checks 37 + bun run typecheck # TypeScript type checking 38 + bun run lint:check # Biome lint (no auto-fix) 39 + bun run format:check # Biome format (no auto-fix) 40 + ``` 41 + 42 + ### Fixing Issues 43 + 44 + ```bash 45 + bun run lint # Auto-fix lint issues 46 + bun run format # Auto-fix formatting 47 + ``` 48 + 49 + ## Code Style 50 + 51 + Enforced by [Biome](https://biomejs.dev/): 52 + 53 + - **Indentation**: Tabs 54 + - **Quotes**: Double quotes 55 + - **Imports**: Auto-organized on save (via `biome.json` assist) 56 + 57 + TypeScript is configured with strict mode and additional safety flags: 58 + - `noUncheckedIndexedAccess` - Array/object index access may be undefined 59 + - `noFallthroughCasesInSwitch` - Require break/return in switch cases 60 + - `noImplicitOverride` - Require `override` keyword 61 + 62 + ## Packages 63 + 64 + ### @sitebase/core 65 + 66 + Core export functionality. No runtime dependencies except Handlebars. 67 + 68 + Key files: 69 + - `src/types.ts` - Type definitions 70 + - `src/export.ts` - Export logic 71 + - `src/config.ts` - Config file loading 72 + - `src/templates.ts` - Handlebars helpers, slugify 73 + - `src/atproto.ts` - ATProto fetch utilities 74 + 75 + ### @sitebase/cli 76 + 77 + CLI wrapper around core. Uses Commander for argument parsing. 78 + 79 + ```bash 80 + cd packages/cli 81 + bun run src/index.ts export --config ../path/to/config.ts 82 + ``` 83 + 84 + ### @sitebase/web 85 + 86 + Hono-based web server with ATProto OAuth. 87 + 88 + ```bash 89 + cd packages/web 90 + bun run dev 91 + ``` 92 + 93 + See `packages/web/CLAUDE.md` for web-specific details. 94 + 95 + ## ATProto Integration 96 + 97 + The project uses ATProto lexicons: 98 + - `site.standard.publication` - Blog/publication metadata 99 + - `site.standard.document` - Individual documents/posts 100 + 101 + Documents are fetched from any PDS via: 102 + 1. Parse AT URI to extract DID 103 + 2. Resolve DID to PDS endpoint via `plc.directory` 104 + 3. Fetch records via `com.atproto.repo.getRecord` / `listRecords` 105 + 106 + ## Adding Features 107 + 108 + ### New Handlebars Helper 109 + 110 + Add to `packages/core/src/templates.ts` in `registerHelpers()`: 111 + 112 + ```typescript 113 + hbs.registerHelper("myHelper", (arg1: unknown) => { 114 + // Return transformed value 115 + return String(arg1).toUpperCase(); 116 + }); 117 + ``` 118 + 119 + ### New Export Option 120 + 121 + 1. Add type to `packages/core/src/types.ts` in `ExportOptions` and `ExportTarget` 122 + 2. Handle in `packages/core/src/export.ts` in `exportPublication()` 123 + 3. Update `packages/core/src/config.ts` if it affects config loading 124 + 4. Document in README.md 125 + 126 + ## Workspace Dependencies 127 + 128 + Use workspace protocol for internal deps: 129 + 130 + ```json 131 + { 132 + "dependencies": { 133 + "@sitebase/core": "workspace:*" 134 + } 135 + } 136 + ```
+60 -14
bun.lock
··· 18 18 "name": "@sitebase/cli", 19 19 "version": "0.0.1", 20 20 "bin": { 21 - "sitebase": "./src/index.ts", 21 + "sitebase": "./bin/sitebase", 22 22 }, 23 23 "dependencies": { 24 + "@atproto/api": "^0.18.16", 25 + "@atproto/oauth-client-node": "^0.2.1", 24 26 "@sitebase/core": "workspace:*", 25 27 "commander": "^12.1.0", 26 28 }, ··· 50 52 }, 51 53 }, 52 54 "packages": { 53 - "@atproto-labs/did-resolver": ["@atproto-labs/did-resolver@0.2.5", "", { "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.2.4", "zod": "^3.23.8" } }, "sha512-he7EC6OMSifNs01a4RT9mta/yYitoKDzlK9ty2TFV5Uj/+HpB4vYMRdIDFrRW0Hcsehy90E2t/dw0t7361MEKQ=="], 55 + "@atproto-labs/did-resolver": ["@atproto-labs/did-resolver@0.1.13", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "@atproto-labs/simple-store": "0.2.0", "@atproto-labs/simple-store-memory": "0.1.3", "@atproto/did": "0.1.5", "zod": "^3.23.8" } }, "sha512-DG3YNaCKc6PAIv1Gsz3E1Kufw2t14OBxe4LdKK7KKLCNoex51hm+A5yMevShe3BSll+QosqWYIEgkPSc5xBoGQ=="], 54 56 55 57 "@atproto-labs/fetch": ["@atproto-labs/fetch@0.2.3", "", { "dependencies": { "@atproto-labs/pipe": "0.1.1" } }, "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw=="], 56 58 57 - "@atproto-labs/fetch-node": ["@atproto-labs/fetch-node@0.2.0", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "ipaddr.js": "^2.1.0", "undici": "^6.14.1" } }, "sha512-Krq09nH/aeoiU2s9xdHA0FjTEFWG9B5FFenipv1iRixCcPc7V3DhTNDawxG9gI8Ny0k4dBVS9WTRN/IDzBx86Q=="], 59 + "@atproto-labs/fetch-node": ["@atproto-labs/fetch-node@0.1.9", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "ipaddr.js": "^2.1.0", "undici": "^6.14.1" } }, "sha512-8sHDDXZEzQptLu8ddUU/8U+THS6dumgPynVX0/1PjUYd4S/FWyPcz6yMIiVChTfzKnZvYRRz47+qvOKhydrHQw=="], 58 60 59 - "@atproto-labs/handle-resolver": ["@atproto-labs/handle-resolver@0.3.5", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.4", "zod": "^3.23.8" } }, "sha512-r3b+plCh/0arN535Aool9gL6yTSbAPDOyReURbA2TWAaeW4vrSJPwR6yYUx0k0vmVPjkZPIdUVd63bG/+VG5MA=="], 61 + "@atproto-labs/handle-resolver": ["@atproto-labs/handle-resolver@0.1.8", "", { "dependencies": { "@atproto-labs/simple-store": "0.2.0", "@atproto-labs/simple-store-memory": "0.1.3", "@atproto/did": "0.1.5", "zod": "^3.23.8" } }, "sha512-Y0ckccoCGDo/3g4thPkgp9QcORmc+qqEaCBCYCZYtfLIQp4775u22wd+4fyEyJP4DqoReKacninkICgRGfs3dQ=="], 60 62 61 - "@atproto-labs/handle-resolver-node": ["@atproto-labs/handle-resolver-node@0.1.24", "", { "dependencies": { "@atproto-labs/fetch-node": "0.2.0", "@atproto-labs/handle-resolver": "0.3.5", "@atproto/did": "0.2.4" } }, "sha512-w/zvktigmRQpOLQQclp48tbb2K/2XW8j1szoIpT8T8v6P5dZ8GGVDIEF142xQMX9vWToFqMTu1P2yOuz8e3Ilg=="], 63 + "@atproto-labs/handle-resolver-node": ["@atproto-labs/handle-resolver-node@0.1.16", "", { "dependencies": { "@atproto-labs/fetch-node": "0.1.9", "@atproto-labs/handle-resolver": "0.1.8", "@atproto/did": "0.1.5" } }, "sha512-i2F989zjyC7b/odrV3/tOpIT1IDIxR3F0khPG4REfOWcmJ89QcP8BiejJ6KFJk3hbTJHq6X9/pTG1vesCvyIKA=="], 62 64 63 - "@atproto-labs/identity-resolver": ["@atproto-labs/identity-resolver@0.3.5", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.5", "@atproto-labs/handle-resolver": "0.3.5" } }, "sha512-kSxnreUSPhKL77doUbSl/9I6Y9qpkpD7MMJoYFQVU/WG0PB90tzfIb6DNuWsjbU2I5Q91Nzc4Tm4VJMV+OPKGQ=="], 65 + "@atproto-labs/identity-resolver": ["@atproto-labs/identity-resolver@0.1.18", "", { "dependencies": { "@atproto-labs/did-resolver": "0.1.13", "@atproto-labs/handle-resolver": "0.1.8", "@atproto/syntax": "0.4.0" } }, "sha512-DArYXP1hzZJIBcojun0CWEF+TjAhlGKcVq/RwLiGfY1mKq2yPjCiXyHj+5L0+z9jBSZiAB7L65JgcjI2+MFiRg=="], 64 66 65 67 "@atproto-labs/pipe": ["@atproto-labs/pipe@0.1.1", "", {}, "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg=="], 66 68 67 - "@atproto-labs/simple-store": ["@atproto-labs/simple-store@0.3.0", "", {}, "sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ=="], 69 + "@atproto-labs/simple-store": ["@atproto-labs/simple-store@0.2.0", "", {}, "sha512-0bRbAlI8Ayh03wRwncAMEAyUKtZ+AuTS1jgPrfym1WVOAOiottI/ZmgccqLl6w5MbxVcClNQF7WYGKvGwGoIhA=="], 68 70 69 - "@atproto-labs/simple-store-memory": ["@atproto-labs/simple-store-memory@0.1.4", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "lru-cache": "^10.2.0" } }, "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw=="], 71 + "@atproto-labs/simple-store-memory": ["@atproto-labs/simple-store-memory@0.1.3", "", { "dependencies": { "@atproto-labs/simple-store": "0.2.0", "lru-cache": "^10.2.0" } }, "sha512-jkitT9+AtU+0b28DoN92iURLaCt/q/q4yX8q6V+9LSwYlUTqKoj/5NFKvF7x6EBuG+gpUdlcycbH7e60gjOhRQ=="], 70 72 71 73 "@atproto/api": ["@atproto/api@0.18.16", "", { "dependencies": { "@atproto/common-web": "^0.4.12", "@atproto/lexicon": "^0.6.0", "@atproto/syntax": "^0.4.2", "@atproto/xrpc": "^0.7.7", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-tRGKSWr83pP5CQpSboePU21pE+GqLDYy1XHae4HH4hjaT0pr5V8wNgu70kbKB0B02GVUumeDRpJnlHKD+eMzLg=="], 72 74 73 75 "@atproto/common-web": ["@atproto/common-web@0.4.12", "", { "dependencies": { "@atproto/lex-data": "0.0.8", "@atproto/lex-json": "0.0.8", "zod": "^3.23.8" } }, "sha512-3aCJemqM/fkHQrVPbTCHCdiVstKFI+2LkFLvUhO6XZP0EqUZa/rg/CIZBKTFUWu9I5iYiaEiXL9VwcDRpEevSw=="], 74 76 75 - "@atproto/did": ["@atproto/did@0.2.4", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-nxNiCgXeo7pfjojq9fpfZxCO0X0xUipNVKW+AHNZwQKiUDt6zYL0VXEfm8HBUwQOCmKvj2pRRSM1Cur+tUWk3g=="], 77 + "@atproto/did": ["@atproto/did@0.1.5", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-8+1D08QdGE5TF0bB0vV8HLVrVZJeLNITpRTUVEoABNMRaUS7CoYSVb0+JNQDeJIVmqMjOL8dOjvCUDkp3gEaGQ=="], 76 78 77 - "@atproto/jwk": ["@atproto/jwk@0.6.0", "", { "dependencies": { "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-bDoJPvt7TrQVi/rBfBrSSpGykhtIriKxeYCYQTiPRKFfyRhbgpElF0wPXADjIswnbzZdOwbY63az4E/CFVT3Tw=="], 79 + "@atproto/jwk": ["@atproto/jwk@0.3.0", "", { "dependencies": { "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-MIAXyNMGu1tCNHjqW/8jqfE/wgWCIoK2cJ0mR6UxwhNPvkbe35TcpRYJdtQu/E6MUd7TziyDBa/GO4dKAiePhQ=="], 78 80 79 81 "@atproto/jwk-jose": ["@atproto/jwk-jose@0.1.11", "", { "dependencies": { "@atproto/jwk": "0.6.0", "jose": "^5.2.0" } }, "sha512-i4Fnr2sTBYmMmHXl7NJh8GrCH+tDQEVWrcDMDnV5DjJfkgT17wIqvojIw9SNbSL4Uf0OtfEv6AgG0A+mgh8b5Q=="], 80 82 81 - "@atproto/jwk-webcrypto": ["@atproto/jwk-webcrypto@0.2.0", "", { "dependencies": { "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "zod": "^3.23.8" } }, "sha512-UmgRrrEAkWvxwhlwe30UmDOdTEFidlIzBC7C3cCbeJMcBN1x8B3KH+crXrsTqfWQBG58mXgt8wgSK3Kxs2LhFg=="], 83 + "@atproto/jwk-webcrypto": ["@atproto/jwk-webcrypto@0.1.8", "", { "dependencies": { "@atproto/jwk": "0.3.0", "@atproto/jwk-jose": "0.1.8", "zod": "^3.23.8" } }, "sha512-oOW/G40f6M0NbTOo8uZgiSsFtcvlfNFldyxm+V+fVo5yKe6cvgvPNqckpqMsoBe6JYfImdc/zdVak9fCSSh41A=="], 82 84 83 85 "@atproto/lex-data": ["@atproto/lex-data@0.0.8", "", { "dependencies": { "@atproto/syntax": "0.4.2", "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-1Y5tz7BkS7380QuLNXaE8GW8Xba+mRWugt8BKM4BUFYjjUZdmirU8lr72iM4XlEBrzRu8Cfvj+MbsbYaZv+IgA=="], 84 86 ··· 86 88 87 89 "@atproto/lexicon": ["@atproto/lexicon@0.6.0", "", { "dependencies": { "@atproto/common-web": "^0.4.7", "@atproto/syntax": "^0.4.2", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-5veb8aD+J5M0qszLJ+73KSFsFrJBgAY/nM1TSAJvGY7fNc9ZAT+PSUlmIyrdye9YznAZ07yktalls/TwNV7cHQ=="], 88 90 89 - "@atproto/oauth-client": ["@atproto/oauth-client@0.5.13", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.5", "@atproto-labs/fetch": "0.2.3", "@atproto-labs/handle-resolver": "0.3.5", "@atproto-labs/identity-resolver": "0.3.5", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.4", "@atproto/jwk": "0.6.0", "@atproto/oauth-types": "0.6.1", "@atproto/xrpc": "0.7.7", "core-js": "^3", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-FLbqHkC7BAVZ90LHVzSxQf+s8ZNIQI4TsDuhYDyzi7lYtktFHDbgd88KuM2ClJFOtGCsSS17yR1Joy925tDSaA=="], 91 + "@atproto/oauth-client": ["@atproto/oauth-client@0.4.0", "", { "dependencies": { "@atproto-labs/did-resolver": "0.1.13", "@atproto-labs/fetch": "0.2.3", "@atproto-labs/handle-resolver": "0.1.8", "@atproto-labs/identity-resolver": "0.1.18", "@atproto-labs/simple-store": "0.2.0", "@atproto-labs/simple-store-memory": "0.1.3", "@atproto/did": "0.1.5", "@atproto/jwk": "0.3.0", "@atproto/oauth-types": "0.3.0", "@atproto/xrpc": "0.7.0", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-uWVnlhennWkgvzqP0l53sFaw6DM6B4zmq0fv1xs05vt56Sjly8YirAj0GVDXlb37/BQRJQ1WOBzJVYDI3bH9uw=="], 90 92 91 - "@atproto/oauth-client-node": ["@atproto/oauth-client-node@0.3.15", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.5", "@atproto-labs/handle-resolver-node": "0.1.24", "@atproto-labs/simple-store": "0.3.0", "@atproto/did": "0.2.4", "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "@atproto/jwk-webcrypto": "0.2.0", "@atproto/oauth-client": "0.5.13", "@atproto/oauth-types": "0.6.1" } }, "sha512-iuT7QrLli7IyB4px1+lHvm/YoIRfNRpbNG9seJRtu5eX4N5aLsBP6vpXs9rCygd1+/15LcLRAAGKVEcrLT9tXA=="], 93 + "@atproto/oauth-client-node": ["@atproto/oauth-client-node@0.2.24", "", { "dependencies": { "@atproto-labs/did-resolver": "0.1.13", "@atproto-labs/handle-resolver-node": "0.1.16", "@atproto-labs/simple-store": "0.2.0", "@atproto/did": "0.1.5", "@atproto/jwk": "0.3.0", "@atproto/jwk-jose": "0.1.8", "@atproto/jwk-webcrypto": "0.1.8", "@atproto/oauth-client": "0.4.0", "@atproto/oauth-types": "0.3.0" } }, "sha512-WsUiFkHjlm80J2d4czP7msYZoxvKB4hDpZGw34RgMD4VLA2jt03GMH4wTQPIZxfV3/9yrgMoOW/BDC9Iv4MavA=="], 92 94 93 - "@atproto/oauth-types": ["@atproto/oauth-types@0.6.1", "", { "dependencies": { "@atproto/did": "0.2.4", "@atproto/jwk": "0.6.0", "zod": "^3.23.8" } }, "sha512-3z92GN/6zCq9E2GTTfZM27tWEbvi1qwFSA7KoS5+wqBC4kSsLvnLxmbKH402Z40DfWS4YWqw0DkHsgP0LNFDEA=="], 95 + "@atproto/oauth-types": ["@atproto/oauth-types@0.3.0", "", { "dependencies": { "@atproto/jwk": "0.3.0", "zod": "^3.23.8" } }, "sha512-ptfsJARKODXfuOoDQag4a6PpEkDEj4Urz3jOmnQZy2YspPc/TNm1o0HglU0YehELv1vfhh9gEz40BJztPPhiLA=="], 94 96 95 97 "@atproto/syntax": ["@atproto/syntax@0.4.2", "", {}, "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA=="], 96 98 ··· 174 176 175 177 "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], 176 178 179 + "@atproto-labs/identity-resolver/@atproto/syntax": ["@atproto/syntax@0.4.0", "", {}, "sha512-b9y5ceHS8YKOfP3mdKmwAx5yVj9294UN7FG2XzP6V5aKUdFazEYRnR9m5n5ZQFKa3GNvz7de9guZCJ/sUTcOAA=="], 180 + 181 + "@atproto/jwk-jose/@atproto/jwk": ["@atproto/jwk@0.6.0", "", { "dependencies": { "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-bDoJPvt7TrQVi/rBfBrSSpGykhtIriKxeYCYQTiPRKFfyRhbgpElF0wPXADjIswnbzZdOwbY63az4E/CFVT3Tw=="], 182 + 183 + "@atproto/jwk-webcrypto/@atproto/jwk-jose": ["@atproto/jwk-jose@0.1.8", "", { "dependencies": { "@atproto/jwk": "0.3.0", "jose": "^5.2.0" } }, "sha512-aoU2Q0GpIl388KhCcv9YvAxNscALUv3xzLq5gjVPdJ+zmqw94nGZNcjiNvpnbfS+VQM9e2DrrTuwmDXnxfrrSA=="], 184 + 185 + "@atproto/oauth-client/@atproto/xrpc": ["@atproto/xrpc@0.7.0", "", { "dependencies": { "@atproto/lexicon": "^0.4.11", "zod": "^3.23.8" } }, "sha512-SfhP9dGx2qclaScFDb58Jnrmim5nk4geZXCqg6sB0I/KZhZEkr9iIx1hLCp+sxkIfEsmEJjeWO4B0rjUIJW5cw=="], 186 + 187 + "@atproto/oauth-client-node/@atproto/jwk-jose": ["@atproto/jwk-jose@0.1.8", "", { "dependencies": { "@atproto/jwk": "0.3.0", "jose": "^5.2.0" } }, "sha512-aoU2Q0GpIl388KhCcv9YvAxNscALUv3xzLq5gjVPdJ+zmqw94nGZNcjiNvpnbfS+VQM9e2DrrTuwmDXnxfrrSA=="], 188 + 177 189 "@sitebase/web/@atproto/api": ["@atproto/api@0.18.14", "", { "dependencies": { "@atproto/common-web": "^0.4.12", "@atproto/lexicon": "^0.6.0", "@atproto/syntax": "^0.4.2", "@atproto/xrpc": "^0.7.7", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-1pWAPbuG3RA1o8uOAwYWZOddvNjuweYOxwTvys1q/r9NCjoGkZY0uJUy1dr6LKFaDk8bjikd2O1cgsRwFfv6Fw=="], 190 + 191 + "@sitebase/web/@atproto/oauth-client-node": ["@atproto/oauth-client-node@0.3.15", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.5", "@atproto-labs/handle-resolver-node": "0.1.24", "@atproto-labs/simple-store": "0.3.0", "@atproto/did": "0.2.4", "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "@atproto/jwk-webcrypto": "0.2.0", "@atproto/oauth-client": "0.5.13", "@atproto/oauth-types": "0.6.1" } }, "sha512-iuT7QrLli7IyB4px1+lHvm/YoIRfNRpbNG9seJRtu5eX4N5aLsBP6vpXs9rCygd1+/15LcLRAAGKVEcrLT9tXA=="], 178 192 179 193 "bun-types/@types/node": ["@types/node@25.0.8", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg=="], 194 + 195 + "@atproto/oauth-client/@atproto/xrpc/@atproto/lexicon": ["@atproto/lexicon@0.4.14", "", { "dependencies": { "@atproto/common-web": "^0.4.2", "@atproto/syntax": "^0.4.0", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-jiKpmH1QER3Gvc7JVY5brwrfo+etFoe57tKPQX/SmPwjvUsFnJAow5xLIryuBaJgFAhnTZViXKs41t//pahGHQ=="], 196 + 197 + "@sitebase/web/@atproto/oauth-client-node/@atproto-labs/did-resolver": ["@atproto-labs/did-resolver@0.2.5", "", { "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.2.4", "zod": "^3.23.8" } }, "sha512-he7EC6OMSifNs01a4RT9mta/yYitoKDzlK9ty2TFV5Uj/+HpB4vYMRdIDFrRW0Hcsehy90E2t/dw0t7361MEKQ=="], 198 + 199 + "@sitebase/web/@atproto/oauth-client-node/@atproto-labs/handle-resolver-node": ["@atproto-labs/handle-resolver-node@0.1.24", "", { "dependencies": { "@atproto-labs/fetch-node": "0.2.0", "@atproto-labs/handle-resolver": "0.3.5", "@atproto/did": "0.2.4" } }, "sha512-w/zvktigmRQpOLQQclp48tbb2K/2XW8j1szoIpT8T8v6P5dZ8GGVDIEF142xQMX9vWToFqMTu1P2yOuz8e3Ilg=="], 200 + 201 + "@sitebase/web/@atproto/oauth-client-node/@atproto-labs/simple-store": ["@atproto-labs/simple-store@0.3.0", "", {}, "sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ=="], 202 + 203 + "@sitebase/web/@atproto/oauth-client-node/@atproto/did": ["@atproto/did@0.2.4", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-nxNiCgXeo7pfjojq9fpfZxCO0X0xUipNVKW+AHNZwQKiUDt6zYL0VXEfm8HBUwQOCmKvj2pRRSM1Cur+tUWk3g=="], 204 + 205 + "@sitebase/web/@atproto/oauth-client-node/@atproto/jwk": ["@atproto/jwk@0.6.0", "", { "dependencies": { "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-bDoJPvt7TrQVi/rBfBrSSpGykhtIriKxeYCYQTiPRKFfyRhbgpElF0wPXADjIswnbzZdOwbY63az4E/CFVT3Tw=="], 206 + 207 + "@sitebase/web/@atproto/oauth-client-node/@atproto/jwk-webcrypto": ["@atproto/jwk-webcrypto@0.2.0", "", { "dependencies": { "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "zod": "^3.23.8" } }, "sha512-UmgRrrEAkWvxwhlwe30UmDOdTEFidlIzBC7C3cCbeJMcBN1x8B3KH+crXrsTqfWQBG58mXgt8wgSK3Kxs2LhFg=="], 208 + 209 + "@sitebase/web/@atproto/oauth-client-node/@atproto/oauth-client": ["@atproto/oauth-client@0.5.13", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.5", "@atproto-labs/fetch": "0.2.3", "@atproto-labs/handle-resolver": "0.3.5", "@atproto-labs/identity-resolver": "0.3.5", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.4", "@atproto/jwk": "0.6.0", "@atproto/oauth-types": "0.6.1", "@atproto/xrpc": "0.7.7", "core-js": "^3", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-FLbqHkC7BAVZ90LHVzSxQf+s8ZNIQI4TsDuhYDyzi7lYtktFHDbgd88KuM2ClJFOtGCsSS17yR1Joy925tDSaA=="], 210 + 211 + "@sitebase/web/@atproto/oauth-client-node/@atproto/oauth-types": ["@atproto/oauth-types@0.6.1", "", { "dependencies": { "@atproto/did": "0.2.4", "@atproto/jwk": "0.6.0", "zod": "^3.23.8" } }, "sha512-3z92GN/6zCq9E2GTTfZM27tWEbvi1qwFSA7KoS5+wqBC4kSsLvnLxmbKH402Z40DfWS4YWqw0DkHsgP0LNFDEA=="], 212 + 213 + "@sitebase/web/@atproto/oauth-client-node/@atproto-labs/did-resolver/@atproto-labs/simple-store-memory": ["@atproto-labs/simple-store-memory@0.1.4", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "lru-cache": "^10.2.0" } }, "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw=="], 214 + 215 + "@sitebase/web/@atproto/oauth-client-node/@atproto-labs/handle-resolver-node/@atproto-labs/fetch-node": ["@atproto-labs/fetch-node@0.2.0", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "ipaddr.js": "^2.1.0", "undici": "^6.14.1" } }, "sha512-Krq09nH/aeoiU2s9xdHA0FjTEFWG9B5FFenipv1iRixCcPc7V3DhTNDawxG9gI8Ny0k4dBVS9WTRN/IDzBx86Q=="], 216 + 217 + "@sitebase/web/@atproto/oauth-client-node/@atproto-labs/handle-resolver-node/@atproto-labs/handle-resolver": ["@atproto-labs/handle-resolver@0.3.5", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.4", "zod": "^3.23.8" } }, "sha512-r3b+plCh/0arN535Aool9gL6yTSbAPDOyReURbA2TWAaeW4vrSJPwR6yYUx0k0vmVPjkZPIdUVd63bG/+VG5MA=="], 218 + 219 + "@sitebase/web/@atproto/oauth-client-node/@atproto/oauth-client/@atproto-labs/handle-resolver": ["@atproto-labs/handle-resolver@0.3.5", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.4", "zod": "^3.23.8" } }, "sha512-r3b+plCh/0arN535Aool9gL6yTSbAPDOyReURbA2TWAaeW4vrSJPwR6yYUx0k0vmVPjkZPIdUVd63bG/+VG5MA=="], 220 + 221 + "@sitebase/web/@atproto/oauth-client-node/@atproto/oauth-client/@atproto-labs/identity-resolver": ["@atproto-labs/identity-resolver@0.3.5", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.5", "@atproto-labs/handle-resolver": "0.3.5" } }, "sha512-kSxnreUSPhKL77doUbSl/9I6Y9qpkpD7MMJoYFQVU/WG0PB90tzfIb6DNuWsjbU2I5Q91Nzc4Tm4VJMV+OPKGQ=="], 222 + 223 + "@sitebase/web/@atproto/oauth-client-node/@atproto/oauth-client/@atproto-labs/simple-store-memory": ["@atproto-labs/simple-store-memory@0.1.4", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "lru-cache": "^10.2.0" } }, "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw=="], 224 + 225 + "@sitebase/web/@atproto/oauth-client-node/@atproto-labs/handle-resolver-node/@atproto-labs/handle-resolver/@atproto-labs/simple-store-memory": ["@atproto-labs/simple-store-memory@0.1.4", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "lru-cache": "^10.2.0" } }, "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw=="], 180 226 } 181 227 }
+4
install-cli.sh
··· 1 + #!/usr/bin/env bash 2 + 3 + bun run build:cli 4 + cp packages/cli/bin/sitebase ~/.local/bin/sitebase
+3 -1
packages/cli/package.json
··· 7 7 }, 8 8 "scripts": { 9 9 "typecheck": "tsc --noEmit", 10 - "build": "bun build --target node --outfile ./bin/sitebase ./src/index.ts" 10 + "build": "bun build --target bun --outfile ./bin/sitebase ./src/index.ts" 11 11 }, 12 12 "dependencies": { 13 + "@atproto/api": "^0.18.16", 14 + "@atproto/oauth-client-node": "^0.2.1", 13 15 "@sitebase/core": "workspace:*", 14 16 "commander": "^12.1.0" 15 17 }
+221
packages/cli/src/auth.ts
··· 1 + import { readFile, writeFile, mkdir, unlink } from "node:fs/promises"; 2 + import { homedir } from "node:os"; 3 + import { join } from "node:path"; 4 + import { spawn } from "node:child_process"; 5 + import { Agent } from "@atproto/api"; 6 + import { 7 + NodeOAuthClient, 8 + type NodeSavedSession, 9 + type NodeSavedState, 10 + } from "@atproto/oauth-client-node"; 11 + 12 + const SESSION_DIR = join(homedir(), ".sitebase"); 13 + const SESSION_FILE = join(SESSION_DIR, "session.json"); 14 + 15 + const CLIENT_ID = 16 + "http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%2Fcallback&scope=atproto+include%3Asite.standard.authFull"; 17 + 18 + interface StoredSession { 19 + did: string; 20 + session: NodeSavedSession; 21 + } 22 + 23 + let verbose = false; 24 + 25 + export function setVerbose(v: boolean): void { 26 + verbose = v; 27 + } 28 + 29 + function debug(...args: unknown[]): void { 30 + if (verbose) { 31 + console.error("[debug]", ...args); 32 + } 33 + } 34 + 35 + async function readStoredSession(): Promise<StoredSession | null> { 36 + try { 37 + const data = await readFile(SESSION_FILE, "utf-8"); 38 + debug("Read session from", SESSION_FILE); 39 + return JSON.parse(data) as StoredSession; 40 + } catch { 41 + debug("No stored session found at", SESSION_FILE); 42 + return null; 43 + } 44 + } 45 + 46 + async function writeStoredSession(stored: StoredSession): Promise<void> { 47 + await mkdir(SESSION_DIR, { recursive: true }); 48 + await writeFile(SESSION_FILE, JSON.stringify(stored, null, 2), "utf-8"); 49 + debug("Wrote session to", SESSION_FILE); 50 + } 51 + 52 + function createClient( 53 + sessionStore: { 54 + get: (key: string) => Promise<NodeSavedSession | undefined>; 55 + set: (key: string, value: NodeSavedSession) => Promise<void>; 56 + del: (key: string) => Promise<void>; 57 + }, 58 + redirectUri?: string, 59 + ): NodeOAuthClient { 60 + const stateStore = new Map<string, NodeSavedState>(); 61 + 62 + const redirect_uris = redirectUri 63 + ? [redirectUri] 64 + : ["http://127.0.0.1/callback"]; 65 + 66 + debug("Creating OAuth client with client_id:", CLIENT_ID); 67 + debug("redirect_uris:", redirect_uris); 68 + 69 + return new NodeOAuthClient({ 70 + clientMetadata: { 71 + client_id: CLIENT_ID, 72 + redirect_uris: redirect_uris as [string], 73 + application_type: "native", 74 + token_endpoint_auth_method: "none", 75 + dpop_bound_access_tokens: true, 76 + grant_types: ["authorization_code", "refresh_token"], 77 + response_types: ["code"], 78 + scope: "atproto include:site.standard.authFull", 79 + }, 80 + stateStore: { 81 + get: async (key: string) => { 82 + debug("stateStore.get:", key); 83 + return stateStore.get(key); 84 + }, 85 + set: async (key: string, value: NodeSavedState) => { 86 + debug("stateStore.set:", key); 87 + stateStore.set(key, value); 88 + }, 89 + del: async (key: string) => { 90 + debug("stateStore.del:", key); 91 + stateStore.delete(key); 92 + }, 93 + }, 94 + sessionStore: { 95 + get: async (key: string) => { 96 + debug("sessionStore.get:", key); 97 + return sessionStore.get(key); 98 + }, 99 + set: async (key: string, value: NodeSavedSession) => { 100 + debug("sessionStore.set:", key); 101 + return sessionStore.set(key, value); 102 + }, 103 + del: async (key: string) => { 104 + debug("sessionStore.del:", key); 105 + return sessionStore.del(key); 106 + }, 107 + }, 108 + }); 109 + } 110 + 111 + function openBrowser(url: string): void { 112 + const cmd = process.platform === "darwin" ? "open" : "xdg-open"; 113 + debug("Opening browser with command:", cmd, url); 114 + spawn(cmd, [url], { stdio: "ignore", detached: true }).unref(); 115 + } 116 + 117 + export async function login(handle: string): Promise<void> { 118 + let stored: StoredSession | null = null; 119 + 120 + const sessionStore = { 121 + get: async (_key: string) => stored?.session, 122 + set: async (key: string, value: NodeSavedSession) => { 123 + stored = { did: key, session: value }; 124 + await writeStoredSession(stored); 125 + }, 126 + del: async (_key: string) => { 127 + stored = null; 128 + }, 129 + }; 130 + 131 + // Start callback server first to get the ephemeral port 132 + const { promise: callbackPromise, resolve: resolveCallback } = 133 + Promise.withResolvers<URLSearchParams>(); 134 + 135 + const server = Bun.serve({ 136 + port: 0, 137 + async fetch(req) { 138 + const url = new URL(req.url); 139 + debug("Received request:", url.pathname, url.search); 140 + if (url.pathname === "/callback") { 141 + resolveCallback(url.searchParams); 142 + return new Response( 143 + "<html><body><h1>Login successful!</h1><p>You can close this tab.</p></body></html>", 144 + { headers: { "Content-Type": "text/html" } }, 145 + ); 146 + } 147 + return new Response("Not found", { status: 404 }); 148 + }, 149 + }); 150 + 151 + const port = server.port; 152 + const redirectUri = `http://127.0.0.1:${port}/callback`; 153 + debug("Callback server listening on port", port); 154 + debug("Redirect URI:", redirectUri); 155 + 156 + // Create client with port-specific redirect_uri in metadata 157 + const client = createClient(sessionStore, redirectUri); 158 + 159 + try { 160 + debug("Calling client.authorize with handle:", handle); 161 + const authUrl = await client.authorize(handle); 162 + debug("Got auth URL:", authUrl.toString()); 163 + 164 + console.log("Opening browser for authentication..."); 165 + openBrowser(authUrl.toString()); 166 + console.log(`If the browser doesn't open, visit: ${authUrl.toString()}`); 167 + 168 + // Wait for callback 169 + debug("Waiting for OAuth callback..."); 170 + const params = await callbackPromise; 171 + debug("Received callback params:", Object.fromEntries(params.entries())); 172 + 173 + debug("Calling client.callback..."); 174 + await client.callback(params); 175 + 176 + console.log(`\nLogged in as ${stored!.did}`); 177 + } finally { 178 + server.stop(); 179 + debug("Callback server stopped"); 180 + } 181 + } 182 + 183 + export async function logout(): Promise<void> { 184 + try { 185 + await unlink(SESSION_FILE); 186 + console.log("Logged out successfully."); 187 + } catch { 188 + console.log("No active session."); 189 + } 190 + } 191 + 192 + export async function getAuthenticatedAgent(): Promise<{ 193 + agent: Agent; 194 + did: string; 195 + }> { 196 + const stored = await readStoredSession(); 197 + if (!stored) { 198 + throw new Error( 199 + "Not logged in. Run `sitebase auth login <handle>` first.", 200 + ); 201 + } 202 + 203 + const sessionStore = { 204 + get: async (_key: string) => stored.session, 205 + set: async (key: string, value: NodeSavedSession) => { 206 + stored.session = value; 207 + await writeStoredSession(stored); 208 + }, 209 + del: async (_key: string) => { 210 + await unlink(SESSION_FILE).catch(() => {}); 211 + }, 212 + }; 213 + 214 + debug("Restoring session for DID:", stored.did); 215 + const client = createClient(sessionStore); 216 + const session = await client.restore(stored.did); 217 + debug("Session restored successfully"); 218 + const agent = new Agent(session); 219 + 220 + return { agent, did: stored.did }; 221 + }
+81 -2
packages/cli/src/index.ts
··· 1 1 #!/usr/bin/env bun 2 2 import { access, mkdir, writeFile } from "node:fs/promises"; 3 3 import { dirname, join, resolve } from "node:path"; 4 - import { Command } from "commander"; 5 4 import { 6 5 exportFromConfig, 7 6 findConfigFile, 8 7 loadExportConfig, 9 8 } from "@sitebase/core"; 9 + import { Command } from "commander"; 10 + import { getAuthenticatedAgent, login, logout, setVerbose } from "./auth.ts"; 11 + import { formatPublication } from "./publication.ts"; 10 12 11 13 const program = new Command(); 12 14 13 15 program 14 16 .name("sitebase") 15 17 .description("CLI tools for standard.site publications") 16 - .version("0.0.1"); 18 + .version("0.0.1") 19 + .option("-v, --verbose", "Enable verbose debug output") 20 + .hook("preAction", () => { 21 + if (program.opts().verbose) { 22 + setVerbose(true); 23 + } 24 + }); 17 25 18 26 program 19 27 .command("export") ··· 193 201 ); 194 202 console.log(" 2. Customize the export targets as needed"); 195 203 console.log(" 3. Run: sitebase export"); 204 + } catch (error) { 205 + console.error( 206 + `Error: ${error instanceof Error ? error.message : String(error)}`, 207 + ); 208 + process.exit(1); 209 + } 210 + }); 211 + 212 + const auth = program.command("auth").description("Manage authentication"); 213 + 214 + auth 215 + .command("login") 216 + .description("Authenticate with ATProto via OAuth") 217 + .argument("<handle>", "Your ATProto handle (e.g. user.bsky.social)") 218 + .action(async (handle: string) => { 219 + try { 220 + await login(handle); 221 + } catch (error) { 222 + console.error( 223 + `Error: ${error instanceof Error ? error.message : String(error)}`, 224 + ); 225 + process.exit(1); 226 + } 227 + }); 228 + 229 + auth 230 + .command("logout") 231 + .description("Clear stored session") 232 + .action(async () => { 233 + await logout(); 234 + }); 235 + 236 + const pub = program.command("pub").description("Manage publications"); 237 + 238 + pub 239 + .command("list") 240 + .description("List all of your publications") 241 + .action(async () => { 242 + try { 243 + const { agent, did } = await getAuthenticatedAgent(); 244 + const response = await agent.com.atproto.repo.listRecords({ 245 + repo: did, 246 + collection: "site.standard.publication", 247 + }); 248 + } catch (e) {} 249 + }); 250 + 251 + pub 252 + .command("view") 253 + .description("View your publication") 254 + .action(async () => { 255 + try { 256 + const { agent, did } = await getAuthenticatedAgent(); 257 + const response = await agent.com.atproto.repo.listRecords({ 258 + repo: did, 259 + collection: "site.standard.publication", 260 + limit: 1, 261 + }); 262 + 263 + if (!response.data.records.length) { 264 + console.log("No publication found."); 265 + return; 266 + } 267 + 268 + const record = response.data.records[0]!; 269 + console.log( 270 + formatPublication({ 271 + uri: record.uri, 272 + value: record.value as Record<string, unknown>, 273 + }), 274 + ); 196 275 } catch (error) { 197 276 console.error( 198 277 `Error: ${error instanceof Error ? error.message : String(error)}`,
+16
packages/cli/src/publication.ts
··· 1 + export interface PublicationRecord { 2 + uri: string; 3 + value: Record<string, unknown>; 4 + } 5 + 6 + export function formatPublication(record: PublicationRecord): string { 7 + const value = record.value; 8 + const lines: string[] = []; 9 + 10 + lines.push(`Name: ${value.name ?? "(untitled)"}`); 11 + if (value.url) lines.push(`URL: ${value.url}`); 12 + if (value.description) lines.push(`Description: ${value.description}`); 13 + lines.push(`AT URI: ${record.uri}`); 14 + 15 + return lines.join("\n"); 16 + }