everything-dev 2.0.0-rc.6 → 2.0.0-rc.8
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/README.md +2 -2
- 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/build.cjs +1 -1
- package/dist/build.mjs +1 -1
- package/dist/bundle-cache.cjs +1 -1
- package/dist/bundle-cache.cjs.map +1 -1
- package/dist/bundle-cache.mjs +1 -1
- package/dist/bundle-cache.mjs.map +1 -1
- package/dist/cli/init.cjs +22 -10
- package/dist/cli/init.cjs.map +1 -1
- package/dist/cli/init.mjs +22 -10
- package/dist/cli/init.mjs.map +1 -1
- package/dist/cli.cjs +6 -5
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +5 -4
- package/dist/cli.mjs.map +1 -1
- package/dist/commands/deploy.cjs +111 -1
- package/dist/commands/deploy.cjs.map +1 -1
- package/dist/commands/deploy.mjs +112 -2
- package/dist/commands/deploy.mjs.map +1 -1
- package/dist/commands/keys.cjs +1 -1
- package/dist/commands/keys.cjs.map +1 -1
- package/dist/commands/keys.mjs +1 -1
- package/dist/commands/keys.mjs.map +1 -1
- package/dist/commands/smoke.cjs +41 -0
- package/dist/commands/smoke.cjs.map +1 -0
- package/dist/commands/smoke.mjs +41 -0
- package/dist/commands/smoke.mjs.map +1 -0
- package/dist/commands/upgrade.cjs +2 -2
- package/dist/commands/upgrade.cjs.map +1 -1
- package/dist/commands/upgrade.mjs +1 -1
- package/dist/commands/upgrade.mjs.map +1 -1
- package/dist/contract.cjs +27 -1
- package/dist/contract.cjs.map +1 -1
- package/dist/contract.d.cts +65 -1
- package/dist/contract.d.mts +65 -1
- package/dist/contract.meta.cjs +14 -2
- package/dist/contract.meta.cjs.map +1 -1
- package/dist/contract.meta.d.cts +24 -2
- package/dist/contract.meta.d.mts +24 -2
- package/dist/contract.meta.mjs +14 -2
- package/dist/contract.meta.mjs.map +1 -1
- package/dist/contract.mjs +26 -2
- package/dist/contract.mjs.map +1 -1
- 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/dev-log-pipeline.cjs +6 -0
- package/dist/dev-log-pipeline.cjs.map +1 -1
- package/dist/dev-log-pipeline.mjs +6 -0
- package/dist/dev-log-pipeline.mjs.map +1 -1
- package/dist/dev-program.cjs +20 -3
- package/dist/dev-program.cjs.map +1 -1
- package/dist/dev-program.mjs +20 -4
- package/dist/dev-program.mjs.map +1 -1
- package/dist/dev-session.cjs +1 -0
- package/dist/dev-session.cjs.map +1 -1
- package/dist/dev-session.mjs +1 -0
- package/dist/dev-session.mjs.map +1 -1
- package/dist/fastkv.cjs +21 -3
- package/dist/fastkv.cjs.map +1 -1
- package/dist/fastkv.d.cts +14 -1
- package/dist/fastkv.d.mts +14 -1
- package/dist/fastkv.mjs +21 -5
- package/dist/fastkv.mjs.map +1 -1
- package/dist/{mf.cjs → federation.cjs} +6 -13
- package/dist/federation.cjs.map +1 -0
- package/dist/{mf.d.cts → federation.d.cts} +2 -2
- package/dist/{mf.d.mts → federation.d.mts} +2 -2
- package/dist/{mf.mjs → federation.mjs} +6 -13
- package/dist/federation.mjs.map +1 -0
- package/dist/http-client.cjs +84 -57
- package/dist/http-client.cjs.map +1 -1
- package/dist/http-client.mjs +84 -58
- package/dist/http-client.mjs.map +1 -1
- package/dist/image-deploy.cjs +9 -0
- package/dist/image-deploy.cjs.map +1 -1
- package/dist/image-deploy.mjs +9 -1
- package/dist/image-deploy.mjs.map +1 -1
- package/dist/index.cjs +4 -0
- package/dist/index.d.cts +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +3 -3
- 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/orchestrator.cjs +39 -35
- package/dist/orchestrator.cjs.map +1 -1
- package/dist/orchestrator.mjs +39 -35
- package/dist/orchestrator.mjs.map +1 -1
- package/dist/plugin.cjs +2 -0
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.d.cts +24 -0
- package/dist/plugin.d.mts +24 -0
- package/dist/plugin.mjs +2 -0
- package/dist/plugin.mjs.map +1 -1
- package/dist/process-registry.cjs +1 -1
- package/dist/process-registry.mjs +1 -1
- package/dist/publish.cjs +3 -3
- package/dist/publish.cjs.map +1 -1
- package/dist/publish.mjs +4 -4
- package/dist/publish.mjs.map +1 -1
- package/dist/resolver.cjs +293 -0
- package/dist/resolver.cjs.map +1 -0
- package/dist/resolver.d.cts +99 -0
- package/dist/resolver.d.mts +99 -0
- package/dist/resolver.mjs +284 -0
- package/dist/resolver.mjs.map +1 -0
- package/dist/smoke.cjs +262 -0
- package/dist/smoke.cjs.map +1 -0
- package/dist/smoke.mjs +260 -0
- package/dist/smoke.mjs.map +1 -0
- 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/version-manifest-resolve.cjs +3 -3
- package/dist/version-manifest-resolve.cjs.map +1 -1
- package/dist/version-manifest-resolve.mjs +1 -1
- package/dist/version-manifest-resolve.mjs.map +1 -1
- package/dist/workspace.cjs +1 -1
- package/dist/workspace.mjs +1 -1
- package/package.json +18 -18
- 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 +22 -6
- package/skills/talk-to-the-app/SKILL.md +32 -5
- package/dist/bundle-fs-resolve.cjs +0 -235
- package/dist/bundle-fs-resolve.cjs.map +0 -1
- package/dist/bundle-fs-resolve.d.cts +0 -82
- package/dist/bundle-fs-resolve.d.mts +0 -82
- package/dist/bundle-fs-resolve.mjs +0 -228
- package/dist/bundle-fs-resolve.mjs.map +0 -1
- package/dist/bundle-path.cjs +0 -48
- package/dist/bundle-path.cjs.map +0 -1
- package/dist/bundle-path.mjs +0 -45
- package/dist/bundle-path.mjs.map +0 -1
- package/dist/mf.cjs.map +0 -1
- package/dist/mf.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ A consolidated product package for building Module Federation apps with oRPC API
|
|
|
7
7
|
- `everything-dev/types` - Zod schemas and TypeScript types
|
|
8
8
|
- `everything-dev/config` - Config loading and resolution
|
|
9
9
|
- `everything-dev/shared` - Shared dependency policy generation
|
|
10
|
-
- `everything-dev/
|
|
10
|
+
- `everything-dev/federation` - Module Federation runtime utilities
|
|
11
11
|
- `everything-dev/plugin` - Plugin authoring (re-exports from every-plugin)
|
|
12
12
|
- `everything-dev/api` - API plugin loading and router stitching
|
|
13
13
|
- `everything-dev/host` - Host server orchestration
|
|
@@ -42,7 +42,7 @@ everything-dev/shared (generate shared deps policy)
|
|
|
42
42
|
↓
|
|
43
43
|
everything-dev/host (orchestrate)
|
|
44
44
|
├── everything-dev/api (load API plugins via MF)
|
|
45
|
-
└── everything-dev/
|
|
45
|
+
└── everything-dev/federation (load UI remote via MF)
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
## MF Manifest Standard
|
|
@@ -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":";;;AA+BA,SAAgB,qBAGd,SAAc,SAA8C;CA2D5D,MAAM,cAAc,QAAQ,WAAW,OAAO,EAAE,SAAS,WAAwC;EAC/F,IAAI,CAAC,QAAQ,QAAQ,CAAC,QAAQ,QAC5B,MAAM,IAAI,UAAU,gBAAgB;GAClC,SAAS;GACT,MAAM,EAAE,MAAM,sBAAsB;EACtC,CAAC;EAEH,OAAO,KAAK,EAAE,SAAS;GAAE,QAAQ,QAAQ;GAAQ,MAAM,QAAQ;EAAK,EAAE,CAAC;CACzE,CAAC;CAED,MAAM,sBAAsB,QAAQ,WAClC,OAAO,EAAE,SAAS,WAAwC;EACxD,IAAI,CAAC,QAAQ,QAAQ,CAAC,QAAQ,UAAU,CAAC,QAAQ,QAC/C,MAAM,IAAI,UAAU,gBAAgB;GAClC,SAAS;GACT,MAAM,EAAE,MAAM,gCAAgC;EAChD,CAAC;EAEH,OAAO,KAAK,EAAE,QAAQ,CAAC;CACzB,CACF;CAEA,MAAM,eAAiD,GAAG,UACxD,QAAQ,WAAW,OAAO,EAAE,SAAS,WAAwC;EAC3E,IAAI,CAAC,QAAQ,QAAQ,CAAC,QAAQ,QAC5B,MAAM,IAAI,UAAU,gBAAgB;GAClC,SAAS;GACT,MAAM;IAAE,UAAU;IAAW,MAAM;GAAsB;EAC3D,CAAC;EAEH,MAAM,cAAc,QAAQ,KAAK;EACjC,IAAI,CAAC,eAAe,CAAC,MAAM,SAAS,WAAW,GAC7C,MAAM,IAAI,UAAU,aAAa;GAC/B,SAAS,kBAAkB,MAAM,KAAK,MAAM;GAC5C,MAAM;IAAE,eAAe;IAAO;GAAY;EAC5C,CAAC;EAEH,OAAO,KAAK,EAAE,SAAS;GAAE,QAAQ,QAAQ;GAAQ,MAAM,QAAQ;EAAK,EAAE,CAAC;CACzE,CAAC;CAEH,MAAM,eAAe,YAAY,OAAO;CAExC,MAAM,sBAAsB,QAAQ,WAClC,OAAO,EAAE,SAAS,WAAwC;EACxD,IAAI,CAAC,QAAQ,QAAQ,CAAC,QAAQ,QAC5B,MAAM,IAAI,UAAU,gBAAgB;GAClC,SAAS;GACT,MAAM;IAAE,UAAU;IAAW,MAAM;GAAsB;EAC3D,CAAC;EAEH,IAAI,CAAC,QAAQ,cAAc,sBACzB,MAAM,IAAI,UAAU,aAAa;GAC/B,SAAS;GACT,MAAM,EAAE,MAAM,mCAAmC;EACnD,CAAC;EAEH,MAAM,MAAM,QAAQ;EACpB,2BAA2B,IAAI,YAAY;EAC3C,OAAO,KAAK,EACV,SAAS;GACP,QAAQ,QAAQ;GAChB,MAAM,QAAQ;GACd,cAAc;IACZ,GAAG;IACH,sBAAsB,IAAI;IAC1B,cAAc,IAAI,eACd;KACE,GAAG,IAAI;KACP,UAAU,iBAAiB,IAAI,aAAa,UAAU,SAAS,aAAa;IAC9E,IACA;GACN;EACF,EACF,CAAC;CACH,CACF;CAEA,MAAM,kBAAoD,GAAG,UAC3D,QAAQ,WAAW,OAAO,EAAE,SAAS,WAAwC;EAC3E,IAAI,CAAC,QAAQ,QAAQ,CAAC,QAAQ,QAC5B,MAAM,IAAI,UAAU,gBAAgB;GAClC,SAAS;GACT,MAAM;IAAE,UAAU;IAAW,MAAM;GAAsB;EAC3D,CAAC;EAEH,IAAI,CAAC,QAAQ,cAAc,sBACzB,MAAM,IAAI,UAAU,aAAa;GAC/B,SAAS;GACT,MAAM,EAAE,MAAM,mCAAmC;EACnD,CAAC;EAEH,MAAM,SAAS,QAAQ,cAAc;EACrC,IAAI,CAAC,QAAQ,MAAM,CAAC,QAAQ,QAAQ,CAAC,MAAM,SAAS,OAAO,IAAI,GAC7D,MAAM,IAAI,UAAU,aAAa;GAC/B,SAAS,+BAA+B,MAAM,KAAK,MAAM;GACzD,MAAM;IAAE,eAAe;IAAO,aAAa,QAAQ,QAAQ;GAAK;EAClE,CAAC;EAEH,MAAM,MAAM,QAAQ;EACpB,2BAA2B,IAAI,YAAY;EAC3C,OAAO,KAAK,EACV,SAAS;GACP,QAAQ,QAAQ;GAChB,MAAM,QAAQ;GACd,cAAc;IACZ,GAAG;IACH,sBAAsB,IAAI;IAC1B,QAAQ;KAAE,IAAI,OAAO;KAAI,MAAM,OAAO;IAAK;IAC3C,cAAc,IAAI,eACd;KACE,GAAG,IAAI;KACP,UAAU,iBAAiB,IAAI,aAAa,UAAU,SAAS,aAAa;IAC9E,IACA;GACN;EACF,EACF,CAAC;CACH,CAAC;CAEH,MAAM,iBAAiB,wBACrB,QAAQ,WAAW,OAAO,EAAE,SAAS,WAAwC;EAC3E,IAAI,CAAC,QAAQ,QACX,MAAM,IAAI,UAAU,gBAAgB;GAClC,SAAS;GACT,MAAM;IAAE,UAAU;IAAU,MAAM;GAA+C;EACnF,CAAC;EAEH,IAAI,qBAAqB;GACvB,MAAM,WAAW,QAAQ,OAAO,eAAe,CAAC;GAChD,KAAK,MAAM,CAAC,UAAU,YAAY,OAAO,QAAQ,mBAAmB,GAAG;IACrE,MAAM,UAAU,SAAS,aAAa,CAAC;IACvC,MAAM,UAAU,QAAQ,QAAQ,MAAc,CAAC,QAAQ,SAAS,CAAC,CAAC;IAClE,IAAI,QAAQ,SAAS,GACnB,MAAM,IAAI,UAAU,aAAa;KAC/B,SAAS,6BAA6B,SAAS,GAAG,QAAQ,KAAK,GAAG;KAClE,MAAM;MAAE;MAAqB,gBAAgB;KAAS;IACxD,CAAC;GAEL;EACF;EACA,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,QAAQ,OAAO,EAAE,CAAC;CACrD,CAAC;CAEH,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF;AAEA,SAAS,2BACP,cACA;CACA,IAAI,cAAc,UAAU,aAAa,WAAW,UAClD,MAAM,IAAI,UAAU,aAAa,EAC/B,SAAS,gDACX,CAAC;AAEL;AAEA,SAAS,iBACP,KACA,QAC6B;CAC7B,IAAI,CAAC,KAAK,OAAO;CACjB,IAAI,CAAC,QAAQ,OAAO;CACpB,MAAM,SAAS,OAAO,UAAU,GAAG;CACnC,IAAI,OAAO,SAAS,OAAO,OAAO;CAClC,MAAM,IAAI,UAAU,yBAAyB;EAC3C,SAAS;EACT,MAAM,EAAE,QAAQ,OAAO,MAAM,OAAO;CACtC,CAAC;AACH"}
|
|
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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"}
|
package/dist/build.cjs
CHANGED
|
@@ -6,9 +6,9 @@ const require_timing = require('./cli/timing.cjs');
|
|
|
6
6
|
const require_workspace = require('./workspace.cjs');
|
|
7
7
|
const require_shared_deps = require('./shared-deps.cjs');
|
|
8
8
|
const require_string = require('./utils/string.cjs');
|
|
9
|
+
let node_fs_promises = require("node:fs/promises");
|
|
9
10
|
let node_process = require("node:process");
|
|
10
11
|
node_process = require_runtime.__toESM(node_process, 1);
|
|
11
|
-
let node_fs_promises = require("node:fs/promises");
|
|
12
12
|
|
|
13
13
|
//#region src/build.ts
|
|
14
14
|
const buildCommands = {
|
package/dist/build.mjs
CHANGED
|
@@ -5,8 +5,8 @@ import { formatDuration } from "./cli/timing.mjs";
|
|
|
5
5
|
import { ensureFreshDeps, findWorkspaceRoot } from "./workspace.mjs";
|
|
6
6
|
import { syncResolvedSharedDeps } from "./shared-deps.mjs";
|
|
7
7
|
import { padRight } from "./utils/string.mjs";
|
|
8
|
-
import process from "node:process";
|
|
9
8
|
import { access, readFile } from "node:fs/promises";
|
|
9
|
+
import process from "node:process";
|
|
10
10
|
|
|
11
11
|
//#region src/build.ts
|
|
12
12
|
const buildCommands = {
|
package/dist/bundle-cache.cjs
CHANGED
|
@@ -34,7 +34,7 @@ function bundleCachePath(url, input) {
|
|
|
34
34
|
const match = /^\/bundles\/([^/]+)\/([^/]+)\/(.+)$/.exec(parsed.pathname);
|
|
35
35
|
if (!match) return null;
|
|
36
36
|
const [, account, gateway, rest] = match;
|
|
37
|
-
if (!rest || rest.endsWith("/")) return null;
|
|
37
|
+
if (!account || !gateway || !rest || rest.endsWith("/")) return null;
|
|
38
38
|
let decoded;
|
|
39
39
|
try {
|
|
40
40
|
decoded = decodeURIComponent(rest);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle-cache.cjs","names":["path","readFile","mkdir","createHash","writeFile","rename"],"sources":["../src/bundle-cache.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\n/**\n * Bundle disk cache (ADR 0011 amendment — foreign-namespace resilience):\n * the child tier loads base workspaces from foreign origins; this cache\n * keeps the last-known-good bytes so an origin outage degrades to stale\n * serving instead of a hard failure. One cache root, two entrances: the\n * host's `/bundles/*` proxy handler (browser traffic) and the CLI fetch\n * adapter (boot-time outbound). Keyed exactly like the staged layout —\n * `<account>/<gateway>/<rest>` — but a separate root from BOS_BUNDLE_DIR:\n * cached bytes are a resilience artifact, never a deployment (plan 043 —\n * never point a runtime's BOS_BUNDLE_DIR at another runtime's layout).\n */\n\nexport function bundleCacheRoot(input?: { cacheDir?: string }): string {\n return path.resolve(input?.cacheDir ?? process.env.BOS_BUNDLE_CACHE_DIR ?? joinDefault());\n}\n\nfunction joinDefault(): string {\n return path.join(process.cwd(), \".bos\", \"bundle-cache\");\n}\n\nexport function bundleCachePath(url: string, input?: { cacheDir?: string }): string | null {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") return null;\n const match = /^\\/bundles\\/([^/]+)\\/([^/]+)\\/(.+)$/.exec(parsed.pathname);\n if (!match) return null;\n const [, account, gateway, rest] = match;\n if (!rest || rest.endsWith(\"/\")) return null;\n let decoded: string;\n try {\n decoded = decodeURIComponent(rest);\n } catch {\n return null;\n }\n const root = path.resolve(bundleCacheRoot(input));\n const filePath = path.resolve(root, account, gateway, decoded);\n if (!filePath.startsWith(root + path.sep)) return null;\n return filePath;\n}\n\nexport async function readBundleCache(\n url: string,\n input?: { cacheDir?: string },\n): Promise<Uint8Array | null> {\n const filePath = bundleCachePath(url, input);\n if (!filePath) return null;\n try {\n return new Uint8Array(await readFile(filePath));\n } catch {\n return null;\n }\n}\n\nexport async function writeBundleCache(\n url: string,\n bytes: Uint8Array,\n input?: { cacheDir?: string },\n): Promise<void> {\n const filePath = bundleCachePath(url, input);\n if (!filePath) return;\n try {\n await mkdir(path.dirname(filePath), { recursive: true });\n const tmp = `${filePath}.${process.pid}.${createHash(\"sha256\").update(bytes).digest(\"hex\").slice(0, 8)}.tmp`;\n await writeFile(tmp, bytes);\n await rename(tmp, filePath);\n } catch {\n // cache writes are best-effort — never fail the serving path\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAgBA,SAAgB,gBAAgB,OAAuC;CACrE,OAAOA,kBAAK,QAAQ,OAAO,YAAY,QAAQ,IAAI,wBAAwB,YAAY,CAAC;AAC1F;AAEA,SAAS,cAAsB;CAC7B,OAAOA,kBAAK,KAAK,QAAQ,IAAI,GAAG,QAAQ,cAAc;AACxD;AAEA,SAAgB,gBAAgB,KAAa,OAA8C;CACzF,IAAI;CACJ,IAAI;EACF,SAAS,IAAI,IAAI,GAAG;CACtB,QAAQ;EACN,OAAO;CACT;CACA,IAAI,OAAO,aAAa,YAAY,OAAO,aAAa,SAAS,OAAO;CACxE,MAAM,QAAQ,sCAAsC,KAAK,OAAO,QAAQ;CACxE,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,GAAG,SAAS,SAAS,QAAQ;CACnC,IAAI,CAAC,QAAQ,KAAK,SAAS,GAAG,GAAG,OAAO;
|
|
1
|
+
{"version":3,"file":"bundle-cache.cjs","names":["path","readFile","mkdir","createHash","writeFile","rename"],"sources":["../src/bundle-cache.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\n/**\n * Bundle disk cache (ADR 0011 amendment — foreign-namespace resilience):\n * the child tier loads base workspaces from foreign origins; this cache\n * keeps the last-known-good bytes so an origin outage degrades to stale\n * serving instead of a hard failure. One cache root, two entrances: the\n * host's `/bundles/*` proxy handler (browser traffic) and the CLI fetch\n * adapter (boot-time outbound). Keyed exactly like the staged layout —\n * `<account>/<gateway>/<rest>` — but a separate root from BOS_BUNDLE_DIR:\n * cached bytes are a resilience artifact, never a deployment (plan 043 —\n * never point a runtime's BOS_BUNDLE_DIR at another runtime's layout).\n */\n\nexport function bundleCacheRoot(input?: { cacheDir?: string }): string {\n return path.resolve(input?.cacheDir ?? process.env.BOS_BUNDLE_CACHE_DIR ?? joinDefault());\n}\n\nfunction joinDefault(): string {\n return path.join(process.cwd(), \".bos\", \"bundle-cache\");\n}\n\nexport function bundleCachePath(url: string, input?: { cacheDir?: string }): string | null {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") return null;\n const match = /^\\/bundles\\/([^/]+)\\/([^/]+)\\/(.+)$/.exec(parsed.pathname);\n if (!match) return null;\n const [, account, gateway, rest] = match;\n if (!account || !gateway || !rest || rest.endsWith(\"/\")) return null;\n let decoded: string;\n try {\n decoded = decodeURIComponent(rest);\n } catch {\n return null;\n }\n const root = path.resolve(bundleCacheRoot(input));\n const filePath = path.resolve(root, account, gateway, decoded);\n if (!filePath.startsWith(root + path.sep)) return null;\n return filePath;\n}\n\nexport async function readBundleCache(\n url: string,\n input?: { cacheDir?: string },\n): Promise<Uint8Array | null> {\n const filePath = bundleCachePath(url, input);\n if (!filePath) return null;\n try {\n return new Uint8Array(await readFile(filePath));\n } catch {\n return null;\n }\n}\n\nexport async function writeBundleCache(\n url: string,\n bytes: Uint8Array,\n input?: { cacheDir?: string },\n): Promise<void> {\n const filePath = bundleCachePath(url, input);\n if (!filePath) return;\n try {\n await mkdir(path.dirname(filePath), { recursive: true });\n const tmp = `${filePath}.${process.pid}.${createHash(\"sha256\").update(bytes).digest(\"hex\").slice(0, 8)}.tmp`;\n await writeFile(tmp, bytes);\n await rename(tmp, filePath);\n } catch {\n // cache writes are best-effort — never fail the serving path\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAgBA,SAAgB,gBAAgB,OAAuC;CACrE,OAAOA,kBAAK,QAAQ,OAAO,YAAY,QAAQ,IAAI,wBAAwB,YAAY,CAAC;AAC1F;AAEA,SAAS,cAAsB;CAC7B,OAAOA,kBAAK,KAAK,QAAQ,IAAI,GAAG,QAAQ,cAAc;AACxD;AAEA,SAAgB,gBAAgB,KAAa,OAA8C;CACzF,IAAI;CACJ,IAAI;EACF,SAAS,IAAI,IAAI,GAAG;CACtB,QAAQ;EACN,OAAO;CACT;CACA,IAAI,OAAO,aAAa,YAAY,OAAO,aAAa,SAAS,OAAO;CACxE,MAAM,QAAQ,sCAAsC,KAAK,OAAO,QAAQ;CACxE,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,GAAG,SAAS,SAAS,QAAQ;CACnC,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,QAAQ,KAAK,SAAS,GAAG,GAAG,OAAO;CAChE,IAAI;CACJ,IAAI;EACF,UAAU,mBAAmB,IAAI;CACnC,QAAQ;EACN,OAAO;CACT;CACA,MAAM,OAAOA,kBAAK,QAAQ,gBAAgB,KAAK,CAAC;CAChD,MAAM,WAAWA,kBAAK,QAAQ,MAAM,SAAS,SAAS,OAAO;CAC7D,IAAI,CAAC,SAAS,WAAW,OAAOA,kBAAK,GAAG,GAAG,OAAO;CAClD,OAAO;AACT;AAEA,eAAsB,gBACpB,KACA,OAC4B;CAC5B,MAAM,WAAW,gBAAgB,KAAK,KAAK;CAC3C,IAAI,CAAC,UAAU,OAAO;CACtB,IAAI;EACF,OAAO,IAAI,WAAW,UAAMC,2BAAS,QAAQ,CAAC;CAChD,QAAQ;EACN,OAAO;CACT;AACF;AAEA,eAAsB,iBACpB,KACA,OACA,OACe;CACf,MAAM,WAAW,gBAAgB,KAAK,KAAK;CAC3C,IAAI,CAAC,UAAU;CACf,IAAI;EACF,UAAMC,wBAAMF,kBAAK,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;EACvD,MAAM,MAAM,GAAG,SAAS,GAAG,QAAQ,IAAI,OAAGG,wBAAW,QAAQ,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE;EACvG,UAAMC,4BAAU,KAAK,KAAK;EAC1B,UAAMC,yBAAO,KAAK,QAAQ;CAC5B,QAAQ,CAER;AACF"}
|
package/dist/bundle-cache.mjs
CHANGED
|
@@ -31,7 +31,7 @@ function bundleCachePath(url, input) {
|
|
|
31
31
|
const match = /^\/bundles\/([^/]+)\/([^/]+)\/(.+)$/.exec(parsed.pathname);
|
|
32
32
|
if (!match) return null;
|
|
33
33
|
const [, account, gateway, rest] = match;
|
|
34
|
-
if (!rest || rest.endsWith("/")) return null;
|
|
34
|
+
if (!account || !gateway || !rest || rest.endsWith("/")) return null;
|
|
35
35
|
let decoded;
|
|
36
36
|
try {
|
|
37
37
|
decoded = decodeURIComponent(rest);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle-cache.mjs","names":[],"sources":["../src/bundle-cache.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\n/**\n * Bundle disk cache (ADR 0011 amendment — foreign-namespace resilience):\n * the child tier loads base workspaces from foreign origins; this cache\n * keeps the last-known-good bytes so an origin outage degrades to stale\n * serving instead of a hard failure. One cache root, two entrances: the\n * host's `/bundles/*` proxy handler (browser traffic) and the CLI fetch\n * adapter (boot-time outbound). Keyed exactly like the staged layout —\n * `<account>/<gateway>/<rest>` — but a separate root from BOS_BUNDLE_DIR:\n * cached bytes are a resilience artifact, never a deployment (plan 043 —\n * never point a runtime's BOS_BUNDLE_DIR at another runtime's layout).\n */\n\nexport function bundleCacheRoot(input?: { cacheDir?: string }): string {\n return path.resolve(input?.cacheDir ?? process.env.BOS_BUNDLE_CACHE_DIR ?? joinDefault());\n}\n\nfunction joinDefault(): string {\n return path.join(process.cwd(), \".bos\", \"bundle-cache\");\n}\n\nexport function bundleCachePath(url: string, input?: { cacheDir?: string }): string | null {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") return null;\n const match = /^\\/bundles\\/([^/]+)\\/([^/]+)\\/(.+)$/.exec(parsed.pathname);\n if (!match) return null;\n const [, account, gateway, rest] = match;\n if (!rest || rest.endsWith(\"/\")) return null;\n let decoded: string;\n try {\n decoded = decodeURIComponent(rest);\n } catch {\n return null;\n }\n const root = path.resolve(bundleCacheRoot(input));\n const filePath = path.resolve(root, account, gateway, decoded);\n if (!filePath.startsWith(root + path.sep)) return null;\n return filePath;\n}\n\nexport async function readBundleCache(\n url: string,\n input?: { cacheDir?: string },\n): Promise<Uint8Array | null> {\n const filePath = bundleCachePath(url, input);\n if (!filePath) return null;\n try {\n return new Uint8Array(await readFile(filePath));\n } catch {\n return null;\n }\n}\n\nexport async function writeBundleCache(\n url: string,\n bytes: Uint8Array,\n input?: { cacheDir?: string },\n): Promise<void> {\n const filePath = bundleCachePath(url, input);\n if (!filePath) return;\n try {\n await mkdir(path.dirname(filePath), { recursive: true });\n const tmp = `${filePath}.${process.pid}.${createHash(\"sha256\").update(bytes).digest(\"hex\").slice(0, 8)}.tmp`;\n await writeFile(tmp, bytes);\n await rename(tmp, filePath);\n } catch {\n // cache writes are best-effort — never fail the serving path\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,SAAgB,gBAAgB,OAAuC;CACrE,OAAO,KAAK,QAAQ,OAAO,YAAY,QAAQ,IAAI,wBAAwB,YAAY,CAAC;AAC1F;AAEA,SAAS,cAAsB;CAC7B,OAAO,KAAK,KAAK,QAAQ,IAAI,GAAG,QAAQ,cAAc;AACxD;AAEA,SAAgB,gBAAgB,KAAa,OAA8C;CACzF,IAAI;CACJ,IAAI;EACF,SAAS,IAAI,IAAI,GAAG;CACtB,QAAQ;EACN,OAAO;CACT;CACA,IAAI,OAAO,aAAa,YAAY,OAAO,aAAa,SAAS,OAAO;CACxE,MAAM,QAAQ,sCAAsC,KAAK,OAAO,QAAQ;CACxE,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,GAAG,SAAS,SAAS,QAAQ;CACnC,IAAI,CAAC,QAAQ,KAAK,SAAS,GAAG,GAAG,OAAO;
|
|
1
|
+
{"version":3,"file":"bundle-cache.mjs","names":[],"sources":["../src/bundle-cache.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\n/**\n * Bundle disk cache (ADR 0011 amendment — foreign-namespace resilience):\n * the child tier loads base workspaces from foreign origins; this cache\n * keeps the last-known-good bytes so an origin outage degrades to stale\n * serving instead of a hard failure. One cache root, two entrances: the\n * host's `/bundles/*` proxy handler (browser traffic) and the CLI fetch\n * adapter (boot-time outbound). Keyed exactly like the staged layout —\n * `<account>/<gateway>/<rest>` — but a separate root from BOS_BUNDLE_DIR:\n * cached bytes are a resilience artifact, never a deployment (plan 043 —\n * never point a runtime's BOS_BUNDLE_DIR at another runtime's layout).\n */\n\nexport function bundleCacheRoot(input?: { cacheDir?: string }): string {\n return path.resolve(input?.cacheDir ?? process.env.BOS_BUNDLE_CACHE_DIR ?? joinDefault());\n}\n\nfunction joinDefault(): string {\n return path.join(process.cwd(), \".bos\", \"bundle-cache\");\n}\n\nexport function bundleCachePath(url: string, input?: { cacheDir?: string }): string | null {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") return null;\n const match = /^\\/bundles\\/([^/]+)\\/([^/]+)\\/(.+)$/.exec(parsed.pathname);\n if (!match) return null;\n const [, account, gateway, rest] = match;\n if (!account || !gateway || !rest || rest.endsWith(\"/\")) return null;\n let decoded: string;\n try {\n decoded = decodeURIComponent(rest);\n } catch {\n return null;\n }\n const root = path.resolve(bundleCacheRoot(input));\n const filePath = path.resolve(root, account, gateway, decoded);\n if (!filePath.startsWith(root + path.sep)) return null;\n return filePath;\n}\n\nexport async function readBundleCache(\n url: string,\n input?: { cacheDir?: string },\n): Promise<Uint8Array | null> {\n const filePath = bundleCachePath(url, input);\n if (!filePath) return null;\n try {\n return new Uint8Array(await readFile(filePath));\n } catch {\n return null;\n }\n}\n\nexport async function writeBundleCache(\n url: string,\n bytes: Uint8Array,\n input?: { cacheDir?: string },\n): Promise<void> {\n const filePath = bundleCachePath(url, input);\n if (!filePath) return;\n try {\n await mkdir(path.dirname(filePath), { recursive: true });\n const tmp = `${filePath}.${process.pid}.${createHash(\"sha256\").update(bytes).digest(\"hex\").slice(0, 8)}.tmp`;\n await writeFile(tmp, bytes);\n await rename(tmp, filePath);\n } catch {\n // cache writes are best-effort — never fail the serving path\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,SAAgB,gBAAgB,OAAuC;CACrE,OAAO,KAAK,QAAQ,OAAO,YAAY,QAAQ,IAAI,wBAAwB,YAAY,CAAC;AAC1F;AAEA,SAAS,cAAsB;CAC7B,OAAO,KAAK,KAAK,QAAQ,IAAI,GAAG,QAAQ,cAAc;AACxD;AAEA,SAAgB,gBAAgB,KAAa,OAA8C;CACzF,IAAI;CACJ,IAAI;EACF,SAAS,IAAI,IAAI,GAAG;CACtB,QAAQ;EACN,OAAO;CACT;CACA,IAAI,OAAO,aAAa,YAAY,OAAO,aAAa,SAAS,OAAO;CACxE,MAAM,QAAQ,sCAAsC,KAAK,OAAO,QAAQ;CACxE,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,GAAG,SAAS,SAAS,QAAQ;CACnC,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,QAAQ,KAAK,SAAS,GAAG,GAAG,OAAO;CAChE,IAAI;CACJ,IAAI;EACF,UAAU,mBAAmB,IAAI;CACnC,QAAQ;EACN,OAAO;CACT;CACA,MAAM,OAAO,KAAK,QAAQ,gBAAgB,KAAK,CAAC;CAChD,MAAM,WAAW,KAAK,QAAQ,MAAM,SAAS,SAAS,OAAO;CAC7D,IAAI,CAAC,SAAS,WAAW,OAAO,KAAK,GAAG,GAAG,OAAO;CAClD,OAAO;AACT;AAEA,eAAsB,gBACpB,KACA,OAC4B;CAC5B,MAAM,WAAW,gBAAgB,KAAK,KAAK;CAC3C,IAAI,CAAC,UAAU,OAAO;CACtB,IAAI;EACF,OAAO,IAAI,WAAW,MAAM,SAAS,QAAQ,CAAC;CAChD,QAAQ;EACN,OAAO;CACT;AACF;AAEA,eAAsB,iBACpB,KACA,OACA,OACe;CACf,MAAM,WAAW,gBAAgB,KAAK,KAAK;CAC3C,IAAI,CAAC,UAAU;CACf,IAAI;EACF,MAAM,MAAM,KAAK,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;EACvD,MAAM,MAAM,GAAG,SAAS,GAAG,QAAQ,IAAI,GAAG,WAAW,QAAQ,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE;EACvG,MAAM,UAAU,KAAK,KAAK;EAC1B,MAAM,OAAO,KAAK,QAAQ;CAC5B,QAAQ,CAER;AACF"}
|
package/dist/cli/init.cjs
CHANGED
|
@@ -1264,10 +1264,12 @@ The host exposes several surfaces for programmatic agent access:
|
|
|
1264
1264
|
|
|
1265
1265
|
### Authentication for agents
|
|
1266
1266
|
|
|
1267
|
-
|
|
1267
|
+
Every surface requires a session or API key — anonymous requests get \`401\` (including MCP \`tools/list\`).
|
|
1268
|
+
|
|
1269
|
+
1. Sign in with your NEAR wallet (SIWN) at the website — or run \`bos login\` from a machine with a browser.
|
|
1268
1270
|
2. Navigate to **Settings → API Keys** at \`/settings/api-keys\`.
|
|
1269
|
-
3. Create a new key — the full secret (\`
|
|
1270
|
-
4. Pass it on every request: \`x-api-key:
|
|
1271
|
+
3. Create a new key — the full secret (\`api_...\`) is shown once. Copy it immediately.
|
|
1272
|
+
4. Pass it on every request: \`x-api-key: api_your_key_here\`
|
|
1271
1273
|
|
|
1272
1274
|
### Architecture note: remotes are code bundles
|
|
1273
1275
|
|
|
@@ -1351,12 +1353,12 @@ function buildChildLlmsTxt(title) {
|
|
|
1351
1353
|
|
|
1352
1354
|
## Auth
|
|
1353
1355
|
|
|
1354
|
-
Authenticate to the API using an API key:
|
|
1356
|
+
Authenticate to the API using an API key (every surface — REST, RPC, MCP — requires a session or key; anonymous requests get \`401\`):
|
|
1355
1357
|
|
|
1356
|
-
1. Sign in with your NEAR wallet (SIWN) at the website.
|
|
1358
|
+
1. Sign in with your NEAR wallet (SIWN) at the website — or run \`bos login\` from a machine with a browser.
|
|
1357
1359
|
2. Go to **Settings → API Keys** at \`/settings/api-keys\`.
|
|
1358
|
-
3. Create a key — the full secret (\`
|
|
1359
|
-
4. Pass it on every request: \`x-api-key:
|
|
1360
|
+
3. Create a key — the full secret (\`api_...\`) is shown once.
|
|
1361
|
+
4. Pass it on every request: \`x-api-key: api_your_key_here\`
|
|
1360
1362
|
|
|
1361
1363
|
The \`x-api-key\` header works for \`/api/*\` (REST), \`/api/rpc/*\` (oRPC), and \`/api/mcp\` (MCP).
|
|
1362
1364
|
|
|
@@ -1387,12 +1389,22 @@ Transport: Streamable HTTP (stateless). Connect your MCP client to \`{origin}/ap
|
|
|
1387
1389
|
|
|
1388
1390
|
### Authentication
|
|
1389
1391
|
|
|
1390
|
-
|
|
1392
|
+
Every API surface (REST, RPC, MCP) requires a session or an API key — anonymous
|
|
1393
|
+
requests get \`401\`. Use an **API key**:
|
|
1391
1394
|
|
|
1392
1395
|
1. Sign in with your NEAR wallet at the website (SIWN).
|
|
1393
1396
|
2. Navigate to **Settings → API Keys** at \`/settings/api-keys\`.
|
|
1394
|
-
3. Create a key — the full secret (\`
|
|
1395
|
-
4. Pass it on every request: \`x-api-key:
|
|
1397
|
+
3. Create a key — the full secret (\`api_...\`) is shown once.
|
|
1398
|
+
4. Pass it on every request: \`x-api-key: api_your_key_here\`
|
|
1399
|
+
|
|
1400
|
+
Or from a machine with a browser: \`bos login\` — one approval, and the CLI
|
|
1401
|
+
persists an API key in \`.bos/session.json\` for headless use.
|
|
1402
|
+
|
|
1403
|
+
### Deep dive
|
|
1404
|
+
|
|
1405
|
+
The full API/auth skill is served by this app at
|
|
1406
|
+
\`{origin}/skills/everything-dev/talk-to-the-app/SKILL.md\` (MCP, REST, RPC,
|
|
1407
|
+
API keys, troubleshooting refusals).
|
|
1396
1408
|
|
|
1397
1409
|
### REST / OpenAPI
|
|
1398
1410
|
|