everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
1 lines • 13.4 kB
Source Map (JSON)
{"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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"}