···26261. Start a temporary localhost callback server
27272. Open your browser to the Bluesky authorization page
28283. After you approve, print the DPoP-bound access token and DID
2929+4. Save credentials (`BSKY_DID`, `BSKY_ACCESS_TOKEN`, `BSKY_REFRESH_TOKEN`, `BSKY_EXPIRES_AT`) to `.env`
3030+3131+If `.env` already exists, only the `BSKY_*` variables are updated — other variables are preserved.
29323033### Options
3134···3639### Notes
37403841The access token is DPoP-bound, meaning it requires a DPoP proof JWT for each API request. The token cannot be used as a simple Bearer token. Token refresh is not implemented in this version.
4242+4343+## PLC Test
4444+4545+Verify your saved DID against the PLC directory:
4646+4747+```
4848+bun plc_test.js
4949+```
5050+5151+This reads `BSKY_DID` from `.env` and:
5252+- Resolves the DID document via `https://plc.directory/{did}`
5353+- Fetches the audit log
5454+- Prints a summary of handles, services, verification methods, and operation history
5555+5656+### Options
5757+5858+- `--did <did>` — Check a specific DID (overrides `.env`)
5959+- `-v, --verbose` — Show full API responses
+33-1
bsky_oauth.js
···2233import { NodeOAuthClient } from '@atproto/oauth-client-node';
44import { Command } from 'commander';
55-import { writeFileSync } from 'node:fs';
55+import { readFileSync, writeFileSync } from 'node:fs';
6677function createStore() {
88 const map = new Map();
···1616 map.delete(key);
1717 },
1818 };
1919+}
2020+2121+function saveToEnv(vars) {
2222+ const envPath = new URL('.env', import.meta.url).pathname;
2323+ let lines = [];
2424+ try {
2525+ lines = readFileSync(envPath, 'utf-8').split('\n');
2626+ } catch {}
2727+ // Strip trailing empty lines from parsed content
2828+ while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
2929+ const updated = new Set();
3030+ for (let i = 0; i < lines.length; i++) {
3131+ const m = lines[i].match(/^([A-Za-z_][A-Za-z0-9_]*)=/);
3232+ if (m && m[1] in vars) {
3333+ lines[i] = `${m[1]}=${vars[m[1]]}`;
3434+ updated.add(m[1]);
3535+ }
3636+ }
3737+ for (const [key, value] of Object.entries(vars)) {
3838+ if (!updated.has(key)) {
3939+ lines.push(`${key}=${value}`);
4040+ }
4141+ }
4242+ writeFileSync(envPath, lines.join('\n') + '\n');
1943}
20442145async function main() {
···169193 if (output) {
170194 writeFileSync(output, `${JSON.stringify(outputData, null, 2)}\n`);
171195 }
196196+197197+ saveToEnv({
198198+ BSKY_DID: outputData.did,
199199+ BSKY_ACCESS_TOKEN: outputData.accessToken ?? '',
200200+ BSKY_REFRESH_TOKEN: outputData.refreshToken ?? '',
201201+ BSKY_EXPIRES_AT: outputData.expiresAt ?? '',
202202+ });
203203+ console.log('Saved credentials to .env');
172204 } catch (err) {
173205 console.error(err instanceof Error ? err.message : String(err));
174206 process.exitCode = 1;
+106
plc_test.js
···11+#!/usr/bin/env bun
22+33+import { readFileSync } from 'node:fs';
44+import { Command } from 'commander';
55+66+function loadEnv() {
77+ const envPath = new URL('.env', import.meta.url).pathname;
88+ const vars = {};
99+ let content;
1010+ try {
1111+ content = readFileSync(envPath, 'utf-8');
1212+ } catch {
1313+ return vars;
1414+ }
1515+ for (const line of content.split('\n')) {
1616+ const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)/);
1717+ if (m) vars[m[1]] = m[2];
1818+ }
1919+ return vars;
2020+}
2121+2222+async function main() {
2323+ const program = new Command();
2424+2525+ program
2626+ .name('plc_test')
2727+ .description('Verify PLC directory entry for saved Bluesky DID')
2828+ .option('-v, --verbose', 'Show full API responses')
2929+ .option('--did <did>', 'DID to check (overrides .env)')
3030+ .parse();
3131+3232+ const opts = program.opts();
3333+3434+ try {
3535+ const env = loadEnv();
3636+ const did = opts.did || env.BSKY_DID;
3737+3838+ if (!did) {
3939+ throw new Error('No DID found. Run bsky_oauth.js first or pass --did <did>.');
4040+ }
4141+4242+ if (!did.startsWith('did:plc:')) {
4343+ throw new Error(`Expected a did:plc: identifier, got: ${did}`);
4444+ }
4545+4646+ console.log(`Checking DID: ${did}\n`);
4747+4848+ // Resolve DID document
4949+ const docUrl = `https://plc.directory/${did}`;
5050+ const docRes = await fetch(docUrl);
5151+ if (!docRes.ok) {
5252+ throw new Error(`PLC directory returned ${docRes.status} for ${docUrl}`);
5353+ }
5454+ const doc = await docRes.json();
5555+5656+ if (opts.verbose) {
5757+ console.log('[verbose] DID document:');
5858+ console.log(JSON.stringify(doc, null, 2));
5959+ console.log();
6060+ }
6161+6262+ // Validate expected fields
6363+ const handles = doc.alsoKnownAs ?? [];
6464+ const services = doc.service ?? [];
6565+ const verificationMethods = doc.verificationMethod ?? [];
6666+6767+ console.log(`DID document:`);
6868+ console.log(` id: ${doc.id}`);
6969+ console.log(` handles: ${handles.join(', ') || '(none)'}`);
7070+ console.log(` services: ${services.map(s => `${s.id} (${s.type})`).join(', ') || '(none)'}`);
7171+ console.log(` verification methods: ${verificationMethods.length}`);
7272+ console.log();
7373+7474+ // Fetch audit log
7575+ const auditUrl = `https://plc.directory/${did}/log/audit`;
7676+ const auditRes = await fetch(auditUrl);
7777+ if (!auditRes.ok) {
7878+ throw new Error(`PLC directory returned ${auditRes.status} for ${auditUrl}`);
7979+ }
8080+ const auditLog = await auditRes.json();
8181+8282+ if (opts.verbose) {
8383+ console.log('[verbose] Audit log:');
8484+ console.log(JSON.stringify(auditLog, null, 2));
8585+ console.log();
8686+ }
8787+8888+ console.log(`Audit log: ${auditLog.length} operation(s)`);
8989+ if (auditLog.length > 0) {
9090+ const first = auditLog[0];
9191+ const last = auditLog[auditLog.length - 1];
9292+ console.log(` first: ${first.createdAt}`);
9393+ if (auditLog.length > 1) {
9494+ console.log(` latest: ${last.createdAt}`);
9595+ }
9696+ }
9797+ console.log();
9898+9999+ console.log('All checks passed.');
100100+ } catch (err) {
101101+ console.error(err instanceof Error ? err.message : String(err));
102102+ process.exitCode = 1;
103103+ }
104104+}
105105+106106+await main();