everything-dev 2.0.0-rc.7 → 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.
- package/dist/api/auth-middleware.cjs +14 -1
- package/dist/api/auth-middleware.cjs.map +1 -1
- package/dist/api/auth-middleware.d.cts +9 -0
- package/dist/api/auth-middleware.d.mts +9 -0
- package/dist/api/auth-middleware.mjs +14 -1
- package/dist/api/auth-middleware.mjs.map +1 -1
- package/dist/apps/upload/src/client.cjs +7 -1
- package/dist/apps/upload/src/client.cjs.map +1 -1
- package/dist/apps/upload/src/client.mjs +7 -1
- package/dist/apps/upload/src/client.mjs.map +1 -1
- package/dist/auth-login.cjs +24 -1
- package/dist/auth-login.cjs.map +1 -1
- package/dist/auth-login.mjs +24 -1
- package/dist/auth-login.mjs.map +1 -1
- package/dist/auth-session.cjs +4 -2
- package/dist/auth-session.cjs.map +1 -1
- package/dist/auth-session.mjs +4 -2
- package/dist/auth-session.mjs.map +1 -1
- package/dist/cdn-deploy.cjs +11 -7
- package/dist/cdn-deploy.cjs.map +1 -1
- package/dist/cdn-deploy.mjs +11 -7
- package/dist/cdn-deploy.mjs.map +1 -1
- package/dist/cli/import.cjs +1140 -0
- package/dist/cli/import.cjs.map +1 -0
- package/dist/cli/import.mjs +1126 -0
- package/dist/cli/import.mjs.map +1 -0
- package/dist/cli/init.cjs +33 -14
- package/dist/cli/init.cjs.map +1 -1
- package/dist/cli/init.d.cts +8 -2
- package/dist/cli/init.d.mts +8 -2
- package/dist/cli/init.mjs +33 -15
- package/dist/cli/init.mjs.map +1 -1
- package/dist/cli.cjs +59 -3
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +59 -3
- package/dist/cli.mjs.map +1 -1
- package/dist/commands/import.cjs +49 -0
- package/dist/commands/import.cjs.map +1 -0
- package/dist/commands/import.mjs +46 -0
- package/dist/commands/import.mjs.map +1 -0
- package/dist/commands/keys.cjs +25 -4
- package/dist/commands/keys.cjs.map +1 -1
- package/dist/commands/keys.mjs +25 -4
- package/dist/commands/keys.mjs.map +1 -1
- package/dist/config.d.cts +1 -0
- package/dist/config.d.mts +1 -0
- package/dist/contract.cjs +89 -0
- package/dist/contract.cjs.map +1 -1
- package/dist/contract.d.cts +190 -1
- package/dist/contract.d.mts +190 -1
- package/dist/contract.meta.cjs +17 -1
- package/dist/contract.meta.cjs.map +1 -1
- package/dist/contract.meta.d.cts +27 -1
- package/dist/contract.meta.d.mts +27 -1
- package/dist/contract.meta.mjs +17 -1
- package/dist/contract.meta.mjs.map +1 -1
- package/dist/contract.mjs +84 -1
- package/dist/contract.mjs.map +1 -1
- package/dist/custody-signer.cjs +70 -0
- package/dist/custody-signer.cjs.map +1 -0
- package/dist/custody-signer.mjs +68 -0
- package/dist/custody-signer.mjs.map +1 -0
- package/dist/db/core.cjs +22 -4
- package/dist/db/core.cjs.map +1 -1
- package/dist/db/core.mjs +22 -4
- package/dist/db/core.mjs.map +1 -1
- package/dist/descriptor/schema.cjs +8 -5
- package/dist/descriptor/schema.cjs.map +1 -1
- package/dist/descriptor/schema.d.cts +1 -0
- package/dist/descriptor/schema.d.mts +1 -0
- package/dist/descriptor/schema.mjs +8 -5
- package/dist/descriptor/schema.mjs.map +1 -1
- package/dist/descriptor/serialize.cjs +1 -1
- package/dist/descriptor/serialize.cjs.map +1 -1
- package/dist/descriptor/serialize.mjs +1 -1
- package/dist/descriptor/serialize.mjs.map +1 -1
- package/dist/http-client.cjs +14 -4
- package/dist/http-client.cjs.map +1 -1
- package/dist/http-client.mjs +14 -4
- package/dist/http-client.mjs.map +1 -1
- package/dist/index.cjs +6 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/infra/planner.cjs +6 -2
- package/dist/infra/planner.cjs.map +1 -1
- package/dist/infra/planner.mjs +6 -2
- package/dist/infra/planner.mjs.map +1 -1
- package/dist/outlayer-login.cjs +54 -0
- package/dist/outlayer-login.cjs.map +1 -0
- package/dist/outlayer-login.mjs +54 -0
- package/dist/outlayer-login.mjs.map +1 -0
- package/dist/plugin.cjs +2 -0
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.d.cts +74 -0
- package/dist/plugin.d.mts +74 -0
- package/dist/plugin.mjs +2 -0
- package/dist/plugin.mjs.map +1 -1
- package/dist/publish.cjs +145 -22
- package/dist/publish.cjs.map +1 -1
- package/dist/publish.mjs +147 -24
- package/dist/publish.mjs.map +1 -1
- package/dist/source-archive.cjs +51 -0
- package/dist/source-archive.cjs.map +1 -0
- package/dist/source-archive.mjs +50 -0
- package/dist/source-archive.mjs.map +1 -0
- package/dist/storage-upload.cjs +6 -1
- package/dist/storage-upload.cjs.map +1 -1
- package/dist/storage-upload.mjs +6 -1
- package/dist/storage-upload.mjs.map +1 -1
- package/dist/types.cjs +5 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +2 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +5 -1
- package/dist/types.mjs.map +1 -1
- package/dist/ui/auth.d.cts +217 -123
- package/dist/ui/auth.d.mts +217 -123
- package/dist/ui/router-client.cjs +2 -1
- package/dist/ui/router-client.cjs.map +1 -1
- package/dist/ui/router-client.mjs +2 -1
- package/dist/ui/router-client.mjs.map +1 -1
- package/dist/utils/workspace-subtree.cjs +37 -0
- package/dist/utils/workspace-subtree.cjs.map +1 -0
- package/dist/utils/workspace-subtree.mjs +36 -0
- package/dist/utils/workspace-subtree.mjs.map +1 -0
- package/dist/version-manifest-deploy.cjs +4 -2
- package/dist/version-manifest-deploy.cjs.map +1 -1
- package/dist/version-manifest-deploy.mjs +4 -2
- package/dist/version-manifest-deploy.mjs.map +1 -1
- package/dist/version-manifest-resolve.cjs +2 -1
- package/dist/version-manifest-resolve.cjs.map +1 -1
- package/dist/version-manifest-resolve.d.cts +3 -0
- package/dist/version-manifest-resolve.d.mts +3 -0
- package/dist/version-manifest-resolve.mjs +2 -1
- package/dist/version-manifest-resolve.mjs.map +1 -1
- package/package.json +11 -10
- package/skills/api-and-auth/SKILL.md +12 -9
- package/skills/api-and-auth/references/middleware.md +38 -0
- package/skills/cli-reference/SKILL.md +38 -6
- package/skills/init-upgrade/SKILL.md +76 -3
- package/skills/talk-to-the-app/SKILL.md +32 -5
|
@@ -132,6 +132,18 @@ function createAuthMiddleware(builder, options) {
|
|
|
132
132
|
}
|
|
133
133
|
return next({ context: { apiKey: context.apiKey } });
|
|
134
134
|
});
|
|
135
|
+
const requirePermission = (resource, action, scope) => builder.middleware(async ({ context, next }, input) => {
|
|
136
|
+
let granted;
|
|
137
|
+
if (scope?.scope === "team") {
|
|
138
|
+
const teamId = scope.teamId?.(input);
|
|
139
|
+
granted = (context.organization?.teams?.find((t) => t.id === teamId))?.permissions?.[resource];
|
|
140
|
+
} else granted = context.permissions?.[resource];
|
|
141
|
+
if (!granted?.includes(action)) throw new _orpc_server.ORPCError("FORBIDDEN", {
|
|
142
|
+
message: `Requires permission: ${resource}:${action}`,
|
|
143
|
+
data: { requiredPermissions: [`${resource}:${action}`] }
|
|
144
|
+
});
|
|
145
|
+
return next({ context });
|
|
146
|
+
});
|
|
135
147
|
return {
|
|
136
148
|
requireAuth,
|
|
137
149
|
requireAuthOrApiKey,
|
|
@@ -139,7 +151,8 @@ function createAuthMiddleware(builder, options) {
|
|
|
139
151
|
requireAdmin,
|
|
140
152
|
requireOrganization,
|
|
141
153
|
requireOrgRole,
|
|
142
|
-
requireApiKey
|
|
154
|
+
requireApiKey,
|
|
155
|
+
requirePermission
|
|
143
156
|
};
|
|
144
157
|
}
|
|
145
158
|
function assertOrganizationApproval(organization) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-middleware.cjs","names":["ORPCError"],"sources":["../../src/api/auth-middleware.ts"],"sourcesContent":["/**\n * Server-side auth middleware factory — framework home for the factory that\n * used to live as five near-identical copies (`api/src/lib/auth.ts` and each\n * plugin's `src/lib/auth.ts`). See citynode.app#207.\n *\n * Generic over the workspace's auth context (the generated `AuthPluginContext`\n * satisfies `AuthContextShape`), so each workspace imports the same factory and\n * passes its own context type for precise middleware narrowing:\n *\n * const { requireAuth, requireAdmin, ... } = createAuthMiddleware<AuthContext>(builder);\n */\nimport type { DecoratedMiddleware } from \"@orpc/server\";\nimport { ORPCError } from \"@orpc/server\";\nimport type { z } from \"zod\";\n\n/** Structural minimum the middlewares read — the generated `AuthPluginContext` satisfies it. */\nexport interface AuthContextShape {\n user?: { role?: string | null } | null;\n userId?: string | null;\n apiKey?: { permissions?: Record<string, string[]> | null } | null;\n organization?: {\n activeOrganizationId?: string | null;\n member?: { id?: string | null; role?: string | null } | null;\n organization?: { metadata?: Record<string, unknown> | null; status?: string } | null;\n } | null;\n}\n\ntype OrgMetaType<TSchema extends z.ZodType | undefined> = TSchema extends z.ZodType\n ? z.infer<TSchema>\n : Record<string, unknown>;\n\nexport function createAuthMiddleware<\n TContext extends AuthContextShape = AuthContextShape,\n TOrgMetaSchema extends z.ZodType | undefined = undefined,\n>(builder: any, options?: { orgMetaSchema?: TOrgMetaSchema }) {\n // TContext is used as-is (no intersection) so the narrowed middleware\n // outputs stay exactly the workspace's own types when it passes its\n // AuthPluginContext — the constraint only guarantees the guards can read\n // the structural props.\n type Ctx = TContext;\n type TOrgMeta = OrgMetaType<TOrgMetaSchema>;\n type UserMiddleware = DecoratedMiddleware<\n Ctx,\n { userId: string; user: NonNullable<Ctx[\"user\"]> },\n any,\n any,\n any\n >;\n type OrgMiddleware = DecoratedMiddleware<\n Ctx,\n {\n userId: string;\n user: NonNullable<Ctx[\"user\"]>;\n organization: NonNullable<Ctx[\"organization\"]> & {\n activeOrganizationId: string;\n organization:\n | (NonNullable<NonNullable<Ctx[\"organization\"]>[\"organization\"]> & {\n metadata: TOrgMeta | null;\n })\n | null;\n };\n },\n any,\n any,\n any\n >;\n type MemberMiddleware = DecoratedMiddleware<\n Ctx,\n {\n userId: string;\n user: NonNullable<Ctx[\"user\"]>;\n organization: NonNullable<Ctx[\"organization\"]> & {\n activeOrganizationId: string;\n member: NonNullable<NonNullable<Ctx[\"organization\"]>[\"member\"]>;\n organization:\n | (NonNullable<NonNullable<Ctx[\"organization\"]>[\"organization\"]> & {\n metadata: TOrgMeta | null;\n })\n | null;\n };\n },\n any,\n any,\n any\n >;\n type ApiKeyMiddleware = DecoratedMiddleware<\n Ctx,\n { apiKey: NonNullable<Ctx[\"apiKey\"]> },\n any,\n any,\n any\n >;\n\n const requireAuth = builder.middleware(async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.user || !context.userId) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"Authentication required\",\n data: { hint: \"Sign in to continue\" },\n });\n }\n return next({ context: { userId: context.userId, user: context.user } });\n }) as UserMiddleware;\n\n const requireAuthOrApiKey = builder.middleware(\n async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.user && !context.userId && !context.apiKey) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"Authentication required\",\n data: { hint: \"Sign in or provide an API key\" },\n });\n }\n return next({ context });\n },\n ) as DecoratedMiddleware<Ctx, Record<string, never>, any, any, any>;\n\n const requireRole = <TRoles extends readonly string[]>(...roles: TRoles) =>\n builder.middleware(async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.user || !context.userId) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"Authentication required\",\n data: { authType: \"session\", hint: \"Sign in to continue\" },\n });\n }\n const currentRole = context.user.role;\n if (!currentRole || !roles.includes(currentRole)) {\n throw new ORPCError(\"FORBIDDEN\", {\n message: `Requires role: ${roles.join(\" or \")}`,\n data: { requiredRoles: roles, currentRole },\n });\n }\n return next({ context: { userId: context.userId, user: context.user } });\n }) as UserMiddleware;\n\n const requireAdmin = requireRole(\"admin\");\n\n const requireOrganization = builder.middleware(\n async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.user || !context.userId) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"Authentication required\",\n data: { authType: \"session\", hint: \"Sign in to continue\" },\n });\n }\n if (!context.organization?.activeOrganizationId) {\n throw new ORPCError(\"FORBIDDEN\", {\n message: \"Active organization required\",\n data: { hint: \"Select or create an organization\" },\n });\n }\n const org = context.organization;\n assertOrganizationApproval(org.organization);\n return next({\n context: {\n userId: context.userId,\n user: context.user,\n organization: {\n ...org,\n activeOrganizationId: org.activeOrganizationId,\n organization: org.organization\n ? {\n ...org.organization,\n metadata: parseOrgMetadata(org.organization.metadata, options?.orgMetaSchema),\n }\n : null,\n },\n },\n });\n },\n ) as OrgMiddleware;\n\n const requireOrgRole = <TRoles extends readonly string[]>(...roles: TRoles) =>\n builder.middleware(async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.user || !context.userId) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"Authentication required\",\n data: { authType: \"session\", hint: \"Sign in to continue\" },\n });\n }\n if (!context.organization?.activeOrganizationId) {\n throw new ORPCError(\"FORBIDDEN\", {\n message: \"Active organization required\",\n data: { hint: \"Select or create an organization\" },\n });\n }\n const member = context.organization?.member;\n if (!member?.id || !member?.role || !roles.includes(member.role)) {\n throw new ORPCError(\"FORBIDDEN\", {\n message: `Requires organization role: ${roles.join(\" or \")}`,\n data: { requiredRoles: roles, currentRole: member?.role ?? null },\n });\n }\n const org = context.organization;\n assertOrganizationApproval(org.organization);\n return next({\n context: {\n userId: context.userId,\n user: context.user,\n organization: {\n ...org,\n activeOrganizationId: org.activeOrganizationId,\n member: { id: member.id, role: member.role },\n organization: org.organization\n ? {\n ...org.organization,\n metadata: parseOrgMetadata(org.organization.metadata, options?.orgMetaSchema),\n }\n : null,\n },\n },\n });\n }) as MemberMiddleware;\n\n const requireApiKey = (requiredPermissions?: Record<string, string[]>) =>\n builder.middleware(async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.apiKey) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"API key required\",\n data: { authType: \"apiKey\", hint: \"Provide a valid API key via x-api-key header\" },\n });\n }\n if (requiredPermissions) {\n const keyPerms = context.apiKey.permissions ?? {};\n for (const [resource, actions] of Object.entries(requiredPermissions)) {\n const allowed = keyPerms[resource] ?? [];\n const missing = actions.filter((a: string) => !allowed.includes(a));\n if (missing.length > 0) {\n throw new ORPCError(\"FORBIDDEN\", {\n message: `API key lacks permission: ${resource}:${missing.join(\",\")}`,\n data: { requiredPermissions, keyPermissions: keyPerms },\n });\n }\n }\n }\n return next({ context: { apiKey: context.apiKey } });\n }) as ApiKeyMiddleware;\n\n return {\n requireAuth,\n requireAuthOrApiKey,\n requireRole,\n requireAdmin,\n requireOrganization,\n requireOrgRole,\n requireApiKey,\n };\n}\n\nfunction assertOrganizationApproval(\n organization: NonNullable<AuthContextShape[\"organization\"]>[\"organization\"],\n) {\n if (organization?.status && organization.status !== \"active\") {\n throw new ORPCError(\"FORBIDDEN\", {\n message: \"Organization requires platform-admin approval\",\n });\n }\n}\n\nfunction parseOrgMetadata<TSchema extends z.ZodType | undefined>(\n raw: Record<string, unknown> | null | undefined,\n schema: TSchema | undefined,\n): OrgMetaType<TSchema> | null {\n if (!raw) return null;\n if (!schema) return raw as OrgMetaType<TSchema>;\n const result = schema.safeParse(raw);\n if (result.success) return result.data as OrgMetaType<TSchema>;\n throw new ORPCError(\"INTERNAL_SERVER_ERROR\", {\n message: \"Invalid organization metadata\",\n data: { errors: result.error.issues },\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"auth-middleware.cjs","names":["ORPCError"],"sources":["../../src/api/auth-middleware.ts"],"sourcesContent":["/**\n * Server-side auth middleware factory — framework home for the factory that\n * used to live as five near-identical copies (`api/src/lib/auth.ts` and each\n * plugin's `src/lib/auth.ts`). See citynode.app#207.\n *\n * Generic over the workspace's auth context (the generated `AuthPluginContext`\n * satisfies `AuthContextShape`), so each workspace imports the same factory and\n * passes its own context type for precise middleware narrowing:\n *\n * const { requireAuth, requireAdmin, ... } = createAuthMiddleware<AuthContext>(builder);\n */\nimport type { DecoratedMiddleware } from \"@orpc/server\";\nimport { ORPCError } from \"@orpc/server\";\nimport type { z } from \"zod\";\n\n/** Structural minimum the middlewares read — the generated `AuthPluginContext` satisfies it. */\nexport interface AuthContextShape {\n user?: { role?: string | null } | null;\n userId?: string | null;\n apiKey?: { permissions?: Record<string, string[]> | null } | null;\n permissions?: Record<string, string[]> | null;\n organization?: {\n activeOrganizationId?: string | null;\n member?: { id?: string | null; role?: string | null } | null;\n organization?: { metadata?: Record<string, unknown> | null; status?: string } | null;\n teams?: Array<{ id: string; permissions?: Record<string, string[]> }> | null;\n } | null;\n}\n\ntype OrgMetaType<TSchema extends z.ZodType | undefined> = TSchema extends z.ZodType\n ? z.infer<TSchema>\n : Record<string, unknown>;\n\nexport function createAuthMiddleware<\n TContext extends AuthContextShape = AuthContextShape,\n TOrgMetaSchema extends z.ZodType | undefined = undefined,\n>(builder: any, options?: { orgMetaSchema?: TOrgMetaSchema }) {\n // TContext is used as-is (no intersection) so the narrowed middleware\n // outputs stay exactly the workspace's own types when it passes its\n // AuthPluginContext — the constraint only guarantees the guards can read\n // the structural props.\n type Ctx = TContext;\n type TOrgMeta = OrgMetaType<TOrgMetaSchema>;\n type UserMiddleware = DecoratedMiddleware<\n Ctx,\n { userId: string; user: NonNullable<Ctx[\"user\"]> },\n any,\n any,\n any\n >;\n type OrgMiddleware = DecoratedMiddleware<\n Ctx,\n {\n userId: string;\n user: NonNullable<Ctx[\"user\"]>;\n organization: NonNullable<Ctx[\"organization\"]> & {\n activeOrganizationId: string;\n organization:\n | (NonNullable<NonNullable<Ctx[\"organization\"]>[\"organization\"]> & {\n metadata: TOrgMeta | null;\n })\n | null;\n };\n },\n any,\n any,\n any\n >;\n type MemberMiddleware = DecoratedMiddleware<\n Ctx,\n {\n userId: string;\n user: NonNullable<Ctx[\"user\"]>;\n organization: NonNullable<Ctx[\"organization\"]> & {\n activeOrganizationId: string;\n member: NonNullable<NonNullable<Ctx[\"organization\"]>[\"member\"]>;\n organization:\n | (NonNullable<NonNullable<Ctx[\"organization\"]>[\"organization\"]> & {\n metadata: TOrgMeta | null;\n })\n | null;\n };\n },\n any,\n any,\n any\n >;\n type ApiKeyMiddleware = DecoratedMiddleware<\n Ctx,\n { apiKey: NonNullable<Ctx[\"apiKey\"]> },\n any,\n any,\n any\n >;\n\n const requireAuth = builder.middleware(async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.user || !context.userId) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"Authentication required\",\n data: { hint: \"Sign in to continue\" },\n });\n }\n return next({ context: { userId: context.userId, user: context.user } });\n }) as UserMiddleware;\n\n const requireAuthOrApiKey = builder.middleware(\n async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.user && !context.userId && !context.apiKey) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"Authentication required\",\n data: { hint: \"Sign in or provide an API key\" },\n });\n }\n return next({ context });\n },\n ) as DecoratedMiddleware<Ctx, Record<string, never>, any, any, any>;\n\n const requireRole = <TRoles extends readonly string[]>(...roles: TRoles) =>\n builder.middleware(async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.user || !context.userId) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"Authentication required\",\n data: { authType: \"session\", hint: \"Sign in to continue\" },\n });\n }\n const currentRole = context.user.role;\n if (!currentRole || !roles.includes(currentRole)) {\n throw new ORPCError(\"FORBIDDEN\", {\n message: `Requires role: ${roles.join(\" or \")}`,\n data: { requiredRoles: roles, currentRole },\n });\n }\n return next({ context: { userId: context.userId, user: context.user } });\n }) as UserMiddleware;\n\n const requireAdmin = requireRole(\"admin\");\n\n const requireOrganization = builder.middleware(\n async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.user || !context.userId) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"Authentication required\",\n data: { authType: \"session\", hint: \"Sign in to continue\" },\n });\n }\n if (!context.organization?.activeOrganizationId) {\n throw new ORPCError(\"FORBIDDEN\", {\n message: \"Active organization required\",\n data: { hint: \"Select or create an organization\" },\n });\n }\n const org = context.organization;\n assertOrganizationApproval(org.organization);\n return next({\n context: {\n userId: context.userId,\n user: context.user,\n organization: {\n ...org,\n activeOrganizationId: org.activeOrganizationId,\n organization: org.organization\n ? {\n ...org.organization,\n metadata: parseOrgMetadata(org.organization.metadata, options?.orgMetaSchema),\n }\n : null,\n },\n },\n });\n },\n ) as OrgMiddleware;\n\n const requireOrgRole = <TRoles extends readonly string[]>(...roles: TRoles) =>\n builder.middleware(async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.user || !context.userId) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"Authentication required\",\n data: { authType: \"session\", hint: \"Sign in to continue\" },\n });\n }\n if (!context.organization?.activeOrganizationId) {\n throw new ORPCError(\"FORBIDDEN\", {\n message: \"Active organization required\",\n data: { hint: \"Select or create an organization\" },\n });\n }\n const member = context.organization?.member;\n if (!member?.id || !member?.role || !roles.includes(member.role)) {\n throw new ORPCError(\"FORBIDDEN\", {\n message: `Requires organization role: ${roles.join(\" or \")}`,\n data: { requiredRoles: roles, currentRole: member?.role ?? null },\n });\n }\n const org = context.organization;\n assertOrganizationApproval(org.organization);\n return next({\n context: {\n userId: context.userId,\n user: context.user,\n organization: {\n ...org,\n activeOrganizationId: org.activeOrganizationId,\n member: { id: member.id, role: member.role },\n organization: org.organization\n ? {\n ...org.organization,\n metadata: parseOrgMetadata(org.organization.metadata, options?.orgMetaSchema),\n }\n : null,\n },\n },\n });\n }) as MemberMiddleware;\n\n const requireApiKey = (requiredPermissions?: Record<string, string[]>) =>\n builder.middleware(async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.apiKey) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"API key required\",\n data: { authType: \"apiKey\", hint: \"Provide a valid API key via x-api-key header\" },\n });\n }\n if (requiredPermissions) {\n const keyPerms = context.apiKey.permissions ?? {};\n for (const [resource, actions] of Object.entries(requiredPermissions)) {\n const allowed = keyPerms[resource] ?? [];\n const missing = actions.filter((a: string) => !allowed.includes(a));\n if (missing.length > 0) {\n throw new ORPCError(\"FORBIDDEN\", {\n message: `API key lacks permission: ${resource}:${missing.join(\",\")}`,\n data: { requiredPermissions, keyPermissions: keyPerms },\n });\n }\n }\n }\n return next({ context: { apiKey: context.apiKey } });\n }) as ApiKeyMiddleware;\n\n const requirePermission = (\n resource: string,\n action: string,\n scope?: { scope?: \"organization\" | \"team\"; teamId?: (input: any) => string | undefined },\n ) =>\n builder.middleware(async ({ context, next }: { context: Ctx; next: any }, input: any) => {\n let granted: string[] | undefined;\n if (scope?.scope === \"team\") {\n const teamId = scope.teamId?.(input);\n const team = context.organization?.teams?.find((t) => t.id === teamId);\n granted = team?.permissions?.[resource];\n } else {\n granted = context.permissions?.[resource];\n }\n if (!granted?.includes(action)) {\n throw new ORPCError(\"FORBIDDEN\", {\n message: `Requires permission: ${resource}:${action}`,\n data: { requiredPermissions: [`${resource}:${action}`] },\n });\n }\n return next({ context });\n }) as DecoratedMiddleware<Ctx, Record<string, never>, any, any, any>;\n\n return {\n requireAuth,\n requireAuthOrApiKey,\n requireRole,\n requireAdmin,\n requireOrganization,\n requireOrgRole,\n requireApiKey,\n requirePermission,\n };\n}\n\nfunction assertOrganizationApproval(\n organization: NonNullable<AuthContextShape[\"organization\"]>[\"organization\"],\n) {\n if (organization?.status && organization.status !== \"active\") {\n throw new ORPCError(\"FORBIDDEN\", {\n message: \"Organization requires platform-admin approval\",\n });\n }\n}\n\nfunction parseOrgMetadata<TSchema extends z.ZodType | undefined>(\n raw: Record<string, unknown> | null | undefined,\n schema: TSchema | undefined,\n): OrgMetaType<TSchema> | null {\n if (!raw) return null;\n if (!schema) return raw as OrgMetaType<TSchema>;\n const result = schema.safeParse(raw);\n if (result.success) return result.data as OrgMetaType<TSchema>;\n throw new ORPCError(\"INTERNAL_SERVER_ERROR\", {\n message: \"Invalid organization metadata\",\n data: { errors: result.error.issues },\n });\n}\n"],"mappings":"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"}
|
|
@@ -10,6 +10,7 @@ interface AuthContextShape {
|
|
|
10
10
|
apiKey?: {
|
|
11
11
|
permissions?: Record<string, string[]> | null;
|
|
12
12
|
} | null;
|
|
13
|
+
permissions?: Record<string, string[]> | null;
|
|
13
14
|
organization?: {
|
|
14
15
|
activeOrganizationId?: string | null;
|
|
15
16
|
member?: {
|
|
@@ -20,6 +21,10 @@ interface AuthContextShape {
|
|
|
20
21
|
metadata?: Record<string, unknown> | null;
|
|
21
22
|
status?: string;
|
|
22
23
|
} | null;
|
|
24
|
+
teams?: Array<{
|
|
25
|
+
id: string;
|
|
26
|
+
permissions?: Record<string, string[]>;
|
|
27
|
+
}> | null;
|
|
23
28
|
} | null;
|
|
24
29
|
}
|
|
25
30
|
type OrgMetaType<TSchema extends z.ZodType | undefined> = TSchema extends z.ZodType ? z.infer<TSchema> : Record<string, unknown>;
|
|
@@ -63,6 +68,10 @@ declare function createAuthMiddleware<TContext extends AuthContextShape = AuthCo
|
|
|
63
68
|
requireApiKey: (requiredPermissions?: Record<string, string[]>) => DecoratedMiddleware<TContext, {
|
|
64
69
|
apiKey: NonNullable<TContext["apiKey"]>;
|
|
65
70
|
}, any, any, any>;
|
|
71
|
+
requirePermission: (resource: string, action: string, scope?: {
|
|
72
|
+
scope?: "organization" | "team";
|
|
73
|
+
teamId?: (input: any) => string | undefined;
|
|
74
|
+
}) => DecoratedMiddleware<TContext, Record<string, never>, any, any, any>;
|
|
66
75
|
};
|
|
67
76
|
//#endregion
|
|
68
77
|
export { AuthContextShape, createAuthMiddleware };
|
|
@@ -10,6 +10,7 @@ interface AuthContextShape {
|
|
|
10
10
|
apiKey?: {
|
|
11
11
|
permissions?: Record<string, string[]> | null;
|
|
12
12
|
} | null;
|
|
13
|
+
permissions?: Record<string, string[]> | null;
|
|
13
14
|
organization?: {
|
|
14
15
|
activeOrganizationId?: string | null;
|
|
15
16
|
member?: {
|
|
@@ -20,6 +21,10 @@ interface AuthContextShape {
|
|
|
20
21
|
metadata?: Record<string, unknown> | null;
|
|
21
22
|
status?: string;
|
|
22
23
|
} | null;
|
|
24
|
+
teams?: Array<{
|
|
25
|
+
id: string;
|
|
26
|
+
permissions?: Record<string, string[]>;
|
|
27
|
+
}> | null;
|
|
23
28
|
} | null;
|
|
24
29
|
}
|
|
25
30
|
type OrgMetaType<TSchema extends z.ZodType | undefined> = TSchema extends z.ZodType ? z.infer<TSchema> : Record<string, unknown>;
|
|
@@ -63,6 +68,10 @@ declare function createAuthMiddleware<TContext extends AuthContextShape = AuthCo
|
|
|
63
68
|
requireApiKey: (requiredPermissions?: Record<string, string[]>) => DecoratedMiddleware<TContext, {
|
|
64
69
|
apiKey: NonNullable<TContext["apiKey"]>;
|
|
65
70
|
}, any, any, any>;
|
|
71
|
+
requirePermission: (resource: string, action: string, scope?: {
|
|
72
|
+
scope?: "organization" | "team";
|
|
73
|
+
teamId?: (input: any) => string | undefined;
|
|
74
|
+
}) => DecoratedMiddleware<TContext, Record<string, never>, any, any, any>;
|
|
66
75
|
};
|
|
67
76
|
//#endregion
|
|
68
77
|
export { AuthContextShape, createAuthMiddleware };
|
|
@@ -131,6 +131,18 @@ function createAuthMiddleware(builder, options) {
|
|
|
131
131
|
}
|
|
132
132
|
return next({ context: { apiKey: context.apiKey } });
|
|
133
133
|
});
|
|
134
|
+
const requirePermission = (resource, action, scope) => builder.middleware(async ({ context, next }, input) => {
|
|
135
|
+
let granted;
|
|
136
|
+
if (scope?.scope === "team") {
|
|
137
|
+
const teamId = scope.teamId?.(input);
|
|
138
|
+
granted = (context.organization?.teams?.find((t) => t.id === teamId))?.permissions?.[resource];
|
|
139
|
+
} else granted = context.permissions?.[resource];
|
|
140
|
+
if (!granted?.includes(action)) throw new ORPCError("FORBIDDEN", {
|
|
141
|
+
message: `Requires permission: ${resource}:${action}`,
|
|
142
|
+
data: { requiredPermissions: [`${resource}:${action}`] }
|
|
143
|
+
});
|
|
144
|
+
return next({ context });
|
|
145
|
+
});
|
|
134
146
|
return {
|
|
135
147
|
requireAuth,
|
|
136
148
|
requireAuthOrApiKey,
|
|
@@ -138,7 +150,8 @@ function createAuthMiddleware(builder, options) {
|
|
|
138
150
|
requireAdmin,
|
|
139
151
|
requireOrganization,
|
|
140
152
|
requireOrgRole,
|
|
141
|
-
requireApiKey
|
|
153
|
+
requireApiKey,
|
|
154
|
+
requirePermission
|
|
142
155
|
};
|
|
143
156
|
}
|
|
144
157
|
function assertOrganizationApproval(organization) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-middleware.mjs","names":[],"sources":["../../src/api/auth-middleware.ts"],"sourcesContent":["/**\n * Server-side auth middleware factory — framework home for the factory that\n * used to live as five near-identical copies (`api/src/lib/auth.ts` and each\n * plugin's `src/lib/auth.ts`). See citynode.app#207.\n *\n * Generic over the workspace's auth context (the generated `AuthPluginContext`\n * satisfies `AuthContextShape`), so each workspace imports the same factory and\n * passes its own context type for precise middleware narrowing:\n *\n * const { requireAuth, requireAdmin, ... } = createAuthMiddleware<AuthContext>(builder);\n */\nimport type { DecoratedMiddleware } from \"@orpc/server\";\nimport { ORPCError } from \"@orpc/server\";\nimport type { z } from \"zod\";\n\n/** Structural minimum the middlewares read — the generated `AuthPluginContext` satisfies it. */\nexport interface AuthContextShape {\n user?: { role?: string | null } | null;\n userId?: string | null;\n apiKey?: { permissions?: Record<string, string[]> | null } | null;\n organization?: {\n activeOrganizationId?: string | null;\n member?: { id?: string | null; role?: string | null } | null;\n organization?: { metadata?: Record<string, unknown> | null; status?: string } | null;\n } | null;\n}\n\ntype OrgMetaType<TSchema extends z.ZodType | undefined> = TSchema extends z.ZodType\n ? z.infer<TSchema>\n : Record<string, unknown>;\n\nexport function createAuthMiddleware<\n TContext extends AuthContextShape = AuthContextShape,\n TOrgMetaSchema extends z.ZodType | undefined = undefined,\n>(builder: any, options?: { orgMetaSchema?: TOrgMetaSchema }) {\n // TContext is used as-is (no intersection) so the narrowed middleware\n // outputs stay exactly the workspace's own types when it passes its\n // AuthPluginContext — the constraint only guarantees the guards can read\n // the structural props.\n type Ctx = TContext;\n type TOrgMeta = OrgMetaType<TOrgMetaSchema>;\n type UserMiddleware = DecoratedMiddleware<\n Ctx,\n { userId: string; user: NonNullable<Ctx[\"user\"]> },\n any,\n any,\n any\n >;\n type OrgMiddleware = DecoratedMiddleware<\n Ctx,\n {\n userId: string;\n user: NonNullable<Ctx[\"user\"]>;\n organization: NonNullable<Ctx[\"organization\"]> & {\n activeOrganizationId: string;\n organization:\n | (NonNullable<NonNullable<Ctx[\"organization\"]>[\"organization\"]> & {\n metadata: TOrgMeta | null;\n })\n | null;\n };\n },\n any,\n any,\n any\n >;\n type MemberMiddleware = DecoratedMiddleware<\n Ctx,\n {\n userId: string;\n user: NonNullable<Ctx[\"user\"]>;\n organization: NonNullable<Ctx[\"organization\"]> & {\n activeOrganizationId: string;\n member: NonNullable<NonNullable<Ctx[\"organization\"]>[\"member\"]>;\n organization:\n | (NonNullable<NonNullable<Ctx[\"organization\"]>[\"organization\"]> & {\n metadata: TOrgMeta | null;\n })\n | null;\n };\n },\n any,\n any,\n any\n >;\n type ApiKeyMiddleware = DecoratedMiddleware<\n Ctx,\n { apiKey: NonNullable<Ctx[\"apiKey\"]> },\n any,\n any,\n any\n >;\n\n const requireAuth = builder.middleware(async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.user || !context.userId) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"Authentication required\",\n data: { hint: \"Sign in to continue\" },\n });\n }\n return next({ context: { userId: context.userId, user: context.user } });\n }) as UserMiddleware;\n\n const requireAuthOrApiKey = builder.middleware(\n async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.user && !context.userId && !context.apiKey) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"Authentication required\",\n data: { hint: \"Sign in or provide an API key\" },\n });\n }\n return next({ context });\n },\n ) as DecoratedMiddleware<Ctx, Record<string, never>, any, any, any>;\n\n const requireRole = <TRoles extends readonly string[]>(...roles: TRoles) =>\n builder.middleware(async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.user || !context.userId) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"Authentication required\",\n data: { authType: \"session\", hint: \"Sign in to continue\" },\n });\n }\n const currentRole = context.user.role;\n if (!currentRole || !roles.includes(currentRole)) {\n throw new ORPCError(\"FORBIDDEN\", {\n message: `Requires role: ${roles.join(\" or \")}`,\n data: { requiredRoles: roles, currentRole },\n });\n }\n return next({ context: { userId: context.userId, user: context.user } });\n }) as UserMiddleware;\n\n const requireAdmin = requireRole(\"admin\");\n\n const requireOrganization = builder.middleware(\n async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.user || !context.userId) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"Authentication required\",\n data: { authType: \"session\", hint: \"Sign in to continue\" },\n });\n }\n if (!context.organization?.activeOrganizationId) {\n throw new ORPCError(\"FORBIDDEN\", {\n message: \"Active organization required\",\n data: { hint: \"Select or create an organization\" },\n });\n }\n const org = context.organization;\n assertOrganizationApproval(org.organization);\n return next({\n context: {\n userId: context.userId,\n user: context.user,\n organization: {\n ...org,\n activeOrganizationId: org.activeOrganizationId,\n organization: org.organization\n ? {\n ...org.organization,\n metadata: parseOrgMetadata(org.organization.metadata, options?.orgMetaSchema),\n }\n : null,\n },\n },\n });\n },\n ) as OrgMiddleware;\n\n const requireOrgRole = <TRoles extends readonly string[]>(...roles: TRoles) =>\n builder.middleware(async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.user || !context.userId) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"Authentication required\",\n data: { authType: \"session\", hint: \"Sign in to continue\" },\n });\n }\n if (!context.organization?.activeOrganizationId) {\n throw new ORPCError(\"FORBIDDEN\", {\n message: \"Active organization required\",\n data: { hint: \"Select or create an organization\" },\n });\n }\n const member = context.organization?.member;\n if (!member?.id || !member?.role || !roles.includes(member.role)) {\n throw new ORPCError(\"FORBIDDEN\", {\n message: `Requires organization role: ${roles.join(\" or \")}`,\n data: { requiredRoles: roles, currentRole: member?.role ?? null },\n });\n }\n const org = context.organization;\n assertOrganizationApproval(org.organization);\n return next({\n context: {\n userId: context.userId,\n user: context.user,\n organization: {\n ...org,\n activeOrganizationId: org.activeOrganizationId,\n member: { id: member.id, role: member.role },\n organization: org.organization\n ? {\n ...org.organization,\n metadata: parseOrgMetadata(org.organization.metadata, options?.orgMetaSchema),\n }\n : null,\n },\n },\n });\n }) as MemberMiddleware;\n\n const requireApiKey = (requiredPermissions?: Record<string, string[]>) =>\n builder.middleware(async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.apiKey) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"API key required\",\n data: { authType: \"apiKey\", hint: \"Provide a valid API key via x-api-key header\" },\n });\n }\n if (requiredPermissions) {\n const keyPerms = context.apiKey.permissions ?? {};\n for (const [resource, actions] of Object.entries(requiredPermissions)) {\n const allowed = keyPerms[resource] ?? [];\n const missing = actions.filter((a: string) => !allowed.includes(a));\n if (missing.length > 0) {\n throw new ORPCError(\"FORBIDDEN\", {\n message: `API key lacks permission: ${resource}:${missing.join(\",\")}`,\n data: { requiredPermissions, keyPermissions: keyPerms },\n });\n }\n }\n }\n return next({ context: { apiKey: context.apiKey } });\n }) as ApiKeyMiddleware;\n\n return {\n requireAuth,\n requireAuthOrApiKey,\n requireRole,\n requireAdmin,\n requireOrganization,\n requireOrgRole,\n requireApiKey,\n };\n}\n\nfunction assertOrganizationApproval(\n organization: NonNullable<AuthContextShape[\"organization\"]>[\"organization\"],\n) {\n if (organization?.status && organization.status !== \"active\") {\n throw new ORPCError(\"FORBIDDEN\", {\n message: \"Organization requires platform-admin approval\",\n });\n }\n}\n\nfunction parseOrgMetadata<TSchema extends z.ZodType | undefined>(\n raw: Record<string, unknown> | null | undefined,\n schema: TSchema | undefined,\n): OrgMetaType<TSchema> | null {\n if (!raw) return null;\n if (!schema) return raw as OrgMetaType<TSchema>;\n const result = schema.safeParse(raw);\n if (result.success) return result.data as OrgMetaType<TSchema>;\n throw new ORPCError(\"INTERNAL_SERVER_ERROR\", {\n message: \"Invalid organization metadata\",\n data: { errors: result.error.issues },\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"auth-middleware.mjs","names":[],"sources":["../../src/api/auth-middleware.ts"],"sourcesContent":["/**\n * Server-side auth middleware factory — framework home for the factory that\n * used to live as five near-identical copies (`api/src/lib/auth.ts` and each\n * plugin's `src/lib/auth.ts`). See citynode.app#207.\n *\n * Generic over the workspace's auth context (the generated `AuthPluginContext`\n * satisfies `AuthContextShape`), so each workspace imports the same factory and\n * passes its own context type for precise middleware narrowing:\n *\n * const { requireAuth, requireAdmin, ... } = createAuthMiddleware<AuthContext>(builder);\n */\nimport type { DecoratedMiddleware } from \"@orpc/server\";\nimport { ORPCError } from \"@orpc/server\";\nimport type { z } from \"zod\";\n\n/** Structural minimum the middlewares read — the generated `AuthPluginContext` satisfies it. */\nexport interface AuthContextShape {\n user?: { role?: string | null } | null;\n userId?: string | null;\n apiKey?: { permissions?: Record<string, string[]> | null } | null;\n permissions?: Record<string, string[]> | null;\n organization?: {\n activeOrganizationId?: string | null;\n member?: { id?: string | null; role?: string | null } | null;\n organization?: { metadata?: Record<string, unknown> | null; status?: string } | null;\n teams?: Array<{ id: string; permissions?: Record<string, string[]> }> | null;\n } | null;\n}\n\ntype OrgMetaType<TSchema extends z.ZodType | undefined> = TSchema extends z.ZodType\n ? z.infer<TSchema>\n : Record<string, unknown>;\n\nexport function createAuthMiddleware<\n TContext extends AuthContextShape = AuthContextShape,\n TOrgMetaSchema extends z.ZodType | undefined = undefined,\n>(builder: any, options?: { orgMetaSchema?: TOrgMetaSchema }) {\n // TContext is used as-is (no intersection) so the narrowed middleware\n // outputs stay exactly the workspace's own types when it passes its\n // AuthPluginContext — the constraint only guarantees the guards can read\n // the structural props.\n type Ctx = TContext;\n type TOrgMeta = OrgMetaType<TOrgMetaSchema>;\n type UserMiddleware = DecoratedMiddleware<\n Ctx,\n { userId: string; user: NonNullable<Ctx[\"user\"]> },\n any,\n any,\n any\n >;\n type OrgMiddleware = DecoratedMiddleware<\n Ctx,\n {\n userId: string;\n user: NonNullable<Ctx[\"user\"]>;\n organization: NonNullable<Ctx[\"organization\"]> & {\n activeOrganizationId: string;\n organization:\n | (NonNullable<NonNullable<Ctx[\"organization\"]>[\"organization\"]> & {\n metadata: TOrgMeta | null;\n })\n | null;\n };\n },\n any,\n any,\n any\n >;\n type MemberMiddleware = DecoratedMiddleware<\n Ctx,\n {\n userId: string;\n user: NonNullable<Ctx[\"user\"]>;\n organization: NonNullable<Ctx[\"organization\"]> & {\n activeOrganizationId: string;\n member: NonNullable<NonNullable<Ctx[\"organization\"]>[\"member\"]>;\n organization:\n | (NonNullable<NonNullable<Ctx[\"organization\"]>[\"organization\"]> & {\n metadata: TOrgMeta | null;\n })\n | null;\n };\n },\n any,\n any,\n any\n >;\n type ApiKeyMiddleware = DecoratedMiddleware<\n Ctx,\n { apiKey: NonNullable<Ctx[\"apiKey\"]> },\n any,\n any,\n any\n >;\n\n const requireAuth = builder.middleware(async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.user || !context.userId) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"Authentication required\",\n data: { hint: \"Sign in to continue\" },\n });\n }\n return next({ context: { userId: context.userId, user: context.user } });\n }) as UserMiddleware;\n\n const requireAuthOrApiKey = builder.middleware(\n async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.user && !context.userId && !context.apiKey) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"Authentication required\",\n data: { hint: \"Sign in or provide an API key\" },\n });\n }\n return next({ context });\n },\n ) as DecoratedMiddleware<Ctx, Record<string, never>, any, any, any>;\n\n const requireRole = <TRoles extends readonly string[]>(...roles: TRoles) =>\n builder.middleware(async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.user || !context.userId) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"Authentication required\",\n data: { authType: \"session\", hint: \"Sign in to continue\" },\n });\n }\n const currentRole = context.user.role;\n if (!currentRole || !roles.includes(currentRole)) {\n throw new ORPCError(\"FORBIDDEN\", {\n message: `Requires role: ${roles.join(\" or \")}`,\n data: { requiredRoles: roles, currentRole },\n });\n }\n return next({ context: { userId: context.userId, user: context.user } });\n }) as UserMiddleware;\n\n const requireAdmin = requireRole(\"admin\");\n\n const requireOrganization = builder.middleware(\n async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.user || !context.userId) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"Authentication required\",\n data: { authType: \"session\", hint: \"Sign in to continue\" },\n });\n }\n if (!context.organization?.activeOrganizationId) {\n throw new ORPCError(\"FORBIDDEN\", {\n message: \"Active organization required\",\n data: { hint: \"Select or create an organization\" },\n });\n }\n const org = context.organization;\n assertOrganizationApproval(org.organization);\n return next({\n context: {\n userId: context.userId,\n user: context.user,\n organization: {\n ...org,\n activeOrganizationId: org.activeOrganizationId,\n organization: org.organization\n ? {\n ...org.organization,\n metadata: parseOrgMetadata(org.organization.metadata, options?.orgMetaSchema),\n }\n : null,\n },\n },\n });\n },\n ) as OrgMiddleware;\n\n const requireOrgRole = <TRoles extends readonly string[]>(...roles: TRoles) =>\n builder.middleware(async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.user || !context.userId) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"Authentication required\",\n data: { authType: \"session\", hint: \"Sign in to continue\" },\n });\n }\n if (!context.organization?.activeOrganizationId) {\n throw new ORPCError(\"FORBIDDEN\", {\n message: \"Active organization required\",\n data: { hint: \"Select or create an organization\" },\n });\n }\n const member = context.organization?.member;\n if (!member?.id || !member?.role || !roles.includes(member.role)) {\n throw new ORPCError(\"FORBIDDEN\", {\n message: `Requires organization role: ${roles.join(\" or \")}`,\n data: { requiredRoles: roles, currentRole: member?.role ?? null },\n });\n }\n const org = context.organization;\n assertOrganizationApproval(org.organization);\n return next({\n context: {\n userId: context.userId,\n user: context.user,\n organization: {\n ...org,\n activeOrganizationId: org.activeOrganizationId,\n member: { id: member.id, role: member.role },\n organization: org.organization\n ? {\n ...org.organization,\n metadata: parseOrgMetadata(org.organization.metadata, options?.orgMetaSchema),\n }\n : null,\n },\n },\n });\n }) as MemberMiddleware;\n\n const requireApiKey = (requiredPermissions?: Record<string, string[]>) =>\n builder.middleware(async ({ context, next }: { context: Ctx; next: any }) => {\n if (!context.apiKey) {\n throw new ORPCError(\"UNAUTHORIZED\", {\n message: \"API key required\",\n data: { authType: \"apiKey\", hint: \"Provide a valid API key via x-api-key header\" },\n });\n }\n if (requiredPermissions) {\n const keyPerms = context.apiKey.permissions ?? {};\n for (const [resource, actions] of Object.entries(requiredPermissions)) {\n const allowed = keyPerms[resource] ?? [];\n const missing = actions.filter((a: string) => !allowed.includes(a));\n if (missing.length > 0) {\n throw new ORPCError(\"FORBIDDEN\", {\n message: `API key lacks permission: ${resource}:${missing.join(\",\")}`,\n data: { requiredPermissions, keyPermissions: keyPerms },\n });\n }\n }\n }\n return next({ context: { apiKey: context.apiKey } });\n }) as ApiKeyMiddleware;\n\n const requirePermission = (\n resource: string,\n action: string,\n scope?: { scope?: \"organization\" | \"team\"; teamId?: (input: any) => string | undefined },\n ) =>\n builder.middleware(async ({ context, next }: { context: Ctx; next: any }, input: any) => {\n let granted: string[] | undefined;\n if (scope?.scope === \"team\") {\n const teamId = scope.teamId?.(input);\n const team = context.organization?.teams?.find((t) => t.id === teamId);\n granted = team?.permissions?.[resource];\n } else {\n granted = context.permissions?.[resource];\n }\n if (!granted?.includes(action)) {\n throw new ORPCError(\"FORBIDDEN\", {\n message: `Requires permission: ${resource}:${action}`,\n data: { requiredPermissions: [`${resource}:${action}`] },\n });\n }\n return next({ context });\n }) as DecoratedMiddleware<Ctx, Record<string, never>, any, any, any>;\n\n return {\n requireAuth,\n requireAuthOrApiKey,\n requireRole,\n requireAdmin,\n requireOrganization,\n requireOrgRole,\n requireApiKey,\n requirePermission,\n };\n}\n\nfunction assertOrganizationApproval(\n organization: NonNullable<AuthContextShape[\"organization\"]>[\"organization\"],\n) {\n if (organization?.status && organization.status !== \"active\") {\n throw new ORPCError(\"FORBIDDEN\", {\n message: \"Organization requires platform-admin approval\",\n });\n }\n}\n\nfunction parseOrgMetadata<TSchema extends z.ZodType | undefined>(\n raw: Record<string, unknown> | null | undefined,\n schema: TSchema | undefined,\n): OrgMetaType<TSchema> | null {\n if (!raw) return null;\n if (!schema) return raw as OrgMetaType<TSchema>;\n const result = schema.safeParse(raw);\n if (result.success) return result.data as OrgMetaType<TSchema>;\n throw new ORPCError(\"INTERNAL_SERVER_ERROR\", {\n message: \"Invalid organization metadata\",\n data: { errors: result.error.issues },\n });\n}\n"],"mappings":"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"}
|
|
@@ -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":"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"}
|
package/dist/auth-login.cjs
CHANGED
|
@@ -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) {
|
package/dist/auth-login.cjs.map
CHANGED
|
@@ -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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"}
|