@getcirrus/pds 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +655 -171
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +326 -3462
- package/dist/index.js.map +1 -1
- package/package.json +11 -7
package/dist/cli.js
CHANGED
|
@@ -8,9 +8,10 @@ import { spawn } from "node:child_process";
|
|
|
8
8
|
import { experimental_patchConfig, experimental_readRawConfig } from "wrangler";
|
|
9
9
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { resolve } from "node:path";
|
|
11
|
-
import {
|
|
12
|
-
import { check, didDocument, getPdsEndpoint } from "@atproto/common-web";
|
|
11
|
+
import { CompositeDidDocumentResolver, DohJsonHandleResolver, PlcDidDocumentResolver, WebDidDocumentResolver } from "@atcute/identity-resolver";
|
|
13
12
|
import pc from "picocolors";
|
|
13
|
+
import { Client, ClientResponseError, ok } from "@atcute/client";
|
|
14
|
+
import { getPdsEndpoint } from "@atcute/identity";
|
|
14
15
|
|
|
15
16
|
//#region src/cli/utils/wrangler.ts
|
|
16
17
|
/**
|
|
@@ -77,6 +78,85 @@ async function setSecret(name, value) {
|
|
|
77
78
|
child.on("error", reject);
|
|
78
79
|
});
|
|
79
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Get account_id from wrangler config
|
|
83
|
+
*/
|
|
84
|
+
function getAccountId() {
|
|
85
|
+
const { rawConfig } = experimental_readRawConfig({});
|
|
86
|
+
return rawConfig.account_id;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Set account_id in wrangler config
|
|
90
|
+
*/
|
|
91
|
+
function setAccountId(accountId) {
|
|
92
|
+
const { configPath } = experimental_readRawConfig({});
|
|
93
|
+
if (!configPath) throw new Error("No wrangler config found");
|
|
94
|
+
experimental_patchConfig(configPath, { account_id: accountId });
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Set custom domain routes in wrangler config
|
|
98
|
+
*/
|
|
99
|
+
function setCustomDomains(domains) {
|
|
100
|
+
const { configPath } = experimental_readRawConfig({});
|
|
101
|
+
if (!configPath) throw new Error("No wrangler config found");
|
|
102
|
+
experimental_patchConfig(configPath, { routes: domains.map((pattern) => ({
|
|
103
|
+
pattern,
|
|
104
|
+
custom_domain: true
|
|
105
|
+
})) });
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Detect available Cloudflare accounts by running wrangler whoami.
|
|
109
|
+
* Returns array of accounts if multiple found, null if single account or already configured.
|
|
110
|
+
*/
|
|
111
|
+
async function detectCloudflareAccounts() {
|
|
112
|
+
if (getAccountId()) return null;
|
|
113
|
+
const { stdout, stderr } = await runWranglerWithOutput(["whoami"]);
|
|
114
|
+
const output = stdout + stderr;
|
|
115
|
+
const accounts = [];
|
|
116
|
+
const regex = /│\s*([^│]+?)\s*│\s*([a-f0-9]{32})\s*│/g;
|
|
117
|
+
let match;
|
|
118
|
+
while ((match = regex.exec(output)) !== null) {
|
|
119
|
+
const name = match[1]?.trim();
|
|
120
|
+
const id = match[2];
|
|
121
|
+
if (name && id && name !== "Account Name") accounts.push({
|
|
122
|
+
name,
|
|
123
|
+
id
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return accounts.length > 1 ? accounts : null;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Run a wrangler command and capture output
|
|
130
|
+
*/
|
|
131
|
+
function runWranglerWithOutput(args) {
|
|
132
|
+
return new Promise((resolve$1) => {
|
|
133
|
+
const child = spawn("wrangler", args, { stdio: [
|
|
134
|
+
"pipe",
|
|
135
|
+
"pipe",
|
|
136
|
+
"pipe"
|
|
137
|
+
] });
|
|
138
|
+
let stdout = "";
|
|
139
|
+
let stderr = "";
|
|
140
|
+
child.stdout?.on("data", (data) => {
|
|
141
|
+
stdout += data.toString();
|
|
142
|
+
});
|
|
143
|
+
child.stderr?.on("data", (data) => {
|
|
144
|
+
stderr += data.toString();
|
|
145
|
+
});
|
|
146
|
+
child.on("close", () => {
|
|
147
|
+
resolve$1({
|
|
148
|
+
stdout,
|
|
149
|
+
stderr
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
child.on("error", () => {
|
|
153
|
+
resolve$1({
|
|
154
|
+
stdout,
|
|
155
|
+
stderr
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
80
160
|
|
|
81
161
|
//#endregion
|
|
82
162
|
//#region src/cli/utils/dotenv.ts
|
|
@@ -395,13 +475,32 @@ function getDomain(url) {
|
|
|
395
475
|
return url;
|
|
396
476
|
}
|
|
397
477
|
}
|
|
478
|
+
/**
|
|
479
|
+
* Detect which package manager is being used based on npm_config_user_agent
|
|
480
|
+
*/
|
|
481
|
+
function detectPackageManager() {
|
|
482
|
+
const userAgent = process.env.npm_config_user_agent || "";
|
|
483
|
+
if (userAgent.startsWith("yarn")) return "yarn";
|
|
484
|
+
if (userAgent.startsWith("pnpm")) return "pnpm";
|
|
485
|
+
if (userAgent.startsWith("bun")) return "bun";
|
|
486
|
+
return "npm";
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Format a command for the detected package manager
|
|
490
|
+
* npm always needs "run" for scripts, pnpm/yarn/bun can use shorthand
|
|
491
|
+
* except for "deploy" which conflicts with pnpm's built-in deploy command
|
|
492
|
+
*/
|
|
493
|
+
function formatCommand(pm, ...args) {
|
|
494
|
+
if (pm === "npm" || args[0] === "deploy") return `${pm} run ${args.join(" ")}`;
|
|
495
|
+
return `${pm} ${args.join(" ")}`;
|
|
496
|
+
}
|
|
398
497
|
|
|
399
498
|
//#endregion
|
|
400
499
|
//#region src/cli/utils/handle-resolver.ts
|
|
401
500
|
/**
|
|
402
501
|
* Utilities for resolving AT Protocol handles to DIDs
|
|
403
502
|
*/
|
|
404
|
-
const resolver = new
|
|
503
|
+
const resolver = new DohJsonHandleResolver({ dohUrl: "https://cloudflare-dns.com/dns-query" });
|
|
405
504
|
/**
|
|
406
505
|
* Resolve a handle to a DID using the AT Protocol handle resolution methods.
|
|
407
506
|
* Uses DNS-over-HTTPS via Cloudflare for DNS resolution.
|
|
@@ -409,7 +508,7 @@ const resolver = new AtprotoDohHandleResolver({ dohEndpoint: "https://cloudflare
|
|
|
409
508
|
async function resolveHandleToDid(handle) {
|
|
410
509
|
try {
|
|
411
510
|
return await resolver.resolve(handle, { signal: AbortSignal.timeout(1e4) });
|
|
412
|
-
} catch
|
|
511
|
+
} catch {
|
|
413
512
|
return null;
|
|
414
513
|
}
|
|
415
514
|
}
|
|
@@ -419,20 +518,31 @@ async function resolveHandleToDid(handle) {
|
|
|
419
518
|
/**
|
|
420
519
|
* DID resolution for Cloudflare Workers
|
|
421
520
|
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
* that's compatible with Workers.
|
|
521
|
+
* Uses @atcute/identity-resolver which is already Workers-compatible
|
|
522
|
+
* (uses redirect: "manual" internally).
|
|
425
523
|
*/
|
|
426
524
|
const PLC_DIRECTORY = "https://plc.directory";
|
|
427
525
|
const TIMEOUT_MS = 3e3;
|
|
526
|
+
/**
|
|
527
|
+
* Wrapper that always uses globalThis.fetch so it can be mocked in tests.
|
|
528
|
+
* @atcute resolvers capture the fetch reference at construction time,
|
|
529
|
+
* so we need this indirection to allow test mocking.
|
|
530
|
+
*/
|
|
531
|
+
const stubbableFetch = (input, init) => globalThis.fetch(input, init);
|
|
428
532
|
var DidResolver = class {
|
|
429
|
-
|
|
533
|
+
resolver;
|
|
430
534
|
timeout;
|
|
431
535
|
cache;
|
|
432
536
|
constructor(opts = {}) {
|
|
433
|
-
this.plcUrl = opts.plcUrl ?? PLC_DIRECTORY;
|
|
434
537
|
this.timeout = opts.timeout ?? TIMEOUT_MS;
|
|
435
538
|
this.cache = opts.didCache;
|
|
539
|
+
this.resolver = new CompositeDidDocumentResolver({ methods: {
|
|
540
|
+
plc: new PlcDidDocumentResolver({
|
|
541
|
+
apiUrl: opts.plcUrl ?? PLC_DIRECTORY,
|
|
542
|
+
fetch: stubbableFetch
|
|
543
|
+
}),
|
|
544
|
+
web: new WebDidDocumentResolver({ fetch: stubbableFetch })
|
|
545
|
+
} });
|
|
436
546
|
}
|
|
437
547
|
async resolve(did) {
|
|
438
548
|
if (this.cache) {
|
|
@@ -448,57 +558,18 @@ var DidResolver = class {
|
|
|
448
558
|
return doc;
|
|
449
559
|
}
|
|
450
560
|
async resolveNoCache(did) {
|
|
451
|
-
if (did.startsWith("did:web:")) return this.resolveDidWeb(did);
|
|
452
|
-
if (did.startsWith("did:plc:")) return this.resolveDidPlc(did);
|
|
453
|
-
throw new Error(`Unsupported DID method: ${did}`);
|
|
454
|
-
}
|
|
455
|
-
async resolveDidWeb(did) {
|
|
456
|
-
const parts = did.split(":").slice(2);
|
|
457
|
-
if (parts.length === 0) throw new Error(`Invalid did:web format: ${did}`);
|
|
458
|
-
if (parts.length > 1) throw new Error(`Unsupported did:web with path: ${did}`);
|
|
459
|
-
const domain = decodeURIComponent(parts[0]);
|
|
460
|
-
const url = new URL(`https://${domain}/.well-known/did.json`);
|
|
461
|
-
if (url.hostname === "localhost") url.protocol = "http:";
|
|
462
561
|
const controller = new AbortController();
|
|
463
562
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
464
563
|
try {
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
if (res.status >= 300 && res.status < 400) return null;
|
|
471
|
-
if (!res.ok) return null;
|
|
472
|
-
const doc = await res.json();
|
|
473
|
-
return this.validateDidDoc(did, doc);
|
|
474
|
-
} finally {
|
|
475
|
-
clearTimeout(timeoutId);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
async resolveDidPlc(did) {
|
|
479
|
-
const url = new URL(`/${encodeURIComponent(did)}`, this.plcUrl);
|
|
480
|
-
const controller = new AbortController();
|
|
481
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
482
|
-
try {
|
|
483
|
-
const res = await fetch(url.toString(), {
|
|
484
|
-
signal: controller.signal,
|
|
485
|
-
redirect: "manual",
|
|
486
|
-
headers: { accept: "application/did+ld+json,application/json" }
|
|
487
|
-
});
|
|
488
|
-
if (res.status >= 300 && res.status < 400) return null;
|
|
489
|
-
if (res.status === 404) return null;
|
|
490
|
-
if (!res.ok) throw new Error(`PLC directory error: ${res.status} ${res.statusText}`);
|
|
491
|
-
const doc = await res.json();
|
|
492
|
-
return this.validateDidDoc(did, doc);
|
|
564
|
+
const doc = await this.resolver.resolve(did, { signal: controller.signal });
|
|
565
|
+
if (doc.id !== did) return null;
|
|
566
|
+
return doc;
|
|
567
|
+
} catch {
|
|
568
|
+
return null;
|
|
493
569
|
} finally {
|
|
494
570
|
clearTimeout(timeoutId);
|
|
495
571
|
}
|
|
496
572
|
}
|
|
497
|
-
validateDidDoc(did, doc) {
|
|
498
|
-
if (!check.is(doc, didDocument)) return null;
|
|
499
|
-
if (doc.id !== did) return null;
|
|
500
|
-
return doc;
|
|
501
|
-
}
|
|
502
573
|
};
|
|
503
574
|
|
|
504
575
|
//#endregion
|
|
@@ -530,6 +601,31 @@ async function promptWorkerName(handle, currentWorkerName) {
|
|
|
530
601
|
});
|
|
531
602
|
}
|
|
532
603
|
/**
|
|
604
|
+
* Ensure a Cloudflare account_id is configured.
|
|
605
|
+
* If multiple accounts detected, prompts user to select one.
|
|
606
|
+
*/
|
|
607
|
+
async function ensureAccountConfigured() {
|
|
608
|
+
const spinner = p.spinner();
|
|
609
|
+
spinner.start("Checking Cloudflare account...");
|
|
610
|
+
const accounts = await detectCloudflareAccounts();
|
|
611
|
+
if (accounts === null) {
|
|
612
|
+
spinner.stop("Cloudflare account configured");
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
spinner.stop(`Found ${accounts.length} Cloudflare accounts`);
|
|
616
|
+
const selectedId = await promptSelect({
|
|
617
|
+
message: "Select your Cloudflare account:",
|
|
618
|
+
options: accounts.map((acc) => ({
|
|
619
|
+
value: acc.id,
|
|
620
|
+
label: acc.name,
|
|
621
|
+
hint: acc.id.slice(0, 8) + "..."
|
|
622
|
+
}))
|
|
623
|
+
});
|
|
624
|
+
setAccountId(selectedId);
|
|
625
|
+
const selectedName = accounts.find((a) => a.id === selectedId)?.name;
|
|
626
|
+
p.log.success(`Account "${selectedName}" saved to wrangler.jsonc`);
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
533
629
|
* Run wrangler types to regenerate TypeScript types
|
|
534
630
|
*/
|
|
535
631
|
function runWranglerTypes() {
|
|
@@ -563,6 +659,7 @@ const initCommand = defineCommand({
|
|
|
563
659
|
default: false
|
|
564
660
|
} },
|
|
565
661
|
async run({ args }) {
|
|
662
|
+
const pm = detectPackageManager();
|
|
566
663
|
p.intro("🦋 PDS Setup");
|
|
567
664
|
const isProduction = args.production;
|
|
568
665
|
if (isProduction) p.log.info("Production mode: secrets will be deployed to Cloudflare");
|
|
@@ -674,6 +771,7 @@ const initCommand = defineCommand({
|
|
|
674
771
|
});
|
|
675
772
|
workerName = await promptWorkerName(handle, currentWorkerName);
|
|
676
773
|
initialActive = "false";
|
|
774
|
+
await ensureAccountConfigured();
|
|
677
775
|
} else {
|
|
678
776
|
p.log.info("A fresh start in the Atmosphere! ✨");
|
|
679
777
|
hostname = await promptText({
|
|
@@ -700,6 +798,7 @@ const initCommand = defineCommand({
|
|
|
700
798
|
});
|
|
701
799
|
workerName = await promptWorkerName(handle, currentWorkerName);
|
|
702
800
|
initialActive = "true";
|
|
801
|
+
await ensureAccountConfigured();
|
|
703
802
|
if (handle === hostname) p.note([
|
|
704
803
|
"Your handle matches your PDS hostname, so your PDS will",
|
|
705
804
|
"automatically handle domain verification for you!",
|
|
@@ -760,6 +859,7 @@ const initCommand = defineCommand({
|
|
|
760
859
|
SIGNING_KEY_PUBLIC: signingKeyPublic,
|
|
761
860
|
INITIAL_ACTIVE: initialActive
|
|
762
861
|
});
|
|
862
|
+
setCustomDomains([hostname]);
|
|
763
863
|
spinner.stop("wrangler.jsonc updated");
|
|
764
864
|
const local = !isProduction;
|
|
765
865
|
if (isProduction) spinner.start("Deploying secrets to Cloudflare...");
|
|
@@ -807,19 +907,19 @@ const initCommand = defineCommand({
|
|
|
807
907
|
if (isMigrating) p.note([
|
|
808
908
|
deployedSecrets ? "Deploy your worker and run the migration:" : "Push secrets, deploy, and run the migration:",
|
|
809
909
|
"",
|
|
810
|
-
...deployedSecrets ? [] : [
|
|
811
|
-
|
|
812
|
-
|
|
910
|
+
...deployedSecrets ? [] : [` ${formatCommand(pm, "pds", "init", "--production")}`, ""],
|
|
911
|
+
` ${formatCommand(pm, "deploy")}`,
|
|
912
|
+
` ${formatCommand(pm, "pds", "migrate")}`,
|
|
813
913
|
"",
|
|
814
914
|
"To test locally first:",
|
|
815
|
-
|
|
816
|
-
|
|
915
|
+
` ${formatCommand(pm, "dev")} # in one terminal`,
|
|
916
|
+
` ${formatCommand(pm, "pds", "migrate", "--dev")} # in another`,
|
|
817
917
|
"",
|
|
818
918
|
"Then update your identity and flip the switch! 🦋",
|
|
819
919
|
" https://atproto.com/guides/account-migration"
|
|
820
920
|
].join("\n"), "Next Steps 🧳");
|
|
821
|
-
if (deployedSecrets) p.outro(
|
|
822
|
-
else p.outro(
|
|
921
|
+
if (deployedSecrets) p.outro(`Run '${formatCommand(pm, "deploy")}' to launch your PDS! 🚀`);
|
|
922
|
+
else p.outro(`Run '${formatCommand(pm, "dev")}' to start your PDS locally! 🦋`);
|
|
823
923
|
}
|
|
824
924
|
});
|
|
825
925
|
/**
|
|
@@ -837,85 +937,53 @@ async function getOrGenerateSecret(name, devVars, generate) {
|
|
|
837
937
|
|
|
838
938
|
//#endregion
|
|
839
939
|
//#region src/cli/utils/pds-client.ts
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
940
|
+
/**
|
|
941
|
+
* HTTP client for AT Protocol PDS XRPC endpoints
|
|
942
|
+
* Uses @atcute/client for type-safe XRPC calls
|
|
943
|
+
*/
|
|
944
|
+
/**
|
|
945
|
+
* Create a fetch handler that adds optional auth token
|
|
946
|
+
*/
|
|
947
|
+
function createAuthHandler(baseUrl, token) {
|
|
948
|
+
return async (pathname, init) => {
|
|
949
|
+
const url = new URL(pathname, baseUrl);
|
|
950
|
+
const headers = new Headers(init.headers);
|
|
951
|
+
if (token) headers.set("Authorization", `Bearer ${token}`);
|
|
952
|
+
return fetch(url, {
|
|
953
|
+
...init,
|
|
954
|
+
headers
|
|
955
|
+
});
|
|
956
|
+
};
|
|
957
|
+
}
|
|
848
958
|
var PDSClient = class {
|
|
959
|
+
client;
|
|
849
960
|
authToken;
|
|
850
961
|
constructor(baseUrl, authToken) {
|
|
851
962
|
this.baseUrl = baseUrl;
|
|
852
963
|
this.authToken = authToken;
|
|
964
|
+
this.client = new Client({ handler: createAuthHandler(baseUrl, authToken) });
|
|
853
965
|
}
|
|
854
966
|
/**
|
|
855
967
|
* Set the auth token for subsequent requests
|
|
856
968
|
*/
|
|
857
969
|
setAuthToken(token) {
|
|
858
970
|
this.authToken = token;
|
|
859
|
-
|
|
860
|
-
/**
|
|
861
|
-
* Make an XRPC request
|
|
862
|
-
*/
|
|
863
|
-
async xrpc(method, endpoint, options = {}) {
|
|
864
|
-
const url = new URL(`/xrpc/${endpoint}`, this.baseUrl);
|
|
865
|
-
if (options.params) for (const [key, value] of Object.entries(options.params)) url.searchParams.set(key, value);
|
|
866
|
-
const headers = {};
|
|
867
|
-
if (options.auth && this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
868
|
-
if (options.contentType) headers["Content-Type"] = options.contentType;
|
|
869
|
-
else if (options.body && !(options.body instanceof Uint8Array)) headers["Content-Type"] = "application/json";
|
|
870
|
-
const res = await fetch(url.toString(), {
|
|
871
|
-
method,
|
|
872
|
-
headers,
|
|
873
|
-
body: options.body ? options.body instanceof Uint8Array ? options.body : JSON.stringify(options.body) : void 0
|
|
874
|
-
});
|
|
875
|
-
if (!res.ok) {
|
|
876
|
-
const errorBody = await res.json().catch(() => ({}));
|
|
877
|
-
throw new PDSClientError(res.status, errorBody.error ?? "Unknown", errorBody.message ?? `Request failed: ${res.status}`);
|
|
878
|
-
}
|
|
879
|
-
if ((res.headers.get("content-type") ?? "").includes("application/json")) return res.json();
|
|
880
|
-
return {};
|
|
881
|
-
}
|
|
882
|
-
/**
|
|
883
|
-
* Make a raw request that returns bytes
|
|
884
|
-
*/
|
|
885
|
-
async xrpcBytes(method, endpoint, options = {}) {
|
|
886
|
-
const url = new URL(`/xrpc/${endpoint}`, this.baseUrl);
|
|
887
|
-
if (options.params) for (const [key, value] of Object.entries(options.params)) url.searchParams.set(key, value);
|
|
888
|
-
const headers = {};
|
|
889
|
-
if (options.auth && this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
890
|
-
if (options.contentType) headers["Content-Type"] = options.contentType;
|
|
891
|
-
const res = await fetch(url.toString(), {
|
|
892
|
-
method,
|
|
893
|
-
headers,
|
|
894
|
-
body: options.body
|
|
895
|
-
});
|
|
896
|
-
if (!res.ok) {
|
|
897
|
-
const errorBody = await res.json().catch(() => ({}));
|
|
898
|
-
throw new PDSClientError(res.status, errorBody.error ?? "Unknown", errorBody.message ?? `Request failed: ${res.status}`);
|
|
899
|
-
}
|
|
900
|
-
return {
|
|
901
|
-
bytes: new Uint8Array(await res.arrayBuffer()),
|
|
902
|
-
mimeType: res.headers.get("content-type") ?? "application/octet-stream"
|
|
903
|
-
};
|
|
971
|
+
this.client = new Client({ handler: createAuthHandler(this.baseUrl, token) });
|
|
904
972
|
}
|
|
905
973
|
/**
|
|
906
974
|
* Create a session with identifier and password
|
|
907
975
|
*/
|
|
908
976
|
async createSession(identifier, password) {
|
|
909
|
-
return this.
|
|
977
|
+
return ok(this.client.post("com.atproto.server.createSession", { input: {
|
|
910
978
|
identifier,
|
|
911
979
|
password
|
|
912
|
-
} });
|
|
980
|
+
} }));
|
|
913
981
|
}
|
|
914
982
|
/**
|
|
915
983
|
* Get repository description including collections
|
|
916
984
|
*/
|
|
917
985
|
async describeRepo(did) {
|
|
918
|
-
return this.
|
|
986
|
+
return ok(this.client.get("com.atproto.repo.describeRepo", { params: { repo: did } }));
|
|
919
987
|
}
|
|
920
988
|
/**
|
|
921
989
|
* Get profile stats from AppView (posts, follows, followers counts)
|
|
@@ -938,96 +1006,199 @@ var PDSClient = class {
|
|
|
938
1006
|
* Export repository as CAR file
|
|
939
1007
|
*/
|
|
940
1008
|
async getRepo(did) {
|
|
941
|
-
const
|
|
942
|
-
|
|
1009
|
+
const response = await this.client.get("com.atproto.sync.getRepo", {
|
|
1010
|
+
params: { did },
|
|
1011
|
+
as: "bytes"
|
|
1012
|
+
});
|
|
1013
|
+
if (!response.ok) throw new ClientResponseError({
|
|
1014
|
+
status: response.status,
|
|
1015
|
+
headers: response.headers,
|
|
1016
|
+
data: response.data
|
|
1017
|
+
});
|
|
1018
|
+
return response.data;
|
|
943
1019
|
}
|
|
944
1020
|
/**
|
|
945
1021
|
* Get a blob by CID
|
|
946
1022
|
*/
|
|
947
1023
|
async getBlob(did, cid) {
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1024
|
+
const response = await this.client.get("com.atproto.sync.getBlob", {
|
|
1025
|
+
params: {
|
|
1026
|
+
did,
|
|
1027
|
+
cid
|
|
1028
|
+
},
|
|
1029
|
+
as: "bytes"
|
|
1030
|
+
});
|
|
1031
|
+
if (!response.ok) throw new ClientResponseError({
|
|
1032
|
+
status: response.status,
|
|
1033
|
+
headers: response.headers,
|
|
1034
|
+
data: response.data
|
|
1035
|
+
});
|
|
1036
|
+
return {
|
|
1037
|
+
bytes: response.data,
|
|
1038
|
+
mimeType: response.headers.get("content-type") ?? "application/octet-stream"
|
|
1039
|
+
};
|
|
952
1040
|
}
|
|
953
1041
|
/**
|
|
954
1042
|
* List blobs in repository
|
|
955
1043
|
*/
|
|
956
1044
|
async listBlobs(did, cursor) {
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1045
|
+
return ok(this.client.get("com.atproto.sync.listBlobs", { params: {
|
|
1046
|
+
did,
|
|
1047
|
+
...cursor && { cursor }
|
|
1048
|
+
} }));
|
|
960
1049
|
}
|
|
961
1050
|
/**
|
|
962
1051
|
* Get user preferences
|
|
963
1052
|
*/
|
|
964
1053
|
async getPreferences() {
|
|
965
|
-
return (await this.
|
|
1054
|
+
return (await ok(this.client.get("app.bsky.actor.getPreferences", { params: {} }))).preferences;
|
|
966
1055
|
}
|
|
967
1056
|
/**
|
|
968
1057
|
* Update user preferences
|
|
969
1058
|
*/
|
|
970
1059
|
async putPreferences(preferences) {
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1060
|
+
const url = new URL("/xrpc/app.bsky.actor.putPreferences", this.baseUrl);
|
|
1061
|
+
const headers = { "Content-Type": "application/json" };
|
|
1062
|
+
if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
1063
|
+
const res = await fetch(url.toString(), {
|
|
1064
|
+
method: "POST",
|
|
1065
|
+
headers,
|
|
1066
|
+
body: JSON.stringify({ preferences })
|
|
974
1067
|
});
|
|
1068
|
+
if (!res.ok) {
|
|
1069
|
+
const errorBody = await res.json().catch(() => ({}));
|
|
1070
|
+
throw new ClientResponseError({
|
|
1071
|
+
status: res.status,
|
|
1072
|
+
headers: res.headers,
|
|
1073
|
+
data: {
|
|
1074
|
+
error: errorBody.error ?? "Unknown",
|
|
1075
|
+
message: errorBody.message
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
975
1079
|
}
|
|
976
1080
|
/**
|
|
977
1081
|
* Get account status including migration progress
|
|
978
1082
|
*/
|
|
979
1083
|
async getAccountStatus() {
|
|
980
|
-
|
|
1084
|
+
const url = new URL("/xrpc/com.atproto.server.getAccountStatus", this.baseUrl);
|
|
1085
|
+
const headers = {};
|
|
1086
|
+
if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
1087
|
+
const res = await fetch(url.toString(), {
|
|
1088
|
+
method: "GET",
|
|
1089
|
+
headers
|
|
1090
|
+
});
|
|
1091
|
+
if (!res.ok) {
|
|
1092
|
+
const errorBody = await res.json().catch(() => ({}));
|
|
1093
|
+
throw new ClientResponseError({
|
|
1094
|
+
status: res.status,
|
|
1095
|
+
headers: res.headers,
|
|
1096
|
+
data: {
|
|
1097
|
+
error: errorBody.error ?? "Unknown",
|
|
1098
|
+
message: errorBody.message
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
return res.json();
|
|
981
1103
|
}
|
|
982
1104
|
/**
|
|
983
1105
|
* Import repository from CAR file
|
|
984
1106
|
*/
|
|
985
1107
|
async importRepo(carBytes) {
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1108
|
+
const url = new URL("/xrpc/com.atproto.repo.importRepo", this.baseUrl);
|
|
1109
|
+
const headers = { "Content-Type": "application/vnd.ipld.car" };
|
|
1110
|
+
if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
1111
|
+
const res = await fetch(url.toString(), {
|
|
1112
|
+
method: "POST",
|
|
1113
|
+
headers,
|
|
1114
|
+
body: carBytes
|
|
990
1115
|
});
|
|
1116
|
+
if (!res.ok) {
|
|
1117
|
+
const errorBody = await res.json().catch(() => ({}));
|
|
1118
|
+
throw new ClientResponseError({
|
|
1119
|
+
status: res.status,
|
|
1120
|
+
headers: res.headers,
|
|
1121
|
+
data: {
|
|
1122
|
+
error: errorBody.error ?? "Unknown",
|
|
1123
|
+
message: errorBody.message
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
return res.json();
|
|
991
1128
|
}
|
|
992
1129
|
/**
|
|
993
1130
|
* List blobs that are missing (referenced but not imported)
|
|
994
1131
|
*/
|
|
995
1132
|
async listMissingBlobs(limit, cursor) {
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
params,
|
|
1001
|
-
auth: true
|
|
1002
|
-
});
|
|
1133
|
+
return ok(this.client.get("com.atproto.repo.listMissingBlobs", { params: {
|
|
1134
|
+
...limit && { limit },
|
|
1135
|
+
...cursor && { cursor }
|
|
1136
|
+
} }));
|
|
1003
1137
|
}
|
|
1004
1138
|
/**
|
|
1005
1139
|
* Upload a blob
|
|
1006
1140
|
*/
|
|
1007
1141
|
async uploadBlob(bytes, mimeType) {
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1142
|
+
const url = new URL("/xrpc/com.atproto.repo.uploadBlob", this.baseUrl);
|
|
1143
|
+
const headers = { "Content-Type": mimeType };
|
|
1144
|
+
if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
1145
|
+
const res = await fetch(url.toString(), {
|
|
1146
|
+
method: "POST",
|
|
1147
|
+
headers,
|
|
1148
|
+
body: bytes
|
|
1149
|
+
});
|
|
1150
|
+
if (!res.ok) {
|
|
1151
|
+
const errorBody = await res.json().catch(() => ({}));
|
|
1152
|
+
throw new ClientResponseError({
|
|
1153
|
+
status: res.status,
|
|
1154
|
+
headers: res.headers,
|
|
1155
|
+
data: {
|
|
1156
|
+
error: errorBody.error ?? "Unknown",
|
|
1157
|
+
message: errorBody.message
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
return (await res.json()).blob;
|
|
1013
1162
|
}
|
|
1014
1163
|
/**
|
|
1015
1164
|
* Reset migration state (only works on deactivated accounts)
|
|
1165
|
+
* Custom endpoint - not in standard lexicons
|
|
1016
1166
|
*/
|
|
1017
1167
|
async resetMigration() {
|
|
1018
|
-
|
|
1168
|
+
const url = new URL("/xrpc/gg.mk.experimental.resetMigration", this.baseUrl);
|
|
1169
|
+
const headers = {};
|
|
1170
|
+
if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
1171
|
+
const res = await fetch(url.toString(), {
|
|
1172
|
+
method: "POST",
|
|
1173
|
+
headers
|
|
1174
|
+
});
|
|
1175
|
+
if (!res.ok) {
|
|
1176
|
+
const errorBody = await res.json().catch(() => ({}));
|
|
1177
|
+
throw new ClientResponseError({
|
|
1178
|
+
status: res.status,
|
|
1179
|
+
headers: res.headers,
|
|
1180
|
+
data: {
|
|
1181
|
+
error: errorBody.error ?? "Unknown",
|
|
1182
|
+
message: errorBody.message
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
return res.json();
|
|
1019
1187
|
}
|
|
1020
1188
|
/**
|
|
1021
1189
|
* Activate account to enable writes
|
|
1022
1190
|
*/
|
|
1023
1191
|
async activateAccount() {
|
|
1024
|
-
await this.
|
|
1192
|
+
await ok(this.client.post("com.atproto.server.activateAccount", { as: null }));
|
|
1025
1193
|
}
|
|
1026
1194
|
/**
|
|
1027
1195
|
* Deactivate account to disable writes
|
|
1028
1196
|
*/
|
|
1029
1197
|
async deactivateAccount() {
|
|
1030
|
-
await this.
|
|
1198
|
+
await ok(this.client.post("com.atproto.server.deactivateAccount", {
|
|
1199
|
+
input: {},
|
|
1200
|
+
as: null
|
|
1201
|
+
}));
|
|
1031
1202
|
}
|
|
1032
1203
|
/**
|
|
1033
1204
|
* Check if the PDS is reachable
|
|
@@ -1039,17 +1210,128 @@ var PDSClient = class {
|
|
|
1039
1210
|
return false;
|
|
1040
1211
|
}
|
|
1041
1212
|
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Get DID document from PDS
|
|
1215
|
+
*/
|
|
1216
|
+
async getDidDocument() {
|
|
1217
|
+
const res = await fetch(new URL("/.well-known/did.json", this.baseUrl));
|
|
1218
|
+
if (!res.ok) throw new Error("Failed to fetch DID document");
|
|
1219
|
+
return res.json();
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Resolve handle to DID via public API
|
|
1223
|
+
*/
|
|
1224
|
+
async resolveHandle(handle) {
|
|
1225
|
+
try {
|
|
1226
|
+
const res = await fetch(`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`);
|
|
1227
|
+
if (!res.ok) return null;
|
|
1228
|
+
return (await res.json()).did;
|
|
1229
|
+
} catch {
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Resolve DID to get service endpoints (supports did:plc and did:web)
|
|
1235
|
+
*/
|
|
1236
|
+
async resolveDid(did) {
|
|
1237
|
+
try {
|
|
1238
|
+
let doc;
|
|
1239
|
+
if (did.startsWith("did:plc:")) {
|
|
1240
|
+
const res = await fetch(`https://plc.directory/${did}`);
|
|
1241
|
+
if (!res.ok) return { pdsEndpoint: null };
|
|
1242
|
+
doc = await res.json();
|
|
1243
|
+
} else if (did.startsWith("did:web:")) {
|
|
1244
|
+
const hostname = did.slice(8);
|
|
1245
|
+
const res = await fetch(`https://${hostname}/.well-known/did.json`);
|
|
1246
|
+
if (!res.ok) return { pdsEndpoint: null };
|
|
1247
|
+
doc = await res.json();
|
|
1248
|
+
} else return { pdsEndpoint: null };
|
|
1249
|
+
return { pdsEndpoint: (doc.service?.find((s) => s.type === "AtprotoPersonalDataServer"))?.serviceEndpoint ?? null };
|
|
1250
|
+
} catch {
|
|
1251
|
+
return { pdsEndpoint: null };
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Check if profile is indexed by AppView
|
|
1256
|
+
*/
|
|
1257
|
+
async checkAppViewIndexing(did) {
|
|
1258
|
+
try {
|
|
1259
|
+
return (await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(did)}`)).ok;
|
|
1260
|
+
} catch {
|
|
1261
|
+
return false;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Get firehose status (subscribers, seq)
|
|
1266
|
+
* Custom endpoint - not in standard lexicons
|
|
1267
|
+
*/
|
|
1268
|
+
async getFirehoseStatus() {
|
|
1269
|
+
const url = new URL("/xrpc/gg.mk.experimental.getFirehoseStatus", this.baseUrl);
|
|
1270
|
+
const headers = {};
|
|
1271
|
+
if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
1272
|
+
const res = await fetch(url.toString(), {
|
|
1273
|
+
method: "GET",
|
|
1274
|
+
headers
|
|
1275
|
+
});
|
|
1276
|
+
if (!res.ok) {
|
|
1277
|
+
const errorBody = await res.json().catch(() => ({}));
|
|
1278
|
+
throw new ClientResponseError({
|
|
1279
|
+
status: res.status,
|
|
1280
|
+
headers: res.headers,
|
|
1281
|
+
data: {
|
|
1282
|
+
error: errorBody.error ?? "Unknown",
|
|
1283
|
+
message: errorBody.message
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
return res.json();
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Check handle verification via HTTP well-known
|
|
1291
|
+
*/
|
|
1292
|
+
async checkHandleViaHttp(handle) {
|
|
1293
|
+
try {
|
|
1294
|
+
const res = await fetch(`https://${handle}/.well-known/atproto-did`);
|
|
1295
|
+
if (!res.ok) return null;
|
|
1296
|
+
return (await res.text()).trim() || null;
|
|
1297
|
+
} catch {
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Check handle verification via DNS TXT record (using DNS-over-HTTPS)
|
|
1303
|
+
*/
|
|
1304
|
+
async checkHandleViaDns(handle) {
|
|
1305
|
+
try {
|
|
1306
|
+
const res = await fetch(`https://cloudflare-dns.com/dns-query?name=_atproto.${handle}&type=TXT`, { headers: { Accept: "application/dns-json" } });
|
|
1307
|
+
if (!res.ok) return null;
|
|
1308
|
+
const txtRecord = (await res.json()).Answer?.find((a) => a.data?.includes("did="));
|
|
1309
|
+
if (!txtRecord) return null;
|
|
1310
|
+
return txtRecord.data.match(/did=([^\s"]+)/)?.[1] ?? null;
|
|
1311
|
+
} catch {
|
|
1312
|
+
return null;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Request the relay to crawl this PDS.
|
|
1317
|
+
* This notifies the Bluesky relay that the PDS is active and ready for federation.
|
|
1318
|
+
*/
|
|
1319
|
+
async requestCrawl(pdsHostname, relayUrl = "https://bsky.network") {
|
|
1320
|
+
try {
|
|
1321
|
+
const url = new URL("/xrpc/com.atproto.sync.requestCrawl", relayUrl);
|
|
1322
|
+
return (await fetch(url.toString(), {
|
|
1323
|
+
method: "POST",
|
|
1324
|
+
headers: { "Content-Type": "application/json" },
|
|
1325
|
+
body: JSON.stringify({ hostname: pdsHostname })
|
|
1326
|
+
})).ok;
|
|
1327
|
+
} catch {
|
|
1328
|
+
return false;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1042
1331
|
};
|
|
1043
1332
|
|
|
1044
1333
|
//#endregion
|
|
1045
1334
|
//#region src/cli/commands/migrate.ts
|
|
1046
|
-
function detectPackageManager() {
|
|
1047
|
-
const userAgent = process.env.npm_config_user_agent || "";
|
|
1048
|
-
if (userAgent.startsWith("yarn")) return "yarn";
|
|
1049
|
-
if (userAgent.startsWith("pnpm")) return "pnpm";
|
|
1050
|
-
if (userAgent.startsWith("bun")) return "bun";
|
|
1051
|
-
return "npm";
|
|
1052
|
-
}
|
|
1053
1335
|
const brightNote$1 = (lines) => lines.map((l) => `\x1b[0m${l}`).join("\n");
|
|
1054
1336
|
/**
|
|
1055
1337
|
* Format number with commas
|
|
@@ -1083,8 +1365,7 @@ const migrateCommand = defineCommand({
|
|
|
1083
1365
|
}
|
|
1084
1366
|
},
|
|
1085
1367
|
async run({ args }) {
|
|
1086
|
-
const
|
|
1087
|
-
const pm = packageManager === "npm" ? "npm run" : packageManager;
|
|
1368
|
+
const pm = detectPackageManager();
|
|
1088
1369
|
const isDev = args.dev;
|
|
1089
1370
|
const vars = getVars();
|
|
1090
1371
|
let targetUrl;
|
|
@@ -1104,11 +1385,11 @@ const migrateCommand = defineCommand({
|
|
|
1104
1385
|
spinner.stop(`PDS not responding at ${targetDomain}`);
|
|
1105
1386
|
if (isDev) {
|
|
1106
1387
|
p.log.error(`Your local PDS isn't running at ${targetUrl}`);
|
|
1107
|
-
p.log.info(`Start it with: ${pm
|
|
1388
|
+
p.log.info(`Start it with: ${formatCommand(pm, "dev")}`);
|
|
1108
1389
|
} else {
|
|
1109
1390
|
p.log.error(`Your PDS isn't responding at ${targetUrl}`);
|
|
1110
|
-
p.log.info(
|
|
1111
|
-
p.log.info(`Or test locally first: ${pm
|
|
1391
|
+
p.log.info(`Make sure your worker is deployed: ${formatCommand(pm, "deploy")}`);
|
|
1392
|
+
p.log.info(`Or test locally first: ${formatCommand(pm, "pds", "migrate", "--dev")}`);
|
|
1112
1393
|
}
|
|
1113
1394
|
p.outro("Migration cancelled.");
|
|
1114
1395
|
process.exit(1);
|
|
@@ -1169,7 +1450,7 @@ const migrateCommand = defineCommand({
|
|
|
1169
1450
|
p.log.info("Your account is already live");
|
|
1170
1451
|
p.log.info("");
|
|
1171
1452
|
p.log.info("If you need to re-import, first deactivate:");
|
|
1172
|
-
p.log.info(
|
|
1453
|
+
p.log.info(` ${formatCommand(pm, "pds", "deactivate")}`);
|
|
1173
1454
|
p.outro("Migration cancelled.");
|
|
1174
1455
|
process.exit(1);
|
|
1175
1456
|
}
|
|
@@ -1285,7 +1566,7 @@ const migrateCommand = defineCommand({
|
|
|
1285
1566
|
spinner.stop("Authenticated successfully");
|
|
1286
1567
|
} catch (err) {
|
|
1287
1568
|
spinner.stop("Login failed");
|
|
1288
|
-
if (err instanceof
|
|
1569
|
+
if (err instanceof ClientResponseError) p.log.error(`Authentication failed: ${err.description ?? err.message}`);
|
|
1289
1570
|
else p.log.error(err instanceof Error ? err.message : "Authentication failed");
|
|
1290
1571
|
p.outro("Migration cancelled.");
|
|
1291
1572
|
process.exit(1);
|
|
@@ -1385,7 +1666,7 @@ function showNextSteps(pm, sourceDomain) {
|
|
|
1385
1666
|
` (Requires email verification from ${sourceDomain})`,
|
|
1386
1667
|
"",
|
|
1387
1668
|
pc.bold("2. Flip the switch"),
|
|
1388
|
-
` ${pm
|
|
1669
|
+
` ${formatCommand(pm, "pds", "activate")}`,
|
|
1389
1670
|
"",
|
|
1390
1671
|
"Docs: https://atproto.com/guides/account-migration"
|
|
1391
1672
|
]), "Almost there!");
|
|
@@ -1407,6 +1688,7 @@ const activateCommand = defineCommand({
|
|
|
1407
1688
|
default: false
|
|
1408
1689
|
} },
|
|
1409
1690
|
async run({ args }) {
|
|
1691
|
+
const pm = detectPackageManager();
|
|
1410
1692
|
const isDev = args.dev;
|
|
1411
1693
|
p.intro("🦋 Activate Account");
|
|
1412
1694
|
const vars = getVars();
|
|
@@ -1437,7 +1719,7 @@ const activateCommand = defineCommand({
|
|
|
1437
1719
|
if (!await client.healthCheck()) {
|
|
1438
1720
|
spinner.stop(`PDS not responding at ${targetDomain}`);
|
|
1439
1721
|
p.log.error(`Your PDS isn't responding at ${targetUrl}`);
|
|
1440
|
-
if (!isDev) p.log.info(
|
|
1722
|
+
if (!isDev) p.log.info(`Make sure your worker is deployed: ${formatCommand(pm, "deploy")}`);
|
|
1441
1723
|
p.outro("Activation cancelled.");
|
|
1442
1724
|
process.exit(1);
|
|
1443
1725
|
}
|
|
@@ -1446,8 +1728,23 @@ const activateCommand = defineCommand({
|
|
|
1446
1728
|
const status = await client.getAccountStatus();
|
|
1447
1729
|
spinner.stop("Account status retrieved");
|
|
1448
1730
|
if (status.active) {
|
|
1449
|
-
p.log.
|
|
1450
|
-
|
|
1731
|
+
p.log.info("Your account is already active.");
|
|
1732
|
+
const pdsHostname$1 = config.PDS_HOSTNAME;
|
|
1733
|
+
if (pdsHostname$1 && !isDev) {
|
|
1734
|
+
const pingRelay = await p.confirm({
|
|
1735
|
+
message: "Notify the relay? (useful if posts aren't being indexed)",
|
|
1736
|
+
initialValue: false
|
|
1737
|
+
});
|
|
1738
|
+
if (p.isCancel(pingRelay)) {
|
|
1739
|
+
p.cancel("Cancelled.");
|
|
1740
|
+
process.exit(0);
|
|
1741
|
+
}
|
|
1742
|
+
if (pingRelay) {
|
|
1743
|
+
spinner.start("Notifying relay...");
|
|
1744
|
+
if (await client.requestCrawl(pdsHostname$1)) spinner.stop("Relay notified");
|
|
1745
|
+
else spinner.stop("Could not notify relay");
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1451
1748
|
p.outro("All good!");
|
|
1452
1749
|
return;
|
|
1453
1750
|
}
|
|
@@ -1477,6 +1774,15 @@ const activateCommand = defineCommand({
|
|
|
1477
1774
|
p.outro("Activation failed.");
|
|
1478
1775
|
process.exit(1);
|
|
1479
1776
|
}
|
|
1777
|
+
const pdsHostname = config.PDS_HOSTNAME;
|
|
1778
|
+
if (pdsHostname && !isDev) {
|
|
1779
|
+
spinner.start("Notifying relay...");
|
|
1780
|
+
if (await client.requestCrawl(pdsHostname)) spinner.stop("Relay notified");
|
|
1781
|
+
else {
|
|
1782
|
+
spinner.stop("Could not notify relay");
|
|
1783
|
+
p.log.warn("Run 'pds activate' again later to retry notifying the relay.");
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1480
1786
|
p.log.success("Welcome to the Atmosphere! 🦋");
|
|
1481
1787
|
p.log.info("Your account is now live and accepting writes.");
|
|
1482
1788
|
p.outro("All set!");
|
|
@@ -1501,6 +1807,7 @@ const deactivateCommand = defineCommand({
|
|
|
1501
1807
|
default: false
|
|
1502
1808
|
} },
|
|
1503
1809
|
async run({ args }) {
|
|
1810
|
+
const pm = detectPackageManager();
|
|
1504
1811
|
const isDev = args.dev;
|
|
1505
1812
|
p.intro("🦋 Deactivate Account");
|
|
1506
1813
|
const vars = getVars();
|
|
@@ -1531,7 +1838,7 @@ const deactivateCommand = defineCommand({
|
|
|
1531
1838
|
if (!await client.healthCheck()) {
|
|
1532
1839
|
spinner.stop(`PDS not responding at ${targetDomain}`);
|
|
1533
1840
|
p.log.error(`Your PDS isn't responding at ${targetUrl}`);
|
|
1534
|
-
if (!isDev) p.log.info(
|
|
1841
|
+
if (!isDev) p.log.info(`Make sure your worker is deployed: ${formatCommand(pm, "deploy")}`);
|
|
1535
1842
|
p.outro("Deactivation cancelled.");
|
|
1536
1843
|
process.exit(1);
|
|
1537
1844
|
}
|
|
@@ -1577,14 +1884,190 @@ const deactivateCommand = defineCommand({
|
|
|
1577
1884
|
p.log.info("Writes are now disabled.");
|
|
1578
1885
|
p.log.info("");
|
|
1579
1886
|
p.log.info("To re-import your data:");
|
|
1580
|
-
p.log.info(
|
|
1887
|
+
p.log.info(` ${formatCommand(pm, "pds", "migrate", "--clean")}`);
|
|
1581
1888
|
p.log.info("");
|
|
1582
1889
|
p.log.info("To re-enable writes:");
|
|
1583
|
-
p.log.info(
|
|
1890
|
+
p.log.info(` ${formatCommand(pm, "pds", "activate")}`);
|
|
1584
1891
|
p.outro("Deactivated.");
|
|
1585
1892
|
}
|
|
1586
1893
|
});
|
|
1587
1894
|
|
|
1895
|
+
//#endregion
|
|
1896
|
+
//#region src/cli/commands/status.ts
|
|
1897
|
+
/**
|
|
1898
|
+
* Status command - comprehensive PDS health and configuration check
|
|
1899
|
+
*/
|
|
1900
|
+
const CHECK = pc.green("✓");
|
|
1901
|
+
const CROSS = pc.red("✗");
|
|
1902
|
+
const WARN = pc.yellow("!");
|
|
1903
|
+
const INFO = pc.cyan("ℹ");
|
|
1904
|
+
const statusCommand = defineCommand({
|
|
1905
|
+
meta: {
|
|
1906
|
+
name: "status",
|
|
1907
|
+
description: "Check PDS health and configuration"
|
|
1908
|
+
},
|
|
1909
|
+
args: { dev: {
|
|
1910
|
+
type: "boolean",
|
|
1911
|
+
description: "Target local development server instead of production",
|
|
1912
|
+
default: false
|
|
1913
|
+
} },
|
|
1914
|
+
async run({ args }) {
|
|
1915
|
+
const isDev = args.dev;
|
|
1916
|
+
const wranglerVars = getVars();
|
|
1917
|
+
const config = {
|
|
1918
|
+
...readDevVars(),
|
|
1919
|
+
...wranglerVars
|
|
1920
|
+
};
|
|
1921
|
+
let targetUrl;
|
|
1922
|
+
try {
|
|
1923
|
+
targetUrl = getTargetUrl(isDev, config.PDS_HOSTNAME);
|
|
1924
|
+
} catch (err) {
|
|
1925
|
+
console.error(pc.red("Error:"), err instanceof Error ? err.message : "Configuration error");
|
|
1926
|
+
console.log(pc.dim("Run 'pds init' first to configure your PDS."));
|
|
1927
|
+
process.exit(1);
|
|
1928
|
+
}
|
|
1929
|
+
const authToken = config.AUTH_TOKEN;
|
|
1930
|
+
const did = config.DID;
|
|
1931
|
+
const handle = config.HANDLE;
|
|
1932
|
+
const pdsHostname = config.PDS_HOSTNAME;
|
|
1933
|
+
if (!authToken) {
|
|
1934
|
+
console.error(pc.red("Error:"), "No AUTH_TOKEN found. Run 'pds init' first.");
|
|
1935
|
+
process.exit(1);
|
|
1936
|
+
}
|
|
1937
|
+
console.log();
|
|
1938
|
+
console.log(pc.bold("PDS Status Check"));
|
|
1939
|
+
console.log("=".repeat(50));
|
|
1940
|
+
console.log(`Endpoint: ${pc.cyan(targetUrl)}`);
|
|
1941
|
+
console.log();
|
|
1942
|
+
const client = new PDSClient(targetUrl, authToken);
|
|
1943
|
+
let hasErrors = false;
|
|
1944
|
+
let hasWarnings = false;
|
|
1945
|
+
console.log(pc.bold("Connectivity"));
|
|
1946
|
+
if (await client.healthCheck()) console.log(` ${CHECK} PDS reachable`);
|
|
1947
|
+
else {
|
|
1948
|
+
console.log(` ${CROSS} PDS not responding`);
|
|
1949
|
+
hasErrors = true;
|
|
1950
|
+
console.log();
|
|
1951
|
+
console.log(pc.red("Cannot continue - PDS is not reachable."));
|
|
1952
|
+
if (!isDev) console.log(pc.dim("Make sure your worker is deployed: wrangler deploy"));
|
|
1953
|
+
process.exit(1);
|
|
1954
|
+
}
|
|
1955
|
+
let status;
|
|
1956
|
+
try {
|
|
1957
|
+
status = await client.getAccountStatus();
|
|
1958
|
+
console.log(` ${CHECK} Account status retrieved`);
|
|
1959
|
+
} catch (err) {
|
|
1960
|
+
console.log(` ${CROSS} Failed to get account status`);
|
|
1961
|
+
hasErrors = true;
|
|
1962
|
+
console.log();
|
|
1963
|
+
console.log(pc.red("Error:"), err instanceof Error ? err.message : "Unknown error");
|
|
1964
|
+
process.exit(1);
|
|
1965
|
+
}
|
|
1966
|
+
console.log();
|
|
1967
|
+
console.log(pc.bold("Repository"));
|
|
1968
|
+
if (status.repoCommit && status.indexedRecords > 0) {
|
|
1969
|
+
const shortCid = status.repoCommit.slice(0, 12) + "..." + status.repoCommit.slice(-4);
|
|
1970
|
+
const shortRev = status.repoRev ? status.repoRev.slice(0, 8) + "..." : "none";
|
|
1971
|
+
console.log(` ${CHECK} Initialized: ${pc.dim(shortCid)} (rev: ${shortRev})`);
|
|
1972
|
+
console.log(` ${INFO} ${status.repoBlocks.toLocaleString()} blocks, ${status.indexedRecords.toLocaleString()} records`);
|
|
1973
|
+
} else {
|
|
1974
|
+
console.log(` ${WARN} Repository empty (no records)`);
|
|
1975
|
+
console.log(pc.dim(" Run 'pds migrate' to import from another PDS"));
|
|
1976
|
+
hasWarnings = true;
|
|
1977
|
+
}
|
|
1978
|
+
console.log();
|
|
1979
|
+
console.log(pc.bold("Identity"));
|
|
1980
|
+
if (did) {
|
|
1981
|
+
const didType = did.startsWith("did:plc:") ? "did:plc" : did.startsWith("did:web:") ? "did:web" : "unknown";
|
|
1982
|
+
console.log(` ${INFO} DID: ${pc.dim(did)} (${didType})`);
|
|
1983
|
+
}
|
|
1984
|
+
if (handle) console.log(` ${INFO} Handle: ${pc.cyan(`@${handle}`)}`);
|
|
1985
|
+
if (did) {
|
|
1986
|
+
const resolved = await client.resolveDid(did);
|
|
1987
|
+
const resolveMethod = did.startsWith("did:plc:") ? "plc.directory" : did.startsWith("did:web:") ? "/.well-known/did.json" : "unknown";
|
|
1988
|
+
if (resolved.pdsEndpoint) {
|
|
1989
|
+
const expectedEndpoint = `https://${pdsHostname}`;
|
|
1990
|
+
if (resolved.pdsEndpoint === expectedEndpoint || resolved.pdsEndpoint === pdsHostname) console.log(` ${CHECK} DID resolves to this PDS (via ${resolveMethod})`);
|
|
1991
|
+
else {
|
|
1992
|
+
console.log(` ${CROSS} DID resolves to different PDS`);
|
|
1993
|
+
console.log(pc.dim(` Resolved via: ${resolveMethod}`));
|
|
1994
|
+
console.log(pc.dim(` Expected: ${expectedEndpoint}`));
|
|
1995
|
+
console.log(pc.dim(` Got: ${resolved.pdsEndpoint}`));
|
|
1996
|
+
hasErrors = true;
|
|
1997
|
+
}
|
|
1998
|
+
} else {
|
|
1999
|
+
console.log(` ${WARN} Could not resolve DID`);
|
|
2000
|
+
if (did.startsWith("did:plc:")) console.log(pc.dim(" Check plc.directory or update DID document"));
|
|
2001
|
+
else if (did.startsWith("did:web:")) console.log(pc.dim(" Ensure /.well-known/did.json is accessible"));
|
|
2002
|
+
hasWarnings = true;
|
|
2003
|
+
}
|
|
2004
|
+
} else {
|
|
2005
|
+
console.log(` ${WARN} DID not configured`);
|
|
2006
|
+
hasWarnings = true;
|
|
2007
|
+
}
|
|
2008
|
+
if (handle) {
|
|
2009
|
+
const [httpDid, dnsDid] = await Promise.all([client.checkHandleViaHttp(handle), client.checkHandleViaDns(handle)]);
|
|
2010
|
+
const httpValid = httpDid === did;
|
|
2011
|
+
const dnsValid = dnsDid === did;
|
|
2012
|
+
if (httpValid || dnsValid) {
|
|
2013
|
+
const methods = [];
|
|
2014
|
+
if (dnsValid) methods.push("DNS");
|
|
2015
|
+
if (httpValid) methods.push("HTTP");
|
|
2016
|
+
console.log(` ${CHECK} Handle verified via ${methods.join(" + ")}`);
|
|
2017
|
+
} else if (httpDid || dnsDid) {
|
|
2018
|
+
console.log(` ${CROSS} Handle resolves to different DID`);
|
|
2019
|
+
console.log(pc.dim(` Expected: ${did}`));
|
|
2020
|
+
if (httpDid) console.log(pc.dim(` HTTP well-known: ${httpDid}`));
|
|
2021
|
+
if (dnsDid) console.log(pc.dim(` DNS TXT: ${dnsDid}`));
|
|
2022
|
+
hasErrors = true;
|
|
2023
|
+
} else {
|
|
2024
|
+
console.log(` ${WARN} Handle not resolving`);
|
|
2025
|
+
if (handle === pdsHostname) console.log(pc.dim(" Ensure /.well-known/atproto-did returns your DID"));
|
|
2026
|
+
else console.log(pc.dim(` Add DNS TXT record: _atproto.${handle} → did=...`));
|
|
2027
|
+
hasWarnings = true;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
console.log();
|
|
2031
|
+
if (status.expectedBlobs > 0) {
|
|
2032
|
+
console.log(pc.bold("Blobs"));
|
|
2033
|
+
if (status.importedBlobs === status.expectedBlobs) console.log(` ${CHECK} ${status.importedBlobs}/${status.expectedBlobs} blobs imported`);
|
|
2034
|
+
else {
|
|
2035
|
+
const missing = status.expectedBlobs - status.importedBlobs;
|
|
2036
|
+
console.log(` ${WARN} ${status.importedBlobs}/${status.expectedBlobs} blobs imported (${missing} missing)`);
|
|
2037
|
+
hasWarnings = true;
|
|
2038
|
+
}
|
|
2039
|
+
console.log();
|
|
2040
|
+
}
|
|
2041
|
+
console.log(pc.bold("Federation"));
|
|
2042
|
+
if (did) if (await client.checkAppViewIndexing(did)) console.log(` ${CHECK} Profile indexed by AppView`);
|
|
2043
|
+
else {
|
|
2044
|
+
console.log(` ${WARN} Profile not found on AppView`);
|
|
2045
|
+
console.log(pc.dim(" This may be normal for new accounts"));
|
|
2046
|
+
hasWarnings = true;
|
|
2047
|
+
}
|
|
2048
|
+
try {
|
|
2049
|
+
const firehose = await client.getFirehoseStatus();
|
|
2050
|
+
console.log(` ${INFO} ${firehose.subscribers} firehose subscriber${firehose.subscribers !== 1 ? "s" : ""}, seq: ${firehose.latestSeq ?? "none"}`);
|
|
2051
|
+
} catch {
|
|
2052
|
+
console.log(` ${pc.dim(" Could not get firehose status")}`);
|
|
2053
|
+
}
|
|
2054
|
+
console.log();
|
|
2055
|
+
console.log(pc.bold("Account"));
|
|
2056
|
+
if (status.active) console.log(` ${CHECK} Active (accepting writes)`);
|
|
2057
|
+
else {
|
|
2058
|
+
console.log(` ${WARN} Deactivated (writes disabled)`);
|
|
2059
|
+
console.log(pc.dim(" Run 'pds activate' when ready to go live"));
|
|
2060
|
+
hasWarnings = true;
|
|
2061
|
+
}
|
|
2062
|
+
console.log();
|
|
2063
|
+
if (hasErrors) {
|
|
2064
|
+
console.log(pc.red(pc.bold("Some checks failed!")));
|
|
2065
|
+
process.exit(1);
|
|
2066
|
+
} else if (hasWarnings) console.log(pc.yellow("All checks passed with warnings."));
|
|
2067
|
+
else console.log(pc.green(pc.bold("All checks passed!")));
|
|
2068
|
+
}
|
|
2069
|
+
});
|
|
2070
|
+
|
|
1588
2071
|
//#endregion
|
|
1589
2072
|
//#region src/cli/index.ts
|
|
1590
2073
|
/**
|
|
@@ -1601,7 +2084,8 @@ runMain(defineCommand({
|
|
|
1601
2084
|
secret: secretCommand,
|
|
1602
2085
|
migrate: migrateCommand,
|
|
1603
2086
|
activate: activateCommand,
|
|
1604
|
-
deactivate: deactivateCommand
|
|
2087
|
+
deactivate: deactivateCommand,
|
|
2088
|
+
status: statusCommand
|
|
1605
2089
|
}
|
|
1606
2090
|
}));
|
|
1607
2091
|
|