alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
162 lines (161 loc) • 6.55 kB
JavaScript
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