UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

162 lines (161 loc) 6.55 kB
import { $atom, $module, createMiddleware, z } from "alepha"; import { UnauthorizedError } from "alepha/server"; //#region ../../src/security/atoms/currentTenantAtom.ts /** * Atom storing the active tenant for the current request. * * Transport-agnostic — works with HTTP, MCP, pipelines, jobs, and any context * that sets the atom before calling tenant-scoped logic. * * Typically set by an app-level middleware that resolves the tenant from the * request `Host` header (or another signal) and writes the resolved id to the * store. Framework code that reads this atom: * * - Repository scoping: `withOrganization` / `stampOrganization` prefer this * value over `currentUserAtom.organization` so cross-tenant users (admins, * agency operators) are scoped to the tenant they are currently acting in * rather than the one they belong to. * - Session creation: the value is persisted into the JWT as a `tenant` claim, * and the issuer resolver rejects tokens whose claim does not match the * tenant resolved from the current request. * * `id` is a free-form string so the framework stays neutral on tenant identity * (slug, UUID, composite). Pick whatever matches the column marked with * `PG_ORGANIZATION` in your entities. */ const currentTenantAtom = $atom({ name: "alepha.security.tenant", schema: z.object({ id: z.text({ description: "Tenant identifier (slug, UUID, or composite)." }) }).optional() }); //#endregion //#region ../../src/security/schemas/userAccountInfoSchema.ts const userAccountInfoSchema = z.object({ id: z.text({ description: "Unique identifier for the user." }), name: z.text({ description: "Full name of the user." }).optional(), firstName: z.text({ description: "Given name of the user (OIDC `given_name`)." }).optional(), lastName: z.text({ description: "Family name of the user (OIDC `family_name`)." }).optional(), email: z.text({ description: "Email address of the user.", format: "email" }).optional(), username: z.text({ description: "Preferred username of the user." }).optional(), picture: z.text({ description: "URL to the user's profile picture." }).optional(), sessionId: z.text({ description: "Session identifier for the user, if applicable." }).optional(), organization: z.uuid().describe("Organization the user belongs to.").optional(), roles: z.array(z.text()).describe("List of roles assigned to the user.").optional(), realm: z.text({ description: "The realm (issuer) the user was authenticated from." }).optional() }); //#endregion //#region ../../src/security/atoms/currentUserAtom.ts /** * Atom storing the current authenticated user. * * Transport-agnostic — works with HTTP, MCP, pipelines, jobs, and any context * that sets the atom before calling secured logic. */ const currentUserAtom = $atom({ name: "alepha.security.user", schema: userAccountInfoSchema.optional() }); //#endregion //#region ../../src/security/errors/InvalidCredentialsError.ts /** * Error thrown when the provided credentials are invalid. * * Message can not be changed to avoid leaking information. * Cause is omitted for the same reason. */ var InvalidCredentialsError = class extends UnauthorizedError { name = "UnauthorizedError"; constructor() { super("Invalid credentials"); } }; //#endregion //#region ../../src/security/errors/InvalidPermissionError.ts var InvalidPermissionError = class extends Error { constructor(name) { super(`Permission '${name}' is invalid`); } }; //#endregion //#region ../../src/security/errors/SecurityError.ts var SecurityError = class extends Error { name = "SecurityError"; status = 403; }; //#endregion //#region ../../src/security/primitives/$secure.browser.ts /** * Browser-side middleware that enforces authentication and authorization. * * Resolves the user from `currentUserAtom` only (no HTTP header resolution). * Checks roles from the user object and permissions from the user's roles. * * In the browser, an unauthenticated or unauthorized user is not an exception — * the middleware short-circuits by returning `undefined` and the handler is not called. * Components should use `action.can()` to conditionally render UI elements. * * ```typescript * class OrderController { * getOrders = $action({ * use: [$secure()], * handler: async ({ query }) => { ... }, * }); * * deleteOrder = $action({ * use: [$secure({ permissions: ["orders:delete"] })], * handler: async ({ params }) => { ... }, * }); * } * ``` */ function $secure(options) { return createMiddleware({ name: "$secure", options: options ?? void 0, handler: ({ alepha, next }) => { return async (...args) => { const user = alepha.store.get(currentUserAtom); if (!user) return; if (options?.issuers?.length) { if (!user.realm || !options.issuers.includes(user.realm)) return; } if (options?.roles?.length) { if (!options.roles.some((role) => user.roles?.includes(role))) return; } if (options?.guard) { if (!options.guard(user)) return; } return next(...args); }; } }); } //#endregion //#region ../../src/security/schemas/permissionSchema.ts const permissionSchema = z.object({ name: z.text({ description: "Name of the permission." }), group: z.text({ description: "Group of the permission." }).optional(), description: z.text({ description: "Describe the permission." }).optional(), method: z.text({ description: "HTTP method of the permission. When available." }).optional(), path: z.text({ description: "Pathname of the permission. When available." }).optional() }); //#endregion //#region ../../src/security/schemas/roleSchema.ts const roleSchema = z.object({ name: z.text({ description: "Name of the role." }), description: z.text({ description: "Describe the role." }).optional(), default: z.boolean().describe("If true, this role will be assigned to all users by default.").optional(), permissions: z.array(z.object({ name: z.text({ description: "Name of the permission." }), ownership: z.boolean().describe("If true, user will only have access to it's own resources.").optional(), exclude: z.array(z.text()).describe("Exclude some permissions. Useful when 'name' is a wildcard.").optional() })) }); //#endregion //#region ../../src/security/index.browser.ts const AlephaSecurity = $module({ name: "alepha.security" }); //#endregion export { $secure, AlephaSecurity, InvalidCredentialsError, InvalidPermissionError, SecurityError, currentTenantAtom, currentUserAtom, permissionSchema, roleSchema, userAccountInfoSchema }; //# sourceMappingURL=index.browser.js.map