everything-dev 2.0.0-rc.8 → 2.0.0-rc.9

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.
Files changed (115) hide show
  1. package/dist/apps/upload/src/client.cjs +7 -1
  2. package/dist/apps/upload/src/client.cjs.map +1 -1
  3. package/dist/apps/upload/src/client.mjs +7 -1
  4. package/dist/apps/upload/src/client.mjs.map +1 -1
  5. package/dist/auth-login.cjs +24 -1
  6. package/dist/auth-login.cjs.map +1 -1
  7. package/dist/auth-login.mjs +24 -1
  8. package/dist/auth-login.mjs.map +1 -1
  9. package/dist/auth-session.cjs +4 -2
  10. package/dist/auth-session.cjs.map +1 -1
  11. package/dist/auth-session.mjs +4 -2
  12. package/dist/auth-session.mjs.map +1 -1
  13. package/dist/cdn-deploy.cjs +11 -7
  14. package/dist/cdn-deploy.cjs.map +1 -1
  15. package/dist/cdn-deploy.mjs +11 -7
  16. package/dist/cdn-deploy.mjs.map +1 -1
  17. package/dist/cli/import.cjs +1140 -0
  18. package/dist/cli/import.cjs.map +1 -0
  19. package/dist/cli/import.mjs +1126 -0
  20. package/dist/cli/import.mjs.map +1 -0
  21. package/dist/cli/init.cjs +11 -4
  22. package/dist/cli/init.cjs.map +1 -1
  23. package/dist/cli/init.d.cts +8 -2
  24. package/dist/cli/init.d.mts +8 -2
  25. package/dist/cli/init.mjs +11 -5
  26. package/dist/cli/init.mjs.map +1 -1
  27. package/dist/cli.cjs +55 -0
  28. package/dist/cli.cjs.map +1 -1
  29. package/dist/cli.mjs +55 -0
  30. package/dist/cli.mjs.map +1 -1
  31. package/dist/commands/import.cjs +49 -0
  32. package/dist/commands/import.cjs.map +1 -0
  33. package/dist/commands/import.mjs +46 -0
  34. package/dist/commands/import.mjs.map +1 -0
  35. package/dist/commands/keys.cjs +24 -3
  36. package/dist/commands/keys.cjs.map +1 -1
  37. package/dist/commands/keys.mjs +24 -3
  38. package/dist/commands/keys.mjs.map +1 -1
  39. package/dist/config.d.cts +1 -0
  40. package/dist/config.d.mts +1 -0
  41. package/dist/contract.cjs +89 -0
  42. package/dist/contract.cjs.map +1 -1
  43. package/dist/contract.d.cts +190 -1
  44. package/dist/contract.d.mts +190 -1
  45. package/dist/contract.meta.cjs +16 -0
  46. package/dist/contract.meta.cjs.map +1 -1
  47. package/dist/contract.meta.d.cts +26 -0
  48. package/dist/contract.meta.d.mts +26 -0
  49. package/dist/contract.meta.mjs +16 -0
  50. package/dist/contract.meta.mjs.map +1 -1
  51. package/dist/contract.mjs +84 -1
  52. package/dist/contract.mjs.map +1 -1
  53. package/dist/custody-signer.cjs +70 -0
  54. package/dist/custody-signer.cjs.map +1 -0
  55. package/dist/custody-signer.mjs +68 -0
  56. package/dist/custody-signer.mjs.map +1 -0
  57. package/dist/descriptor/schema.cjs +8 -5
  58. package/dist/descriptor/schema.cjs.map +1 -1
  59. package/dist/descriptor/schema.d.cts +1 -0
  60. package/dist/descriptor/schema.d.mts +1 -0
  61. package/dist/descriptor/schema.mjs +8 -5
  62. package/dist/descriptor/schema.mjs.map +1 -1
  63. package/dist/descriptor/serialize.cjs +1 -1
  64. package/dist/descriptor/serialize.cjs.map +1 -1
  65. package/dist/descriptor/serialize.mjs +1 -1
  66. package/dist/descriptor/serialize.mjs.map +1 -1
  67. package/dist/index.cjs +6 -0
  68. package/dist/index.d.cts +2 -2
  69. package/dist/index.d.mts +2 -2
  70. package/dist/index.mjs +2 -2
  71. package/dist/outlayer-login.cjs +54 -0
  72. package/dist/outlayer-login.cjs.map +1 -0
  73. package/dist/outlayer-login.mjs +54 -0
  74. package/dist/outlayer-login.mjs.map +1 -0
  75. package/dist/plugin.cjs +2 -0
  76. package/dist/plugin.cjs.map +1 -1
  77. package/dist/plugin.d.cts +74 -0
  78. package/dist/plugin.d.mts +74 -0
  79. package/dist/plugin.mjs +2 -0
  80. package/dist/plugin.mjs.map +1 -1
  81. package/dist/publish.cjs +145 -22
  82. package/dist/publish.cjs.map +1 -1
  83. package/dist/publish.mjs +147 -24
  84. package/dist/publish.mjs.map +1 -1
  85. package/dist/source-archive.cjs +51 -0
  86. package/dist/source-archive.cjs.map +1 -0
  87. package/dist/source-archive.mjs +50 -0
  88. package/dist/source-archive.mjs.map +1 -0
  89. package/dist/storage-upload.cjs +6 -1
  90. package/dist/storage-upload.cjs.map +1 -1
  91. package/dist/storage-upload.mjs +6 -1
  92. package/dist/storage-upload.mjs.map +1 -1
  93. package/dist/types.cjs +5 -1
  94. package/dist/types.cjs.map +1 -1
  95. package/dist/types.d.cts +2 -0
  96. package/dist/types.d.mts +2 -0
  97. package/dist/types.mjs +5 -1
  98. package/dist/types.mjs.map +1 -1
  99. package/dist/utils/workspace-subtree.cjs +37 -0
  100. package/dist/utils/workspace-subtree.cjs.map +1 -0
  101. package/dist/utils/workspace-subtree.mjs +36 -0
  102. package/dist/utils/workspace-subtree.mjs.map +1 -0
  103. package/dist/version-manifest-deploy.cjs +4 -2
  104. package/dist/version-manifest-deploy.cjs.map +1 -1
  105. package/dist/version-manifest-deploy.mjs +4 -2
  106. package/dist/version-manifest-deploy.mjs.map +1 -1
  107. package/dist/version-manifest-resolve.cjs +2 -1
  108. package/dist/version-manifest-resolve.cjs.map +1 -1
  109. package/dist/version-manifest-resolve.d.cts +3 -0
  110. package/dist/version-manifest-resolve.d.mts +3 -0
  111. package/dist/version-manifest-resolve.mjs +2 -1
  112. package/dist/version-manifest-resolve.mjs.map +1 -1
  113. package/package.json +3 -2
  114. package/skills/cli-reference/SKILL.md +17 -1
  115. package/skills/init-upgrade/SKILL.md +76 -3
@@ -65,13 +65,18 @@ const IntegrityReportResultSchema = zod.z.object({
65
65
  });
66
66
  const STATUS_ATTEMPTS = 3;
67
67
  const TRANSPORT_ATTEMPTS = 5;
68
+ /** 522/524 are Cloudflare's "connection timed out"/"origin exceeded the proxy
69
+ * timeout" codes — the origin may still have applied the upload, and the
70
+ * server-side SRI dedupe makes a retry idempotent. */
68
71
  const RETRYABLE_STATUSES = /* @__PURE__ */ new Set([
69
72
  408,
70
73
  429,
71
74
  500,
72
75
  502,
73
76
  503,
74
- 504
77
+ 504,
78
+ 522,
79
+ 524
75
80
  ]);
76
81
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
77
82
  const backoffMs = (attempt) => 1e3 * attempt + Math.floor(Math.random() * 500);
@@ -93,6 +98,7 @@ var UploadTransferError = class UploadTransferError extends effect.Data.TaggedEr
93
98
  function describeTransferFailure(status, detail) {
94
99
  const trimmed = detail.slice(0, 300);
95
100
  if (status === 408 || /request timeout|bundle upload timed out/i.test(trimmed)) return `${trimmed} — the serving host timed out the upload; raise BOS_STORAGE_UPLOAD_TIMEOUT_MS there or upload smaller batches (BOS_MAX_BUNDLE_UPLOAD_BYTES)`;
101
+ if (status === 524) return `Cloudflare gave up waiting for the upload service (proxy timeout) — the upload may still have been applied (server-side SRI dedupe makes retries idempotent). Upload smaller batches (BOS_MAX_BUNDLE_UPLOAD_BYTES)`;
96
102
  try {
97
103
  const parsed = JSON.parse(detail);
98
104
  if (parsed?.code === "INTERNAL_SERVER_ERROR" && parsed?.message === "Internal Server Error") return `${trimmed} — the host hid the underlying error; check the host logs for the cause`;
@@ -1 +1 @@
1
- {"version":3,"file":"client.cjs","names":["z","WorkspaceVersionManifestSchema","Data"],"sources":["../../../../../../apps/upload/src/client.ts"],"sourcesContent":["import { Data } from \"effect\";\nimport { WorkspaceVersionManifestSchema } from \"every-plugin/version-manifest\";\nimport { z } from \"zod\";\n\nexport const BundleFileSchema = z.object({\n path: z.string().min(1).max(512),\n contentBase64: z.string().min(1),\n});\n\nexport const UploadRequestSchema = z.object({\n account: z.string().min(1),\n gateway: z.string().min(1),\n workspace: z.string().min(1),\n files: z.array(BundleFileSchema).min(1).max(10_000),\n});\n\nexport const UploadResponseSchema = z.object({\n stored: z.number().int(),\n /** files recognized as byte-identical to what the store already holds (SRI metadata match) and skipped */\n unchanged: z.number().int().optional(),\n totalBytes: z.number().int(),\n integrity: z.record(z.string(), z.string()),\n storage: z\n .enum([\"s3\", \"memory\"])\n .optional()\n .describe(\n \"Resolved bundle-storage backend: `s3` (R2/MinIO, persistent) or `memory` (ephemeral — bytes are lost on restart)\",\n ),\n});\n\nconst NamespaceQuerySchema = z.object({\n account: z.string().min(1),\n gateway: z.string().min(1),\n});\n\nexport const ListBundlesQuerySchema = NamespaceQuerySchema.extend({\n workspace: z.string().min(1).optional(),\n});\n\nexport const BundleObjectSummarySchema = z.object({\n key: z.string(),\n size: z.number().int(),\n lastModified: z.string().nullable().optional(),\n});\n\nexport const ListBundlesResultSchema = z.object({\n storage: z.enum([\"s3\", \"memory\"]),\n prefix: z.string(),\n objects: z.array(BundleObjectSummarySchema),\n truncated: z.boolean(),\n});\n\nexport const ListVersionsQuerySchema = NamespaceQuerySchema.extend({\n workspace: z.string().min(1),\n});\n\nexport const VersionSummarySchema = z.object({\n version: z.string().regex(/^[0-9a-f]{16}$/),\n entry: z.string(),\n entryIntegrity: z.string(),\n gitSha: z.string().optional(),\n builtAt: z.string().optional(),\n fileCount: z.number().int().optional(),\n lastModified: z.string().nullable().optional(),\n});\n\nexport const ListVersionsResultSchema = z.object({\n storage: z.enum([\"s3\", \"memory\"]),\n versions: z.array(VersionSummarySchema),\n truncated: z.boolean(),\n});\n\nexport const IntegrityReportQuerySchema = NamespaceQuerySchema.extend({\n workspace: z.string().min(1),\n version: z.string().regex(/^[0-9a-f]{16}$/),\n});\n\nexport const IntegrityReportResultSchema = z.object({\n storage: z.enum([\"s3\", \"memory\"]),\n manifest: WorkspaceVersionManifestSchema,\n /** SRI recompute of the manifest's entry object against `entryIntegrity`;\n * null when the entry object is missing from the store. */\n entryVerified: z.boolean().nullable(),\n});\n\nexport type BundleFile = z.infer<typeof BundleFileSchema>;\nexport type UploadRequest = z.infer<typeof UploadRequestSchema>;\nexport type UploadResponse = z.infer<typeof UploadResponseSchema>;\nexport type ListBundlesQuery = z.infer<typeof ListBundlesQuerySchema>;\nexport type BundleObjectSummary = z.infer<typeof BundleObjectSummarySchema>;\nexport type ListBundlesResult = z.infer<typeof ListBundlesResultSchema>;\nexport type ListVersionsQuery = z.infer<typeof ListVersionsQuerySchema>;\nexport type VersionSummary = z.infer<typeof VersionSummarySchema>;\nexport type ListVersionsResult = z.infer<typeof ListVersionsResultSchema>;\nexport type IntegrityReportQuery = z.infer<typeof IntegrityReportQuerySchema>;\nexport type IntegrityReportResult = z.infer<typeof IntegrityReportResultSchema>;\n\nexport interface UploadClientConfig {\n origin: string;\n apiKey?: string;\n fetchImpl?: typeof fetch;\n}\n\nexport interface UploadClient {\n uploadBundle(input: UploadRequest): Promise<UploadResponse>;\n listBundles(input: ListBundlesQuery): Promise<ListBundlesResult>;\n listVersions(input: ListVersionsQuery): Promise<ListVersionsResult>;\n integrityReport(input: IntegrityReportQuery): Promise<IntegrityReportResult>;\n}\n\nconst STATUS_ATTEMPTS = 3;\nconst TRANSPORT_ATTEMPTS = 5;\nconst RETRYABLE_STATUSES = new Set([408, 429, 500, 502, 503, 504]);\nconst sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\nconst backoffMs = (attempt: number) => 1000 * attempt + Math.floor(Math.random() * 500);\n\nclass UploadTransferError extends Data.TaggedError(\"UploadTransferError\")<{\n readonly status: number;\n readonly detail: string;\n readonly note?: string;\n}> {\n constructor(status: number, detail: string, note?: string) {\n super({ status, detail, note });\n }\n\n override get message() {\n return `${this.detail}${this.note ?? \"\"}`;\n }\n\n withNote(note: string) {\n return new UploadTransferError(this.status, this.detail, `${this.note ?? \"\"}${note}`);\n }\n}\n\nfunction describeTransferFailure(status: number, detail: string): string {\n const trimmed = detail.slice(0, 300);\n if (status === 408 || /request timeout|bundle upload timed out/i.test(trimmed)) {\n return `${trimmed} — the serving host timed out the upload; raise BOS_STORAGE_UPLOAD_TIMEOUT_MS there or upload smaller batches (BOS_MAX_BUNDLE_UPLOAD_BYTES)`;\n }\n try {\n const parsed = JSON.parse(detail) as { code?: unknown; message?: unknown };\n if (parsed?.code === \"INTERNAL_SERVER_ERROR\" && parsed?.message === \"Internal Server Error\") {\n return `${trimmed} — the host hid the underlying error; check the host logs for the cause`;\n }\n } catch {\n // not a JSON error body\n }\n return trimmed;\n}\n\nasync function requestJson<T>(\n config: UploadClientConfig,\n label: string,\n path: string,\n parse: (value: unknown) => T,\n init?: RequestInit,\n): Promise<T> {\n const fetchImpl = config.fetchImpl ?? fetch;\n const headers: Record<string, string> = { ...(init?.headers as Record<string, string>) };\n if (config.apiKey) headers[\"x-api-key\"] = config.apiKey;\n if (init?.body && !headers[\"content-type\"]) headers[\"content-type\"] = \"application/json\";\n\n let lastError: UploadTransferError | Error | undefined;\n let transportFailures = 0;\n let statusFailures = 0;\n for (let attempt = 1; attempt <= TRANSPORT_ATTEMPTS; attempt++) {\n try {\n const response = await fetchImpl(`${config.origin.replace(/\\/$/, \"\")}${path}`, {\n ...init,\n headers: transportFailures > 0 ? { ...headers, connection: \"close\" } : headers,\n });\n\n if (response.ok) {\n return parse(await response.json());\n }\n\n const detail = await response.text().catch(() => \"\");\n lastError = new UploadTransferError(\n response.status,\n `[upload] ${label} failed: ${response.status} ${describeTransferFailure(response.status, detail)}`,\n );\n if (!RETRYABLE_STATUSES.has(response.status)) break;\n statusFailures += 1;\n if (statusFailures >= STATUS_ATTEMPTS) break;\n } catch (error) {\n transportFailures += 1;\n lastError = new UploadTransferError(\n 0,\n `[upload] ${label} failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n if (attempt < TRANSPORT_ATTEMPTS) await sleep(backoffMs(attempt));\n }\n\n if (lastError instanceof UploadTransferError && lastError.status === 0) {\n lastError = lastError.withNote(\n \" (Bun fetch note: re-run with fetch verbose diagnostics for detail)\",\n );\n }\n throw lastError ?? new Error(`[upload] ${label} failed`);\n}\n\nconst parseJsonBody = <T>(label: string, schema: { parse: (value: unknown) => T }) => {\n return (response: unknown): T => {\n try {\n return schema.parse(response);\n } catch (error) {\n throw new UploadTransferError(\n 0,\n `[upload] ${label} returned an unexpected payload: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n };\n};\n\nexport function createUploadClient(config: UploadClientConfig): UploadClient {\n return {\n uploadBundle(input) {\n const payload = UploadRequestSchema.parse(input);\n return requestJson(\n config,\n `bundle upload for ${payload.workspace}`,\n \"/api/storage/bundles\",\n parseJsonBody(\"bundle upload\", UploadResponseSchema),\n { method: \"POST\", body: JSON.stringify(payload) },\n );\n },\n\n listBundles(input) {\n const query = new URLSearchParams({ account: input.account, gateway: input.gateway });\n if (input.workspace) query.set(\"workspace\", input.workspace);\n return requestJson(\n config,\n \"bundle listing\",\n `/api/storage/bundles?${query.toString()}`,\n parseJsonBody(\"bundle listing\", ListBundlesResultSchema),\n { method: \"GET\" },\n );\n },\n\n listVersions(input) {\n const query = new URLSearchParams({\n account: input.account,\n gateway: input.gateway,\n workspace: input.workspace,\n });\n return requestJson(\n config,\n \"version listing\",\n `/api/storage/versions?${query.toString()}`,\n parseJsonBody(\"version listing\", ListVersionsResultSchema),\n { method: \"GET\" },\n );\n },\n\n integrityReport(input) {\n const query = new URLSearchParams({\n account: input.account,\n gateway: input.gateway,\n workspace: input.workspace,\n version: input.version,\n });\n return requestJson(\n config,\n \"integrity report\",\n `/api/storage/integrity?${query.toString()}`,\n parseJsonBody(\"integrity report\", IntegrityReportResultSchema),\n { method: \"GET\" },\n );\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.cjs","names":["z","WorkspaceVersionManifestSchema","Data"],"sources":["../../../../../../apps/upload/src/client.ts"],"sourcesContent":["import { Data } from \"effect\";\nimport { WorkspaceVersionManifestSchema } from \"every-plugin/version-manifest\";\nimport { z } from \"zod\";\n\nexport const BundleFileSchema = z.object({\n path: z.string().min(1).max(512),\n contentBase64: z.string().min(1),\n});\n\nexport const UploadRequestSchema = z.object({\n account: z.string().min(1),\n gateway: z.string().min(1),\n workspace: z.string().min(1),\n files: z.array(BundleFileSchema).min(1).max(10_000),\n});\n\nexport const UploadResponseSchema = z.object({\n stored: z.number().int(),\n /** files recognized as byte-identical to what the store already holds (SRI metadata match) and skipped */\n unchanged: z.number().int().optional(),\n totalBytes: z.number().int(),\n integrity: z.record(z.string(), z.string()),\n storage: z\n .enum([\"s3\", \"memory\"])\n .optional()\n .describe(\n \"Resolved bundle-storage backend: `s3` (R2/MinIO, persistent) or `memory` (ephemeral — bytes are lost on restart)\",\n ),\n});\n\nconst NamespaceQuerySchema = z.object({\n account: z.string().min(1),\n gateway: z.string().min(1),\n});\n\nexport const ListBundlesQuerySchema = NamespaceQuerySchema.extend({\n workspace: z.string().min(1).optional(),\n});\n\nexport const BundleObjectSummarySchema = z.object({\n key: z.string(),\n size: z.number().int(),\n lastModified: z.string().nullable().optional(),\n});\n\nexport const ListBundlesResultSchema = z.object({\n storage: z.enum([\"s3\", \"memory\"]),\n prefix: z.string(),\n objects: z.array(BundleObjectSummarySchema),\n truncated: z.boolean(),\n});\n\nexport const ListVersionsQuerySchema = NamespaceQuerySchema.extend({\n workspace: z.string().min(1),\n});\n\nexport const VersionSummarySchema = z.object({\n version: z.string().regex(/^[0-9a-f]{16}$/),\n entry: z.string(),\n entryIntegrity: z.string(),\n gitSha: z.string().optional(),\n builtAt: z.string().optional(),\n fileCount: z.number().int().optional(),\n lastModified: z.string().nullable().optional(),\n});\n\nexport const ListVersionsResultSchema = z.object({\n storage: z.enum([\"s3\", \"memory\"]),\n versions: z.array(VersionSummarySchema),\n truncated: z.boolean(),\n});\n\nexport const IntegrityReportQuerySchema = NamespaceQuerySchema.extend({\n workspace: z.string().min(1),\n version: z.string().regex(/^[0-9a-f]{16}$/),\n});\n\nexport const IntegrityReportResultSchema = z.object({\n storage: z.enum([\"s3\", \"memory\"]),\n manifest: WorkspaceVersionManifestSchema,\n /** SRI recompute of the manifest's entry object against `entryIntegrity`;\n * null when the entry object is missing from the store. */\n entryVerified: z.boolean().nullable(),\n});\n\nexport type BundleFile = z.infer<typeof BundleFileSchema>;\nexport type UploadRequest = z.infer<typeof UploadRequestSchema>;\nexport type UploadResponse = z.infer<typeof UploadResponseSchema>;\nexport type ListBundlesQuery = z.infer<typeof ListBundlesQuerySchema>;\nexport type BundleObjectSummary = z.infer<typeof BundleObjectSummarySchema>;\nexport type ListBundlesResult = z.infer<typeof ListBundlesResultSchema>;\nexport type ListVersionsQuery = z.infer<typeof ListVersionsQuerySchema>;\nexport type VersionSummary = z.infer<typeof VersionSummarySchema>;\nexport type ListVersionsResult = z.infer<typeof ListVersionsResultSchema>;\nexport type IntegrityReportQuery = z.infer<typeof IntegrityReportQuerySchema>;\nexport type IntegrityReportResult = z.infer<typeof IntegrityReportResultSchema>;\n\nexport interface UploadClientConfig {\n origin: string;\n apiKey?: string;\n fetchImpl?: typeof fetch;\n}\n\nexport interface UploadClient {\n uploadBundle(input: UploadRequest): Promise<UploadResponse>;\n listBundles(input: ListBundlesQuery): Promise<ListBundlesResult>;\n listVersions(input: ListVersionsQuery): Promise<ListVersionsResult>;\n integrityReport(input: IntegrityReportQuery): Promise<IntegrityReportResult>;\n}\n\nconst STATUS_ATTEMPTS = 3;\nconst TRANSPORT_ATTEMPTS = 5;\n/** 522/524 are Cloudflare's \"connection timed out\"/\"origin exceeded the proxy\n * timeout\" codes — the origin may still have applied the upload, and the\n * server-side SRI dedupe makes a retry idempotent. */\nconst RETRYABLE_STATUSES = new Set([408, 429, 500, 502, 503, 504, 522, 524]);\nconst sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\nconst backoffMs = (attempt: number) => 1000 * attempt + Math.floor(Math.random() * 500);\n\nclass UploadTransferError extends Data.TaggedError(\"UploadTransferError\")<{\n readonly status: number;\n readonly detail: string;\n readonly note?: string;\n}> {\n constructor(status: number, detail: string, note?: string) {\n super({ status, detail, note });\n }\n\n override get message() {\n return `${this.detail}${this.note ?? \"\"}`;\n }\n\n withNote(note: string) {\n return new UploadTransferError(this.status, this.detail, `${this.note ?? \"\"}${note}`);\n }\n}\n\nfunction describeTransferFailure(status: number, detail: string): string {\n const trimmed = detail.slice(0, 300);\n if (status === 408 || /request timeout|bundle upload timed out/i.test(trimmed)) {\n return `${trimmed} — the serving host timed out the upload; raise BOS_STORAGE_UPLOAD_TIMEOUT_MS there or upload smaller batches (BOS_MAX_BUNDLE_UPLOAD_BYTES)`;\n }\n if (status === 524) {\n return `Cloudflare gave up waiting for the upload service (proxy timeout) — the upload may still have been applied (server-side SRI dedupe makes retries idempotent). Upload smaller batches (BOS_MAX_BUNDLE_UPLOAD_BYTES)`;\n }\n try {\n const parsed = JSON.parse(detail) as { code?: unknown; message?: unknown };\n if (parsed?.code === \"INTERNAL_SERVER_ERROR\" && parsed?.message === \"Internal Server Error\") {\n return `${trimmed} — the host hid the underlying error; check the host logs for the cause`;\n }\n } catch {\n // not a JSON error body\n }\n return trimmed;\n}\n\nasync function requestJson<T>(\n config: UploadClientConfig,\n label: string,\n path: string,\n parse: (value: unknown) => T,\n init?: RequestInit,\n): Promise<T> {\n const fetchImpl = config.fetchImpl ?? fetch;\n const headers: Record<string, string> = { ...(init?.headers as Record<string, string>) };\n if (config.apiKey) headers[\"x-api-key\"] = config.apiKey;\n if (init?.body && !headers[\"content-type\"]) headers[\"content-type\"] = \"application/json\";\n\n let lastError: UploadTransferError | Error | undefined;\n let transportFailures = 0;\n let statusFailures = 0;\n for (let attempt = 1; attempt <= TRANSPORT_ATTEMPTS; attempt++) {\n try {\n const response = await fetchImpl(`${config.origin.replace(/\\/$/, \"\")}${path}`, {\n ...init,\n headers: transportFailures > 0 ? { ...headers, connection: \"close\" } : headers,\n });\n\n if (response.ok) {\n return parse(await response.json());\n }\n\n const detail = await response.text().catch(() => \"\");\n lastError = new UploadTransferError(\n response.status,\n `[upload] ${label} failed: ${response.status} ${describeTransferFailure(response.status, detail)}`,\n );\n if (!RETRYABLE_STATUSES.has(response.status)) break;\n statusFailures += 1;\n if (statusFailures >= STATUS_ATTEMPTS) break;\n } catch (error) {\n transportFailures += 1;\n lastError = new UploadTransferError(\n 0,\n `[upload] ${label} failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n if (attempt < TRANSPORT_ATTEMPTS) await sleep(backoffMs(attempt));\n }\n\n if (lastError instanceof UploadTransferError && lastError.status === 0) {\n lastError = lastError.withNote(\n \" (Bun fetch note: re-run with fetch verbose diagnostics for detail)\",\n );\n }\n throw lastError ?? new Error(`[upload] ${label} failed`);\n}\n\nconst parseJsonBody = <T>(label: string, schema: { parse: (value: unknown) => T }) => {\n return (response: unknown): T => {\n try {\n return schema.parse(response);\n } catch (error) {\n throw new UploadTransferError(\n 0,\n `[upload] ${label} returned an unexpected payload: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n };\n};\n\nexport function createUploadClient(config: UploadClientConfig): UploadClient {\n return {\n uploadBundle(input) {\n const payload = UploadRequestSchema.parse(input);\n return requestJson(\n config,\n `bundle upload for ${payload.workspace}`,\n \"/api/storage/bundles\",\n parseJsonBody(\"bundle upload\", UploadResponseSchema),\n { method: \"POST\", body: JSON.stringify(payload) },\n );\n },\n\n listBundles(input) {\n const query = new URLSearchParams({ account: input.account, gateway: input.gateway });\n if (input.workspace) query.set(\"workspace\", input.workspace);\n return requestJson(\n config,\n \"bundle listing\",\n `/api/storage/bundles?${query.toString()}`,\n parseJsonBody(\"bundle listing\", ListBundlesResultSchema),\n { method: \"GET\" },\n );\n },\n\n listVersions(input) {\n const query = new URLSearchParams({\n account: input.account,\n gateway: input.gateway,\n workspace: input.workspace,\n });\n return requestJson(\n config,\n \"version listing\",\n `/api/storage/versions?${query.toString()}`,\n parseJsonBody(\"version listing\", ListVersionsResultSchema),\n { method: \"GET\" },\n );\n },\n\n integrityReport(input) {\n const query = new URLSearchParams({\n account: input.account,\n gateway: input.gateway,\n workspace: input.workspace,\n version: input.version,\n });\n return requestJson(\n config,\n \"integrity report\",\n `/api/storage/integrity?${query.toString()}`,\n parseJsonBody(\"integrity report\", IntegrityReportResultSchema),\n { method: \"GET\" },\n );\n },\n };\n}\n"],"mappings":"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"}
@@ -65,13 +65,18 @@ const IntegrityReportResultSchema = z.object({
65
65
  });
66
66
  const STATUS_ATTEMPTS = 3;
67
67
  const TRANSPORT_ATTEMPTS = 5;
68
+ /** 522/524 are Cloudflare's "connection timed out"/"origin exceeded the proxy
69
+ * timeout" codes — the origin may still have applied the upload, and the
70
+ * server-side SRI dedupe makes a retry idempotent. */
68
71
  const RETRYABLE_STATUSES = /* @__PURE__ */ new Set([
69
72
  408,
70
73
  429,
71
74
  500,
72
75
  502,
73
76
  503,
74
- 504
77
+ 504,
78
+ 522,
79
+ 524
75
80
  ]);
76
81
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
77
82
  const backoffMs = (attempt) => 1e3 * attempt + Math.floor(Math.random() * 500);
@@ -93,6 +98,7 @@ var UploadTransferError = class UploadTransferError extends Data.TaggedError("Up
93
98
  function describeTransferFailure(status, detail) {
94
99
  const trimmed = detail.slice(0, 300);
95
100
  if (status === 408 || /request timeout|bundle upload timed out/i.test(trimmed)) return `${trimmed} — the serving host timed out the upload; raise BOS_STORAGE_UPLOAD_TIMEOUT_MS there or upload smaller batches (BOS_MAX_BUNDLE_UPLOAD_BYTES)`;
101
+ if (status === 524) return `Cloudflare gave up waiting for the upload service (proxy timeout) — the upload may still have been applied (server-side SRI dedupe makes retries idempotent). Upload smaller batches (BOS_MAX_BUNDLE_UPLOAD_BYTES)`;
96
102
  try {
97
103
  const parsed = JSON.parse(detail);
98
104
  if (parsed?.code === "INTERNAL_SERVER_ERROR" && parsed?.message === "Internal Server Error") return `${trimmed} — the host hid the underlying error; check the host logs for the cause`;
@@ -1 +1 @@
1
- {"version":3,"file":"client.mjs","names":[],"sources":["../../../../../../apps/upload/src/client.ts"],"sourcesContent":["import { Data } from \"effect\";\nimport { WorkspaceVersionManifestSchema } from \"every-plugin/version-manifest\";\nimport { z } from \"zod\";\n\nexport const BundleFileSchema = z.object({\n path: z.string().min(1).max(512),\n contentBase64: z.string().min(1),\n});\n\nexport const UploadRequestSchema = z.object({\n account: z.string().min(1),\n gateway: z.string().min(1),\n workspace: z.string().min(1),\n files: z.array(BundleFileSchema).min(1).max(10_000),\n});\n\nexport const UploadResponseSchema = z.object({\n stored: z.number().int(),\n /** files recognized as byte-identical to what the store already holds (SRI metadata match) and skipped */\n unchanged: z.number().int().optional(),\n totalBytes: z.number().int(),\n integrity: z.record(z.string(), z.string()),\n storage: z\n .enum([\"s3\", \"memory\"])\n .optional()\n .describe(\n \"Resolved bundle-storage backend: `s3` (R2/MinIO, persistent) or `memory` (ephemeral — bytes are lost on restart)\",\n ),\n});\n\nconst NamespaceQuerySchema = z.object({\n account: z.string().min(1),\n gateway: z.string().min(1),\n});\n\nexport const ListBundlesQuerySchema = NamespaceQuerySchema.extend({\n workspace: z.string().min(1).optional(),\n});\n\nexport const BundleObjectSummarySchema = z.object({\n key: z.string(),\n size: z.number().int(),\n lastModified: z.string().nullable().optional(),\n});\n\nexport const ListBundlesResultSchema = z.object({\n storage: z.enum([\"s3\", \"memory\"]),\n prefix: z.string(),\n objects: z.array(BundleObjectSummarySchema),\n truncated: z.boolean(),\n});\n\nexport const ListVersionsQuerySchema = NamespaceQuerySchema.extend({\n workspace: z.string().min(1),\n});\n\nexport const VersionSummarySchema = z.object({\n version: z.string().regex(/^[0-9a-f]{16}$/),\n entry: z.string(),\n entryIntegrity: z.string(),\n gitSha: z.string().optional(),\n builtAt: z.string().optional(),\n fileCount: z.number().int().optional(),\n lastModified: z.string().nullable().optional(),\n});\n\nexport const ListVersionsResultSchema = z.object({\n storage: z.enum([\"s3\", \"memory\"]),\n versions: z.array(VersionSummarySchema),\n truncated: z.boolean(),\n});\n\nexport const IntegrityReportQuerySchema = NamespaceQuerySchema.extend({\n workspace: z.string().min(1),\n version: z.string().regex(/^[0-9a-f]{16}$/),\n});\n\nexport const IntegrityReportResultSchema = z.object({\n storage: z.enum([\"s3\", \"memory\"]),\n manifest: WorkspaceVersionManifestSchema,\n /** SRI recompute of the manifest's entry object against `entryIntegrity`;\n * null when the entry object is missing from the store. */\n entryVerified: z.boolean().nullable(),\n});\n\nexport type BundleFile = z.infer<typeof BundleFileSchema>;\nexport type UploadRequest = z.infer<typeof UploadRequestSchema>;\nexport type UploadResponse = z.infer<typeof UploadResponseSchema>;\nexport type ListBundlesQuery = z.infer<typeof ListBundlesQuerySchema>;\nexport type BundleObjectSummary = z.infer<typeof BundleObjectSummarySchema>;\nexport type ListBundlesResult = z.infer<typeof ListBundlesResultSchema>;\nexport type ListVersionsQuery = z.infer<typeof ListVersionsQuerySchema>;\nexport type VersionSummary = z.infer<typeof VersionSummarySchema>;\nexport type ListVersionsResult = z.infer<typeof ListVersionsResultSchema>;\nexport type IntegrityReportQuery = z.infer<typeof IntegrityReportQuerySchema>;\nexport type IntegrityReportResult = z.infer<typeof IntegrityReportResultSchema>;\n\nexport interface UploadClientConfig {\n origin: string;\n apiKey?: string;\n fetchImpl?: typeof fetch;\n}\n\nexport interface UploadClient {\n uploadBundle(input: UploadRequest): Promise<UploadResponse>;\n listBundles(input: ListBundlesQuery): Promise<ListBundlesResult>;\n listVersions(input: ListVersionsQuery): Promise<ListVersionsResult>;\n integrityReport(input: IntegrityReportQuery): Promise<IntegrityReportResult>;\n}\n\nconst STATUS_ATTEMPTS = 3;\nconst TRANSPORT_ATTEMPTS = 5;\nconst RETRYABLE_STATUSES = new Set([408, 429, 500, 502, 503, 504]);\nconst sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\nconst backoffMs = (attempt: number) => 1000 * attempt + Math.floor(Math.random() * 500);\n\nclass UploadTransferError extends Data.TaggedError(\"UploadTransferError\")<{\n readonly status: number;\n readonly detail: string;\n readonly note?: string;\n}> {\n constructor(status: number, detail: string, note?: string) {\n super({ status, detail, note });\n }\n\n override get message() {\n return `${this.detail}${this.note ?? \"\"}`;\n }\n\n withNote(note: string) {\n return new UploadTransferError(this.status, this.detail, `${this.note ?? \"\"}${note}`);\n }\n}\n\nfunction describeTransferFailure(status: number, detail: string): string {\n const trimmed = detail.slice(0, 300);\n if (status === 408 || /request timeout|bundle upload timed out/i.test(trimmed)) {\n return `${trimmed} — the serving host timed out the upload; raise BOS_STORAGE_UPLOAD_TIMEOUT_MS there or upload smaller batches (BOS_MAX_BUNDLE_UPLOAD_BYTES)`;\n }\n try {\n const parsed = JSON.parse(detail) as { code?: unknown; message?: unknown };\n if (parsed?.code === \"INTERNAL_SERVER_ERROR\" && parsed?.message === \"Internal Server Error\") {\n return `${trimmed} — the host hid the underlying error; check the host logs for the cause`;\n }\n } catch {\n // not a JSON error body\n }\n return trimmed;\n}\n\nasync function requestJson<T>(\n config: UploadClientConfig,\n label: string,\n path: string,\n parse: (value: unknown) => T,\n init?: RequestInit,\n): Promise<T> {\n const fetchImpl = config.fetchImpl ?? fetch;\n const headers: Record<string, string> = { ...(init?.headers as Record<string, string>) };\n if (config.apiKey) headers[\"x-api-key\"] = config.apiKey;\n if (init?.body && !headers[\"content-type\"]) headers[\"content-type\"] = \"application/json\";\n\n let lastError: UploadTransferError | Error | undefined;\n let transportFailures = 0;\n let statusFailures = 0;\n for (let attempt = 1; attempt <= TRANSPORT_ATTEMPTS; attempt++) {\n try {\n const response = await fetchImpl(`${config.origin.replace(/\\/$/, \"\")}${path}`, {\n ...init,\n headers: transportFailures > 0 ? { ...headers, connection: \"close\" } : headers,\n });\n\n if (response.ok) {\n return parse(await response.json());\n }\n\n const detail = await response.text().catch(() => \"\");\n lastError = new UploadTransferError(\n response.status,\n `[upload] ${label} failed: ${response.status} ${describeTransferFailure(response.status, detail)}`,\n );\n if (!RETRYABLE_STATUSES.has(response.status)) break;\n statusFailures += 1;\n if (statusFailures >= STATUS_ATTEMPTS) break;\n } catch (error) {\n transportFailures += 1;\n lastError = new UploadTransferError(\n 0,\n `[upload] ${label} failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n if (attempt < TRANSPORT_ATTEMPTS) await sleep(backoffMs(attempt));\n }\n\n if (lastError instanceof UploadTransferError && lastError.status === 0) {\n lastError = lastError.withNote(\n \" (Bun fetch note: re-run with fetch verbose diagnostics for detail)\",\n );\n }\n throw lastError ?? new Error(`[upload] ${label} failed`);\n}\n\nconst parseJsonBody = <T>(label: string, schema: { parse: (value: unknown) => T }) => {\n return (response: unknown): T => {\n try {\n return schema.parse(response);\n } catch (error) {\n throw new UploadTransferError(\n 0,\n `[upload] ${label} returned an unexpected payload: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n };\n};\n\nexport function createUploadClient(config: UploadClientConfig): UploadClient {\n return {\n uploadBundle(input) {\n const payload = UploadRequestSchema.parse(input);\n return requestJson(\n config,\n `bundle upload for ${payload.workspace}`,\n \"/api/storage/bundles\",\n parseJsonBody(\"bundle upload\", UploadResponseSchema),\n { method: \"POST\", body: JSON.stringify(payload) },\n );\n },\n\n listBundles(input) {\n const query = new URLSearchParams({ account: input.account, gateway: input.gateway });\n if (input.workspace) query.set(\"workspace\", input.workspace);\n return requestJson(\n config,\n \"bundle listing\",\n `/api/storage/bundles?${query.toString()}`,\n parseJsonBody(\"bundle listing\", ListBundlesResultSchema),\n { method: \"GET\" },\n );\n },\n\n listVersions(input) {\n const query = new URLSearchParams({\n account: input.account,\n gateway: input.gateway,\n workspace: input.workspace,\n });\n return requestJson(\n config,\n \"version listing\",\n `/api/storage/versions?${query.toString()}`,\n parseJsonBody(\"version listing\", ListVersionsResultSchema),\n { method: \"GET\" },\n );\n },\n\n integrityReport(input) {\n const query = new URLSearchParams({\n account: input.account,\n gateway: input.gateway,\n workspace: input.workspace,\n version: input.version,\n });\n return requestJson(\n config,\n \"integrity report\",\n `/api/storage/integrity?${query.toString()}`,\n parseJsonBody(\"integrity report\", IntegrityReportResultSchema),\n { method: \"GET\" },\n );\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.mjs","names":[],"sources":["../../../../../../apps/upload/src/client.ts"],"sourcesContent":["import { Data } from \"effect\";\nimport { WorkspaceVersionManifestSchema } from \"every-plugin/version-manifest\";\nimport { z } from \"zod\";\n\nexport const BundleFileSchema = z.object({\n path: z.string().min(1).max(512),\n contentBase64: z.string().min(1),\n});\n\nexport const UploadRequestSchema = z.object({\n account: z.string().min(1),\n gateway: z.string().min(1),\n workspace: z.string().min(1),\n files: z.array(BundleFileSchema).min(1).max(10_000),\n});\n\nexport const UploadResponseSchema = z.object({\n stored: z.number().int(),\n /** files recognized as byte-identical to what the store already holds (SRI metadata match) and skipped */\n unchanged: z.number().int().optional(),\n totalBytes: z.number().int(),\n integrity: z.record(z.string(), z.string()),\n storage: z\n .enum([\"s3\", \"memory\"])\n .optional()\n .describe(\n \"Resolved bundle-storage backend: `s3` (R2/MinIO, persistent) or `memory` (ephemeral — bytes are lost on restart)\",\n ),\n});\n\nconst NamespaceQuerySchema = z.object({\n account: z.string().min(1),\n gateway: z.string().min(1),\n});\n\nexport const ListBundlesQuerySchema = NamespaceQuerySchema.extend({\n workspace: z.string().min(1).optional(),\n});\n\nexport const BundleObjectSummarySchema = z.object({\n key: z.string(),\n size: z.number().int(),\n lastModified: z.string().nullable().optional(),\n});\n\nexport const ListBundlesResultSchema = z.object({\n storage: z.enum([\"s3\", \"memory\"]),\n prefix: z.string(),\n objects: z.array(BundleObjectSummarySchema),\n truncated: z.boolean(),\n});\n\nexport const ListVersionsQuerySchema = NamespaceQuerySchema.extend({\n workspace: z.string().min(1),\n});\n\nexport const VersionSummarySchema = z.object({\n version: z.string().regex(/^[0-9a-f]{16}$/),\n entry: z.string(),\n entryIntegrity: z.string(),\n gitSha: z.string().optional(),\n builtAt: z.string().optional(),\n fileCount: z.number().int().optional(),\n lastModified: z.string().nullable().optional(),\n});\n\nexport const ListVersionsResultSchema = z.object({\n storage: z.enum([\"s3\", \"memory\"]),\n versions: z.array(VersionSummarySchema),\n truncated: z.boolean(),\n});\n\nexport const IntegrityReportQuerySchema = NamespaceQuerySchema.extend({\n workspace: z.string().min(1),\n version: z.string().regex(/^[0-9a-f]{16}$/),\n});\n\nexport const IntegrityReportResultSchema = z.object({\n storage: z.enum([\"s3\", \"memory\"]),\n manifest: WorkspaceVersionManifestSchema,\n /** SRI recompute of the manifest's entry object against `entryIntegrity`;\n * null when the entry object is missing from the store. */\n entryVerified: z.boolean().nullable(),\n});\n\nexport type BundleFile = z.infer<typeof BundleFileSchema>;\nexport type UploadRequest = z.infer<typeof UploadRequestSchema>;\nexport type UploadResponse = z.infer<typeof UploadResponseSchema>;\nexport type ListBundlesQuery = z.infer<typeof ListBundlesQuerySchema>;\nexport type BundleObjectSummary = z.infer<typeof BundleObjectSummarySchema>;\nexport type ListBundlesResult = z.infer<typeof ListBundlesResultSchema>;\nexport type ListVersionsQuery = z.infer<typeof ListVersionsQuerySchema>;\nexport type VersionSummary = z.infer<typeof VersionSummarySchema>;\nexport type ListVersionsResult = z.infer<typeof ListVersionsResultSchema>;\nexport type IntegrityReportQuery = z.infer<typeof IntegrityReportQuerySchema>;\nexport type IntegrityReportResult = z.infer<typeof IntegrityReportResultSchema>;\n\nexport interface UploadClientConfig {\n origin: string;\n apiKey?: string;\n fetchImpl?: typeof fetch;\n}\n\nexport interface UploadClient {\n uploadBundle(input: UploadRequest): Promise<UploadResponse>;\n listBundles(input: ListBundlesQuery): Promise<ListBundlesResult>;\n listVersions(input: ListVersionsQuery): Promise<ListVersionsResult>;\n integrityReport(input: IntegrityReportQuery): Promise<IntegrityReportResult>;\n}\n\nconst STATUS_ATTEMPTS = 3;\nconst TRANSPORT_ATTEMPTS = 5;\n/** 522/524 are Cloudflare's \"connection timed out\"/\"origin exceeded the proxy\n * timeout\" codes — the origin may still have applied the upload, and the\n * server-side SRI dedupe makes a retry idempotent. */\nconst RETRYABLE_STATUSES = new Set([408, 429, 500, 502, 503, 504, 522, 524]);\nconst sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\nconst backoffMs = (attempt: number) => 1000 * attempt + Math.floor(Math.random() * 500);\n\nclass UploadTransferError extends Data.TaggedError(\"UploadTransferError\")<{\n readonly status: number;\n readonly detail: string;\n readonly note?: string;\n}> {\n constructor(status: number, detail: string, note?: string) {\n super({ status, detail, note });\n }\n\n override get message() {\n return `${this.detail}${this.note ?? \"\"}`;\n }\n\n withNote(note: string) {\n return new UploadTransferError(this.status, this.detail, `${this.note ?? \"\"}${note}`);\n }\n}\n\nfunction describeTransferFailure(status: number, detail: string): string {\n const trimmed = detail.slice(0, 300);\n if (status === 408 || /request timeout|bundle upload timed out/i.test(trimmed)) {\n return `${trimmed} — the serving host timed out the upload; raise BOS_STORAGE_UPLOAD_TIMEOUT_MS there or upload smaller batches (BOS_MAX_BUNDLE_UPLOAD_BYTES)`;\n }\n if (status === 524) {\n return `Cloudflare gave up waiting for the upload service (proxy timeout) — the upload may still have been applied (server-side SRI dedupe makes retries idempotent). Upload smaller batches (BOS_MAX_BUNDLE_UPLOAD_BYTES)`;\n }\n try {\n const parsed = JSON.parse(detail) as { code?: unknown; message?: unknown };\n if (parsed?.code === \"INTERNAL_SERVER_ERROR\" && parsed?.message === \"Internal Server Error\") {\n return `${trimmed} — the host hid the underlying error; check the host logs for the cause`;\n }\n } catch {\n // not a JSON error body\n }\n return trimmed;\n}\n\nasync function requestJson<T>(\n config: UploadClientConfig,\n label: string,\n path: string,\n parse: (value: unknown) => T,\n init?: RequestInit,\n): Promise<T> {\n const fetchImpl = config.fetchImpl ?? fetch;\n const headers: Record<string, string> = { ...(init?.headers as Record<string, string>) };\n if (config.apiKey) headers[\"x-api-key\"] = config.apiKey;\n if (init?.body && !headers[\"content-type\"]) headers[\"content-type\"] = \"application/json\";\n\n let lastError: UploadTransferError | Error | undefined;\n let transportFailures = 0;\n let statusFailures = 0;\n for (let attempt = 1; attempt <= TRANSPORT_ATTEMPTS; attempt++) {\n try {\n const response = await fetchImpl(`${config.origin.replace(/\\/$/, \"\")}${path}`, {\n ...init,\n headers: transportFailures > 0 ? { ...headers, connection: \"close\" } : headers,\n });\n\n if (response.ok) {\n return parse(await response.json());\n }\n\n const detail = await response.text().catch(() => \"\");\n lastError = new UploadTransferError(\n response.status,\n `[upload] ${label} failed: ${response.status} ${describeTransferFailure(response.status, detail)}`,\n );\n if (!RETRYABLE_STATUSES.has(response.status)) break;\n statusFailures += 1;\n if (statusFailures >= STATUS_ATTEMPTS) break;\n } catch (error) {\n transportFailures += 1;\n lastError = new UploadTransferError(\n 0,\n `[upload] ${label} failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n if (attempt < TRANSPORT_ATTEMPTS) await sleep(backoffMs(attempt));\n }\n\n if (lastError instanceof UploadTransferError && lastError.status === 0) {\n lastError = lastError.withNote(\n \" (Bun fetch note: re-run with fetch verbose diagnostics for detail)\",\n );\n }\n throw lastError ?? new Error(`[upload] ${label} failed`);\n}\n\nconst parseJsonBody = <T>(label: string, schema: { parse: (value: unknown) => T }) => {\n return (response: unknown): T => {\n try {\n return schema.parse(response);\n } catch (error) {\n throw new UploadTransferError(\n 0,\n `[upload] ${label} returned an unexpected payload: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n };\n};\n\nexport function createUploadClient(config: UploadClientConfig): UploadClient {\n return {\n uploadBundle(input) {\n const payload = UploadRequestSchema.parse(input);\n return requestJson(\n config,\n `bundle upload for ${payload.workspace}`,\n \"/api/storage/bundles\",\n parseJsonBody(\"bundle upload\", UploadResponseSchema),\n { method: \"POST\", body: JSON.stringify(payload) },\n );\n },\n\n listBundles(input) {\n const query = new URLSearchParams({ account: input.account, gateway: input.gateway });\n if (input.workspace) query.set(\"workspace\", input.workspace);\n return requestJson(\n config,\n \"bundle listing\",\n `/api/storage/bundles?${query.toString()}`,\n parseJsonBody(\"bundle listing\", ListBundlesResultSchema),\n { method: \"GET\" },\n );\n },\n\n listVersions(input) {\n const query = new URLSearchParams({\n account: input.account,\n gateway: input.gateway,\n workspace: input.workspace,\n });\n return requestJson(\n config,\n \"version listing\",\n `/api/storage/versions?${query.toString()}`,\n parseJsonBody(\"version listing\", ListVersionsResultSchema),\n { method: \"GET\" },\n );\n },\n\n integrityReport(input) {\n const query = new URLSearchParams({\n account: input.account,\n gateway: input.gateway,\n workspace: input.workspace,\n version: input.version,\n });\n return requestJson(\n config,\n \"integrity report\",\n `/api/storage/integrity?${query.toString()}`,\n parseJsonBody(\"integrity report\", IntegrityReportResultSchema),\n { method: \"GET\" },\n );\n },\n };\n}\n"],"mappings":";;;;;AAIA,MAAa,mBAAmB,EAAE,OAAO;CACvC,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG;CAC/B,eAAe,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;AACjC,CAAC;AAED,MAAa,sBAAsB,EAAE,OAAO;CAC1C,SAAS,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CACzB,SAAS,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CACzB,WAAW,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CAC3B,OAAO,EAAE,MAAM,gBAAgB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAM;AACpD,CAAC;AAED,MAAa,uBAAuB,EAAE,OAAO;CAC3C,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI;;CAEvB,WAAW,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS;CACrC,YAAY,EAAE,OAAO,CAAC,CAAC,IAAI;CAC3B,WAAW,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC;CAC1C,SAAS,EACN,KAAK,CAAC,MAAM,QAAQ,CAAC,CAAC,CACtB,SAAS,CAAC,CACV,SACC,kHACF;AACJ,CAAC;AAED,MAAM,uBAAuB,EAAE,OAAO;CACpC,SAAS,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CACzB,SAAS,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;AAC3B,CAAC;AAED,MAAa,yBAAyB,qBAAqB,OAAO,EAChE,WAAW,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,EACxC,CAAC;AAED,MAAa,4BAA4B,EAAE,OAAO;CAChD,KAAK,EAAE,OAAO;CACd,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI;CACrB,cAAc,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS;AAC/C,CAAC;AAED,MAAa,0BAA0B,EAAE,OAAO;CAC9C,SAAS,EAAE,KAAK,CAAC,MAAM,QAAQ,CAAC;CAChC,QAAQ,EAAE,OAAO;CACjB,SAAS,EAAE,MAAM,yBAAyB;CAC1C,WAAW,EAAE,QAAQ;AACvB,CAAC;AAED,MAAa,0BAA0B,qBAAqB,OAAO,EACjE,WAAW,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,EAC7B,CAAC;AAED,MAAa,uBAAuB,EAAE,OAAO;CAC3C,SAAS,EAAE,OAAO,CAAC,CAAC,MAAM,gBAAgB;CAC1C,OAAO,EAAE,OAAO;CAChB,gBAAgB,EAAE,OAAO;CACzB,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;CAC5B,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS;CAC7B,WAAW,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS;CACrC,cAAc,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS;AAC/C,CAAC;AAED,MAAa,2BAA2B,EAAE,OAAO;CAC/C,SAAS,EAAE,KAAK,CAAC,MAAM,QAAQ,CAAC;CAChC,UAAU,EAAE,MAAM,oBAAoB;CACtC,WAAW,EAAE,QAAQ;AACvB,CAAC;AAED,MAAa,6BAA6B,qBAAqB,OAAO;CACpE,WAAW,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CAC3B,SAAS,EAAE,OAAO,CAAC,CAAC,MAAM,gBAAgB;AAC5C,CAAC;AAED,MAAa,8BAA8B,EAAE,OAAO;CAClD,SAAS,EAAE,KAAK,CAAC,MAAM,QAAQ,CAAC;CAChC,UAAU;;;CAGV,eAAe,EAAE,QAAQ,CAAC,CAAC,SAAS;AACtC,CAAC;AA2BD,MAAM,kBAAkB;AACxB,MAAM,qBAAqB;;;;AAI3B,MAAM,qCAAqB,IAAI,IAAI;CAAC;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;AAAG,CAAC;AAC3E,MAAM,SAAS,OAAe,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;AAC9E,MAAM,aAAa,YAAoB,MAAO,UAAU,KAAK,MAAM,KAAK,OAAO,IAAI,GAAG;AAEtF,IAAM,sBAAN,MAAM,4BAA4B,KAAK,YAAY,qBAAqB,CAAC,CAItE;CACD,YAAY,QAAgB,QAAgB,MAAe;EACzD,MAAM;GAAE;GAAQ;GAAQ;EAAK,CAAC;CAChC;CAEA,IAAa,UAAU;EACrB,OAAO,GAAG,KAAK,SAAS,KAAK,QAAQ;CACvC;CAEA,SAAS,MAAc;EACrB,OAAO,IAAI,oBAAoB,KAAK,QAAQ,KAAK,QAAQ,GAAG,KAAK,QAAQ,KAAK,MAAM;CACtF;AACF;AAEA,SAAS,wBAAwB,QAAgB,QAAwB;CACvE,MAAM,UAAU,OAAO,MAAM,GAAG,GAAG;CACnC,IAAI,WAAW,OAAO,2CAA2C,KAAK,OAAO,GAC3E,OAAO,GAAG,QAAQ;CAEpB,IAAI,WAAW,KACb,OAAO;CAET,IAAI;EACF,MAAM,SAAS,KAAK,MAAM,MAAM;EAChC,IAAI,QAAQ,SAAS,2BAA2B,QAAQ,YAAY,yBAClE,OAAO,GAAG,QAAQ;CAEtB,QAAQ,CAER;CACA,OAAO;AACT;AAEA,eAAe,YACb,QACA,OACA,MACA,OACA,MACY;CACZ,MAAM,YAAY,OAAO,aAAa;CACtC,MAAM,UAAkC,EAAE,GAAI,MAAM,QAAmC;CACvF,IAAI,OAAO,QAAQ,QAAQ,eAAe,OAAO;CACjD,IAAI,MAAM,QAAQ,CAAC,QAAQ,iBAAiB,QAAQ,kBAAkB;CAEtE,IAAI;CACJ,IAAI,oBAAoB;CACxB,IAAI,iBAAiB;CACrB,KAAK,IAAI,UAAU,GAAG,WAAW,oBAAoB,WAAW;EAC9D,IAAI;GACF,MAAM,WAAW,MAAM,UAAU,GAAG,OAAO,OAAO,QAAQ,OAAO,EAAE,IAAI,QAAQ;IAC7E,GAAG;IACH,SAAS,oBAAoB,IAAI;KAAE,GAAG;KAAS,YAAY;IAAQ,IAAI;GACzE,CAAC;GAED,IAAI,SAAS,IACX,OAAO,MAAM,MAAM,SAAS,KAAK,CAAC;GAGpC,MAAM,SAAS,MAAM,SAAS,KAAK,CAAC,CAAC,YAAY,EAAE;GACnD,YAAY,IAAI,oBACd,SAAS,QACT,YAAY,MAAM,WAAW,SAAS,OAAO,GAAG,wBAAwB,SAAS,QAAQ,MAAM,GACjG;GACA,IAAI,CAAC,mBAAmB,IAAI,SAAS,MAAM,GAAG;GAC9C,kBAAkB;GAClB,IAAI,kBAAkB,iBAAiB;EACzC,SAAS,OAAO;GACd,qBAAqB;GACrB,YAAY,IAAI,oBACd,GACA,YAAY,MAAM,WAAW,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GACpF;EACF;EAEA,IAAI,UAAU,oBAAoB,MAAM,MAAM,UAAU,OAAO,CAAC;CAClE;CAEA,IAAI,qBAAqB,uBAAuB,UAAU,WAAW,GACnE,YAAY,UAAU,SACpB,qEACF;CAEF,MAAM,6BAAa,IAAI,MAAM,YAAY,MAAM,QAAQ;AACzD;AAEA,MAAM,iBAAoB,OAAe,WAA6C;CACpF,QAAQ,aAAyB;EAC/B,IAAI;GACF,OAAO,OAAO,MAAM,QAAQ;EAC9B,SAAS,OAAO;GACd,MAAM,IAAI,oBACR,GACA,YAAY,MAAM,mCAAmC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAC5G;EACF;CACF;AACF;AAEA,SAAgB,mBAAmB,QAA0C;CAC3E,OAAO;EACL,aAAa,OAAO;GAClB,MAAM,UAAU,oBAAoB,MAAM,KAAK;GAC/C,OAAO,YACL,QACA,qBAAqB,QAAQ,aAC7B,wBACA,cAAc,iBAAiB,oBAAoB,GACnD;IAAE,QAAQ;IAAQ,MAAM,KAAK,UAAU,OAAO;GAAE,CAClD;EACF;EAEA,YAAY,OAAO;GACjB,MAAM,QAAQ,IAAI,gBAAgB;IAAE,SAAS,MAAM;IAAS,SAAS,MAAM;GAAQ,CAAC;GACpF,IAAI,MAAM,WAAW,MAAM,IAAI,aAAa,MAAM,SAAS;GAC3D,OAAO,YACL,QACA,kBACA,wBAAwB,MAAM,SAAS,KACvC,cAAc,kBAAkB,uBAAuB,GACvD,EAAE,QAAQ,MAAM,CAClB;EACF;EAEA,aAAa,OAAO;GAMlB,OAAO,YACL,QACA,mBACA,yBAAyB,IART,gBAAgB;IAChC,SAAS,MAAM;IACf,SAAS,MAAM;IACf,WAAW,MAAM;GACnB,CAI+B,CAAC,CAAC,SAAS,KACxC,cAAc,mBAAmB,wBAAwB,GACzD,EAAE,QAAQ,MAAM,CAClB;EACF;EAEA,gBAAgB,OAAO;GAOrB,OAAO,YACL,QACA,oBACA,0BAA0B,IATV,gBAAgB;IAChC,SAAS,MAAM;IACf,SAAS,MAAM;IACf,WAAW,MAAM;IACjB,SAAS,MAAM;GACjB,CAIgC,CAAC,CAAC,SAAS,KACzC,cAAc,oBAAoB,2BAA2B,GAC7D,EAAE,QAAQ,MAAM,CAClB;EACF;CACF;AACF"}
@@ -92,6 +92,10 @@ function buildVerificationUrl(siteUrl, code, opts) {
92
92
  params.set("contract", opts.delegate.contract);
93
93
  params.set("network", opts.delegate.network);
94
94
  }
95
+ if (opts.custody) {
96
+ params.set("custody", "1");
97
+ params.set("network", opts.custody.network);
98
+ }
95
99
  return url.toString();
96
100
  }
97
101
  async function completeLogin(siteUrl, sessionToken, opts) {
@@ -109,7 +113,26 @@ async function completeLogin(siteUrl, sessionToken, opts) {
109
113
  return {
110
114
  sessionToken,
111
115
  accountId: await resolveNearAccount(siteUrl, cookiePair),
112
- apiKey
116
+ apiKey,
117
+ custodyWallet: opts.custody ? await fetchCustodyCredential(siteUrl, cookiePair, opts.custody.network).catch(() => null) : null
118
+ };
119
+ }
120
+ async function fetchCustodyCredential(siteUrl, cookiePair, network) {
121
+ const { status, json } = await authFetch(siteUrl, "/custody/credential", { network }, cookiePair);
122
+ if (status !== 200) {
123
+ const detail = json.message;
124
+ throw new Error(`Custody wallet credential could not be fetched${detail ? `: ${detail}` : ` (${status})`}`);
125
+ }
126
+ const data = json;
127
+ if (!data.apiKey?.startsWith("wk_")) throw new Error("Custody wallet credential returned no API key");
128
+ if (data.network !== "mainnet" && data.network !== "testnet") throw new Error(`Custody wallet credential returned unknown network "${data.network}"`);
129
+ return {
130
+ apiKey: data.apiKey,
131
+ walletId: data.walletId,
132
+ nearAccountId: data.nearAccountId,
133
+ network: data.network,
134
+ handoffUrl: data.handoffUrl ?? null,
135
+ registeredAt: data.registeredAt ?? (/* @__PURE__ */ new Date()).toISOString()
113
136
  };
114
137
  }
115
138
  async function mintApiKey(siteUrl, cookiePair, opts) {
@@ -1 +1 @@
1
- {"version":3,"file":"auth-login.cjs","names":["platform"],"sources":["../src/auth-login.ts"],"sourcesContent":["import { platform } from \"node:os\";\nimport { execa } from \"execa\";\n\nconst DEVICE_CODE_GRANT_TYPE = \"urn:ietf:params:oauth:grant-type:device_code\";\nconst DEFAULT_CLIENT_ID = \"bos-cli\";\nconst DEFAULT_POLL_INTERVAL_SECONDS = 5;\nconst SLOW_DOWN_PENALTY_SECONDS = 5;\n\nexport interface DeviceLoginOptions {\n siteUrl: string;\n /** label for the approving device, shown on the approval page */\n device?: string;\n /** the NEAR account the CLI expects to authenticate as (approval-page copy only) */\n account?: string;\n /** delegate mode — the approval page adds this scoped function-call key to the user's account */\n delegate?: { pubKey: string; contract: string; network: string };\n /** seconds the minted API key stays valid (login mode) */\n expiresIn?: number;\n}\n\nexport interface DeviceApproval {\n sessionToken: string;\n /** the NEAR account signed in on the approving browser (login mode) */\n accountId: string | null;\n /** the minted CLI credential (login mode) */\n apiKey: { key: string; id: string } | null;\n}\n\nexport interface DeviceLoginHandle {\n userCode: string;\n verificationUrl: string;\n waitForApproval: (timeoutMs?: number) => Promise<DeviceApproval>;\n abort: (reason?: string) => void;\n}\n\ninterface DeviceCodeResponse {\n device_code: string;\n user_code: string;\n verification_uri?: string;\n verification_uri_complete?: string;\n expires_in?: number;\n interval?: number;\n}\n\ninterface DeviceTokenSuccess {\n access_token: string;\n token_type: \"Bearer\";\n}\n\ninterface DeviceTokenError {\n error:\n | \"authorization_pending\"\n | \"slow_down\"\n | \"expired_token\"\n | \"access_denied\"\n | \"invalid_grant\"\n | \"invalid_request\"\n | \"invalid_client\";\n error_description?: string;\n}\n\ninterface ApiKeyCreated {\n id: string;\n key: string;\n}\n\ninterface NearAccountsResponse {\n accounts: { accountId: string }[];\n activeAccount: { accountId: string } | null;\n}\n\nasync function authFetch(\n siteUrl: string,\n path: string,\n body: unknown,\n cookiePair?: string,\n): Promise<{ status: number; json: unknown; setCookie: string | null }> {\n // Better Auth's origin check demands an Origin on cookie-bearing\n // requests — present the site's own origin, exactly as a browser would.\n const origin = new URL(siteUrl).origin;\n const headers: Record<string, string> = { \"content-type\": \"application/json\", origin };\n if (cookiePair) headers.cookie = cookiePair;\n const response = await fetch(`${siteUrl.replace(/\\/$/, \"\")}/api/auth${path}`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n });\n return {\n status: response.status,\n json: await response.json().catch(() => ({})),\n setCookie: response.headers.get(\"set-cookie\"),\n };\n}\n\n/**\n * CLI login via the OAuth 2.0 Device Flow (RFC 8628) — the same flow the\n * site's QR pairing uses. The CLI requests a device code, the user approves\n * at `/login/device` in any browser (same machine or not), and the CLI polls\n * the token endpoint until the grant completes.\n */\nexport async function startDeviceLogin(opts: DeviceLoginOptions): Promise<DeviceLoginHandle> {\n const siteUrl = opts.siteUrl.replace(/\\/$/, \"\");\n const clientId = DEFAULT_CLIENT_ID;\n\n const codeResponse = await fetch(`${siteUrl}/api/auth/device/code`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ client_id: clientId }),\n });\n if (!codeResponse.ok) {\n throw new Error(\n `Device login could not start (${codeResponse.status}) — is ${siteUrl} running the platform auth plugin?`,\n );\n }\n const code = (await codeResponse.json()) as DeviceCodeResponse;\n\n const verificationUrl = buildVerificationUrl(siteUrl, code, opts);\n\n let abortReason: string | null = null;\n let rejectPending!: (reason?: Error) => void;\n const pendingReject = new Promise<never>((_, reject) => {\n rejectPending = reject;\n });\n\n const waitForApproval = (timeoutMs = 10 * 60_000): Promise<DeviceApproval> => {\n const deadline = Date.now() + timeoutMs;\n let intervalSeconds = code.interval ?? DEFAULT_POLL_INTERVAL_SECONDS;\n\n const poll = async (): Promise<DeviceApproval> => {\n if (abortReason) throw new Error(abortReason);\n if (Date.now() > deadline) {\n throw new Error(\"Login timed out — the device code expired before approval\");\n }\n\n const { status, json } = await authFetch(siteUrl, \"/device/token\", {\n grant_type: DEVICE_CODE_GRANT_TYPE,\n device_code: code.device_code,\n client_id: clientId,\n });\n\n if (status === 200) {\n const { access_token: sessionToken } = json as DeviceTokenSuccess;\n return await completeLogin(siteUrl, sessionToken, opts);\n }\n\n const error = (json as DeviceTokenError).error;\n if (error === \"authorization_pending\") {\n // keep polling\n } else if (error === \"slow_down\") {\n intervalSeconds += SLOW_DOWN_PENALTY_SECONDS;\n } else if (error === \"expired_token\") {\n throw new Error(\"Login timed out — the device code expired before approval\");\n } else if (error === \"access_denied\") {\n throw new Error(\"Login request was denied on the approval page\");\n } else {\n throw new Error(\n (json as DeviceTokenError).error_description ?? `Login failed (${error ?? status})`,\n );\n }\n\n await new Promise((resolve) => setTimeout(resolve, intervalSeconds * 1000));\n return poll();\n };\n\n return Promise.race([pendingReject, poll()]);\n };\n\n return {\n userCode: code.user_code,\n verificationUrl,\n waitForApproval,\n abort: (reason) => {\n abortReason = reason ?? \"Login aborted\";\n rejectPending(new Error(abortReason));\n },\n };\n}\n\nfunction buildVerificationUrl(\n siteUrl: string,\n code: DeviceCodeResponse,\n opts: DeviceLoginOptions,\n): string {\n const base = code.verification_uri_complete ?? code.verification_uri ?? \"/login/device\";\n const url = new URL(base, siteUrl);\n const params = url.searchParams;\n params.set(\"user_code\", code.user_code);\n if (opts.account) params.set(\"account\", opts.account);\n if (opts.device) params.set(\"device\", opts.device);\n if (opts.delegate) {\n params.set(\"pubKey\", opts.delegate.pubKey);\n params.set(\"contract\", opts.delegate.contract);\n params.set(\"network\", opts.delegate.network);\n }\n return url.toString();\n}\n\nasync function completeLogin(\n siteUrl: string,\n sessionToken: string,\n opts: DeviceLoginOptions,\n): Promise<DeviceApproval> {\n // The token endpoint returns the raw session token; Better Auth session\n // cookies carry an HMAC signature, so exchange the token for a signed\n // session cookie via the device-link claim endpoint (the QR pairing path).\n const claim = await authFetch(siteUrl, \"/device-link/claim\", {\n token: sessionToken,\n client_id: DEFAULT_CLIENT_ID,\n });\n if (claim.status !== 200) {\n const detail = (claim.json as { message?: string }).message;\n throw new Error(\n `Approved session could not be claimed${detail ? `: ${detail}` : ` (${claim.status})`}`,\n );\n }\n const cookiePair = claim.setCookie?.split(\";\")[0];\n if (!cookiePair?.includes(\"=\")) {\n throw new Error(\"Session claim returned no session cookie\");\n }\n\n const apiKey = await mintApiKey(siteUrl, cookiePair, opts);\n const accountId = await resolveNearAccount(siteUrl, cookiePair);\n return { sessionToken, accountId, apiKey };\n}\n\nasync function mintApiKey(\n siteUrl: string,\n cookiePair: string,\n opts: DeviceLoginOptions,\n): Promise<DeviceApproval[\"apiKey\"]> {\n const { status, json } = await authFetch(\n siteUrl,\n \"/api-key/create\",\n {\n configId: \"user-keys\",\n name: `bos login — ${opts.device ?? \"cli\"} — ${new Date().toISOString().slice(0, 16).replace(\"T\", \" \")}`,\n ...(opts.expiresIn ? { expiresIn: opts.expiresIn } : {}),\n },\n cookiePair,\n );\n if (status !== 200) {\n const detail = (json as { message?: string }).message;\n throw new Error(\n `CLI credential could not be created${detail ? `: ${detail}` : ` (${status})`}`,\n );\n }\n const created = json as ApiKeyCreated;\n if (!created.key) throw new Error(\"API key creation returned no key\");\n return { key: created.key, id: created.id };\n}\n\nasync function resolveNearAccount(siteUrl: string, cookiePair: string): Promise<string | null> {\n try {\n const response = await fetch(`${siteUrl.replace(/\\/$/, \"\")}/api/auth/near/list-accounts`, {\n headers: { cookie: cookiePair },\n });\n if (!response.ok) return null;\n const data = (await response.json()) as NearAccountsResponse;\n return data.activeAccount?.accountId ?? null;\n } catch {\n return null;\n }\n}\n\nexport async function openInBrowser(url: string): Promise<void> {\n const os = platform();\n try {\n if (os === \"darwin\") {\n await execa(\"open\", [url], { stdio: \"ignore\" });\n } else if (os === \"win32\") {\n await execa(\"cmd\", [\"/c\", \"start\", \"\", url], {\n stdio: \"ignore\",\n windowsVerbatimArguments: true,\n });\n } else {\n await execa(\"xdg-open\", [url], { stdio: \"ignore\" });\n }\n } catch {\n throw new Error(`Failed to open browser. Open this URL manually:\\n ${url}`);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"auth-login.cjs","names":["platform"],"sources":["../src/auth-login.ts"],"sourcesContent":["import { platform } from \"node:os\";\nimport { execa } from \"execa\";\nimport type { CustodyWalletRecord } from \"./auth-session\";\n\nconst DEVICE_CODE_GRANT_TYPE = \"urn:ietf:params:oauth:grant-type:device_code\";\nconst DEFAULT_CLIENT_ID = \"bos-cli\";\nconst DEFAULT_POLL_INTERVAL_SECONDS = 5;\nconst SLOW_DOWN_PENALTY_SECONDS = 5;\n\nexport interface DeviceLoginOptions {\n siteUrl: string;\n /** label for the approving device, shown on the approval page */\n device?: string;\n /** the NEAR account the CLI expects to authenticate as (approval-page copy only) */\n account?: string;\n /** delegate mode — the approval page adds this scoped function-call key to the user's account */\n delegate?: { pubKey: string; contract: string; network: string };\n /** custody mode — the approval page also registers an OutLayer custody wallet under the user's account */\n custody?: { network: \"mainnet\" | \"testnet\" };\n /** seconds the minted API key stays valid (login mode) */\n expiresIn?: number;\n}\n\nexport interface DeviceApproval {\n sessionToken: string;\n /** the NEAR account signed in on the approving browser (login mode) */\n accountId: string | null;\n /** the minted CLI credential (login mode) */\n apiKey: { key: string; id: string } | null;\n /** the custody wallet registered during approval (custody mode) — null when it could not be fetched */\n custodyWallet: CustodyWalletRecord | null;\n}\n\nexport interface DeviceLoginHandle {\n userCode: string;\n verificationUrl: string;\n waitForApproval: (timeoutMs?: number) => Promise<DeviceApproval>;\n abort: (reason?: string) => void;\n}\n\ninterface DeviceCodeResponse {\n device_code: string;\n user_code: string;\n verification_uri?: string;\n verification_uri_complete?: string;\n expires_in?: number;\n interval?: number;\n}\n\ninterface DeviceTokenSuccess {\n access_token: string;\n token_type: \"Bearer\";\n}\n\ninterface DeviceTokenError {\n error:\n | \"authorization_pending\"\n | \"slow_down\"\n | \"expired_token\"\n | \"access_denied\"\n | \"invalid_grant\"\n | \"invalid_request\"\n | \"invalid_client\";\n error_description?: string;\n}\n\ninterface ApiKeyCreated {\n id: string;\n key: string;\n}\n\ninterface NearAccountsResponse {\n accounts: { accountId: string }[];\n activeAccount: { accountId: string } | null;\n}\n\nasync function authFetch(\n siteUrl: string,\n path: string,\n body: unknown,\n cookiePair?: string,\n): Promise<{ status: number; json: unknown; setCookie: string | null }> {\n // Better Auth's origin check demands an Origin on cookie-bearing\n // requests — present the site's own origin, exactly as a browser would.\n const origin = new URL(siteUrl).origin;\n const headers: Record<string, string> = { \"content-type\": \"application/json\", origin };\n if (cookiePair) headers.cookie = cookiePair;\n const response = await fetch(`${siteUrl.replace(/\\/$/, \"\")}/api/auth${path}`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n });\n return {\n status: response.status,\n json: await response.json().catch(() => ({})),\n setCookie: response.headers.get(\"set-cookie\"),\n };\n}\n\n/**\n * CLI login via the OAuth 2.0 Device Flow (RFC 8628) — the same flow the\n * site's QR pairing uses. The CLI requests a device code, the user approves\n * at `/login/device` in any browser (same machine or not), and the CLI polls\n * the token endpoint until the grant completes.\n */\nexport async function startDeviceLogin(opts: DeviceLoginOptions): Promise<DeviceLoginHandle> {\n const siteUrl = opts.siteUrl.replace(/\\/$/, \"\");\n const clientId = DEFAULT_CLIENT_ID;\n\n const codeResponse = await fetch(`${siteUrl}/api/auth/device/code`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ client_id: clientId }),\n });\n if (!codeResponse.ok) {\n throw new Error(\n `Device login could not start (${codeResponse.status}) — is ${siteUrl} running the platform auth plugin?`,\n );\n }\n const code = (await codeResponse.json()) as DeviceCodeResponse;\n\n const verificationUrl = buildVerificationUrl(siteUrl, code, opts);\n\n let abortReason: string | null = null;\n let rejectPending!: (reason?: Error) => void;\n const pendingReject = new Promise<never>((_, reject) => {\n rejectPending = reject;\n });\n\n const waitForApproval = (timeoutMs = 10 * 60_000): Promise<DeviceApproval> => {\n const deadline = Date.now() + timeoutMs;\n let intervalSeconds = code.interval ?? DEFAULT_POLL_INTERVAL_SECONDS;\n\n const poll = async (): Promise<DeviceApproval> => {\n if (abortReason) throw new Error(abortReason);\n if (Date.now() > deadline) {\n throw new Error(\"Login timed out — the device code expired before approval\");\n }\n\n const { status, json } = await authFetch(siteUrl, \"/device/token\", {\n grant_type: DEVICE_CODE_GRANT_TYPE,\n device_code: code.device_code,\n client_id: clientId,\n });\n\n if (status === 200) {\n const { access_token: sessionToken } = json as DeviceTokenSuccess;\n return await completeLogin(siteUrl, sessionToken, opts);\n }\n\n const error = (json as DeviceTokenError).error;\n if (error === \"authorization_pending\") {\n // keep polling\n } else if (error === \"slow_down\") {\n intervalSeconds += SLOW_DOWN_PENALTY_SECONDS;\n } else if (error === \"expired_token\") {\n throw new Error(\"Login timed out — the device code expired before approval\");\n } else if (error === \"access_denied\") {\n throw new Error(\"Login request was denied on the approval page\");\n } else {\n throw new Error(\n (json as DeviceTokenError).error_description ?? `Login failed (${error ?? status})`,\n );\n }\n\n await new Promise((resolve) => setTimeout(resolve, intervalSeconds * 1000));\n return poll();\n };\n\n return Promise.race([pendingReject, poll()]);\n };\n\n return {\n userCode: code.user_code,\n verificationUrl,\n waitForApproval,\n abort: (reason) => {\n abortReason = reason ?? \"Login aborted\";\n rejectPending(new Error(abortReason));\n },\n };\n}\n\nfunction buildVerificationUrl(\n siteUrl: string,\n code: DeviceCodeResponse,\n opts: DeviceLoginOptions,\n): string {\n const base = code.verification_uri_complete ?? code.verification_uri ?? \"/login/device\";\n const url = new URL(base, siteUrl);\n const params = url.searchParams;\n params.set(\"user_code\", code.user_code);\n if (opts.account) params.set(\"account\", opts.account);\n if (opts.device) params.set(\"device\", opts.device);\n if (opts.delegate) {\n params.set(\"pubKey\", opts.delegate.pubKey);\n params.set(\"contract\", opts.delegate.contract);\n params.set(\"network\", opts.delegate.network);\n }\n if (opts.custody) {\n params.set(\"custody\", \"1\");\n params.set(\"network\", opts.custody.network);\n }\n return url.toString();\n}\n\nasync function completeLogin(\n siteUrl: string,\n sessionToken: string,\n opts: DeviceLoginOptions,\n): Promise<DeviceApproval> {\n // The token endpoint returns the raw session token; Better Auth session\n // cookies carry an HMAC signature, so exchange the token for a signed\n // session cookie via the device-link claim endpoint (the QR pairing path).\n const claim = await authFetch(siteUrl, \"/device-link/claim\", {\n token: sessionToken,\n client_id: DEFAULT_CLIENT_ID,\n });\n if (claim.status !== 200) {\n const detail = (claim.json as { message?: string }).message;\n throw new Error(\n `Approved session could not be claimed${detail ? `: ${detail}` : ` (${claim.status})`}`,\n );\n }\n const cookiePair = claim.setCookie?.split(\";\")[0];\n if (!cookiePair?.includes(\"=\")) {\n throw new Error(\"Session claim returned no session cookie\");\n }\n\n const apiKey = await mintApiKey(siteUrl, cookiePair, opts);\n const accountId = await resolveNearAccount(siteUrl, cookiePair);\n const custodyWallet = opts.custody\n ? await fetchCustodyCredential(siteUrl, cookiePair, opts.custody.network).catch(() => null)\n : null;\n return { sessionToken, accountId, apiKey, custodyWallet };\n}\n\ninterface CustodyCredentialResponse {\n apiKey: string;\n walletId: string;\n nearAccountId: string;\n network: string;\n handoffUrl: string | null;\n registeredAt?: string;\n}\n\nasync function fetchCustodyCredential(\n siteUrl: string,\n cookiePair: string,\n network: \"mainnet\" | \"testnet\",\n): Promise<CustodyWalletRecord> {\n const { status, json } = await authFetch(siteUrl, \"/custody/credential\", { network }, cookiePair);\n if (status !== 200) {\n const detail = (json as { message?: string }).message;\n throw new Error(\n `Custody wallet credential could not be fetched${detail ? `: ${detail}` : ` (${status})`}`,\n );\n }\n const data = json as CustodyCredentialResponse;\n if (!data.apiKey?.startsWith(\"wk_\")) {\n throw new Error(\"Custody wallet credential returned no API key\");\n }\n if (data.network !== \"mainnet\" && data.network !== \"testnet\") {\n throw new Error(`Custody wallet credential returned unknown network \"${data.network}\"`);\n }\n return {\n apiKey: data.apiKey,\n walletId: data.walletId,\n nearAccountId: data.nearAccountId,\n network: data.network,\n handoffUrl: data.handoffUrl ?? null,\n registeredAt: data.registeredAt ?? new Date().toISOString(),\n };\n}\n\nasync function mintApiKey(\n siteUrl: string,\n cookiePair: string,\n opts: DeviceLoginOptions,\n): Promise<DeviceApproval[\"apiKey\"]> {\n const { status, json } = await authFetch(\n siteUrl,\n \"/api-key/create\",\n {\n configId: \"user-keys\",\n name: `bos login — ${opts.device ?? \"cli\"} — ${new Date().toISOString().slice(0, 16).replace(\"T\", \" \")}`,\n ...(opts.expiresIn ? { expiresIn: opts.expiresIn } : {}),\n },\n cookiePair,\n );\n if (status !== 200) {\n const detail = (json as { message?: string }).message;\n throw new Error(\n `CLI credential could not be created${detail ? `: ${detail}` : ` (${status})`}`,\n );\n }\n const created = json as ApiKeyCreated;\n if (!created.key) throw new Error(\"API key creation returned no key\");\n return { key: created.key, id: created.id };\n}\n\nasync function resolveNearAccount(siteUrl: string, cookiePair: string): Promise<string | null> {\n try {\n const response = await fetch(`${siteUrl.replace(/\\/$/, \"\")}/api/auth/near/list-accounts`, {\n headers: { cookie: cookiePair },\n });\n if (!response.ok) return null;\n const data = (await response.json()) as NearAccountsResponse;\n return data.activeAccount?.accountId ?? null;\n } catch {\n return null;\n }\n}\n\nexport async function openInBrowser(url: string): Promise<void> {\n const os = platform();\n try {\n if (os === \"darwin\") {\n await execa(\"open\", [url], { stdio: \"ignore\" });\n } else if (os === \"win32\") {\n await execa(\"cmd\", [\"/c\", \"start\", \"\", url], {\n stdio: \"ignore\",\n windowsVerbatimArguments: true,\n });\n } else {\n await execa(\"xdg-open\", [url], { stdio: \"ignore\" });\n }\n } catch {\n throw new Error(`Failed to open browser. Open this URL manually:\\n ${url}`);\n }\n}\n"],"mappings":"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"}
@@ -92,6 +92,10 @@ function buildVerificationUrl(siteUrl, code, opts) {
92
92
  params.set("contract", opts.delegate.contract);
93
93
  params.set("network", opts.delegate.network);
94
94
  }
95
+ if (opts.custody) {
96
+ params.set("custody", "1");
97
+ params.set("network", opts.custody.network);
98
+ }
95
99
  return url.toString();
96
100
  }
97
101
  async function completeLogin(siteUrl, sessionToken, opts) {
@@ -109,7 +113,26 @@ async function completeLogin(siteUrl, sessionToken, opts) {
109
113
  return {
110
114
  sessionToken,
111
115
  accountId: await resolveNearAccount(siteUrl, cookiePair),
112
- apiKey
116
+ apiKey,
117
+ custodyWallet: opts.custody ? await fetchCustodyCredential(siteUrl, cookiePair, opts.custody.network).catch(() => null) : null
118
+ };
119
+ }
120
+ async function fetchCustodyCredential(siteUrl, cookiePair, network) {
121
+ const { status, json } = await authFetch(siteUrl, "/custody/credential", { network }, cookiePair);
122
+ if (status !== 200) {
123
+ const detail = json.message;
124
+ throw new Error(`Custody wallet credential could not be fetched${detail ? `: ${detail}` : ` (${status})`}`);
125
+ }
126
+ const data = json;
127
+ if (!data.apiKey?.startsWith("wk_")) throw new Error("Custody wallet credential returned no API key");
128
+ if (data.network !== "mainnet" && data.network !== "testnet") throw new Error(`Custody wallet credential returned unknown network "${data.network}"`);
129
+ return {
130
+ apiKey: data.apiKey,
131
+ walletId: data.walletId,
132
+ nearAccountId: data.nearAccountId,
133
+ network: data.network,
134
+ handoffUrl: data.handoffUrl ?? null,
135
+ registeredAt: data.registeredAt ?? (/* @__PURE__ */ new Date()).toISOString()
113
136
  };
114
137
  }
115
138
  async function mintApiKey(siteUrl, cookiePair, opts) {
@@ -1 +1 @@
1
- {"version":3,"file":"auth-login.mjs","names":[],"sources":["../src/auth-login.ts"],"sourcesContent":["import { platform } from \"node:os\";\nimport { execa } from \"execa\";\n\nconst DEVICE_CODE_GRANT_TYPE = \"urn:ietf:params:oauth:grant-type:device_code\";\nconst DEFAULT_CLIENT_ID = \"bos-cli\";\nconst DEFAULT_POLL_INTERVAL_SECONDS = 5;\nconst SLOW_DOWN_PENALTY_SECONDS = 5;\n\nexport interface DeviceLoginOptions {\n siteUrl: string;\n /** label for the approving device, shown on the approval page */\n device?: string;\n /** the NEAR account the CLI expects to authenticate as (approval-page copy only) */\n account?: string;\n /** delegate mode — the approval page adds this scoped function-call key to the user's account */\n delegate?: { pubKey: string; contract: string; network: string };\n /** seconds the minted API key stays valid (login mode) */\n expiresIn?: number;\n}\n\nexport interface DeviceApproval {\n sessionToken: string;\n /** the NEAR account signed in on the approving browser (login mode) */\n accountId: string | null;\n /** the minted CLI credential (login mode) */\n apiKey: { key: string; id: string } | null;\n}\n\nexport interface DeviceLoginHandle {\n userCode: string;\n verificationUrl: string;\n waitForApproval: (timeoutMs?: number) => Promise<DeviceApproval>;\n abort: (reason?: string) => void;\n}\n\ninterface DeviceCodeResponse {\n device_code: string;\n user_code: string;\n verification_uri?: string;\n verification_uri_complete?: string;\n expires_in?: number;\n interval?: number;\n}\n\ninterface DeviceTokenSuccess {\n access_token: string;\n token_type: \"Bearer\";\n}\n\ninterface DeviceTokenError {\n error:\n | \"authorization_pending\"\n | \"slow_down\"\n | \"expired_token\"\n | \"access_denied\"\n | \"invalid_grant\"\n | \"invalid_request\"\n | \"invalid_client\";\n error_description?: string;\n}\n\ninterface ApiKeyCreated {\n id: string;\n key: string;\n}\n\ninterface NearAccountsResponse {\n accounts: { accountId: string }[];\n activeAccount: { accountId: string } | null;\n}\n\nasync function authFetch(\n siteUrl: string,\n path: string,\n body: unknown,\n cookiePair?: string,\n): Promise<{ status: number; json: unknown; setCookie: string | null }> {\n // Better Auth's origin check demands an Origin on cookie-bearing\n // requests — present the site's own origin, exactly as a browser would.\n const origin = new URL(siteUrl).origin;\n const headers: Record<string, string> = { \"content-type\": \"application/json\", origin };\n if (cookiePair) headers.cookie = cookiePair;\n const response = await fetch(`${siteUrl.replace(/\\/$/, \"\")}/api/auth${path}`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n });\n return {\n status: response.status,\n json: await response.json().catch(() => ({})),\n setCookie: response.headers.get(\"set-cookie\"),\n };\n}\n\n/**\n * CLI login via the OAuth 2.0 Device Flow (RFC 8628) — the same flow the\n * site's QR pairing uses. The CLI requests a device code, the user approves\n * at `/login/device` in any browser (same machine or not), and the CLI polls\n * the token endpoint until the grant completes.\n */\nexport async function startDeviceLogin(opts: DeviceLoginOptions): Promise<DeviceLoginHandle> {\n const siteUrl = opts.siteUrl.replace(/\\/$/, \"\");\n const clientId = DEFAULT_CLIENT_ID;\n\n const codeResponse = await fetch(`${siteUrl}/api/auth/device/code`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ client_id: clientId }),\n });\n if (!codeResponse.ok) {\n throw new Error(\n `Device login could not start (${codeResponse.status}) — is ${siteUrl} running the platform auth plugin?`,\n );\n }\n const code = (await codeResponse.json()) as DeviceCodeResponse;\n\n const verificationUrl = buildVerificationUrl(siteUrl, code, opts);\n\n let abortReason: string | null = null;\n let rejectPending!: (reason?: Error) => void;\n const pendingReject = new Promise<never>((_, reject) => {\n rejectPending = reject;\n });\n\n const waitForApproval = (timeoutMs = 10 * 60_000): Promise<DeviceApproval> => {\n const deadline = Date.now() + timeoutMs;\n let intervalSeconds = code.interval ?? DEFAULT_POLL_INTERVAL_SECONDS;\n\n const poll = async (): Promise<DeviceApproval> => {\n if (abortReason) throw new Error(abortReason);\n if (Date.now() > deadline) {\n throw new Error(\"Login timed out — the device code expired before approval\");\n }\n\n const { status, json } = await authFetch(siteUrl, \"/device/token\", {\n grant_type: DEVICE_CODE_GRANT_TYPE,\n device_code: code.device_code,\n client_id: clientId,\n });\n\n if (status === 200) {\n const { access_token: sessionToken } = json as DeviceTokenSuccess;\n return await completeLogin(siteUrl, sessionToken, opts);\n }\n\n const error = (json as DeviceTokenError).error;\n if (error === \"authorization_pending\") {\n // keep polling\n } else if (error === \"slow_down\") {\n intervalSeconds += SLOW_DOWN_PENALTY_SECONDS;\n } else if (error === \"expired_token\") {\n throw new Error(\"Login timed out — the device code expired before approval\");\n } else if (error === \"access_denied\") {\n throw new Error(\"Login request was denied on the approval page\");\n } else {\n throw new Error(\n (json as DeviceTokenError).error_description ?? `Login failed (${error ?? status})`,\n );\n }\n\n await new Promise((resolve) => setTimeout(resolve, intervalSeconds * 1000));\n return poll();\n };\n\n return Promise.race([pendingReject, poll()]);\n };\n\n return {\n userCode: code.user_code,\n verificationUrl,\n waitForApproval,\n abort: (reason) => {\n abortReason = reason ?? \"Login aborted\";\n rejectPending(new Error(abortReason));\n },\n };\n}\n\nfunction buildVerificationUrl(\n siteUrl: string,\n code: DeviceCodeResponse,\n opts: DeviceLoginOptions,\n): string {\n const base = code.verification_uri_complete ?? code.verification_uri ?? \"/login/device\";\n const url = new URL(base, siteUrl);\n const params = url.searchParams;\n params.set(\"user_code\", code.user_code);\n if (opts.account) params.set(\"account\", opts.account);\n if (opts.device) params.set(\"device\", opts.device);\n if (opts.delegate) {\n params.set(\"pubKey\", opts.delegate.pubKey);\n params.set(\"contract\", opts.delegate.contract);\n params.set(\"network\", opts.delegate.network);\n }\n return url.toString();\n}\n\nasync function completeLogin(\n siteUrl: string,\n sessionToken: string,\n opts: DeviceLoginOptions,\n): Promise<DeviceApproval> {\n // The token endpoint returns the raw session token; Better Auth session\n // cookies carry an HMAC signature, so exchange the token for a signed\n // session cookie via the device-link claim endpoint (the QR pairing path).\n const claim = await authFetch(siteUrl, \"/device-link/claim\", {\n token: sessionToken,\n client_id: DEFAULT_CLIENT_ID,\n });\n if (claim.status !== 200) {\n const detail = (claim.json as { message?: string }).message;\n throw new Error(\n `Approved session could not be claimed${detail ? `: ${detail}` : ` (${claim.status})`}`,\n );\n }\n const cookiePair = claim.setCookie?.split(\";\")[0];\n if (!cookiePair?.includes(\"=\")) {\n throw new Error(\"Session claim returned no session cookie\");\n }\n\n const apiKey = await mintApiKey(siteUrl, cookiePair, opts);\n const accountId = await resolveNearAccount(siteUrl, cookiePair);\n return { sessionToken, accountId, apiKey };\n}\n\nasync function mintApiKey(\n siteUrl: string,\n cookiePair: string,\n opts: DeviceLoginOptions,\n): Promise<DeviceApproval[\"apiKey\"]> {\n const { status, json } = await authFetch(\n siteUrl,\n \"/api-key/create\",\n {\n configId: \"user-keys\",\n name: `bos login — ${opts.device ?? \"cli\"} — ${new Date().toISOString().slice(0, 16).replace(\"T\", \" \")}`,\n ...(opts.expiresIn ? { expiresIn: opts.expiresIn } : {}),\n },\n cookiePair,\n );\n if (status !== 200) {\n const detail = (json as { message?: string }).message;\n throw new Error(\n `CLI credential could not be created${detail ? `: ${detail}` : ` (${status})`}`,\n );\n }\n const created = json as ApiKeyCreated;\n if (!created.key) throw new Error(\"API key creation returned no key\");\n return { key: created.key, id: created.id };\n}\n\nasync function resolveNearAccount(siteUrl: string, cookiePair: string): Promise<string | null> {\n try {\n const response = await fetch(`${siteUrl.replace(/\\/$/, \"\")}/api/auth/near/list-accounts`, {\n headers: { cookie: cookiePair },\n });\n if (!response.ok) return null;\n const data = (await response.json()) as NearAccountsResponse;\n return data.activeAccount?.accountId ?? null;\n } catch {\n return null;\n }\n}\n\nexport async function openInBrowser(url: string): Promise<void> {\n const os = platform();\n try {\n if (os === \"darwin\") {\n await execa(\"open\", [url], { stdio: \"ignore\" });\n } else if (os === \"win32\") {\n await execa(\"cmd\", [\"/c\", \"start\", \"\", url], {\n stdio: \"ignore\",\n windowsVerbatimArguments: true,\n });\n } else {\n await execa(\"xdg-open\", [url], { stdio: \"ignore\" });\n }\n } catch {\n throw new Error(`Failed to open browser. Open this URL manually:\\n ${url}`);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"auth-login.mjs","names":[],"sources":["../src/auth-login.ts"],"sourcesContent":["import { platform } from \"node:os\";\nimport { execa } from \"execa\";\nimport type { CustodyWalletRecord } from \"./auth-session\";\n\nconst DEVICE_CODE_GRANT_TYPE = \"urn:ietf:params:oauth:grant-type:device_code\";\nconst DEFAULT_CLIENT_ID = \"bos-cli\";\nconst DEFAULT_POLL_INTERVAL_SECONDS = 5;\nconst SLOW_DOWN_PENALTY_SECONDS = 5;\n\nexport interface DeviceLoginOptions {\n siteUrl: string;\n /** label for the approving device, shown on the approval page */\n device?: string;\n /** the NEAR account the CLI expects to authenticate as (approval-page copy only) */\n account?: string;\n /** delegate mode — the approval page adds this scoped function-call key to the user's account */\n delegate?: { pubKey: string; contract: string; network: string };\n /** custody mode — the approval page also registers an OutLayer custody wallet under the user's account */\n custody?: { network: \"mainnet\" | \"testnet\" };\n /** seconds the minted API key stays valid (login mode) */\n expiresIn?: number;\n}\n\nexport interface DeviceApproval {\n sessionToken: string;\n /** the NEAR account signed in on the approving browser (login mode) */\n accountId: string | null;\n /** the minted CLI credential (login mode) */\n apiKey: { key: string; id: string } | null;\n /** the custody wallet registered during approval (custody mode) — null when it could not be fetched */\n custodyWallet: CustodyWalletRecord | null;\n}\n\nexport interface DeviceLoginHandle {\n userCode: string;\n verificationUrl: string;\n waitForApproval: (timeoutMs?: number) => Promise<DeviceApproval>;\n abort: (reason?: string) => void;\n}\n\ninterface DeviceCodeResponse {\n device_code: string;\n user_code: string;\n verification_uri?: string;\n verification_uri_complete?: string;\n expires_in?: number;\n interval?: number;\n}\n\ninterface DeviceTokenSuccess {\n access_token: string;\n token_type: \"Bearer\";\n}\n\ninterface DeviceTokenError {\n error:\n | \"authorization_pending\"\n | \"slow_down\"\n | \"expired_token\"\n | \"access_denied\"\n | \"invalid_grant\"\n | \"invalid_request\"\n | \"invalid_client\";\n error_description?: string;\n}\n\ninterface ApiKeyCreated {\n id: string;\n key: string;\n}\n\ninterface NearAccountsResponse {\n accounts: { accountId: string }[];\n activeAccount: { accountId: string } | null;\n}\n\nasync function authFetch(\n siteUrl: string,\n path: string,\n body: unknown,\n cookiePair?: string,\n): Promise<{ status: number; json: unknown; setCookie: string | null }> {\n // Better Auth's origin check demands an Origin on cookie-bearing\n // requests — present the site's own origin, exactly as a browser would.\n const origin = new URL(siteUrl).origin;\n const headers: Record<string, string> = { \"content-type\": \"application/json\", origin };\n if (cookiePair) headers.cookie = cookiePair;\n const response = await fetch(`${siteUrl.replace(/\\/$/, \"\")}/api/auth${path}`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n });\n return {\n status: response.status,\n json: await response.json().catch(() => ({})),\n setCookie: response.headers.get(\"set-cookie\"),\n };\n}\n\n/**\n * CLI login via the OAuth 2.0 Device Flow (RFC 8628) — the same flow the\n * site's QR pairing uses. The CLI requests a device code, the user approves\n * at `/login/device` in any browser (same machine or not), and the CLI polls\n * the token endpoint until the grant completes.\n */\nexport async function startDeviceLogin(opts: DeviceLoginOptions): Promise<DeviceLoginHandle> {\n const siteUrl = opts.siteUrl.replace(/\\/$/, \"\");\n const clientId = DEFAULT_CLIENT_ID;\n\n const codeResponse = await fetch(`${siteUrl}/api/auth/device/code`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ client_id: clientId }),\n });\n if (!codeResponse.ok) {\n throw new Error(\n `Device login could not start (${codeResponse.status}) — is ${siteUrl} running the platform auth plugin?`,\n );\n }\n const code = (await codeResponse.json()) as DeviceCodeResponse;\n\n const verificationUrl = buildVerificationUrl(siteUrl, code, opts);\n\n let abortReason: string | null = null;\n let rejectPending!: (reason?: Error) => void;\n const pendingReject = new Promise<never>((_, reject) => {\n rejectPending = reject;\n });\n\n const waitForApproval = (timeoutMs = 10 * 60_000): Promise<DeviceApproval> => {\n const deadline = Date.now() + timeoutMs;\n let intervalSeconds = code.interval ?? DEFAULT_POLL_INTERVAL_SECONDS;\n\n const poll = async (): Promise<DeviceApproval> => {\n if (abortReason) throw new Error(abortReason);\n if (Date.now() > deadline) {\n throw new Error(\"Login timed out — the device code expired before approval\");\n }\n\n const { status, json } = await authFetch(siteUrl, \"/device/token\", {\n grant_type: DEVICE_CODE_GRANT_TYPE,\n device_code: code.device_code,\n client_id: clientId,\n });\n\n if (status === 200) {\n const { access_token: sessionToken } = json as DeviceTokenSuccess;\n return await completeLogin(siteUrl, sessionToken, opts);\n }\n\n const error = (json as DeviceTokenError).error;\n if (error === \"authorization_pending\") {\n // keep polling\n } else if (error === \"slow_down\") {\n intervalSeconds += SLOW_DOWN_PENALTY_SECONDS;\n } else if (error === \"expired_token\") {\n throw new Error(\"Login timed out — the device code expired before approval\");\n } else if (error === \"access_denied\") {\n throw new Error(\"Login request was denied on the approval page\");\n } else {\n throw new Error(\n (json as DeviceTokenError).error_description ?? `Login failed (${error ?? status})`,\n );\n }\n\n await new Promise((resolve) => setTimeout(resolve, intervalSeconds * 1000));\n return poll();\n };\n\n return Promise.race([pendingReject, poll()]);\n };\n\n return {\n userCode: code.user_code,\n verificationUrl,\n waitForApproval,\n abort: (reason) => {\n abortReason = reason ?? \"Login aborted\";\n rejectPending(new Error(abortReason));\n },\n };\n}\n\nfunction buildVerificationUrl(\n siteUrl: string,\n code: DeviceCodeResponse,\n opts: DeviceLoginOptions,\n): string {\n const base = code.verification_uri_complete ?? code.verification_uri ?? \"/login/device\";\n const url = new URL(base, siteUrl);\n const params = url.searchParams;\n params.set(\"user_code\", code.user_code);\n if (opts.account) params.set(\"account\", opts.account);\n if (opts.device) params.set(\"device\", opts.device);\n if (opts.delegate) {\n params.set(\"pubKey\", opts.delegate.pubKey);\n params.set(\"contract\", opts.delegate.contract);\n params.set(\"network\", opts.delegate.network);\n }\n if (opts.custody) {\n params.set(\"custody\", \"1\");\n params.set(\"network\", opts.custody.network);\n }\n return url.toString();\n}\n\nasync function completeLogin(\n siteUrl: string,\n sessionToken: string,\n opts: DeviceLoginOptions,\n): Promise<DeviceApproval> {\n // The token endpoint returns the raw session token; Better Auth session\n // cookies carry an HMAC signature, so exchange the token for a signed\n // session cookie via the device-link claim endpoint (the QR pairing path).\n const claim = await authFetch(siteUrl, \"/device-link/claim\", {\n token: sessionToken,\n client_id: DEFAULT_CLIENT_ID,\n });\n if (claim.status !== 200) {\n const detail = (claim.json as { message?: string }).message;\n throw new Error(\n `Approved session could not be claimed${detail ? `: ${detail}` : ` (${claim.status})`}`,\n );\n }\n const cookiePair = claim.setCookie?.split(\";\")[0];\n if (!cookiePair?.includes(\"=\")) {\n throw new Error(\"Session claim returned no session cookie\");\n }\n\n const apiKey = await mintApiKey(siteUrl, cookiePair, opts);\n const accountId = await resolveNearAccount(siteUrl, cookiePair);\n const custodyWallet = opts.custody\n ? await fetchCustodyCredential(siteUrl, cookiePair, opts.custody.network).catch(() => null)\n : null;\n return { sessionToken, accountId, apiKey, custodyWallet };\n}\n\ninterface CustodyCredentialResponse {\n apiKey: string;\n walletId: string;\n nearAccountId: string;\n network: string;\n handoffUrl: string | null;\n registeredAt?: string;\n}\n\nasync function fetchCustodyCredential(\n siteUrl: string,\n cookiePair: string,\n network: \"mainnet\" | \"testnet\",\n): Promise<CustodyWalletRecord> {\n const { status, json } = await authFetch(siteUrl, \"/custody/credential\", { network }, cookiePair);\n if (status !== 200) {\n const detail = (json as { message?: string }).message;\n throw new Error(\n `Custody wallet credential could not be fetched${detail ? `: ${detail}` : ` (${status})`}`,\n );\n }\n const data = json as CustodyCredentialResponse;\n if (!data.apiKey?.startsWith(\"wk_\")) {\n throw new Error(\"Custody wallet credential returned no API key\");\n }\n if (data.network !== \"mainnet\" && data.network !== \"testnet\") {\n throw new Error(`Custody wallet credential returned unknown network \"${data.network}\"`);\n }\n return {\n apiKey: data.apiKey,\n walletId: data.walletId,\n nearAccountId: data.nearAccountId,\n network: data.network,\n handoffUrl: data.handoffUrl ?? null,\n registeredAt: data.registeredAt ?? new Date().toISOString(),\n };\n}\n\nasync function mintApiKey(\n siteUrl: string,\n cookiePair: string,\n opts: DeviceLoginOptions,\n): Promise<DeviceApproval[\"apiKey\"]> {\n const { status, json } = await authFetch(\n siteUrl,\n \"/api-key/create\",\n {\n configId: \"user-keys\",\n name: `bos login — ${opts.device ?? \"cli\"} — ${new Date().toISOString().slice(0, 16).replace(\"T\", \" \")}`,\n ...(opts.expiresIn ? { expiresIn: opts.expiresIn } : {}),\n },\n cookiePair,\n );\n if (status !== 200) {\n const detail = (json as { message?: string }).message;\n throw new Error(\n `CLI credential could not be created${detail ? `: ${detail}` : ` (${status})`}`,\n );\n }\n const created = json as ApiKeyCreated;\n if (!created.key) throw new Error(\"API key creation returned no key\");\n return { key: created.key, id: created.id };\n}\n\nasync function resolveNearAccount(siteUrl: string, cookiePair: string): Promise<string | null> {\n try {\n const response = await fetch(`${siteUrl.replace(/\\/$/, \"\")}/api/auth/near/list-accounts`, {\n headers: { cookie: cookiePair },\n });\n if (!response.ok) return null;\n const data = (await response.json()) as NearAccountsResponse;\n return data.activeAccount?.accountId ?? null;\n } catch {\n return null;\n }\n}\n\nexport async function openInBrowser(url: string): Promise<void> {\n const os = platform();\n try {\n if (os === \"darwin\") {\n await execa(\"open\", [url], { stdio: \"ignore\" });\n } else if (os === \"win32\") {\n await execa(\"cmd\", [\"/c\", \"start\", \"\", url], {\n stdio: \"ignore\",\n windowsVerbatimArguments: true,\n });\n } else {\n await execa(\"xdg-open\", [url], { stdio: \"ignore\" });\n }\n } catch {\n throw new Error(`Failed to open browser. Open this URL manually:\\n ${url}`);\n }\n}\n"],"mappings":"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"}
@@ -17,7 +17,8 @@ function readSessionHandle(configDir) {
17
17
  version: 1,
18
18
  credential: parsed.credential ?? null,
19
19
  publishKey: parsed.publishKey ?? null,
20
- delegateKey: parsed.delegateKey ?? null
20
+ delegateKey: parsed.delegateKey ?? null,
21
+ custodyWallet: parsed.custodyWallet ?? null
21
22
  };
22
23
  } catch {
23
24
  return null;
@@ -33,7 +34,8 @@ function updateSessionHandle(configDir, update) {
33
34
  version: 1,
34
35
  credential: null,
35
36
  publishKey: null,
36
- delegateKey: null
37
+ delegateKey: null,
38
+ custodyWallet: null
37
39
  });
38
40
  writeSessionHandle(configDir, next);
39
41
  return next;
@@ -1 +1 @@
1
- {"version":3,"file":"auth-session.cjs","names":["join","existsSync","readFileSync","homedir"],"sources":["../src/auth-session.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport interface SessionCredential {\n kind: \"session\";\n apiKey: string;\n apiKeyId: string;\n accountId: string | null;\n label: string;\n siteUrl: string;\n createdAt: string;\n expiresAt: string | null;\n}\n\nexport interface PublishKeyRecord {\n publicKey: string;\n network: \"mainnet\" | \"testnet\";\n contract: string;\n exportedTo: string;\n}\n\nexport interface DelegateKeyRecord {\n publicKey: string;\n privateKey: string;\n accountId: string;\n network: \"mainnet\" | \"testnet\";\n contract: string;\n mintedAt: string;\n}\n\nexport interface BosSessionFile {\n version: 1;\n credential: SessionCredential | null;\n publishKey: PublishKeyRecord | null;\n delegateKey: DelegateKeyRecord | null;\n}\n\nconst SESSION_FILE_NAME = \"session.json\";\n\nexport function getSessionFilePath(configDir: string): string {\n return join(configDir, \".bos\", SESSION_FILE_NAME);\n}\n\nexport function readSessionHandle(configDir: string): BosSessionFile | null {\n const filePath = getSessionFilePath(configDir);\n if (!existsSync(filePath)) return null;\n try {\n const parsed = JSON.parse(readFileSync(filePath, \"utf-8\")) as Partial<BosSessionFile>;\n if (parsed.version !== 1) return null;\n return {\n version: 1,\n credential: parsed.credential ?? null,\n publishKey: parsed.publishKey ?? null,\n delegateKey: parsed.delegateKey ?? null,\n };\n } catch {\n return null;\n }\n}\n\nexport function writeSessionHandle(configDir: string, session: BosSessionFile): void {\n const bosDir = join(configDir, \".bos\");\n mkdirSync(bosDir, { recursive: true });\n writeFileSync(getSessionFilePath(configDir), `${JSON.stringify(session, null, 2)}\\n`, {\n mode: 0o600,\n });\n}\n\nexport function updateSessionHandle(\n configDir: string,\n update: (session: BosSessionFile) => BosSessionFile,\n): BosSessionFile {\n const current = readSessionHandle(configDir) ?? {\n version: 1 as const,\n credential: null,\n publishKey: null,\n delegateKey: null,\n };\n const next = update(current);\n writeSessionHandle(configDir, next);\n return next;\n}\n\nexport function deleteSessionHandle(configDir: string): void {\n const filePath = getSessionFilePath(configDir);\n if (existsSync(filePath)) {\n rmSync(filePath);\n }\n}\n\nexport function nearCredentialsPath(network: \"mainnet\" | \"testnet\", account: string): string {\n return join(homedir(), \".near-credentials\", network, `${account}.json`);\n}\n\nexport function removePublishedKeyFile(\n network: \"mainnet\" | \"testnet\",\n account: string,\n publicKey: string,\n): void {\n const filePath = nearCredentialsPath(network, account);\n if (!existsSync(filePath)) return;\n try {\n const credential = JSON.parse(readFileSync(filePath, \"utf-8\")) as { public_key?: string };\n if (credential.public_key === publicKey) {\n rmSync(filePath);\n }\n } catch {\n // leave malformed files alone\n }\n}\n"],"mappings":";;;;;AAsCA,MAAM,oBAAoB;AAE1B,SAAgB,mBAAmB,WAA2B;CAC5D,WAAOA,gBAAK,WAAW,QAAQ,iBAAiB;AAClD;AAEA,SAAgB,kBAAkB,WAA0C;CAC1E,MAAM,WAAW,mBAAmB,SAAS;CAC7C,IAAI,KAACC,oBAAW,QAAQ,GAAG,OAAO;CAClC,IAAI;EACF,MAAM,SAAS,KAAK,UAAMC,sBAAa,UAAU,OAAO,CAAC;EACzD,IAAI,OAAO,YAAY,GAAG,OAAO;EACjC,OAAO;GACL,SAAS;GACT,YAAY,OAAO,cAAc;GACjC,YAAY,OAAO,cAAc;GACjC,aAAa,OAAO,eAAe;EACrC;CACF,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAgB,mBAAmB,WAAmB,SAA+B;CACnF,MAAM,aAASF,gBAAK,WAAW,MAAM;CACrC,uBAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;CACrC,2BAAc,mBAAmB,SAAS,GAAG,GAAG,KAAK,UAAU,SAAS,MAAM,CAAC,EAAE,KAAK,EACpF,MAAM,IACR,CAAC;AACH;AAEA,SAAgB,oBACd,WACA,QACgB;CAOhB,MAAM,OAAO,OANG,kBAAkB,SAAS,KAAK;EAC9C,SAAS;EACT,YAAY;EACZ,YAAY;EACZ,aAAa;CACf,CAC2B;CAC3B,mBAAmB,WAAW,IAAI;CAClC,OAAO;AACT;AAEA,SAAgB,oBAAoB,WAAyB;CAC3D,MAAM,WAAW,mBAAmB,SAAS;CAC7C,QAAIC,oBAAW,QAAQ,GACrB,oBAAO,QAAQ;AAEnB;AAEA,SAAgB,oBAAoB,SAAgC,SAAyB;CAC3F,WAAOD,oBAAKG,iBAAQ,GAAG,qBAAqB,SAAS,GAAG,QAAQ,MAAM;AACxE;AAEA,SAAgB,uBACd,SACA,SACA,WACM;CACN,MAAM,WAAW,oBAAoB,SAAS,OAAO;CACrD,IAAI,KAACF,oBAAW,QAAQ,GAAG;CAC3B,IAAI;EAEF,IADmB,KAAK,UAAMC,sBAAa,UAAU,OAAO,CAC/C,CAAC,CAAC,eAAe,WAC5B,oBAAO,QAAQ;CAEnB,QAAQ,CAER;AACF"}
1
+ {"version":3,"file":"auth-session.cjs","names":["join","existsSync","readFileSync","homedir"],"sources":["../src/auth-session.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport interface SessionCredential {\n kind: \"session\";\n apiKey: string;\n apiKeyId: string;\n accountId: string | null;\n label: string;\n siteUrl: string;\n createdAt: string;\n expiresAt: string | null;\n}\n\nexport interface PublishKeyRecord {\n publicKey: string;\n network: \"mainnet\" | \"testnet\";\n contract: string;\n exportedTo: string;\n}\n\nexport interface DelegateKeyRecord {\n publicKey: string;\n privateKey: string;\n accountId: string;\n network: \"mainnet\" | \"testnet\";\n contract: string;\n mintedAt: string;\n}\n\nexport interface CustodyWalletRecord {\n apiKey: string;\n walletId: string;\n nearAccountId: string;\n network: \"mainnet\" | \"testnet\";\n handoffUrl: string | null;\n registeredAt: string;\n}\n\nexport interface BosSessionFile {\n version: 1;\n credential: SessionCredential | null;\n publishKey: PublishKeyRecord | null;\n delegateKey: DelegateKeyRecord | null;\n custodyWallet: CustodyWalletRecord | null;\n}\n\nconst SESSION_FILE_NAME = \"session.json\";\n\nexport function getSessionFilePath(configDir: string): string {\n return join(configDir, \".bos\", SESSION_FILE_NAME);\n}\n\nexport function readSessionHandle(configDir: string): BosSessionFile | null {\n const filePath = getSessionFilePath(configDir);\n if (!existsSync(filePath)) return null;\n try {\n const parsed = JSON.parse(readFileSync(filePath, \"utf-8\")) as Partial<BosSessionFile>;\n if (parsed.version !== 1) return null;\n return {\n version: 1,\n credential: parsed.credential ?? null,\n publishKey: parsed.publishKey ?? null,\n delegateKey: parsed.delegateKey ?? null,\n custodyWallet: parsed.custodyWallet ?? null,\n };\n } catch {\n return null;\n }\n}\n\nexport function writeSessionHandle(configDir: string, session: BosSessionFile): void {\n const bosDir = join(configDir, \".bos\");\n mkdirSync(bosDir, { recursive: true });\n writeFileSync(getSessionFilePath(configDir), `${JSON.stringify(session, null, 2)}\\n`, {\n mode: 0o600,\n });\n}\n\nexport function updateSessionHandle(\n configDir: string,\n update: (session: BosSessionFile) => BosSessionFile,\n): BosSessionFile {\n const current = readSessionHandle(configDir) ?? {\n version: 1 as const,\n credential: null,\n publishKey: null,\n delegateKey: null,\n custodyWallet: null,\n };\n const next = update(current);\n writeSessionHandle(configDir, next);\n return next;\n}\n\nexport function deleteSessionHandle(configDir: string): void {\n const filePath = getSessionFilePath(configDir);\n if (existsSync(filePath)) {\n rmSync(filePath);\n }\n}\n\nexport function nearCredentialsPath(network: \"mainnet\" | \"testnet\", account: string): string {\n return join(homedir(), \".near-credentials\", network, `${account}.json`);\n}\n\nexport function removePublishedKeyFile(\n network: \"mainnet\" | \"testnet\",\n account: string,\n publicKey: string,\n): void {\n const filePath = nearCredentialsPath(network, account);\n if (!existsSync(filePath)) return;\n try {\n const credential = JSON.parse(readFileSync(filePath, \"utf-8\")) as { public_key?: string };\n if (credential.public_key === publicKey) {\n rmSync(filePath);\n }\n } catch {\n // leave malformed files alone\n }\n}\n"],"mappings":";;;;;AAgDA,MAAM,oBAAoB;AAE1B,SAAgB,mBAAmB,WAA2B;CAC5D,WAAOA,gBAAK,WAAW,QAAQ,iBAAiB;AAClD;AAEA,SAAgB,kBAAkB,WAA0C;CAC1E,MAAM,WAAW,mBAAmB,SAAS;CAC7C,IAAI,KAACC,oBAAW,QAAQ,GAAG,OAAO;CAClC,IAAI;EACF,MAAM,SAAS,KAAK,UAAMC,sBAAa,UAAU,OAAO,CAAC;EACzD,IAAI,OAAO,YAAY,GAAG,OAAO;EACjC,OAAO;GACL,SAAS;GACT,YAAY,OAAO,cAAc;GACjC,YAAY,OAAO,cAAc;GACjC,aAAa,OAAO,eAAe;GACnC,eAAe,OAAO,iBAAiB;EACzC;CACF,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAgB,mBAAmB,WAAmB,SAA+B;CACnF,MAAM,aAASF,gBAAK,WAAW,MAAM;CACrC,uBAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;CACrC,2BAAc,mBAAmB,SAAS,GAAG,GAAG,KAAK,UAAU,SAAS,MAAM,CAAC,EAAE,KAAK,EACpF,MAAM,IACR,CAAC;AACH;AAEA,SAAgB,oBACd,WACA,QACgB;CAQhB,MAAM,OAAO,OAPG,kBAAkB,SAAS,KAAK;EAC9C,SAAS;EACT,YAAY;EACZ,YAAY;EACZ,aAAa;EACb,eAAe;CACjB,CAC2B;CAC3B,mBAAmB,WAAW,IAAI;CAClC,OAAO;AACT;AAEA,SAAgB,oBAAoB,WAAyB;CAC3D,MAAM,WAAW,mBAAmB,SAAS;CAC7C,QAAIC,oBAAW,QAAQ,GACrB,oBAAO,QAAQ;AAEnB;AAEA,SAAgB,oBAAoB,SAAgC,SAAyB;CAC3F,WAAOD,oBAAKG,iBAAQ,GAAG,qBAAqB,SAAS,GAAG,QAAQ,MAAM;AACxE;AAEA,SAAgB,uBACd,SACA,SACA,WACM;CACN,MAAM,WAAW,oBAAoB,SAAS,OAAO;CACrD,IAAI,KAACF,oBAAW,QAAQ,GAAG;CAC3B,IAAI;EAEF,IADmB,KAAK,UAAMC,sBAAa,UAAU,OAAO,CAC/C,CAAC,CAAC,eAAe,WAC5B,oBAAO,QAAQ;CAEnB,QAAQ,CAER;AACF"}
@@ -17,7 +17,8 @@ function readSessionHandle(configDir) {
17
17
  version: 1,
18
18
  credential: parsed.credential ?? null,
19
19
  publishKey: parsed.publishKey ?? null,
20
- delegateKey: parsed.delegateKey ?? null
20
+ delegateKey: parsed.delegateKey ?? null,
21
+ custodyWallet: parsed.custodyWallet ?? null
21
22
  };
22
23
  } catch {
23
24
  return null;
@@ -33,7 +34,8 @@ function updateSessionHandle(configDir, update) {
33
34
  version: 1,
34
35
  credential: null,
35
36
  publishKey: null,
36
- delegateKey: null
37
+ delegateKey: null,
38
+ custodyWallet: null
37
39
  });
38
40
  writeSessionHandle(configDir, next);
39
41
  return next;
@@ -1 +1 @@
1
- {"version":3,"file":"auth-session.mjs","names":[],"sources":["../src/auth-session.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport interface SessionCredential {\n kind: \"session\";\n apiKey: string;\n apiKeyId: string;\n accountId: string | null;\n label: string;\n siteUrl: string;\n createdAt: string;\n expiresAt: string | null;\n}\n\nexport interface PublishKeyRecord {\n publicKey: string;\n network: \"mainnet\" | \"testnet\";\n contract: string;\n exportedTo: string;\n}\n\nexport interface DelegateKeyRecord {\n publicKey: string;\n privateKey: string;\n accountId: string;\n network: \"mainnet\" | \"testnet\";\n contract: string;\n mintedAt: string;\n}\n\nexport interface BosSessionFile {\n version: 1;\n credential: SessionCredential | null;\n publishKey: PublishKeyRecord | null;\n delegateKey: DelegateKeyRecord | null;\n}\n\nconst SESSION_FILE_NAME = \"session.json\";\n\nexport function getSessionFilePath(configDir: string): string {\n return join(configDir, \".bos\", SESSION_FILE_NAME);\n}\n\nexport function readSessionHandle(configDir: string): BosSessionFile | null {\n const filePath = getSessionFilePath(configDir);\n if (!existsSync(filePath)) return null;\n try {\n const parsed = JSON.parse(readFileSync(filePath, \"utf-8\")) as Partial<BosSessionFile>;\n if (parsed.version !== 1) return null;\n return {\n version: 1,\n credential: parsed.credential ?? null,\n publishKey: parsed.publishKey ?? null,\n delegateKey: parsed.delegateKey ?? null,\n };\n } catch {\n return null;\n }\n}\n\nexport function writeSessionHandle(configDir: string, session: BosSessionFile): void {\n const bosDir = join(configDir, \".bos\");\n mkdirSync(bosDir, { recursive: true });\n writeFileSync(getSessionFilePath(configDir), `${JSON.stringify(session, null, 2)}\\n`, {\n mode: 0o600,\n });\n}\n\nexport function updateSessionHandle(\n configDir: string,\n update: (session: BosSessionFile) => BosSessionFile,\n): BosSessionFile {\n const current = readSessionHandle(configDir) ?? {\n version: 1 as const,\n credential: null,\n publishKey: null,\n delegateKey: null,\n };\n const next = update(current);\n writeSessionHandle(configDir, next);\n return next;\n}\n\nexport function deleteSessionHandle(configDir: string): void {\n const filePath = getSessionFilePath(configDir);\n if (existsSync(filePath)) {\n rmSync(filePath);\n }\n}\n\nexport function nearCredentialsPath(network: \"mainnet\" | \"testnet\", account: string): string {\n return join(homedir(), \".near-credentials\", network, `${account}.json`);\n}\n\nexport function removePublishedKeyFile(\n network: \"mainnet\" | \"testnet\",\n account: string,\n publicKey: string,\n): void {\n const filePath = nearCredentialsPath(network, account);\n if (!existsSync(filePath)) return;\n try {\n const credential = JSON.parse(readFileSync(filePath, \"utf-8\")) as { public_key?: string };\n if (credential.public_key === publicKey) {\n rmSync(filePath);\n }\n } catch {\n // leave malformed files alone\n }\n}\n"],"mappings":";;;;;AAsCA,MAAM,oBAAoB;AAE1B,SAAgB,mBAAmB,WAA2B;CAC5D,OAAO,KAAK,WAAW,QAAQ,iBAAiB;AAClD;AAEA,SAAgB,kBAAkB,WAA0C;CAC1E,MAAM,WAAW,mBAAmB,SAAS;CAC7C,IAAI,CAAC,WAAW,QAAQ,GAAG,OAAO;CAClC,IAAI;EACF,MAAM,SAAS,KAAK,MAAM,aAAa,UAAU,OAAO,CAAC;EACzD,IAAI,OAAO,YAAY,GAAG,OAAO;EACjC,OAAO;GACL,SAAS;GACT,YAAY,OAAO,cAAc;GACjC,YAAY,OAAO,cAAc;GACjC,aAAa,OAAO,eAAe;EACrC;CACF,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAgB,mBAAmB,WAAmB,SAA+B;CACnF,MAAM,SAAS,KAAK,WAAW,MAAM;CACrC,UAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;CACrC,cAAc,mBAAmB,SAAS,GAAG,GAAG,KAAK,UAAU,SAAS,MAAM,CAAC,EAAE,KAAK,EACpF,MAAM,IACR,CAAC;AACH;AAEA,SAAgB,oBACd,WACA,QACgB;CAOhB,MAAM,OAAO,OANG,kBAAkB,SAAS,KAAK;EAC9C,SAAS;EACT,YAAY;EACZ,YAAY;EACZ,aAAa;CACf,CAC2B;CAC3B,mBAAmB,WAAW,IAAI;CAClC,OAAO;AACT;AAEA,SAAgB,oBAAoB,WAAyB;CAC3D,MAAM,WAAW,mBAAmB,SAAS;CAC7C,IAAI,WAAW,QAAQ,GACrB,OAAO,QAAQ;AAEnB;AAEA,SAAgB,oBAAoB,SAAgC,SAAyB;CAC3F,OAAO,KAAK,QAAQ,GAAG,qBAAqB,SAAS,GAAG,QAAQ,MAAM;AACxE;AAEA,SAAgB,uBACd,SACA,SACA,WACM;CACN,MAAM,WAAW,oBAAoB,SAAS,OAAO;CACrD,IAAI,CAAC,WAAW,QAAQ,GAAG;CAC3B,IAAI;EAEF,IADmB,KAAK,MAAM,aAAa,UAAU,OAAO,CAC/C,CAAC,CAAC,eAAe,WAC5B,OAAO,QAAQ;CAEnB,QAAQ,CAER;AACF"}
1
+ {"version":3,"file":"auth-session.mjs","names":[],"sources":["../src/auth-session.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport interface SessionCredential {\n kind: \"session\";\n apiKey: string;\n apiKeyId: string;\n accountId: string | null;\n label: string;\n siteUrl: string;\n createdAt: string;\n expiresAt: string | null;\n}\n\nexport interface PublishKeyRecord {\n publicKey: string;\n network: \"mainnet\" | \"testnet\";\n contract: string;\n exportedTo: string;\n}\n\nexport interface DelegateKeyRecord {\n publicKey: string;\n privateKey: string;\n accountId: string;\n network: \"mainnet\" | \"testnet\";\n contract: string;\n mintedAt: string;\n}\n\nexport interface CustodyWalletRecord {\n apiKey: string;\n walletId: string;\n nearAccountId: string;\n network: \"mainnet\" | \"testnet\";\n handoffUrl: string | null;\n registeredAt: string;\n}\n\nexport interface BosSessionFile {\n version: 1;\n credential: SessionCredential | null;\n publishKey: PublishKeyRecord | null;\n delegateKey: DelegateKeyRecord | null;\n custodyWallet: CustodyWalletRecord | null;\n}\n\nconst SESSION_FILE_NAME = \"session.json\";\n\nexport function getSessionFilePath(configDir: string): string {\n return join(configDir, \".bos\", SESSION_FILE_NAME);\n}\n\nexport function readSessionHandle(configDir: string): BosSessionFile | null {\n const filePath = getSessionFilePath(configDir);\n if (!existsSync(filePath)) return null;\n try {\n const parsed = JSON.parse(readFileSync(filePath, \"utf-8\")) as Partial<BosSessionFile>;\n if (parsed.version !== 1) return null;\n return {\n version: 1,\n credential: parsed.credential ?? null,\n publishKey: parsed.publishKey ?? null,\n delegateKey: parsed.delegateKey ?? null,\n custodyWallet: parsed.custodyWallet ?? null,\n };\n } catch {\n return null;\n }\n}\n\nexport function writeSessionHandle(configDir: string, session: BosSessionFile): void {\n const bosDir = join(configDir, \".bos\");\n mkdirSync(bosDir, { recursive: true });\n writeFileSync(getSessionFilePath(configDir), `${JSON.stringify(session, null, 2)}\\n`, {\n mode: 0o600,\n });\n}\n\nexport function updateSessionHandle(\n configDir: string,\n update: (session: BosSessionFile) => BosSessionFile,\n): BosSessionFile {\n const current = readSessionHandle(configDir) ?? {\n version: 1 as const,\n credential: null,\n publishKey: null,\n delegateKey: null,\n custodyWallet: null,\n };\n const next = update(current);\n writeSessionHandle(configDir, next);\n return next;\n}\n\nexport function deleteSessionHandle(configDir: string): void {\n const filePath = getSessionFilePath(configDir);\n if (existsSync(filePath)) {\n rmSync(filePath);\n }\n}\n\nexport function nearCredentialsPath(network: \"mainnet\" | \"testnet\", account: string): string {\n return join(homedir(), \".near-credentials\", network, `${account}.json`);\n}\n\nexport function removePublishedKeyFile(\n network: \"mainnet\" | \"testnet\",\n account: string,\n publicKey: string,\n): void {\n const filePath = nearCredentialsPath(network, account);\n if (!existsSync(filePath)) return;\n try {\n const credential = JSON.parse(readFileSync(filePath, \"utf-8\")) as { public_key?: string };\n if (credential.public_key === publicKey) {\n rmSync(filePath);\n }\n } catch {\n // leave malformed files alone\n }\n}\n"],"mappings":";;;;;AAgDA,MAAM,oBAAoB;AAE1B,SAAgB,mBAAmB,WAA2B;CAC5D,OAAO,KAAK,WAAW,QAAQ,iBAAiB;AAClD;AAEA,SAAgB,kBAAkB,WAA0C;CAC1E,MAAM,WAAW,mBAAmB,SAAS;CAC7C,IAAI,CAAC,WAAW,QAAQ,GAAG,OAAO;CAClC,IAAI;EACF,MAAM,SAAS,KAAK,MAAM,aAAa,UAAU,OAAO,CAAC;EACzD,IAAI,OAAO,YAAY,GAAG,OAAO;EACjC,OAAO;GACL,SAAS;GACT,YAAY,OAAO,cAAc;GACjC,YAAY,OAAO,cAAc;GACjC,aAAa,OAAO,eAAe;GACnC,eAAe,OAAO,iBAAiB;EACzC;CACF,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAgB,mBAAmB,WAAmB,SAA+B;CACnF,MAAM,SAAS,KAAK,WAAW,MAAM;CACrC,UAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;CACrC,cAAc,mBAAmB,SAAS,GAAG,GAAG,KAAK,UAAU,SAAS,MAAM,CAAC,EAAE,KAAK,EACpF,MAAM,IACR,CAAC;AACH;AAEA,SAAgB,oBACd,WACA,QACgB;CAQhB,MAAM,OAAO,OAPG,kBAAkB,SAAS,KAAK;EAC9C,SAAS;EACT,YAAY;EACZ,YAAY;EACZ,aAAa;EACb,eAAe;CACjB,CAC2B;CAC3B,mBAAmB,WAAW,IAAI;CAClC,OAAO;AACT;AAEA,SAAgB,oBAAoB,WAAyB;CAC3D,MAAM,WAAW,mBAAmB,SAAS;CAC7C,IAAI,WAAW,QAAQ,GACrB,OAAO,QAAQ;AAEnB;AAEA,SAAgB,oBAAoB,SAAgC,SAAyB;CAC3F,OAAO,KAAK,QAAQ,GAAG,qBAAqB,SAAS,GAAG,QAAQ,MAAM;AACxE;AAEA,SAAgB,uBACd,SACA,SACA,WACM;CACN,MAAM,WAAW,oBAAoB,SAAS,OAAO;CACrD,IAAI,CAAC,WAAW,QAAQ,GAAG;CAC3B,IAAI;EAEF,IADmB,KAAK,MAAM,aAAa,UAAU,OAAO,CAC/C,CAAC,CAAC,eAAe,WAC5B,OAAO,QAAQ;CAEnB,QAAQ,CAER;AACF"}
@@ -87,13 +87,17 @@ function resolveCdnDeployInputs(input) {
87
87
  warning,
88
88
  error: "CDN deploy requires bundle-upload credentials — run `bos login --key` and add the printed key to GitHub secrets as BOS_STORAGE_API_KEY (or run `bos login` for interactive deploys)"
89
89
  };
90
- if (input.session?.accountId && input.session.accountId !== input.account) return {
91
- cdnOrigin,
92
- storageOrigin,
93
- apiKey,
94
- warning,
95
- error: `The CLI session was created for ${input.session.accountId}, but the configured account is ${input.account}. Re-run bos login under the matching account.`
96
- };
90
+ const uploadIdentities = new Set([input.session?.accountId, input.custodyAccount].filter((id) => !!id));
91
+ if (uploadIdentities.size > 0 && !uploadIdentities.has(input.account)) {
92
+ const sessionAccount = input.session?.accountId;
93
+ return {
94
+ cdnOrigin,
95
+ storageOrigin,
96
+ apiKey,
97
+ warning,
98
+ error: sessionAccount ? `The CLI session was created for ${sessionAccount}, but the configured account is ${input.account}. Re-run bos login under the matching account.` : `The configured account is ${input.account}, but the custody publish identity is ${input.custodyAccount}. Set the authored config's account to the custody wallet account.`
99
+ };
100
+ }
97
101
  return {
98
102
  cdnOrigin,
99
103
  storageOrigin,