@warlock.js/access 5.28.0 → 5.30.0

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/cjs/index.cjs +9 -9
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/contracts/access-resolver.d.mts +1 -1
  5. package/esm/contracts/access-resolver.d.mts.map +1 -1
  6. package/esm/contracts/types.d.mts +1 -1
  7. package/esm/contracts/types.d.mts.map +1 -1
  8. package/esm/middleware/gate.middleware.d.mts +1 -1
  9. package/esm/middleware/gate.middleware.d.mts.map +1 -1
  10. package/esm/middleware/gate.middleware.mjs +1 -1
  11. package/esm/middleware/gate.middleware.mjs.map +1 -1
  12. package/esm/services/access-config.d.mts +1 -1
  13. package/esm/services/access-config.d.mts.map +1 -1
  14. package/esm/services/access-config.mjs +1 -1
  15. package/esm/services/access-config.mjs.map +1 -1
  16. package/esm/services/access.d.mts +1 -1
  17. package/esm/services/access.d.mts.map +1 -1
  18. package/esm/services/access.mjs +1 -1
  19. package/esm/services/access.mjs.map +1 -1
  20. package/esm/services/default-resolver.d.mts +1 -1
  21. package/esm/services/default-resolver.d.mts.map +1 -1
  22. package/esm/services/default-resolver.mjs +1 -1
  23. package/esm/services/default-resolver.mjs.map +1 -1
  24. package/esm/services/engine.d.mts +1 -1
  25. package/esm/services/engine.d.mts.map +1 -1
  26. package/esm/services/engine.mjs +1 -1
  27. package/esm/services/engine.mjs.map +1 -1
  28. package/esm/services/matcher.mjs +1 -1
  29. package/esm/services/matcher.mjs.map +1 -1
  30. package/esm/services/policies.d.mts +1 -1
  31. package/esm/services/policies.d.mts.map +1 -1
  32. package/esm/services/policies.mjs +1 -1
  33. package/esm/services/policies.mjs.map +1 -1
  34. package/esm/utils/access-config-error.d.mts +1 -1
  35. package/esm/utils/access-config-error.d.mts.map +1 -1
  36. package/esm/utils/access-config-error.mjs +1 -1
  37. package/esm/utils/access-config-error.mjs.map +1 -1
  38. package/esm/utils/access-error-codes.d.mts +1 -1
  39. package/esm/utils/access-error-codes.d.mts.map +1 -1
  40. package/esm/utils/access-error-codes.mjs +1 -1
  41. package/esm/utils/access-error-codes.mjs.map +1 -1
  42. package/llms-full.txt +25 -25
  43. package/llms.txt +5 -5
  44. package/package.json +9 -8
  45. package/skills/check-permissions/SKILL.md +4 -4
  46. package/skills/configure-access/SKILL.md +5 -5
  47. package/skills/define-policies/SKILL.md +3 -3
  48. package/skills/implement-resolver/SKILL.md +2 -2
  49. package/skills/manage-roles/SKILL.md +4 -4
  50. package/skills/overview/SKILL.md +7 -7
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to `@warlock.js/access` are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
6
6
 
7
+ ## 5.30.0 - 2026-10-16
8
+
9
+ ### Changed
10
+
11
+ - Lockstep release; package APIs are unchanged.
12
+
13
+ ## 5.29.0 - 2026-10-01
14
+
15
+ ### Changed
16
+
17
+ - Lockstep release; package APIs are unchanged. Skills refer to other topics by name instead of file paths, so cross-references keep working after `agent-kit` copies them into an app.
18
+
7
19
  ## 5.28.0 - 2026-10-01
8
20
 
9
21
  ### Changed
package/cjs/index.cjs CHANGED
@@ -3,7 +3,7 @@ let _warlock_js_core = require("@warlock.js/core");
3
3
  let _warlock_js_cache = require("@warlock.js/cache");
4
4
  let _warlock_js_logger = require("@warlock.js/logger");
5
5
 
6
- //#region ../access/src/utils/access-error-codes.ts
6
+ //#region ../../packages/access/src/utils/access-error-codes.ts
7
7
  /**
8
8
  * Wire-stable error codes emitted by `@warlock.js/access`. Map these in your
9
9
  * error transformer the way you map `AuthErrorCodes`. A value change here is a
@@ -16,7 +16,7 @@ let AccessErrorCodes = /* @__PURE__ */ function(AccessErrorCodes) {
16
16
  }({});
17
17
 
18
18
  //#endregion
19
- //#region ../access/src/utils/access-config-error.ts
19
+ //#region ../../packages/access/src/utils/access-config-error.ts
20
20
  /**
21
21
  * Thrown when `@warlock.js/access` is misconfigured (e.g. no resolver). Unlike a
22
22
  * runtime resolution failure — which fails CLOSED (denies) — a config error is a
@@ -30,7 +30,7 @@ var AccessConfigError = class extends Error {
30
30
  };
31
31
 
32
32
  //#endregion
33
- //#region ../access/src/services/access-config.ts
33
+ //#region ../../packages/access/src/services/access-config.ts
34
34
  /**
35
35
  * The active configuration, fed once at boot by the framework's access connector
36
36
  * via {@link setAccessConfig} (the connector reads `src/config/access.ts`).
@@ -80,7 +80,7 @@ const accessConfig = {
80
80
  };
81
81
 
82
82
  //#endregion
83
- //#region ../access/src/services/matcher.ts
83
+ //#region ../../packages/access/src/services/matcher.ts
84
84
  /**
85
85
  * Whether a set of granted permission patterns covers a requested permission.
86
86
  *
@@ -100,7 +100,7 @@ function matchesPermission(granted, permission) {
100
100
  }
101
101
 
102
102
  //#endregion
103
- //#region ../access/src/services/policies.ts
103
+ //#region ../../packages/access/src/services/policies.ts
104
104
  const policies = /* @__PURE__ */ new Map();
105
105
  /**
106
106
  * Register an ABAC condition for a permission. It runs ON TOP of the RBAC grant
@@ -123,7 +123,7 @@ function clearPolicies() {
123
123
  }
124
124
 
125
125
  //#endregion
126
- //#region ../access/src/services/engine.ts
126
+ //#region ../../packages/access/src/services/engine.ts
127
127
  const PERMISSIONS_PREFIX = "access.perms.";
128
128
  const ROLES_PREFIX = "access.roles.";
129
129
  /**
@@ -237,7 +237,7 @@ async function check(user, permission, context = {}) {
237
237
  }
238
238
 
239
239
  //#endregion
240
- //#region ../access/src/services/access.ts
240
+ //#region ../../packages/access/src/services/access.ts
241
241
  /** Whether the user holds `permission` (and passes its policy when a resource is given). */
242
242
  function can(user, permission, context) {
243
243
  return check(user, permission, context);
@@ -311,7 +311,7 @@ const access = {
311
311
  };
312
312
 
313
313
  //#endregion
314
- //#region ../access/src/middleware/gate.middleware.ts
314
+ //#region ../../packages/access/src/middleware/gate.middleware.ts
315
315
  /**
316
316
  * The one place access bridges `request.locals.user` (app-augmented via
317
317
  * `@warlock.js/auth`'s `RequestUser`, empty by default) to the
@@ -365,7 +365,7 @@ function gateAll(permissions) {
365
365
  }
366
366
 
367
367
  //#endregion
368
- //#region ../access/src/services/default-resolver.ts
368
+ //#region ../../packages/access/src/services/default-resolver.ts
369
369
  /** Coerce a roles value (`undefined` / single string / array) into a string array. */
370
370
  function toRoleList(value) {
371
371
  if (Array.isArray(value)) return value;
package/cjs/index.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["cache","ForbiddenError","flushUser"],"sources":["../../../../../../access/src/utils/access-error-codes.ts","../../../../../../access/src/utils/access-config-error.ts","../../../../../../access/src/services/access-config.ts","../../../../../../access/src/services/matcher.ts","../../../../../../access/src/services/policies.ts","../../../../../../access/src/services/engine.ts","../../../../../../access/src/services/access.ts","../../../../../../access/src/middleware/gate.middleware.ts","../../../../../../access/src/services/default-resolver.ts"],"sourcesContent":["/**\n * Wire-stable error codes emitted by `@warlock.js/access`. Map these in your\n * error transformer the way you map `AuthErrorCodes`. A value change here is a\n * breaking change for any client that reacts to the code.\n */\nexport enum AccessErrorCodes {\n /** EC100 — authenticated, but missing the required permission. */\n Forbidden = \"EC100\",\n}\n","/**\n * Thrown when `@warlock.js/access` is misconfigured (e.g. no resolver). Unlike a\n * runtime resolution failure — which fails CLOSED (denies) — a config error is a\n * developer mistake, so the engine re-throws it LOUD instead of silently denying.\n */\nexport class AccessConfigError extends Error {\n public constructor(message: string) {\n super(message);\n this.name = \"AccessConfigError\";\n }\n}\n","import type { Auth } from \"@warlock.js/auth\";\nimport type { AccessResolver } from \"../contracts/access-resolver\";\nimport type { AccessConfigurations } from \"../contracts/types\";\nimport { AccessConfigError } from \"../utils/access-config-error\";\n\n/**\n * The active configuration, fed once at boot by the framework's access connector\n * via {@link setAccessConfig} (the connector reads `src/config/access.ts`).\n * Reading it before it's set throws {@link AccessConfigError} — a loud\n * misconfiguration, never a silent deny.\n */\nlet current: AccessConfigurations | undefined;\n\n/**\n * Register the access configuration. The framework's access connector calls this\n * at boot with the default export of `src/config/access.ts`. It validates that a\n * `resolver` is present, so a misconfigured authorization layer fails at STARTUP\n * rather than on the first protected request.\n *\n * @example\n * setAccessConfig({ resolver: new DefaultAccessResolver({ admin: [\"*\"] }) });\n */\nexport function setAccessConfig(configuration: AccessConfigurations): void {\n if (!configuration?.resolver) {\n throw new AccessConfigError(\n \"No access resolver configured. Set `resolver` in src/config/access.ts to a class \" +\n \"implementing AccessResolver — the ejected DatabaseAccessResolver, or \" +\n \"`new DefaultAccessResolver(rolesMap)` for a fixed-role catalog.\",\n );\n }\n\n current = configuration;\n}\n\n/** Clear the active config. Used by the connector on dev-restart and by tests. */\nexport function resetAccessConfig(): void {\n current = undefined;\n}\n\n/** Accessors over the active access configuration. */\nexport const accessConfig = {\n /**\n * The configured resolver. Throws {@link AccessConfigError} if access was never\n * configured (no `src/config/access.ts`, or the connector hasn't run).\n */\n resolver(): AccessResolver {\n if (!current) {\n throw new AccessConfigError(\n \"@warlock.js/access is not configured. Add src/config/access.ts (exporting a `resolver`) \" +\n \"— it is wired at boot by the access connector.\",\n );\n }\n\n return current.resolver;\n },\n\n /** Cache TTL for resolved permission sets. */\n cacheTtl(): string | number {\n return current?.cache?.ttl ?? \"10m\";\n },\n\n /** Whether an unpoliced instance-level check should deny instead of falling back to the RBAC grant. */\n strictPolicies(): boolean {\n return current?.strictPolicies ?? true;\n },\n\n /** Ambient tenant for this user, delegated to the resolver's optional hook. */\n tenant(user: Auth): string | undefined {\n return this.resolver().resolveTenant?.(user);\n },\n};\n","/**\n * Whether a set of granted permission patterns covers a requested permission.\n *\n * Supports three forms:\n * - exact — `\"orders.update\"` covers `\"orders.update\"`\n * - global — `\"*\"` covers everything (the super-grant)\n * - prefix wildcard — `\"orders.*\"` covers `\"orders.update\"` and `\"orders.update.status\"`\n *\n * `\"orders.*\"` does NOT cover the bare `\"orders\"` (no trailing segment).\n */\nexport function matchesPermission(granted: string[], permission: string): boolean {\n for (const pattern of granted) {\n if (pattern === \"*\" || pattern === permission) {\n return true;\n }\n\n // \"orders.*\" → prefix \"orders.\" — matches any nested permission.\n if (pattern.endsWith(\".*\") && permission.startsWith(pattern.slice(0, -1))) {\n return true;\n }\n }\n\n return false;\n}\n","import type { PolicyFn } from \"../contracts/types\";\n\nconst policies = new Map<string, PolicyFn>();\n\n/**\n * Register an ABAC condition for a permission. It runs ON TOP of the RBAC grant\n * whenever an authorization check carries a `resource` — letting you express\n * \"...but only their own / only in their tenant / only while pending\".\n *\n * @example\n * definePolicy(\"orders.update\", (user, order) => order.get(\"customer_id\") === user.id);\n */\nexport function definePolicy(permission: string, policy: PolicyFn): void {\n policies.set(permission, policy);\n}\n\n/** The policy registered for a permission, if any. */\nexport function getPolicy(permission: string): PolicyFn | undefined {\n return policies.get(permission);\n}\n\n/** Remove every registered policy. Primarily for tests. */\nexport function clearPolicies(): void {\n policies.clear();\n}\n","import type { Auth } from \"@warlock.js/auth\";\nimport { cache } from \"@warlock.js/cache\";\nimport { log } from \"@warlock.js/logger\";\nimport type { AccessContext } from \"../contracts/types\";\nimport { AccessConfigError } from \"../utils/access-config-error\";\nimport { accessConfig } from \"./access-config\";\nimport { matchesPermission } from \"./matcher\";\nimport { getPolicy } from \"./policies\";\n\nconst PERMISSIONS_PREFIX = \"access.perms.\";\nconst ROLES_PREFIX = \"access.roles.\";\n\n/**\n * Permissions that have already triggered the \"unpoliced instance check\"\n * warning below — kept so a hot path doesn't log once per request.\n */\nconst warnedUnpolicedPermissions = new Set<string>();\n\n/** Reset the once-per-permission warning tracker. Primarily for tests. */\nexport function resetUnpolicedWarnings(): void {\n warnedUnpolicedPermissions.clear();\n}\n\n/** Resolve the effective tenant for a check (explicit → ambient → undefined). */\nfunction tenantFor(user: Auth, context: AccessContext): string | undefined {\n return context.tenant ?? accessConfig.tenant(user);\n}\n\nfunction cacheKey(prefix: string, user: Auth, tenant?: string): string {\n // Encode each segment so a `.` in an id or tenant can't collide two distinct\n // principals onto the same cache key (and serve one's grants to the other).\n const segment = (value: unknown) => encodeURIComponent(String(value));\n\n return `${prefix}${segment(user.userType)}.${segment(user.id)}.${segment(tenant ?? \"_\")}`;\n}\n\n/**\n * Read a list through the cache, falling back to the loader.\n *\n * The cache is **best-effort**: a cache failure degrades to the loader (the\n * source of truth) and is logged, never denied. A loader failure propagates —\n * so the DECISION fails closed in {@link check}.\n */\nasync function cached(\n key: string,\n loader: () => Promise<string[]>,\n): Promise<string[]> {\n try {\n const hit = await cache.get<string[]>(key);\n\n if (hit) return hit;\n } catch (error) {\n log.error(\"access\", \"cache\", error);\n }\n\n const value = await loader();\n\n try {\n await cache.set(key, value, { ttl: accessConfig.cacheTtl() });\n } catch (error) {\n log.error(\"access\", \"cache\", error);\n }\n\n return value;\n}\n\n/** The user's effective permission set for a tenant (cached). */\nexport function permissionsFor(user: Auth, tenant?: string): Promise<string[]> {\n return cached(cacheKey(PERMISSIONS_PREFIX, user, tenant), () =>\n accessConfig.resolver().resolvePermissions(user, tenant),\n );\n}\n\n/** The user's roles for a tenant (cached). */\nexport function rolesFor(user: Auth, tenant?: string): Promise<string[]> {\n return cached(cacheKey(ROLES_PREFIX, user, tenant), () =>\n accessConfig.resolver().resolveRoles(user, tenant),\n );\n}\n\n/** Drop the cached role/permission sets for a user (after any out-of-band role change). */\nexport async function flush(user: Auth, tenant?: string): Promise<void> {\n try {\n await cache.remove(cacheKey(PERMISSIONS_PREFIX, user, tenant));\n await cache.remove(cacheKey(ROLES_PREFIX, user, tenant));\n } catch (error) {\n log.error(\"access\", \"cache\", error);\n }\n}\n\n/**\n * Surface a resource-scoped check that has no registered ABAC policy, and\n * report whether the check should now DENY.\n *\n * `strictPolicies` defaults to `true`: a forgotten `definePolicy` call (typo'd\n * permission name, or a missing `import \"./policies\"` side-effect — the\n * single most common mistake app teams make with this package, per the\n * `define-policies` skill) must never fail OPEN, so the check denies.\n *\n * `strictPolicies: false` explicitly restores the pre-5.13 behavior: fall\n * back to the RBAC grant alone (ALLOW), which is a plausible IDOR if the\n * permission was meant to be resource-scoped.\n *\n * Either way this warns once per permission (not once per call) so a hot\n * request path doesn't spam logs — mirrors cascade's\n * `warnUndeclaredSensitiveFields`.\n */\nfunction unpolicedShouldDeny(permission: string): boolean {\n const strict = accessConfig.strictPolicies();\n\n if (!warnedUnpolicedPermissions.has(permission)) {\n warnedUnpolicedPermissions.add(permission);\n\n log.warn(\n \"access\",\n \"unpoliced-check\",\n strict\n ? `Instance-level check for permission \"${permission}\" has no registered ABAC policy — ` +\n `DENYING (strictPolicies defaults to true as of 5.13). Register one with ` +\n `definePolicy(\"${permission}\", ...), or make sure the module that calls definePolicy is ` +\n `actually imported. Set strictPolicies: false in the access config to restore the pre-5.13 ` +\n `RBAC-only fallback (ALLOWS — not recommended).`\n : `Instance-level check for permission \"${permission}\" has no registered ABAC policy — falling ` +\n `back to the RBAC grant alone (strictPolicies: false), so any holder of \"${permission}\" can ` +\n `act on ANY resource. If this permission is meant to be resource-scoped, register a policy ` +\n `with definePolicy(\"${permission}\", ...) (and confirm the module that calls it is ` +\n `imported), or remove strictPolicies: false to deny by default.`,\n );\n }\n\n return strict;\n}\n\n/**\n * The core decision: does the user hold `permission`, and — when a `resource`\n * is supplied — does its policy pass too?\n *\n * Fails CLOSED: any error while resolving the decision denies (and logs). An\n * instance-level check for a permission with no registered policy also fails\n * CLOSED by default ({@link AccessConfigurations.strictPolicies}) rather than\n * falling back to the RBAC grant alone.\n */\nexport async function check(\n user: Auth,\n permission: string,\n context: AccessContext = {},\n): Promise<boolean> {\n try {\n const tenant = tenantFor(user, context);\n const permissions = await permissionsFor(user, tenant);\n\n if (!matchesPermission(permissions, permission)) return false;\n\n const policy = getPolicy(permission);\n\n // A policy only runs on an instance check (a resource was supplied);\n // class-level checks (`gate`) stop at the grant.\n if (context.resource !== undefined && policy === undefined) {\n if (unpolicedShouldDeny(permission)) return false;\n }\n\n if (policy === undefined || context.resource === undefined) return true;\n\n const roles = await rolesFor(user, tenant);\n\n return Boolean(\n await policy(user, context.resource, {\n ...context,\n tenant,\n hasRole: (role) => roles.includes(role),\n hasPermission: (perm) => matchesPermission(permissions, perm),\n }),\n );\n } catch (error) {\n if (error instanceof AccessConfigError) throw error; // misconfig is loud, not a silent deny\n\n log.error(\"access\", \"check\", error);\n\n return false;\n }\n}\n","import type { Auth } from \"@warlock.js/auth\";\nimport { ForbiddenError, t } from \"@warlock.js/core\";\nimport type { AccessContext } from \"../contracts/types\";\nimport { AccessErrorCodes } from \"../utils/access-error-codes\";\nimport { check, flush as flushUser, rolesFor } from \"./engine\";\n\n/** Whether the user holds `permission` (and passes its policy when a resource is given). */\nexport function can(\n user: Auth,\n permission: string,\n context?: AccessContext,\n): Promise<boolean> {\n return check(user, permission, context);\n}\n\n/** Inverse of {@link can}. */\nexport async function cannot(\n user: Auth,\n permission: string,\n context?: AccessContext,\n): Promise<boolean> {\n return !(await can(user, permission, context));\n}\n\n/** Whether the user holds EVERY listed permission. Short-circuits on the first miss. */\nexport async function canAll(\n user: Auth,\n permissions: string[],\n context?: AccessContext,\n): Promise<boolean> {\n for (const permission of permissions) {\n if (!(await can(user, permission, context))) return false;\n }\n\n return true;\n}\n\n/** Whether the user holds ANY listed permission. Short-circuits on the first hit. */\nexport async function canAny(\n user: Auth,\n permissions: string[],\n context?: AccessContext,\n): Promise<boolean> {\n for (const permission of permissions) {\n if (await can(user, permission, context)) return true;\n }\n\n return false;\n}\n\nfunction forbidden(): never {\n throw new ForbiddenError(t(\"access.errors.forbidden\"), {\n errorCode: AccessErrorCodes.Forbidden,\n });\n}\n\n/** Assert the user holds `permission`; throw `ForbiddenError` (403) otherwise. */\nexport async function authorize(\n user: Auth,\n permission: string,\n context?: AccessContext,\n): Promise<void> {\n if (!(await can(user, permission, context))) forbidden();\n}\n\n/** Assert the user holds EVERY listed permission; throw otherwise. */\nexport async function authorizeAll(\n user: Auth,\n permissions: string[],\n context?: AccessContext,\n): Promise<void> {\n if (!(await canAll(user, permissions, context))) forbidden();\n}\n\n/** Assert the user holds ANY listed permission; throw otherwise. */\nexport async function authorizeAny(\n user: Auth,\n permissions: string[],\n context?: AccessContext,\n): Promise<void> {\n if (!(await canAny(user, permissions, context))) forbidden();\n}\n\n/** Whether the user has the given role. */\nexport async function hasRole(\n user: Auth,\n role: string,\n tenant?: string,\n): Promise<boolean> {\n return (await rolesFor(user, tenant)).includes(role);\n}\n\n/** Whether the user has ANY of the given roles. */\nexport async function hasAnyRole(\n user: Auth,\n roles: string[],\n tenant?: string,\n): Promise<boolean> {\n const held = await rolesFor(user, tenant);\n\n return roles.some((role) => held.includes(role));\n}\n\n/** Whether the user has ALL of the given roles. */\nexport async function hasAllRoles(\n user: Auth,\n roles: string[],\n tenant?: string,\n): Promise<boolean> {\n const held = await rolesFor(user, tenant);\n\n return roles.every((role) => held.includes(role));\n}\n\n/**\n * The access facade — every check plus `flush` to drop a user's cached set.\n * Role assignment lives on the ejected `UserRole` model in app-land; callers do\n * `UserRole.assign(...)` then `access.flush(user, tenant)`.\n *\n * @example\n * import { access } from \"@warlock.js/access\";\n * if (await access.can(user, \"orders.update\", { resource: order })) { ... }\n * await access.flush(user, \"tenant-1\");\n */\nexport const access = {\n can,\n cannot,\n canAll,\n canAny,\n authorize,\n authorizeAll,\n authorizeAny,\n hasRole,\n hasAnyRole,\n hasAllRoles,\n /** Drop the user's cached role/permission set (call after you mutate role rows). */\n flush: flushUser,\n};\n","import type { Auth } from \"@warlock.js/auth\";\nimport { t, type HttpContext, type Middleware, type Response } from \"@warlock.js/core\";\nimport type { ModelSchema } from \"@warlock.js/cascade\";\nimport { can, canAll, canAny } from \"../services/access\";\nimport { AccessErrorCodes } from \"../utils/access-error-codes\";\n\n/*\n * @warlock.js/access does NOT augment `RequestUser`, deliberately. An\n * interface can extend at most one class hierarchy, so a library-side\n * `interface RequestUser extends Auth<ModelSchema>` PRE-CLAIMS the one\n * extends slot every app needs for its own model — the app's\n * `interface RequestUser extends User {}` then fails with TS2320, and the\n * app can never see its own auth model on `request.locals.user`. (Core\n * documents the same hazard for member-level augmentation in\n * `http/middleware/utils/idempotency-key.ts`.)\n *\n * The contract instead: an app using gates augments `RequestUser` (declared\n * by `@warlock.js/auth`, which also augments core's `RequestLocals` with the\n * `user` key) to its OWN `Auth`-derived model; `authUserOf` below is the\n * single boundary where access asserts that contract to satisfy `can()`'s\n * parameter type.\n */\n\n/**\n * The one place access bridges `request.locals.user` (app-augmented via\n * `@warlock.js/auth`'s `RequestUser`, empty by default) to the\n * `Auth<ModelSchema>` that `can()`/`canAny()`/`canAll()` require. The\n * assertion is the contract boundary, not a shortcut: the app's `RequestUser`\n * augmentation promises an `Auth`-derived model, and the auth middleware\n * placed exactly that instance at `request.locals.user` at runtime. Kept to\n * this single accessor so the promise is asserted once, visibly, instead of\n * scattered.\n */\nfunction authUserOf(request: HttpContext[\"request\"]): Auth<ModelSchema> | undefined {\n return request.locals.user as unknown as Auth<ModelSchema> | undefined;\n}\n\nfunction deny(response: Response) {\n return response.forbidden({\n error: t(\"access.errors.forbidden\"),\n errorCode: AccessErrorCodes.Forbidden,\n });\n}\n\n/**\n * Gate a route on a single permission (class-level). Stack it AFTER\n * `authMiddleware`, which sets `request.locals.user`.\n *\n * @example\n * router.post(\"/orders\", createOrder, {\n * middleware: [authMiddleware([]), gate(\"orders.create\")],\n * });\n */\nexport function gate(permission: string): Middleware {\n return async ({ request, response }: HttpContext) => {\n const user = authUserOf(request);\n\n if (!user) return deny(response);\n\n if (!(await can(user, permission))) return deny(response);\n };\n}\n\n/** Gate a route on holding ANY of the listed permissions. */\nexport function gateAny(permissions: string[]): Middleware {\n return async ({ request, response }: HttpContext) => {\n const user = authUserOf(request);\n\n if (!user) return deny(response);\n\n if (!(await canAny(user, permissions))) return deny(response);\n };\n}\n\n/** Gate a route on holding ALL of the listed permissions. */\nexport function gateAll(permissions: string[]): Middleware {\n return async ({ request, response }: HttpContext) => {\n const user = authUserOf(request);\n\n if (!user) return deny(response);\n\n if (!(await canAll(user, permissions))) return deny(response);\n };\n}\n","import type { Auth } from \"@warlock.js/auth\";\nimport type { AccessResolver } from \"../contracts/access-resolver\";\nimport type { RolesMap } from \"../contracts/types\";\n\n/** Coerce a roles value (`undefined` / single string / array) into a string array. */\nfunction toRoleList(value: unknown): string[] {\n if (Array.isArray(value)) return value as string[];\n\n if (typeof value === \"string\" && value.length > 0) return [value];\n\n return [];\n}\n\n/**\n * The zero-config resolver: reads a user's roles from a model field and maps\n * them to permissions through your `roles` catalog. Works out of the box for\n * both a `roles` array column and a single `role` column.\n *\n * Swap `readRoles` to read from anywhere else (a relation, a token claim); for\n * a different storage shape entirely, implement {@link AccessResolver} directly.\n *\n * @example\n * new DefaultAccessResolver({ editor: [\"orders.*\"], viewer: [\"orders.view\"] });\n * new DefaultAccessResolver(rolesMap, (user) => user.get(\"memberRoles\"));\n */\nexport class DefaultAccessResolver implements AccessResolver {\n public constructor(\n private readonly roles: RolesMap,\n private readonly readRoles: (user: Auth) => unknown = (user) =>\n user.get(\"roles\") ?? user.get(\"role\"),\n ) {}\n\n public async resolveRoles(user: Auth): Promise<string[]> {\n return toRoleList(await this.readRoles(user));\n }\n\n public async resolvePermissions(user: Auth): Promise<string[]> {\n const roles = await this.resolveRoles(user);\n\n // Guard against a role named `__proto__` / `constructor` resolving to an\n // inherited (non-array) value.\n return roles.flatMap((role) => {\n if (!Object.prototype.hasOwnProperty.call(this.roles, role)) {\n return [];\n }\n\n // `hasOwnProperty` proves the KEY is present; it says nothing about the\n // value, which under `noUncheckedIndexedAccess` is `string[] |\n // undefined`. Falling back to NO permissions is the only safe direction\n // here: this function's result is what the caller checks before granting\n // access, so an unreadable role must grant nothing rather than be\n // asserted into something.\n return this.roles[role] ?? [];\n });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["cache","ForbiddenError","flushUser"],"sources":["../../../../../../../packages/access/src/utils/access-error-codes.ts","../../../../../../../packages/access/src/utils/access-config-error.ts","../../../../../../../packages/access/src/services/access-config.ts","../../../../../../../packages/access/src/services/matcher.ts","../../../../../../../packages/access/src/services/policies.ts","../../../../../../../packages/access/src/services/engine.ts","../../../../../../../packages/access/src/services/access.ts","../../../../../../../packages/access/src/middleware/gate.middleware.ts","../../../../../../../packages/access/src/services/default-resolver.ts"],"sourcesContent":["/**\n * Wire-stable error codes emitted by `@warlock.js/access`. Map these in your\n * error transformer the way you map `AuthErrorCodes`. A value change here is a\n * breaking change for any client that reacts to the code.\n */\nexport enum AccessErrorCodes {\n /** EC100 — authenticated, but missing the required permission. */\n Forbidden = \"EC100\",\n}\n","/**\n * Thrown when `@warlock.js/access` is misconfigured (e.g. no resolver). Unlike a\n * runtime resolution failure — which fails CLOSED (denies) — a config error is a\n * developer mistake, so the engine re-throws it LOUD instead of silently denying.\n */\nexport class AccessConfigError extends Error {\n public constructor(message: string) {\n super(message);\n this.name = \"AccessConfigError\";\n }\n}\n","import type { Auth } from \"@warlock.js/auth\";\nimport type { AccessResolver } from \"../contracts/access-resolver\";\nimport type { AccessConfigurations } from \"../contracts/types\";\nimport { AccessConfigError } from \"../utils/access-config-error\";\n\n/**\n * The active configuration, fed once at boot by the framework's access connector\n * via {@link setAccessConfig} (the connector reads `src/config/access.ts`).\n * Reading it before it's set throws {@link AccessConfigError} — a loud\n * misconfiguration, never a silent deny.\n */\nlet current: AccessConfigurations | undefined;\n\n/**\n * Register the access configuration. The framework's access connector calls this\n * at boot with the default export of `src/config/access.ts`. It validates that a\n * `resolver` is present, so a misconfigured authorization layer fails at STARTUP\n * rather than on the first protected request.\n *\n * @example\n * setAccessConfig({ resolver: new DefaultAccessResolver({ admin: [\"*\"] }) });\n */\nexport function setAccessConfig(configuration: AccessConfigurations): void {\n if (!configuration?.resolver) {\n throw new AccessConfigError(\n \"No access resolver configured. Set `resolver` in src/config/access.ts to a class \" +\n \"implementing AccessResolver — the ejected DatabaseAccessResolver, or \" +\n \"`new DefaultAccessResolver(rolesMap)` for a fixed-role catalog.\",\n );\n }\n\n current = configuration;\n}\n\n/** Clear the active config. Used by the connector on dev-restart and by tests. */\nexport function resetAccessConfig(): void {\n current = undefined;\n}\n\n/** Accessors over the active access configuration. */\nexport const accessConfig = {\n /**\n * The configured resolver. Throws {@link AccessConfigError} if access was never\n * configured (no `src/config/access.ts`, or the connector hasn't run).\n */\n resolver(): AccessResolver {\n if (!current) {\n throw new AccessConfigError(\n \"@warlock.js/access is not configured. Add src/config/access.ts (exporting a `resolver`) \" +\n \"— it is wired at boot by the access connector.\",\n );\n }\n\n return current.resolver;\n },\n\n /** Cache TTL for resolved permission sets. */\n cacheTtl(): string | number {\n return current?.cache?.ttl ?? \"10m\";\n },\n\n /** Whether an unpoliced instance-level check should deny instead of falling back to the RBAC grant. */\n strictPolicies(): boolean {\n return current?.strictPolicies ?? true;\n },\n\n /** Ambient tenant for this user, delegated to the resolver's optional hook. */\n tenant(user: Auth): string | undefined {\n return this.resolver().resolveTenant?.(user);\n },\n};\n","/**\n * Whether a set of granted permission patterns covers a requested permission.\n *\n * Supports three forms:\n * - exact — `\"orders.update\"` covers `\"orders.update\"`\n * - global — `\"*\"` covers everything (the super-grant)\n * - prefix wildcard — `\"orders.*\"` covers `\"orders.update\"` and `\"orders.update.status\"`\n *\n * `\"orders.*\"` does NOT cover the bare `\"orders\"` (no trailing segment).\n */\nexport function matchesPermission(granted: string[], permission: string): boolean {\n for (const pattern of granted) {\n if (pattern === \"*\" || pattern === permission) {\n return true;\n }\n\n // \"orders.*\" → prefix \"orders.\" — matches any nested permission.\n if (pattern.endsWith(\".*\") && permission.startsWith(pattern.slice(0, -1))) {\n return true;\n }\n }\n\n return false;\n}\n","import type { PolicyFn } from \"../contracts/types\";\n\nconst policies = new Map<string, PolicyFn>();\n\n/**\n * Register an ABAC condition for a permission. It runs ON TOP of the RBAC grant\n * whenever an authorization check carries a `resource` — letting you express\n * \"...but only their own / only in their tenant / only while pending\".\n *\n * @example\n * definePolicy(\"orders.update\", (user, order) => order.get(\"customer_id\") === user.id);\n */\nexport function definePolicy(permission: string, policy: PolicyFn): void {\n policies.set(permission, policy);\n}\n\n/** The policy registered for a permission, if any. */\nexport function getPolicy(permission: string): PolicyFn | undefined {\n return policies.get(permission);\n}\n\n/** Remove every registered policy. Primarily for tests. */\nexport function clearPolicies(): void {\n policies.clear();\n}\n","import type { Auth } from \"@warlock.js/auth\";\nimport { cache } from \"@warlock.js/cache\";\nimport { log } from \"@warlock.js/logger\";\nimport type { AccessContext } from \"../contracts/types\";\nimport { AccessConfigError } from \"../utils/access-config-error\";\nimport { accessConfig } from \"./access-config\";\nimport { matchesPermission } from \"./matcher\";\nimport { getPolicy } from \"./policies\";\n\nconst PERMISSIONS_PREFIX = \"access.perms.\";\nconst ROLES_PREFIX = \"access.roles.\";\n\n/**\n * Permissions that have already triggered the \"unpoliced instance check\"\n * warning below — kept so a hot path doesn't log once per request.\n */\nconst warnedUnpolicedPermissions = new Set<string>();\n\n/** Reset the once-per-permission warning tracker. Primarily for tests. */\nexport function resetUnpolicedWarnings(): void {\n warnedUnpolicedPermissions.clear();\n}\n\n/** Resolve the effective tenant for a check (explicit → ambient → undefined). */\nfunction tenantFor(user: Auth, context: AccessContext): string | undefined {\n return context.tenant ?? accessConfig.tenant(user);\n}\n\nfunction cacheKey(prefix: string, user: Auth, tenant?: string): string {\n // Encode each segment so a `.` in an id or tenant can't collide two distinct\n // principals onto the same cache key (and serve one's grants to the other).\n const segment = (value: unknown) => encodeURIComponent(String(value));\n\n return `${prefix}${segment(user.userType)}.${segment(user.id)}.${segment(tenant ?? \"_\")}`;\n}\n\n/**\n * Read a list through the cache, falling back to the loader.\n *\n * The cache is **best-effort**: a cache failure degrades to the loader (the\n * source of truth) and is logged, never denied. A loader failure propagates —\n * so the DECISION fails closed in {@link check}.\n */\nasync function cached(\n key: string,\n loader: () => Promise<string[]>,\n): Promise<string[]> {\n try {\n const hit = await cache.get<string[]>(key);\n\n if (hit) return hit;\n } catch (error) {\n log.error(\"access\", \"cache\", error);\n }\n\n const value = await loader();\n\n try {\n await cache.set(key, value, { ttl: accessConfig.cacheTtl() });\n } catch (error) {\n log.error(\"access\", \"cache\", error);\n }\n\n return value;\n}\n\n/** The user's effective permission set for a tenant (cached). */\nexport function permissionsFor(user: Auth, tenant?: string): Promise<string[]> {\n return cached(cacheKey(PERMISSIONS_PREFIX, user, tenant), () =>\n accessConfig.resolver().resolvePermissions(user, tenant),\n );\n}\n\n/** The user's roles for a tenant (cached). */\nexport function rolesFor(user: Auth, tenant?: string): Promise<string[]> {\n return cached(cacheKey(ROLES_PREFIX, user, tenant), () =>\n accessConfig.resolver().resolveRoles(user, tenant),\n );\n}\n\n/** Drop the cached role/permission sets for a user (after any out-of-band role change). */\nexport async function flush(user: Auth, tenant?: string): Promise<void> {\n try {\n await cache.remove(cacheKey(PERMISSIONS_PREFIX, user, tenant));\n await cache.remove(cacheKey(ROLES_PREFIX, user, tenant));\n } catch (error) {\n log.error(\"access\", \"cache\", error);\n }\n}\n\n/**\n * Surface a resource-scoped check that has no registered ABAC policy, and\n * report whether the check should now DENY.\n *\n * `strictPolicies` defaults to `true`: a forgotten `definePolicy` call (typo'd\n * permission name, or a missing `import \"./policies\"` side-effect — the\n * single most common mistake app teams make with this package, per the\n * `define-policies` skill) must never fail OPEN, so the check denies.\n *\n * `strictPolicies: false` explicitly restores the pre-5.13 behavior: fall\n * back to the RBAC grant alone (ALLOW), which is a plausible IDOR if the\n * permission was meant to be resource-scoped.\n *\n * Either way this warns once per permission (not once per call) so a hot\n * request path doesn't spam logs — mirrors cascade's\n * `warnUndeclaredSensitiveFields`.\n */\nfunction unpolicedShouldDeny(permission: string): boolean {\n const strict = accessConfig.strictPolicies();\n\n if (!warnedUnpolicedPermissions.has(permission)) {\n warnedUnpolicedPermissions.add(permission);\n\n log.warn(\n \"access\",\n \"unpoliced-check\",\n strict\n ? `Instance-level check for permission \"${permission}\" has no registered ABAC policy — ` +\n `DENYING (strictPolicies defaults to true as of 5.13). Register one with ` +\n `definePolicy(\"${permission}\", ...), or make sure the module that calls definePolicy is ` +\n `actually imported. Set strictPolicies: false in the access config to restore the pre-5.13 ` +\n `RBAC-only fallback (ALLOWS — not recommended).`\n : `Instance-level check for permission \"${permission}\" has no registered ABAC policy — falling ` +\n `back to the RBAC grant alone (strictPolicies: false), so any holder of \"${permission}\" can ` +\n `act on ANY resource. If this permission is meant to be resource-scoped, register a policy ` +\n `with definePolicy(\"${permission}\", ...) (and confirm the module that calls it is ` +\n `imported), or remove strictPolicies: false to deny by default.`,\n );\n }\n\n return strict;\n}\n\n/**\n * The core decision: does the user hold `permission`, and — when a `resource`\n * is supplied — does its policy pass too?\n *\n * Fails CLOSED: any error while resolving the decision denies (and logs). An\n * instance-level check for a permission with no registered policy also fails\n * CLOSED by default ({@link AccessConfigurations.strictPolicies}) rather than\n * falling back to the RBAC grant alone.\n */\nexport async function check(\n user: Auth,\n permission: string,\n context: AccessContext = {},\n): Promise<boolean> {\n try {\n const tenant = tenantFor(user, context);\n const permissions = await permissionsFor(user, tenant);\n\n if (!matchesPermission(permissions, permission)) return false;\n\n const policy = getPolicy(permission);\n\n // A policy only runs on an instance check (a resource was supplied);\n // class-level checks (`gate`) stop at the grant.\n if (context.resource !== undefined && policy === undefined) {\n if (unpolicedShouldDeny(permission)) return false;\n }\n\n if (policy === undefined || context.resource === undefined) return true;\n\n const roles = await rolesFor(user, tenant);\n\n return Boolean(\n await policy(user, context.resource, {\n ...context,\n tenant,\n hasRole: (role) => roles.includes(role),\n hasPermission: (perm) => matchesPermission(permissions, perm),\n }),\n );\n } catch (error) {\n if (error instanceof AccessConfigError) throw error; // misconfig is loud, not a silent deny\n\n log.error(\"access\", \"check\", error);\n\n return false;\n }\n}\n","import type { Auth } from \"@warlock.js/auth\";\nimport { ForbiddenError, t } from \"@warlock.js/core\";\nimport type { AccessContext } from \"../contracts/types\";\nimport { AccessErrorCodes } from \"../utils/access-error-codes\";\nimport { check, flush as flushUser, rolesFor } from \"./engine\";\n\n/** Whether the user holds `permission` (and passes its policy when a resource is given). */\nexport function can(\n user: Auth,\n permission: string,\n context?: AccessContext,\n): Promise<boolean> {\n return check(user, permission, context);\n}\n\n/** Inverse of {@link can}. */\nexport async function cannot(\n user: Auth,\n permission: string,\n context?: AccessContext,\n): Promise<boolean> {\n return !(await can(user, permission, context));\n}\n\n/** Whether the user holds EVERY listed permission. Short-circuits on the first miss. */\nexport async function canAll(\n user: Auth,\n permissions: string[],\n context?: AccessContext,\n): Promise<boolean> {\n for (const permission of permissions) {\n if (!(await can(user, permission, context))) return false;\n }\n\n return true;\n}\n\n/** Whether the user holds ANY listed permission. Short-circuits on the first hit. */\nexport async function canAny(\n user: Auth,\n permissions: string[],\n context?: AccessContext,\n): Promise<boolean> {\n for (const permission of permissions) {\n if (await can(user, permission, context)) return true;\n }\n\n return false;\n}\n\nfunction forbidden(): never {\n throw new ForbiddenError(t(\"access.errors.forbidden\"), {\n errorCode: AccessErrorCodes.Forbidden,\n });\n}\n\n/** Assert the user holds `permission`; throw `ForbiddenError` (403) otherwise. */\nexport async function authorize(\n user: Auth,\n permission: string,\n context?: AccessContext,\n): Promise<void> {\n if (!(await can(user, permission, context))) forbidden();\n}\n\n/** Assert the user holds EVERY listed permission; throw otherwise. */\nexport async function authorizeAll(\n user: Auth,\n permissions: string[],\n context?: AccessContext,\n): Promise<void> {\n if (!(await canAll(user, permissions, context))) forbidden();\n}\n\n/** Assert the user holds ANY listed permission; throw otherwise. */\nexport async function authorizeAny(\n user: Auth,\n permissions: string[],\n context?: AccessContext,\n): Promise<void> {\n if (!(await canAny(user, permissions, context))) forbidden();\n}\n\n/** Whether the user has the given role. */\nexport async function hasRole(\n user: Auth,\n role: string,\n tenant?: string,\n): Promise<boolean> {\n return (await rolesFor(user, tenant)).includes(role);\n}\n\n/** Whether the user has ANY of the given roles. */\nexport async function hasAnyRole(\n user: Auth,\n roles: string[],\n tenant?: string,\n): Promise<boolean> {\n const held = await rolesFor(user, tenant);\n\n return roles.some((role) => held.includes(role));\n}\n\n/** Whether the user has ALL of the given roles. */\nexport async function hasAllRoles(\n user: Auth,\n roles: string[],\n tenant?: string,\n): Promise<boolean> {\n const held = await rolesFor(user, tenant);\n\n return roles.every((role) => held.includes(role));\n}\n\n/**\n * The access facade — every check plus `flush` to drop a user's cached set.\n * Role assignment lives on the ejected `UserRole` model in app-land; callers do\n * `UserRole.assign(...)` then `access.flush(user, tenant)`.\n *\n * @example\n * import { access } from \"@warlock.js/access\";\n * if (await access.can(user, \"orders.update\", { resource: order })) { ... }\n * await access.flush(user, \"tenant-1\");\n */\nexport const access = {\n can,\n cannot,\n canAll,\n canAny,\n authorize,\n authorizeAll,\n authorizeAny,\n hasRole,\n hasAnyRole,\n hasAllRoles,\n /** Drop the user's cached role/permission set (call after you mutate role rows). */\n flush: flushUser,\n};\n","import type { Auth } from \"@warlock.js/auth\";\nimport { t, type HttpContext, type Middleware, type Response } from \"@warlock.js/core\";\nimport type { ModelSchema } from \"@warlock.js/cascade\";\nimport { can, canAll, canAny } from \"../services/access\";\nimport { AccessErrorCodes } from \"../utils/access-error-codes\";\n\n/*\n * @warlock.js/access does NOT augment `RequestUser`, deliberately. An\n * interface can extend at most one class hierarchy, so a library-side\n * `interface RequestUser extends Auth<ModelSchema>` PRE-CLAIMS the one\n * extends slot every app needs for its own model — the app's\n * `interface RequestUser extends User {}` then fails with TS2320, and the\n * app can never see its own auth model on `request.locals.user`. (Core\n * documents the same hazard for member-level augmentation in\n * `http/middleware/utils/idempotency-key.ts`.)\n *\n * The contract instead: an app using gates augments `RequestUser` (declared\n * by `@warlock.js/auth`, which also augments core's `RequestLocals` with the\n * `user` key) to its OWN `Auth`-derived model; `authUserOf` below is the\n * single boundary where access asserts that contract to satisfy `can()`'s\n * parameter type.\n */\n\n/**\n * The one place access bridges `request.locals.user` (app-augmented via\n * `@warlock.js/auth`'s `RequestUser`, empty by default) to the\n * `Auth<ModelSchema>` that `can()`/`canAny()`/`canAll()` require. The\n * assertion is the contract boundary, not a shortcut: the app's `RequestUser`\n * augmentation promises an `Auth`-derived model, and the auth middleware\n * placed exactly that instance at `request.locals.user` at runtime. Kept to\n * this single accessor so the promise is asserted once, visibly, instead of\n * scattered.\n */\nfunction authUserOf(request: HttpContext[\"request\"]): Auth<ModelSchema> | undefined {\n return request.locals.user as unknown as Auth<ModelSchema> | undefined;\n}\n\nfunction deny(response: Response) {\n return response.forbidden({\n error: t(\"access.errors.forbidden\"),\n errorCode: AccessErrorCodes.Forbidden,\n });\n}\n\n/**\n * Gate a route on a single permission (class-level). Stack it AFTER\n * `authMiddleware`, which sets `request.locals.user`.\n *\n * @example\n * router.post(\"/orders\", createOrder, {\n * middleware: [authMiddleware([]), gate(\"orders.create\")],\n * });\n */\nexport function gate(permission: string): Middleware {\n return async ({ request, response }: HttpContext) => {\n const user = authUserOf(request);\n\n if (!user) return deny(response);\n\n if (!(await can(user, permission))) return deny(response);\n };\n}\n\n/** Gate a route on holding ANY of the listed permissions. */\nexport function gateAny(permissions: string[]): Middleware {\n return async ({ request, response }: HttpContext) => {\n const user = authUserOf(request);\n\n if (!user) return deny(response);\n\n if (!(await canAny(user, permissions))) return deny(response);\n };\n}\n\n/** Gate a route on holding ALL of the listed permissions. */\nexport function gateAll(permissions: string[]): Middleware {\n return async ({ request, response }: HttpContext) => {\n const user = authUserOf(request);\n\n if (!user) return deny(response);\n\n if (!(await canAll(user, permissions))) return deny(response);\n };\n}\n","import type { Auth } from \"@warlock.js/auth\";\nimport type { AccessResolver } from \"../contracts/access-resolver\";\nimport type { RolesMap } from \"../contracts/types\";\n\n/** Coerce a roles value (`undefined` / single string / array) into a string array. */\nfunction toRoleList(value: unknown): string[] {\n if (Array.isArray(value)) return value as string[];\n\n if (typeof value === \"string\" && value.length > 0) return [value];\n\n return [];\n}\n\n/**\n * The zero-config resolver: reads a user's roles from a model field and maps\n * them to permissions through your `roles` catalog. Works out of the box for\n * both a `roles` array column and a single `role` column.\n *\n * Swap `readRoles` to read from anywhere else (a relation, a token claim); for\n * a different storage shape entirely, implement {@link AccessResolver} directly.\n *\n * @example\n * new DefaultAccessResolver({ editor: [\"orders.*\"], viewer: [\"orders.view\"] });\n * new DefaultAccessResolver(rolesMap, (user) => user.get(\"memberRoles\"));\n */\nexport class DefaultAccessResolver implements AccessResolver {\n public constructor(\n private readonly roles: RolesMap,\n private readonly readRoles: (user: Auth) => unknown = (user) =>\n user.get(\"roles\") ?? user.get(\"role\"),\n ) {}\n\n public async resolveRoles(user: Auth): Promise<string[]> {\n return toRoleList(await this.readRoles(user));\n }\n\n public async resolvePermissions(user: Auth): Promise<string[]> {\n const roles = await this.resolveRoles(user);\n\n // Guard against a role named `__proto__` / `constructor` resolving to an\n // inherited (non-array) value.\n return roles.flatMap((role) => {\n if (!Object.prototype.hasOwnProperty.call(this.roles, role)) {\n return [];\n }\n\n // `hasOwnProperty` proves the KEY is present; it says nothing about the\n // value, which under `noUncheckedIndexedAccess` is `string[] |\n // undefined`. Falling back to NO permissions is the only safe direction\n // here: this function's result is what the caller checks before granting\n // access, so an unreadable role must grant nothing rather than be\n // asserted into something.\n return this.roles[role] ?? [];\n });\n }\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { Auth } from "@warlock.js/auth";
2
2
 
3
- //#region ../access/src/contracts/access-resolver.d.ts
3
+ //#region ../../packages/access/src/contracts/access-resolver.d.ts
4
4
  /**
5
5
  * The one adapter the engine depends on. Implement it to connect the engine to
6
6
  * however YOUR app stores roles and permissions — a user column, a pivot table,
@@ -1 +1 @@
1
- {"version":3,"file":"access-resolver.d.mts","names":[],"sources":["../../../../../../../access/src/contracts/access-resolver.ts"],"mappings":";;;;;AAuBA;;;;;;;;;;;;;;;;;;;UAAiB,cAAA;EAaf;EAXA,YAAA,CAAa,IAAA,EAAM,IAAA,EAAM,MAAA,YAAkB,OAAA;EAW5B;EARf,kBAAA,CAAmB,IAAA,EAAM,IAAA,EAAM,MAAA,YAAkB,OAAA;EAQxB;;;;;;EAAzB,aAAA,EAAe,IAAA,EAAM,IAAA;AAAA"}
1
+ {"version":3,"file":"access-resolver.d.mts","names":[],"sources":["../../../../../../../../packages/access/src/contracts/access-resolver.ts"],"mappings":";;;;;AAuBA;;;;;;;;;;;;;;;;;;;UAAiB,cAAA;EAaf;EAXA,YAAA,CAAa,IAAA,EAAM,IAAA,EAAM,MAAA,YAAkB,OAAA;EAW5B;EARf,kBAAA,CAAmB,IAAA,EAAM,IAAA,EAAM,MAAA,YAAkB,OAAA;EAQxB;;;;;;EAAzB,aAAA,EAAe,IAAA,EAAM,IAAA;AAAA"}
@@ -1,7 +1,7 @@
1
1
  import { AccessResolver } from "./access-resolver.mjs";
2
2
  import { Auth } from "@warlock.js/auth";
3
3
 
4
- //#region ../access/src/contracts/types.d.ts
4
+ //#region ../../packages/access/src/contracts/types.d.ts
5
5
  /** A role name mapped to the permission strings it grants. */
6
6
  type RolesMap = Record<string, string[]>;
7
7
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../access/src/contracts/types.ts"],"mappings":";;;;;KAIY,QAAA,GAAW,MAAM;AAA7B;;;;AAA6B;AAA7B,KAOY,aAAA;EAAa,iFAEvB,QAAA,YAFuB;EAIvB,MAAA;EAAA,CACC,GAAA;AAAA;;KAIS,aAAA,GAAgB,aAAa;EACvC,MAAA,WADuB;EAGvB,OAAA,GAAU,IAAA,sBAH6B;EAKvC,aAAA,GAAgB,UAAA;AAAA;;KAIN,QAAA,IACV,IAAA,EAAM,IAAA,EACN,QAAA,WACA,OAAA,EAAS,aAAA,eACI,OAAA;AAAA,KAEH,oBAAA;EAVM;;AAAkB;AAIpC;;EAYE,QAAA,EAAU,cAAc,EAXlB;EAcN,KAAA;IAXa,2CAaX,GAAA;EAAA;EAhBI;;;;;;;AAGc;AAEtB;;;;;EA2BE,cAAA;AAAA"}
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../../packages/access/src/contracts/types.ts"],"mappings":";;;;;KAIY,QAAA,GAAW,MAAM;AAA7B;;;;AAA6B;AAA7B,KAOY,aAAA;EAAa,iFAEvB,QAAA,YAFuB;EAIvB,MAAA;EAAA,CACC,GAAA;AAAA;;KAIS,aAAA,GAAgB,aAAa;EACvC,MAAA,WADuB;EAGvB,OAAA,GAAU,IAAA,sBAH6B;EAKvC,aAAA,GAAgB,UAAA;AAAA;;KAIN,QAAA,IACV,IAAA,EAAM,IAAA,EACN,QAAA,WACA,OAAA,EAAS,aAAA,eACI,OAAA;AAAA,KAEH,oBAAA;EAVM;;AAAkB;AAIpC;;EAYE,QAAA,EAAU,cAAc,EAXlB;EAcN,KAAA;IAXa,2CAaX,GAAA;EAAA;EAhBI;;;;;;;AAGc;AAEtB;;;;;EA2BE,cAAA;AAAA"}
@@ -1,6 +1,6 @@
1
1
  import { Middleware } from "@warlock.js/core";
2
2
 
3
- //#region ../access/src/middleware/gate.middleware.d.ts
3
+ //#region ../../packages/access/src/middleware/gate.middleware.d.ts
4
4
  /**
5
5
  * Gate a route on a single permission (class-level). Stack it AFTER
6
6
  * `authMiddleware`, which sets `request.locals.user`.
@@ -1 +1 @@
1
- {"version":3,"file":"gate.middleware.d.mts","names":[],"sources":["../../../../../../../access/src/middleware/gate.middleware.ts"],"mappings":";;;;;AAqDA;;;;AAAoD;AAWpD;;iBAXgB,IAAA,CAAK,UAAA,WAAqB,UAAU;;iBAWpC,OAAA,CAAQ,WAAA,aAAwB,UAAU;AAW1D;AAAA,iBAAgB,OAAA,CAAQ,WAAA,aAAwB,UAAU"}
1
+ {"version":3,"file":"gate.middleware.d.mts","names":[],"sources":["../../../../../../../../packages/access/src/middleware/gate.middleware.ts"],"mappings":";;;;;AAqDA;;;;AAAoD;AAWpD;;iBAXgB,IAAA,CAAK,UAAA,WAAqB,UAAU;;iBAWpC,OAAA,CAAQ,WAAA,aAAwB,UAAU;AAW1D;AAAA,iBAAgB,OAAA,CAAQ,WAAA,aAAwB,UAAU"}
@@ -2,7 +2,7 @@ import { AccessErrorCodes } from "../utils/access-error-codes.mjs";
2
2
  import { can, canAll, canAny } from "../services/access.mjs";
3
3
  import { t } from "@warlock.js/core";
4
4
 
5
- //#region ../access/src/middleware/gate.middleware.ts
5
+ //#region ../../packages/access/src/middleware/gate.middleware.ts
6
6
  /**
7
7
  * The one place access bridges `request.locals.user` (app-augmented via
8
8
  * `@warlock.js/auth`'s `RequestUser`, empty by default) to the
@@ -1 +1 @@
1
- {"version":3,"file":"gate.middleware.mjs","names":[],"sources":["../../../../../../../access/src/middleware/gate.middleware.ts"],"sourcesContent":["import type { Auth } from \"@warlock.js/auth\";\nimport { t, type HttpContext, type Middleware, type Response } from \"@warlock.js/core\";\nimport type { ModelSchema } from \"@warlock.js/cascade\";\nimport { can, canAll, canAny } from \"../services/access\";\nimport { AccessErrorCodes } from \"../utils/access-error-codes\";\n\n/*\n * @warlock.js/access does NOT augment `RequestUser`, deliberately. An\n * interface can extend at most one class hierarchy, so a library-side\n * `interface RequestUser extends Auth<ModelSchema>` PRE-CLAIMS the one\n * extends slot every app needs for its own model — the app's\n * `interface RequestUser extends User {}` then fails with TS2320, and the\n * app can never see its own auth model on `request.locals.user`. (Core\n * documents the same hazard for member-level augmentation in\n * `http/middleware/utils/idempotency-key.ts`.)\n *\n * The contract instead: an app using gates augments `RequestUser` (declared\n * by `@warlock.js/auth`, which also augments core's `RequestLocals` with the\n * `user` key) to its OWN `Auth`-derived model; `authUserOf` below is the\n * single boundary where access asserts that contract to satisfy `can()`'s\n * parameter type.\n */\n\n/**\n * The one place access bridges `request.locals.user` (app-augmented via\n * `@warlock.js/auth`'s `RequestUser`, empty by default) to the\n * `Auth<ModelSchema>` that `can()`/`canAny()`/`canAll()` require. The\n * assertion is the contract boundary, not a shortcut: the app's `RequestUser`\n * augmentation promises an `Auth`-derived model, and the auth middleware\n * placed exactly that instance at `request.locals.user` at runtime. Kept to\n * this single accessor so the promise is asserted once, visibly, instead of\n * scattered.\n */\nfunction authUserOf(request: HttpContext[\"request\"]): Auth<ModelSchema> | undefined {\n return request.locals.user as unknown as Auth<ModelSchema> | undefined;\n}\n\nfunction deny(response: Response) {\n return response.forbidden({\n error: t(\"access.errors.forbidden\"),\n errorCode: AccessErrorCodes.Forbidden,\n });\n}\n\n/**\n * Gate a route on a single permission (class-level). Stack it AFTER\n * `authMiddleware`, which sets `request.locals.user`.\n *\n * @example\n * router.post(\"/orders\", createOrder, {\n * middleware: [authMiddleware([]), gate(\"orders.create\")],\n * });\n */\nexport function gate(permission: string): Middleware {\n return async ({ request, response }: HttpContext) => {\n const user = authUserOf(request);\n\n if (!user) return deny(response);\n\n if (!(await can(user, permission))) return deny(response);\n };\n}\n\n/** Gate a route on holding ANY of the listed permissions. */\nexport function gateAny(permissions: string[]): Middleware {\n return async ({ request, response }: HttpContext) => {\n const user = authUserOf(request);\n\n if (!user) return deny(response);\n\n if (!(await canAny(user, permissions))) return deny(response);\n };\n}\n\n/** Gate a route on holding ALL of the listed permissions. */\nexport function gateAll(permissions: string[]): Middleware {\n return async ({ request, response }: HttpContext) => {\n const user = authUserOf(request);\n\n if (!user) return deny(response);\n\n if (!(await canAll(user, permissions))) return deny(response);\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAiCA,SAAS,WAAW,SAAgE;CAClF,OAAO,QAAQ,OAAO;AACxB;AAEA,SAAS,KAAK,UAAoB;CAChC,OAAO,SAAS,UAAU;EACxB,OAAO,EAAE,yBAAyB;EAClC;CACF,CAAC;AACH;;;;;;;;;;AAWA,SAAgB,KAAK,YAAgC;CACnD,OAAO,OAAO,EAAE,SAAS,eAA4B;EACnD,MAAM,OAAO,WAAW,OAAO;EAE/B,IAAI,CAAC,MAAM,OAAO,KAAK,QAAQ;EAE/B,IAAI,CAAE,MAAM,IAAI,MAAM,UAAU,GAAI,OAAO,KAAK,QAAQ;CAC1D;AACF;;AAGA,SAAgB,QAAQ,aAAmC;CACzD,OAAO,OAAO,EAAE,SAAS,eAA4B;EACnD,MAAM,OAAO,WAAW,OAAO;EAE/B,IAAI,CAAC,MAAM,OAAO,KAAK,QAAQ;EAE/B,IAAI,CAAE,MAAM,OAAO,MAAM,WAAW,GAAI,OAAO,KAAK,QAAQ;CAC9D;AACF;;AAGA,SAAgB,QAAQ,aAAmC;CACzD,OAAO,OAAO,EAAE,SAAS,eAA4B;EACnD,MAAM,OAAO,WAAW,OAAO;EAE/B,IAAI,CAAC,MAAM,OAAO,KAAK,QAAQ;EAE/B,IAAI,CAAE,MAAM,OAAO,MAAM,WAAW,GAAI,OAAO,KAAK,QAAQ;CAC9D;AACF"}
1
+ {"version":3,"file":"gate.middleware.mjs","names":[],"sources":["../../../../../../../../packages/access/src/middleware/gate.middleware.ts"],"sourcesContent":["import type { Auth } from \"@warlock.js/auth\";\nimport { t, type HttpContext, type Middleware, type Response } from \"@warlock.js/core\";\nimport type { ModelSchema } from \"@warlock.js/cascade\";\nimport { can, canAll, canAny } from \"../services/access\";\nimport { AccessErrorCodes } from \"../utils/access-error-codes\";\n\n/*\n * @warlock.js/access does NOT augment `RequestUser`, deliberately. An\n * interface can extend at most one class hierarchy, so a library-side\n * `interface RequestUser extends Auth<ModelSchema>` PRE-CLAIMS the one\n * extends slot every app needs for its own model — the app's\n * `interface RequestUser extends User {}` then fails with TS2320, and the\n * app can never see its own auth model on `request.locals.user`. (Core\n * documents the same hazard for member-level augmentation in\n * `http/middleware/utils/idempotency-key.ts`.)\n *\n * The contract instead: an app using gates augments `RequestUser` (declared\n * by `@warlock.js/auth`, which also augments core's `RequestLocals` with the\n * `user` key) to its OWN `Auth`-derived model; `authUserOf` below is the\n * single boundary where access asserts that contract to satisfy `can()`'s\n * parameter type.\n */\n\n/**\n * The one place access bridges `request.locals.user` (app-augmented via\n * `@warlock.js/auth`'s `RequestUser`, empty by default) to the\n * `Auth<ModelSchema>` that `can()`/`canAny()`/`canAll()` require. The\n * assertion is the contract boundary, not a shortcut: the app's `RequestUser`\n * augmentation promises an `Auth`-derived model, and the auth middleware\n * placed exactly that instance at `request.locals.user` at runtime. Kept to\n * this single accessor so the promise is asserted once, visibly, instead of\n * scattered.\n */\nfunction authUserOf(request: HttpContext[\"request\"]): Auth<ModelSchema> | undefined {\n return request.locals.user as unknown as Auth<ModelSchema> | undefined;\n}\n\nfunction deny(response: Response) {\n return response.forbidden({\n error: t(\"access.errors.forbidden\"),\n errorCode: AccessErrorCodes.Forbidden,\n });\n}\n\n/**\n * Gate a route on a single permission (class-level). Stack it AFTER\n * `authMiddleware`, which sets `request.locals.user`.\n *\n * @example\n * router.post(\"/orders\", createOrder, {\n * middleware: [authMiddleware([]), gate(\"orders.create\")],\n * });\n */\nexport function gate(permission: string): Middleware {\n return async ({ request, response }: HttpContext) => {\n const user = authUserOf(request);\n\n if (!user) return deny(response);\n\n if (!(await can(user, permission))) return deny(response);\n };\n}\n\n/** Gate a route on holding ANY of the listed permissions. */\nexport function gateAny(permissions: string[]): Middleware {\n return async ({ request, response }: HttpContext) => {\n const user = authUserOf(request);\n\n if (!user) return deny(response);\n\n if (!(await canAny(user, permissions))) return deny(response);\n };\n}\n\n/** Gate a route on holding ALL of the listed permissions. */\nexport function gateAll(permissions: string[]): Middleware {\n return async ({ request, response }: HttpContext) => {\n const user = authUserOf(request);\n\n if (!user) return deny(response);\n\n if (!(await canAll(user, permissions))) return deny(response);\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAiCA,SAAS,WAAW,SAAgE;CAClF,OAAO,QAAQ,OAAO;AACxB;AAEA,SAAS,KAAK,UAAoB;CAChC,OAAO,SAAS,UAAU;EACxB,OAAO,EAAE,yBAAyB;EAClC;CACF,CAAC;AACH;;;;;;;;;;AAWA,SAAgB,KAAK,YAAgC;CACnD,OAAO,OAAO,EAAE,SAAS,eAA4B;EACnD,MAAM,OAAO,WAAW,OAAO;EAE/B,IAAI,CAAC,MAAM,OAAO,KAAK,QAAQ;EAE/B,IAAI,CAAE,MAAM,IAAI,MAAM,UAAU,GAAI,OAAO,KAAK,QAAQ;CAC1D;AACF;;AAGA,SAAgB,QAAQ,aAAmC;CACzD,OAAO,OAAO,EAAE,SAAS,eAA4B;EACnD,MAAM,OAAO,WAAW,OAAO;EAE/B,IAAI,CAAC,MAAM,OAAO,KAAK,QAAQ;EAE/B,IAAI,CAAE,MAAM,OAAO,MAAM,WAAW,GAAI,OAAO,KAAK,QAAQ;CAC9D;AACF;;AAGA,SAAgB,QAAQ,aAAmC;CACzD,OAAO,OAAO,EAAE,SAAS,eAA4B;EACnD,MAAM,OAAO,WAAW,OAAO;EAE/B,IAAI,CAAC,MAAM,OAAO,KAAK,QAAQ;EAE/B,IAAI,CAAE,MAAM,OAAO,MAAM,WAAW,GAAI,OAAO,KAAK,QAAQ;CAC9D;AACF"}
@@ -1,7 +1,7 @@
1
1
  import { AccessConfigurations } from "../contracts/types.mjs";
2
2
  import { Auth } from "@warlock.js/auth";
3
3
 
4
- //#region ../access/src/services/access-config.d.ts
4
+ //#region ../../packages/access/src/services/access-config.d.ts
5
5
  /**
6
6
  * Register the access configuration. The framework's access connector calls this
7
7
  * at boot with the default export of `src/config/access.ts`. It validates that a
@@ -1 +1 @@
1
- {"version":3,"file":"access-config.d.mts","names":[],"sources":["../../../../../../../access/src/services/access-config.ts"],"mappings":";;;;;;AAsBA;;;;AAAmE;AAanE;;iBAbgB,eAAA,CAAgB,aAAmC,EAApB,oBAAoB;;iBAanD,iBAAA"}
1
+ {"version":3,"file":"access-config.d.mts","names":[],"sources":["../../../../../../../../packages/access/src/services/access-config.ts"],"mappings":";;;;;;AAsBA;;;;AAAmE;AAanE;;iBAbgB,eAAA,CAAgB,aAAmC,EAApB,oBAAoB;;iBAanD,iBAAA"}
@@ -1,6 +1,6 @@
1
1
  import { AccessConfigError } from "../utils/access-config-error.mjs";
2
2
 
3
- //#region ../access/src/services/access-config.ts
3
+ //#region ../../packages/access/src/services/access-config.ts
4
4
  /**
5
5
  * The active configuration, fed once at boot by the framework's access connector
6
6
  * via {@link setAccessConfig} (the connector reads `src/config/access.ts`).
@@ -1 +1 @@
1
- {"version":3,"file":"access-config.mjs","names":[],"sources":["../../../../../../../access/src/services/access-config.ts"],"sourcesContent":["import type { Auth } from \"@warlock.js/auth\";\nimport type { AccessResolver } from \"../contracts/access-resolver\";\nimport type { AccessConfigurations } from \"../contracts/types\";\nimport { AccessConfigError } from \"../utils/access-config-error\";\n\n/**\n * The active configuration, fed once at boot by the framework's access connector\n * via {@link setAccessConfig} (the connector reads `src/config/access.ts`).\n * Reading it before it's set throws {@link AccessConfigError} — a loud\n * misconfiguration, never a silent deny.\n */\nlet current: AccessConfigurations | undefined;\n\n/**\n * Register the access configuration. The framework's access connector calls this\n * at boot with the default export of `src/config/access.ts`. It validates that a\n * `resolver` is present, so a misconfigured authorization layer fails at STARTUP\n * rather than on the first protected request.\n *\n * @example\n * setAccessConfig({ resolver: new DefaultAccessResolver({ admin: [\"*\"] }) });\n */\nexport function setAccessConfig(configuration: AccessConfigurations): void {\n if (!configuration?.resolver) {\n throw new AccessConfigError(\n \"No access resolver configured. Set `resolver` in src/config/access.ts to a class \" +\n \"implementing AccessResolver — the ejected DatabaseAccessResolver, or \" +\n \"`new DefaultAccessResolver(rolesMap)` for a fixed-role catalog.\",\n );\n }\n\n current = configuration;\n}\n\n/** Clear the active config. Used by the connector on dev-restart and by tests. */\nexport function resetAccessConfig(): void {\n current = undefined;\n}\n\n/** Accessors over the active access configuration. */\nexport const accessConfig = {\n /**\n * The configured resolver. Throws {@link AccessConfigError} if access was never\n * configured (no `src/config/access.ts`, or the connector hasn't run).\n */\n resolver(): AccessResolver {\n if (!current) {\n throw new AccessConfigError(\n \"@warlock.js/access is not configured. Add src/config/access.ts (exporting a `resolver`) \" +\n \"— it is wired at boot by the access connector.\",\n );\n }\n\n return current.resolver;\n },\n\n /** Cache TTL for resolved permission sets. */\n cacheTtl(): string | number {\n return current?.cache?.ttl ?? \"10m\";\n },\n\n /** Whether an unpoliced instance-level check should deny instead of falling back to the RBAC grant. */\n strictPolicies(): boolean {\n return current?.strictPolicies ?? true;\n },\n\n /** Ambient tenant for this user, delegated to the resolver's optional hook. */\n tenant(user: Auth): string | undefined {\n return this.resolver().resolveTenant?.(user);\n },\n};\n"],"mappings":";;;;;;;;;AAWA,IAAI;;;;;;;;;;AAWJ,SAAgB,gBAAgB,eAA2C;CACzE,IAAI,CAAC,eAAe,UAClB,MAAM,IAAI,kBACR,uNAGF;CAGF,UAAU;AACZ;;AAGA,SAAgB,oBAA0B;CACxC,UAAU;AACZ;;AAGA,MAAa,eAAe;;;;;CAK1B,WAA2B;EACzB,IAAI,CAAC,SACH,MAAM,IAAI,kBACR,wIAEF;EAGF,OAAO,QAAQ;CACjB;;CAGA,WAA4B;EAC1B,OAAO,SAAS,OAAO,OAAO;CAChC;;CAGA,iBAA0B;EACxB,OAAO,SAAS,kBAAkB;CACpC;;CAGA,OAAO,MAAgC;EACrC,OAAO,KAAK,SAAS,CAAC,CAAC,gBAAgB,IAAI;CAC7C;AACF"}
1
+ {"version":3,"file":"access-config.mjs","names":[],"sources":["../../../../../../../../packages/access/src/services/access-config.ts"],"sourcesContent":["import type { Auth } from \"@warlock.js/auth\";\nimport type { AccessResolver } from \"../contracts/access-resolver\";\nimport type { AccessConfigurations } from \"../contracts/types\";\nimport { AccessConfigError } from \"../utils/access-config-error\";\n\n/**\n * The active configuration, fed once at boot by the framework's access connector\n * via {@link setAccessConfig} (the connector reads `src/config/access.ts`).\n * Reading it before it's set throws {@link AccessConfigError} — a loud\n * misconfiguration, never a silent deny.\n */\nlet current: AccessConfigurations | undefined;\n\n/**\n * Register the access configuration. The framework's access connector calls this\n * at boot with the default export of `src/config/access.ts`. It validates that a\n * `resolver` is present, so a misconfigured authorization layer fails at STARTUP\n * rather than on the first protected request.\n *\n * @example\n * setAccessConfig({ resolver: new DefaultAccessResolver({ admin: [\"*\"] }) });\n */\nexport function setAccessConfig(configuration: AccessConfigurations): void {\n if (!configuration?.resolver) {\n throw new AccessConfigError(\n \"No access resolver configured. Set `resolver` in src/config/access.ts to a class \" +\n \"implementing AccessResolver — the ejected DatabaseAccessResolver, or \" +\n \"`new DefaultAccessResolver(rolesMap)` for a fixed-role catalog.\",\n );\n }\n\n current = configuration;\n}\n\n/** Clear the active config. Used by the connector on dev-restart and by tests. */\nexport function resetAccessConfig(): void {\n current = undefined;\n}\n\n/** Accessors over the active access configuration. */\nexport const accessConfig = {\n /**\n * The configured resolver. Throws {@link AccessConfigError} if access was never\n * configured (no `src/config/access.ts`, or the connector hasn't run).\n */\n resolver(): AccessResolver {\n if (!current) {\n throw new AccessConfigError(\n \"@warlock.js/access is not configured. Add src/config/access.ts (exporting a `resolver`) \" +\n \"— it is wired at boot by the access connector.\",\n );\n }\n\n return current.resolver;\n },\n\n /** Cache TTL for resolved permission sets. */\n cacheTtl(): string | number {\n return current?.cache?.ttl ?? \"10m\";\n },\n\n /** Whether an unpoliced instance-level check should deny instead of falling back to the RBAC grant. */\n strictPolicies(): boolean {\n return current?.strictPolicies ?? true;\n },\n\n /** Ambient tenant for this user, delegated to the resolver's optional hook. */\n tenant(user: Auth): string | undefined {\n return this.resolver().resolveTenant?.(user);\n },\n};\n"],"mappings":";;;;;;;;;AAWA,IAAI;;;;;;;;;;AAWJ,SAAgB,gBAAgB,eAA2C;CACzE,IAAI,CAAC,eAAe,UAClB,MAAM,IAAI,kBACR,uNAGF;CAGF,UAAU;AACZ;;AAGA,SAAgB,oBAA0B;CACxC,UAAU;AACZ;;AAGA,MAAa,eAAe;;;;;CAK1B,WAA2B;EACzB,IAAI,CAAC,SACH,MAAM,IAAI,kBACR,wIAEF;EAGF,OAAO,QAAQ;CACjB;;CAGA,WAA4B;EAC1B,OAAO,SAAS,OAAO,OAAO;CAChC;;CAGA,iBAA0B;EACxB,OAAO,SAAS,kBAAkB;CACpC;;CAGA,OAAO,MAAgC;EACrC,OAAO,KAAK,SAAS,CAAC,CAAC,gBAAgB,IAAI;CAC7C;AACF"}
@@ -2,7 +2,7 @@ import { AccessContext } from "../contracts/types.mjs";
2
2
  import { flush } from "./engine.mjs";
3
3
  import { Auth } from "@warlock.js/auth";
4
4
 
5
- //#region ../access/src/services/access.d.ts
5
+ //#region ../../packages/access/src/services/access.d.ts
6
6
  /** Whether the user holds `permission` (and passes its policy when a resource is given). */
7
7
  declare function can(user: Auth, permission: string, context?: AccessContext): Promise<boolean>;
8
8
  /** Inverse of {@link can}. */
@@ -1 +1 @@
1
- {"version":3,"file":"access.d.mts","names":[],"sources":["../../../../../../../access/src/services/access.ts"],"mappings":";;;;;;iBAOgB,GAAA,CACd,IAAA,EAAM,IAAA,EACN,UAAA,UACA,OAAA,GAAU,aAAA,GACT,OAAA;AAJH;AAAA,iBASsB,MAAA,CACpB,IAAA,EAAM,IAAA,EACN,UAAA,UACA,OAAA,GAAU,aAAA,GACT,OAAA;;iBAKmB,MAAA,CACpB,IAAA,EAAM,IAAA,EACN,WAAA,YACA,OAAA,GAAU,aAAA,GACT,OAAA;;iBASmB,MAAA,CACpB,IAAA,EAAM,IAAA,EACN,WAAA,YACA,OAAA,GAAU,aAAA,GACT,OAAA;;iBAemB,SAAA,CACpB,IAAA,EAAM,IAAA,EACN,UAAA,UACA,OAAA,GAAU,aAAA,GACT,OAAA;;iBAKmB,YAAA,CACpB,IAAA,EAAM,IAAA,EACN,WAAA,YACA,OAAA,GAAU,aAAA,GACT,OAAA;;iBAKmB,YAAA,CACpB,IAAA,EAAM,IAAA,EACN,WAAA,YACA,OAAA,GAAU,aAAA,GACT,OAAA;;iBAKmB,OAAA,CACpB,IAAA,EAAM,IAAA,EACN,IAAA,UACA,MAAA,YACC,OAAO;;iBAKY,UAAA,CACpB,IAAA,EAAM,IAAA,EACN,KAAA,YACA,MAAA,YACC,OAAO;AAjFV;AAAA,iBAwFsB,WAAA,CACpB,IAAA,EAAM,IAAA,EACN,KAAA,YACA,MAAA,YACC,OAAO;;;;;;;;;;;cAgBG,MAAA;;;;;;;;;;mCA/FH"}
1
+ {"version":3,"file":"access.d.mts","names":[],"sources":["../../../../../../../../packages/access/src/services/access.ts"],"mappings":";;;;;;iBAOgB,GAAA,CACd,IAAA,EAAM,IAAA,EACN,UAAA,UACA,OAAA,GAAU,aAAA,GACT,OAAA;AAJH;AAAA,iBASsB,MAAA,CACpB,IAAA,EAAM,IAAA,EACN,UAAA,UACA,OAAA,GAAU,aAAA,GACT,OAAA;;iBAKmB,MAAA,CACpB,IAAA,EAAM,IAAA,EACN,WAAA,YACA,OAAA,GAAU,aAAA,GACT,OAAA;;iBASmB,MAAA,CACpB,IAAA,EAAM,IAAA,EACN,WAAA,YACA,OAAA,GAAU,aAAA,GACT,OAAA;;iBAemB,SAAA,CACpB,IAAA,EAAM,IAAA,EACN,UAAA,UACA,OAAA,GAAU,aAAA,GACT,OAAA;;iBAKmB,YAAA,CACpB,IAAA,EAAM,IAAA,EACN,WAAA,YACA,OAAA,GAAU,aAAA,GACT,OAAA;;iBAKmB,YAAA,CACpB,IAAA,EAAM,IAAA,EACN,WAAA,YACA,OAAA,GAAU,aAAA,GACT,OAAA;;iBAKmB,OAAA,CACpB,IAAA,EAAM,IAAA,EACN,IAAA,UACA,MAAA,YACC,OAAO;;iBAKY,UAAA,CACpB,IAAA,EAAM,IAAA,EACN,KAAA,YACA,MAAA,YACC,OAAO;AAjFV;AAAA,iBAwFsB,WAAA,CACpB,IAAA,EAAM,IAAA,EACN,KAAA,YACA,MAAA,YACC,OAAO;;;;;;;;;;;cAgBG,MAAA;;;;;;;;;;mCA/FH"}
@@ -2,7 +2,7 @@ import { AccessErrorCodes } from "../utils/access-error-codes.mjs";
2
2
  import { check, flush, rolesFor } from "./engine.mjs";
3
3
  import { ForbiddenError, t } from "@warlock.js/core";
4
4
 
5
- //#region ../access/src/services/access.ts
5
+ //#region ../../packages/access/src/services/access.ts
6
6
  /** Whether the user holds `permission` (and passes its policy when a resource is given). */
7
7
  function can(user, permission, context) {
8
8
  return check(user, permission, context);
@@ -1 +1 @@
1
- {"version":3,"file":"access.mjs","names":["flushUser"],"sources":["../../../../../../../access/src/services/access.ts"],"sourcesContent":["import type { Auth } from \"@warlock.js/auth\";\nimport { ForbiddenError, t } from \"@warlock.js/core\";\nimport type { AccessContext } from \"../contracts/types\";\nimport { AccessErrorCodes } from \"../utils/access-error-codes\";\nimport { check, flush as flushUser, rolesFor } from \"./engine\";\n\n/** Whether the user holds `permission` (and passes its policy when a resource is given). */\nexport function can(\n user: Auth,\n permission: string,\n context?: AccessContext,\n): Promise<boolean> {\n return check(user, permission, context);\n}\n\n/** Inverse of {@link can}. */\nexport async function cannot(\n user: Auth,\n permission: string,\n context?: AccessContext,\n): Promise<boolean> {\n return !(await can(user, permission, context));\n}\n\n/** Whether the user holds EVERY listed permission. Short-circuits on the first miss. */\nexport async function canAll(\n user: Auth,\n permissions: string[],\n context?: AccessContext,\n): Promise<boolean> {\n for (const permission of permissions) {\n if (!(await can(user, permission, context))) return false;\n }\n\n return true;\n}\n\n/** Whether the user holds ANY listed permission. Short-circuits on the first hit. */\nexport async function canAny(\n user: Auth,\n permissions: string[],\n context?: AccessContext,\n): Promise<boolean> {\n for (const permission of permissions) {\n if (await can(user, permission, context)) return true;\n }\n\n return false;\n}\n\nfunction forbidden(): never {\n throw new ForbiddenError(t(\"access.errors.forbidden\"), {\n errorCode: AccessErrorCodes.Forbidden,\n });\n}\n\n/** Assert the user holds `permission`; throw `ForbiddenError` (403) otherwise. */\nexport async function authorize(\n user: Auth,\n permission: string,\n context?: AccessContext,\n): Promise<void> {\n if (!(await can(user, permission, context))) forbidden();\n}\n\n/** Assert the user holds EVERY listed permission; throw otherwise. */\nexport async function authorizeAll(\n user: Auth,\n permissions: string[],\n context?: AccessContext,\n): Promise<void> {\n if (!(await canAll(user, permissions, context))) forbidden();\n}\n\n/** Assert the user holds ANY listed permission; throw otherwise. */\nexport async function authorizeAny(\n user: Auth,\n permissions: string[],\n context?: AccessContext,\n): Promise<void> {\n if (!(await canAny(user, permissions, context))) forbidden();\n}\n\n/** Whether the user has the given role. */\nexport async function hasRole(\n user: Auth,\n role: string,\n tenant?: string,\n): Promise<boolean> {\n return (await rolesFor(user, tenant)).includes(role);\n}\n\n/** Whether the user has ANY of the given roles. */\nexport async function hasAnyRole(\n user: Auth,\n roles: string[],\n tenant?: string,\n): Promise<boolean> {\n const held = await rolesFor(user, tenant);\n\n return roles.some((role) => held.includes(role));\n}\n\n/** Whether the user has ALL of the given roles. */\nexport async function hasAllRoles(\n user: Auth,\n roles: string[],\n tenant?: string,\n): Promise<boolean> {\n const held = await rolesFor(user, tenant);\n\n return roles.every((role) => held.includes(role));\n}\n\n/**\n * The access facade — every check plus `flush` to drop a user's cached set.\n * Role assignment lives on the ejected `UserRole` model in app-land; callers do\n * `UserRole.assign(...)` then `access.flush(user, tenant)`.\n *\n * @example\n * import { access } from \"@warlock.js/access\";\n * if (await access.can(user, \"orders.update\", { resource: order })) { ... }\n * await access.flush(user, \"tenant-1\");\n */\nexport const access = {\n can,\n cannot,\n canAll,\n canAny,\n authorize,\n authorizeAll,\n authorizeAny,\n hasRole,\n hasAnyRole,\n hasAllRoles,\n /** Drop the user's cached role/permission set (call after you mutate role rows). */\n flush: flushUser,\n};\n"],"mappings":";;;;;;AAOA,SAAgB,IACd,MACA,YACA,SACkB;CAClB,OAAO,MAAM,MAAM,YAAY,OAAO;AACxC;;AAGA,eAAsB,OACpB,MACA,YACA,SACkB;CAClB,OAAO,CAAE,MAAM,IAAI,MAAM,YAAY,OAAO;AAC9C;;AAGA,eAAsB,OACpB,MACA,aACA,SACkB;CAClB,KAAK,MAAM,cAAc,aACvB,IAAI,CAAE,MAAM,IAAI,MAAM,YAAY,OAAO,GAAI,OAAO;CAGtD,OAAO;AACT;;AAGA,eAAsB,OACpB,MACA,aACA,SACkB;CAClB,KAAK,MAAM,cAAc,aACvB,IAAI,MAAM,IAAI,MAAM,YAAY,OAAO,GAAG,OAAO;CAGnD,OAAO;AACT;AAEA,SAAS,YAAmB;CAC1B,MAAM,IAAI,eAAe,EAAE,yBAAyB,GAAG,EACrD,mBACF,CAAC;AACH;;AAGA,eAAsB,UACpB,MACA,YACA,SACe;CACf,IAAI,CAAE,MAAM,IAAI,MAAM,YAAY,OAAO,GAAI,UAAU;AACzD;;AAGA,eAAsB,aACpB,MACA,aACA,SACe;CACf,IAAI,CAAE,MAAM,OAAO,MAAM,aAAa,OAAO,GAAI,UAAU;AAC7D;;AAGA,eAAsB,aACpB,MACA,aACA,SACe;CACf,IAAI,CAAE,MAAM,OAAO,MAAM,aAAa,OAAO,GAAI,UAAU;AAC7D;;AAGA,eAAsB,QACpB,MACA,MACA,QACkB;CAClB,QAAQ,MAAM,SAAS,MAAM,MAAM,EAAC,CAAE,SAAS,IAAI;AACrD;;AAGA,eAAsB,WACpB,MACA,OACA,QACkB;CAClB,MAAM,OAAO,MAAM,SAAS,MAAM,MAAM;CAExC,OAAO,MAAM,MAAM,SAAS,KAAK,SAAS,IAAI,CAAC;AACjD;;AAGA,eAAsB,YACpB,MACA,OACA,QACkB;CAClB,MAAM,OAAO,MAAM,SAAS,MAAM,MAAM;CAExC,OAAO,MAAM,OAAO,SAAS,KAAK,SAAS,IAAI,CAAC;AAClD;;;;;;;;;;;AAYA,MAAa,SAAS;CACpB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;;CAEOA;AACT"}
1
+ {"version":3,"file":"access.mjs","names":["flushUser"],"sources":["../../../../../../../../packages/access/src/services/access.ts"],"sourcesContent":["import type { Auth } from \"@warlock.js/auth\";\nimport { ForbiddenError, t } from \"@warlock.js/core\";\nimport type { AccessContext } from \"../contracts/types\";\nimport { AccessErrorCodes } from \"../utils/access-error-codes\";\nimport { check, flush as flushUser, rolesFor } from \"./engine\";\n\n/** Whether the user holds `permission` (and passes its policy when a resource is given). */\nexport function can(\n user: Auth,\n permission: string,\n context?: AccessContext,\n): Promise<boolean> {\n return check(user, permission, context);\n}\n\n/** Inverse of {@link can}. */\nexport async function cannot(\n user: Auth,\n permission: string,\n context?: AccessContext,\n): Promise<boolean> {\n return !(await can(user, permission, context));\n}\n\n/** Whether the user holds EVERY listed permission. Short-circuits on the first miss. */\nexport async function canAll(\n user: Auth,\n permissions: string[],\n context?: AccessContext,\n): Promise<boolean> {\n for (const permission of permissions) {\n if (!(await can(user, permission, context))) return false;\n }\n\n return true;\n}\n\n/** Whether the user holds ANY listed permission. Short-circuits on the first hit. */\nexport async function canAny(\n user: Auth,\n permissions: string[],\n context?: AccessContext,\n): Promise<boolean> {\n for (const permission of permissions) {\n if (await can(user, permission, context)) return true;\n }\n\n return false;\n}\n\nfunction forbidden(): never {\n throw new ForbiddenError(t(\"access.errors.forbidden\"), {\n errorCode: AccessErrorCodes.Forbidden,\n });\n}\n\n/** Assert the user holds `permission`; throw `ForbiddenError` (403) otherwise. */\nexport async function authorize(\n user: Auth,\n permission: string,\n context?: AccessContext,\n): Promise<void> {\n if (!(await can(user, permission, context))) forbidden();\n}\n\n/** Assert the user holds EVERY listed permission; throw otherwise. */\nexport async function authorizeAll(\n user: Auth,\n permissions: string[],\n context?: AccessContext,\n): Promise<void> {\n if (!(await canAll(user, permissions, context))) forbidden();\n}\n\n/** Assert the user holds ANY listed permission; throw otherwise. */\nexport async function authorizeAny(\n user: Auth,\n permissions: string[],\n context?: AccessContext,\n): Promise<void> {\n if (!(await canAny(user, permissions, context))) forbidden();\n}\n\n/** Whether the user has the given role. */\nexport async function hasRole(\n user: Auth,\n role: string,\n tenant?: string,\n): Promise<boolean> {\n return (await rolesFor(user, tenant)).includes(role);\n}\n\n/** Whether the user has ANY of the given roles. */\nexport async function hasAnyRole(\n user: Auth,\n roles: string[],\n tenant?: string,\n): Promise<boolean> {\n const held = await rolesFor(user, tenant);\n\n return roles.some((role) => held.includes(role));\n}\n\n/** Whether the user has ALL of the given roles. */\nexport async function hasAllRoles(\n user: Auth,\n roles: string[],\n tenant?: string,\n): Promise<boolean> {\n const held = await rolesFor(user, tenant);\n\n return roles.every((role) => held.includes(role));\n}\n\n/**\n * The access facade — every check plus `flush` to drop a user's cached set.\n * Role assignment lives on the ejected `UserRole` model in app-land; callers do\n * `UserRole.assign(...)` then `access.flush(user, tenant)`.\n *\n * @example\n * import { access } from \"@warlock.js/access\";\n * if (await access.can(user, \"orders.update\", { resource: order })) { ... }\n * await access.flush(user, \"tenant-1\");\n */\nexport const access = {\n can,\n cannot,\n canAll,\n canAny,\n authorize,\n authorizeAll,\n authorizeAny,\n hasRole,\n hasAnyRole,\n hasAllRoles,\n /** Drop the user's cached role/permission set (call after you mutate role rows). */\n flush: flushUser,\n};\n"],"mappings":";;;;;;AAOA,SAAgB,IACd,MACA,YACA,SACkB;CAClB,OAAO,MAAM,MAAM,YAAY,OAAO;AACxC;;AAGA,eAAsB,OACpB,MACA,YACA,SACkB;CAClB,OAAO,CAAE,MAAM,IAAI,MAAM,YAAY,OAAO;AAC9C;;AAGA,eAAsB,OACpB,MACA,aACA,SACkB;CAClB,KAAK,MAAM,cAAc,aACvB,IAAI,CAAE,MAAM,IAAI,MAAM,YAAY,OAAO,GAAI,OAAO;CAGtD,OAAO;AACT;;AAGA,eAAsB,OACpB,MACA,aACA,SACkB;CAClB,KAAK,MAAM,cAAc,aACvB,IAAI,MAAM,IAAI,MAAM,YAAY,OAAO,GAAG,OAAO;CAGnD,OAAO;AACT;AAEA,SAAS,YAAmB;CAC1B,MAAM,IAAI,eAAe,EAAE,yBAAyB,GAAG,EACrD,mBACF,CAAC;AACH;;AAGA,eAAsB,UACpB,MACA,YACA,SACe;CACf,IAAI,CAAE,MAAM,IAAI,MAAM,YAAY,OAAO,GAAI,UAAU;AACzD;;AAGA,eAAsB,aACpB,MACA,aACA,SACe;CACf,IAAI,CAAE,MAAM,OAAO,MAAM,aAAa,OAAO,GAAI,UAAU;AAC7D;;AAGA,eAAsB,aACpB,MACA,aACA,SACe;CACf,IAAI,CAAE,MAAM,OAAO,MAAM,aAAa,OAAO,GAAI,UAAU;AAC7D;;AAGA,eAAsB,QACpB,MACA,MACA,QACkB;CAClB,QAAQ,MAAM,SAAS,MAAM,MAAM,EAAC,CAAE,SAAS,IAAI;AACrD;;AAGA,eAAsB,WACpB,MACA,OACA,QACkB;CAClB,MAAM,OAAO,MAAM,SAAS,MAAM,MAAM;CAExC,OAAO,MAAM,MAAM,SAAS,KAAK,SAAS,IAAI,CAAC;AACjD;;AAGA,eAAsB,YACpB,MACA,OACA,QACkB;CAClB,MAAM,OAAO,MAAM,SAAS,MAAM,MAAM;CAExC,OAAO,MAAM,OAAO,SAAS,KAAK,SAAS,IAAI,CAAC;AAClD;;;;;;;;;;;AAYA,MAAa,SAAS;CACpB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;;CAEOA;AACT"}
@@ -2,7 +2,7 @@ import { AccessResolver } from "../contracts/access-resolver.mjs";
2
2
  import { RolesMap } from "../contracts/types.mjs";
3
3
  import { Auth } from "@warlock.js/auth";
4
4
 
5
- //#region ../access/src/services/default-resolver.d.ts
5
+ //#region ../../packages/access/src/services/default-resolver.d.ts
6
6
  /**
7
7
  * The zero-config resolver: reads a user's roles from a model field and maps
8
8
  * them to permissions through your `roles` catalog. Works out of the box for
@@ -1 +1 @@
1
- {"version":3,"file":"default-resolver.d.mts","names":[],"sources":["../../../../../../../access/src/services/default-resolver.ts"],"mappings":";;;;;;;AAyBA;;;;;;;;;;cAAa,qBAAA,YAAiC,cAAA;EAAA,iBAEzB,KAAA;EAAA,iBACA,SAAA;cADA,KAAA,EAAO,QAAA,EACP,SAAA,IAAY,IAAA,EAAM,IAAA;EAIxB,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,OAAA;EAI1B,kBAAA,CAAmB,IAAA,EAAM,IAAA,GAAO,OAAA;AAAA"}
1
+ {"version":3,"file":"default-resolver.d.mts","names":[],"sources":["../../../../../../../../packages/access/src/services/default-resolver.ts"],"mappings":";;;;;;;AAyBA;;;;;;;;;;cAAa,qBAAA,YAAiC,cAAA;EAAA,iBAEzB,KAAA;EAAA,iBACA,SAAA;cADA,KAAA,EAAO,QAAA,EACP,SAAA,IAAY,IAAA,EAAM,IAAA;EAIxB,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,OAAA;EAI1B,kBAAA,CAAmB,IAAA,EAAM,IAAA,GAAO,OAAA;AAAA"}
@@ -1,4 +1,4 @@
1
- //#region ../access/src/services/default-resolver.ts
1
+ //#region ../../packages/access/src/services/default-resolver.ts
2
2
  /** Coerce a roles value (`undefined` / single string / array) into a string array. */
3
3
  function toRoleList(value) {
4
4
  if (Array.isArray(value)) return value;
@@ -1 +1 @@
1
- {"version":3,"file":"default-resolver.mjs","names":[],"sources":["../../../../../../../access/src/services/default-resolver.ts"],"sourcesContent":["import type { Auth } from \"@warlock.js/auth\";\nimport type { AccessResolver } from \"../contracts/access-resolver\";\nimport type { RolesMap } from \"../contracts/types\";\n\n/** Coerce a roles value (`undefined` / single string / array) into a string array. */\nfunction toRoleList(value: unknown): string[] {\n if (Array.isArray(value)) return value as string[];\n\n if (typeof value === \"string\" && value.length > 0) return [value];\n\n return [];\n}\n\n/**\n * The zero-config resolver: reads a user's roles from a model field and maps\n * them to permissions through your `roles` catalog. Works out of the box for\n * both a `roles` array column and a single `role` column.\n *\n * Swap `readRoles` to read from anywhere else (a relation, a token claim); for\n * a different storage shape entirely, implement {@link AccessResolver} directly.\n *\n * @example\n * new DefaultAccessResolver({ editor: [\"orders.*\"], viewer: [\"orders.view\"] });\n * new DefaultAccessResolver(rolesMap, (user) => user.get(\"memberRoles\"));\n */\nexport class DefaultAccessResolver implements AccessResolver {\n public constructor(\n private readonly roles: RolesMap,\n private readonly readRoles: (user: Auth) => unknown = (user) =>\n user.get(\"roles\") ?? user.get(\"role\"),\n ) {}\n\n public async resolveRoles(user: Auth): Promise<string[]> {\n return toRoleList(await this.readRoles(user));\n }\n\n public async resolvePermissions(user: Auth): Promise<string[]> {\n const roles = await this.resolveRoles(user);\n\n // Guard against a role named `__proto__` / `constructor` resolving to an\n // inherited (non-array) value.\n return roles.flatMap((role) => {\n if (!Object.prototype.hasOwnProperty.call(this.roles, role)) {\n return [];\n }\n\n // `hasOwnProperty` proves the KEY is present; it says nothing about the\n // value, which under `noUncheckedIndexedAccess` is `string[] |\n // undefined`. Falling back to NO permissions is the only safe direction\n // here: this function's result is what the caller checks before granting\n // access, so an unreadable role must grant nothing rather than be\n // asserted into something.\n return this.roles[role] ?? [];\n });\n }\n}\n"],"mappings":";;AAKA,SAAS,WAAW,OAA0B;CAC5C,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO;CAEjC,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK;CAEhE,OAAO,CAAC;AACV;;;;;;;;;;;;;AAcA,IAAa,wBAAb,MAA6D;CAExC;CACA;CAFnB,AAAO,YACL,AAAiB,OACjB,AAAiB,aAAsC,SACrD,KAAK,IAAI,OAAO,KAAK,KAAK,IAAI,MAAM,GACtC;EAHiB;EACA;CAEhB;CAEH,MAAa,aAAa,MAA+B;EACvD,OAAO,WAAW,MAAM,KAAK,UAAU,IAAI,CAAC;CAC9C;CAEA,MAAa,mBAAmB,MAA+B;EAK7D,QAAO,MAJa,KAAK,aAAa,IAAI,EAI9B,CAAC,SAAS,SAAS;GAC7B,IAAI,CAAC,OAAO,UAAU,eAAe,KAAK,KAAK,OAAO,IAAI,GACxD,OAAO,CAAC;GASV,OAAO,KAAK,MAAM,SAAS,CAAC;EAC9B,CAAC;CACH;AACF"}
1
+ {"version":3,"file":"default-resolver.mjs","names":[],"sources":["../../../../../../../../packages/access/src/services/default-resolver.ts"],"sourcesContent":["import type { Auth } from \"@warlock.js/auth\";\nimport type { AccessResolver } from \"../contracts/access-resolver\";\nimport type { RolesMap } from \"../contracts/types\";\n\n/** Coerce a roles value (`undefined` / single string / array) into a string array. */\nfunction toRoleList(value: unknown): string[] {\n if (Array.isArray(value)) return value as string[];\n\n if (typeof value === \"string\" && value.length > 0) return [value];\n\n return [];\n}\n\n/**\n * The zero-config resolver: reads a user's roles from a model field and maps\n * them to permissions through your `roles` catalog. Works out of the box for\n * both a `roles` array column and a single `role` column.\n *\n * Swap `readRoles` to read from anywhere else (a relation, a token claim); for\n * a different storage shape entirely, implement {@link AccessResolver} directly.\n *\n * @example\n * new DefaultAccessResolver({ editor: [\"orders.*\"], viewer: [\"orders.view\"] });\n * new DefaultAccessResolver(rolesMap, (user) => user.get(\"memberRoles\"));\n */\nexport class DefaultAccessResolver implements AccessResolver {\n public constructor(\n private readonly roles: RolesMap,\n private readonly readRoles: (user: Auth) => unknown = (user) =>\n user.get(\"roles\") ?? user.get(\"role\"),\n ) {}\n\n public async resolveRoles(user: Auth): Promise<string[]> {\n return toRoleList(await this.readRoles(user));\n }\n\n public async resolvePermissions(user: Auth): Promise<string[]> {\n const roles = await this.resolveRoles(user);\n\n // Guard against a role named `__proto__` / `constructor` resolving to an\n // inherited (non-array) value.\n return roles.flatMap((role) => {\n if (!Object.prototype.hasOwnProperty.call(this.roles, role)) {\n return [];\n }\n\n // `hasOwnProperty` proves the KEY is present; it says nothing about the\n // value, which under `noUncheckedIndexedAccess` is `string[] |\n // undefined`. Falling back to NO permissions is the only safe direction\n // here: this function's result is what the caller checks before granting\n // access, so an unreadable role must grant nothing rather than be\n // asserted into something.\n return this.roles[role] ?? [];\n });\n }\n}\n"],"mappings":";;AAKA,SAAS,WAAW,OAA0B;CAC5C,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO;CAEjC,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK;CAEhE,OAAO,CAAC;AACV;;;;;;;;;;;;;AAcA,IAAa,wBAAb,MAA6D;CAExC;CACA;CAFnB,AAAO,YACL,AAAiB,OACjB,AAAiB,aAAsC,SACrD,KAAK,IAAI,OAAO,KAAK,KAAK,IAAI,MAAM,GACtC;EAHiB;EACA;CAEhB;CAEH,MAAa,aAAa,MAA+B;EACvD,OAAO,WAAW,MAAM,KAAK,UAAU,IAAI,CAAC;CAC9C;CAEA,MAAa,mBAAmB,MAA+B;EAK7D,QAAO,MAJa,KAAK,aAAa,IAAI,EAI9B,CAAC,SAAS,SAAS;GAC7B,IAAI,CAAC,OAAO,UAAU,eAAe,KAAK,KAAK,OAAO,IAAI,GACxD,OAAO,CAAC;GASV,OAAO,KAAK,MAAM,SAAS,CAAC;EAC9B,CAAC;CACH;AACF"}
@@ -1,6 +1,6 @@
1
1
  import { Auth } from "@warlock.js/auth";
2
2
 
3
- //#region ../access/src/services/engine.d.ts
3
+ //#region ../../packages/access/src/services/engine.d.ts
4
4
  /** Drop the cached role/permission sets for a user (after any out-of-band role change). */
5
5
  declare function flush(user: Auth, tenant?: string): Promise<void>;
6
6
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.mts","names":[],"sources":["../../../../../../../access/src/services/engine.ts"],"mappings":";;;;iBAiFsB,KAAA,CAAM,IAAA,EAAM,IAAA,EAAM,MAAA,YAAkB,OAAO"}
1
+ {"version":3,"file":"engine.d.mts","names":[],"sources":["../../../../../../../../packages/access/src/services/engine.ts"],"mappings":";;;;iBAiFsB,KAAA,CAAM,IAAA,EAAM,IAAA,EAAM,MAAA,YAAkB,OAAO"}
@@ -5,7 +5,7 @@ import { getPolicy } from "./policies.mjs";
5
5
  import { cache } from "@warlock.js/cache";
6
6
  import { log } from "@warlock.js/logger";
7
7
 
8
- //#region ../access/src/services/engine.ts
8
+ //#region ../../packages/access/src/services/engine.ts
9
9
  const PERMISSIONS_PREFIX = "access.perms.";
10
10
  const ROLES_PREFIX = "access.roles.";
11
11
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"engine.mjs","names":[],"sources":["../../../../../../../access/src/services/engine.ts"],"sourcesContent":["import type { Auth } from \"@warlock.js/auth\";\nimport { cache } from \"@warlock.js/cache\";\nimport { log } from \"@warlock.js/logger\";\nimport type { AccessContext } from \"../contracts/types\";\nimport { AccessConfigError } from \"../utils/access-config-error\";\nimport { accessConfig } from \"./access-config\";\nimport { matchesPermission } from \"./matcher\";\nimport { getPolicy } from \"./policies\";\n\nconst PERMISSIONS_PREFIX = \"access.perms.\";\nconst ROLES_PREFIX = \"access.roles.\";\n\n/**\n * Permissions that have already triggered the \"unpoliced instance check\"\n * warning below — kept so a hot path doesn't log once per request.\n */\nconst warnedUnpolicedPermissions = new Set<string>();\n\n/** Reset the once-per-permission warning tracker. Primarily for tests. */\nexport function resetUnpolicedWarnings(): void {\n warnedUnpolicedPermissions.clear();\n}\n\n/** Resolve the effective tenant for a check (explicit → ambient → undefined). */\nfunction tenantFor(user: Auth, context: AccessContext): string | undefined {\n return context.tenant ?? accessConfig.tenant(user);\n}\n\nfunction cacheKey(prefix: string, user: Auth, tenant?: string): string {\n // Encode each segment so a `.` in an id or tenant can't collide two distinct\n // principals onto the same cache key (and serve one's grants to the other).\n const segment = (value: unknown) => encodeURIComponent(String(value));\n\n return `${prefix}${segment(user.userType)}.${segment(user.id)}.${segment(tenant ?? \"_\")}`;\n}\n\n/**\n * Read a list through the cache, falling back to the loader.\n *\n * The cache is **best-effort**: a cache failure degrades to the loader (the\n * source of truth) and is logged, never denied. A loader failure propagates —\n * so the DECISION fails closed in {@link check}.\n */\nasync function cached(\n key: string,\n loader: () => Promise<string[]>,\n): Promise<string[]> {\n try {\n const hit = await cache.get<string[]>(key);\n\n if (hit) return hit;\n } catch (error) {\n log.error(\"access\", \"cache\", error);\n }\n\n const value = await loader();\n\n try {\n await cache.set(key, value, { ttl: accessConfig.cacheTtl() });\n } catch (error) {\n log.error(\"access\", \"cache\", error);\n }\n\n return value;\n}\n\n/** The user's effective permission set for a tenant (cached). */\nexport function permissionsFor(user: Auth, tenant?: string): Promise<string[]> {\n return cached(cacheKey(PERMISSIONS_PREFIX, user, tenant), () =>\n accessConfig.resolver().resolvePermissions(user, tenant),\n );\n}\n\n/** The user's roles for a tenant (cached). */\nexport function rolesFor(user: Auth, tenant?: string): Promise<string[]> {\n return cached(cacheKey(ROLES_PREFIX, user, tenant), () =>\n accessConfig.resolver().resolveRoles(user, tenant),\n );\n}\n\n/** Drop the cached role/permission sets for a user (after any out-of-band role change). */\nexport async function flush(user: Auth, tenant?: string): Promise<void> {\n try {\n await cache.remove(cacheKey(PERMISSIONS_PREFIX, user, tenant));\n await cache.remove(cacheKey(ROLES_PREFIX, user, tenant));\n } catch (error) {\n log.error(\"access\", \"cache\", error);\n }\n}\n\n/**\n * Surface a resource-scoped check that has no registered ABAC policy, and\n * report whether the check should now DENY.\n *\n * `strictPolicies` defaults to `true`: a forgotten `definePolicy` call (typo'd\n * permission name, or a missing `import \"./policies\"` side-effect — the\n * single most common mistake app teams make with this package, per the\n * `define-policies` skill) must never fail OPEN, so the check denies.\n *\n * `strictPolicies: false` explicitly restores the pre-5.13 behavior: fall\n * back to the RBAC grant alone (ALLOW), which is a plausible IDOR if the\n * permission was meant to be resource-scoped.\n *\n * Either way this warns once per permission (not once per call) so a hot\n * request path doesn't spam logs — mirrors cascade's\n * `warnUndeclaredSensitiveFields`.\n */\nfunction unpolicedShouldDeny(permission: string): boolean {\n const strict = accessConfig.strictPolicies();\n\n if (!warnedUnpolicedPermissions.has(permission)) {\n warnedUnpolicedPermissions.add(permission);\n\n log.warn(\n \"access\",\n \"unpoliced-check\",\n strict\n ? `Instance-level check for permission \"${permission}\" has no registered ABAC policy — ` +\n `DENYING (strictPolicies defaults to true as of 5.13). Register one with ` +\n `definePolicy(\"${permission}\", ...), or make sure the module that calls definePolicy is ` +\n `actually imported. Set strictPolicies: false in the access config to restore the pre-5.13 ` +\n `RBAC-only fallback (ALLOWS — not recommended).`\n : `Instance-level check for permission \"${permission}\" has no registered ABAC policy — falling ` +\n `back to the RBAC grant alone (strictPolicies: false), so any holder of \"${permission}\" can ` +\n `act on ANY resource. If this permission is meant to be resource-scoped, register a policy ` +\n `with definePolicy(\"${permission}\", ...) (and confirm the module that calls it is ` +\n `imported), or remove strictPolicies: false to deny by default.`,\n );\n }\n\n return strict;\n}\n\n/**\n * The core decision: does the user hold `permission`, and — when a `resource`\n * is supplied — does its policy pass too?\n *\n * Fails CLOSED: any error while resolving the decision denies (and logs). An\n * instance-level check for a permission with no registered policy also fails\n * CLOSED by default ({@link AccessConfigurations.strictPolicies}) rather than\n * falling back to the RBAC grant alone.\n */\nexport async function check(\n user: Auth,\n permission: string,\n context: AccessContext = {},\n): Promise<boolean> {\n try {\n const tenant = tenantFor(user, context);\n const permissions = await permissionsFor(user, tenant);\n\n if (!matchesPermission(permissions, permission)) return false;\n\n const policy = getPolicy(permission);\n\n // A policy only runs on an instance check (a resource was supplied);\n // class-level checks (`gate`) stop at the grant.\n if (context.resource !== undefined && policy === undefined) {\n if (unpolicedShouldDeny(permission)) return false;\n }\n\n if (policy === undefined || context.resource === undefined) return true;\n\n const roles = await rolesFor(user, tenant);\n\n return Boolean(\n await policy(user, context.resource, {\n ...context,\n tenant,\n hasRole: (role) => roles.includes(role),\n hasPermission: (perm) => matchesPermission(permissions, perm),\n }),\n );\n } catch (error) {\n if (error instanceof AccessConfigError) throw error; // misconfig is loud, not a silent deny\n\n log.error(\"access\", \"check\", error);\n\n return false;\n }\n}\n"],"mappings":";;;;;;;;AASA,MAAM,qBAAqB;AAC3B,MAAM,eAAe;;;;;AAMrB,MAAM,6CAA6B,IAAI,IAAY;;AAQnD,SAAS,UAAU,MAAY,SAA4C;CACzE,OAAO,QAAQ,UAAU,aAAa,OAAO,IAAI;AACnD;AAEA,SAAS,SAAS,QAAgB,MAAY,QAAyB;CAGrE,MAAM,WAAW,UAAmB,mBAAmB,OAAO,KAAK,CAAC;CAEpE,OAAO,GAAG,SAAS,QAAQ,KAAK,QAAQ,EAAE,GAAG,QAAQ,KAAK,EAAE,EAAE,GAAG,QAAQ,UAAU,GAAG;AACxF;;;;;;;;AASA,eAAe,OACb,KACA,QACmB;CACnB,IAAI;EACF,MAAM,MAAM,MAAM,MAAM,IAAc,GAAG;EAEzC,IAAI,KAAK,OAAO;CAClB,SAAS,OAAO;EACd,IAAI,MAAM,UAAU,SAAS,KAAK;CACpC;CAEA,MAAM,QAAQ,MAAM,OAAO;CAE3B,IAAI;EACF,MAAM,MAAM,IAAI,KAAK,OAAO,EAAE,KAAK,aAAa,SAAS,EAAE,CAAC;CAC9D,SAAS,OAAO;EACd,IAAI,MAAM,UAAU,SAAS,KAAK;CACpC;CAEA,OAAO;AACT;;AAGA,SAAgB,eAAe,MAAY,QAAoC;CAC7E,OAAO,OAAO,SAAS,oBAAoB,MAAM,MAAM,SACrD,aAAa,SAAS,CAAC,CAAC,mBAAmB,MAAM,MAAM,CACzD;AACF;;AAGA,SAAgB,SAAS,MAAY,QAAoC;CACvE,OAAO,OAAO,SAAS,cAAc,MAAM,MAAM,SAC/C,aAAa,SAAS,CAAC,CAAC,aAAa,MAAM,MAAM,CACnD;AACF;;AAGA,eAAsB,MAAM,MAAY,QAAgC;CACtE,IAAI;EACF,MAAM,MAAM,OAAO,SAAS,oBAAoB,MAAM,MAAM,CAAC;EAC7D,MAAM,MAAM,OAAO,SAAS,cAAc,MAAM,MAAM,CAAC;CACzD,SAAS,OAAO;EACd,IAAI,MAAM,UAAU,SAAS,KAAK;CACpC;AACF;;;;;;;;;;;;;;;;;;AAmBA,SAAS,oBAAoB,YAA6B;CACxD,MAAM,SAAS,aAAa,eAAe;CAE3C,IAAI,CAAC,2BAA2B,IAAI,UAAU,GAAG;EAC/C,2BAA2B,IAAI,UAAU;EAEzC,IAAI,KACF,UACA,mBACA,SACI,wCAAwC,WAAW,0HAEhC,WAAW,wMAG9B,wCAAwC,WAAW,oHAC0B,WAAW,qHAEhE,WAAW,gHAEzC;CACF;CAEA,OAAO;AACT;;;;;;;;;;AAWA,eAAsB,MACpB,MACA,YACA,UAAyB,CAAC,GACR;CAClB,IAAI;EACF,MAAM,SAAS,UAAU,MAAM,OAAO;EACtC,MAAM,cAAc,MAAM,eAAe,MAAM,MAAM;EAErD,IAAI,CAAC,kBAAkB,aAAa,UAAU,GAAG,OAAO;EAExD,MAAM,SAAS,UAAU,UAAU;EAInC,IAAI,QAAQ,aAAa,UAAa,WAAW,QAC/C;OAAI,oBAAoB,UAAU,GAAG,OAAO;EAAK;EAGnD,IAAI,WAAW,UAAa,QAAQ,aAAa,QAAW,OAAO;EAEnE,MAAM,QAAQ,MAAM,SAAS,MAAM,MAAM;EAEzC,OAAO,QACL,MAAM,OAAO,MAAM,QAAQ,UAAU;GACnC,GAAG;GACH;GACA,UAAU,SAAS,MAAM,SAAS,IAAI;GACtC,gBAAgB,SAAS,kBAAkB,aAAa,IAAI;EAC9D,CAAC,CACH;CACF,SAAS,OAAO;EACd,IAAI,iBAAiB,mBAAmB,MAAM;EAE9C,IAAI,MAAM,UAAU,SAAS,KAAK;EAElC,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"engine.mjs","names":[],"sources":["../../../../../../../../packages/access/src/services/engine.ts"],"sourcesContent":["import type { Auth } from \"@warlock.js/auth\";\nimport { cache } from \"@warlock.js/cache\";\nimport { log } from \"@warlock.js/logger\";\nimport type { AccessContext } from \"../contracts/types\";\nimport { AccessConfigError } from \"../utils/access-config-error\";\nimport { accessConfig } from \"./access-config\";\nimport { matchesPermission } from \"./matcher\";\nimport { getPolicy } from \"./policies\";\n\nconst PERMISSIONS_PREFIX = \"access.perms.\";\nconst ROLES_PREFIX = \"access.roles.\";\n\n/**\n * Permissions that have already triggered the \"unpoliced instance check\"\n * warning below — kept so a hot path doesn't log once per request.\n */\nconst warnedUnpolicedPermissions = new Set<string>();\n\n/** Reset the once-per-permission warning tracker. Primarily for tests. */\nexport function resetUnpolicedWarnings(): void {\n warnedUnpolicedPermissions.clear();\n}\n\n/** Resolve the effective tenant for a check (explicit → ambient → undefined). */\nfunction tenantFor(user: Auth, context: AccessContext): string | undefined {\n return context.tenant ?? accessConfig.tenant(user);\n}\n\nfunction cacheKey(prefix: string, user: Auth, tenant?: string): string {\n // Encode each segment so a `.` in an id or tenant can't collide two distinct\n // principals onto the same cache key (and serve one's grants to the other).\n const segment = (value: unknown) => encodeURIComponent(String(value));\n\n return `${prefix}${segment(user.userType)}.${segment(user.id)}.${segment(tenant ?? \"_\")}`;\n}\n\n/**\n * Read a list through the cache, falling back to the loader.\n *\n * The cache is **best-effort**: a cache failure degrades to the loader (the\n * source of truth) and is logged, never denied. A loader failure propagates —\n * so the DECISION fails closed in {@link check}.\n */\nasync function cached(\n key: string,\n loader: () => Promise<string[]>,\n): Promise<string[]> {\n try {\n const hit = await cache.get<string[]>(key);\n\n if (hit) return hit;\n } catch (error) {\n log.error(\"access\", \"cache\", error);\n }\n\n const value = await loader();\n\n try {\n await cache.set(key, value, { ttl: accessConfig.cacheTtl() });\n } catch (error) {\n log.error(\"access\", \"cache\", error);\n }\n\n return value;\n}\n\n/** The user's effective permission set for a tenant (cached). */\nexport function permissionsFor(user: Auth, tenant?: string): Promise<string[]> {\n return cached(cacheKey(PERMISSIONS_PREFIX, user, tenant), () =>\n accessConfig.resolver().resolvePermissions(user, tenant),\n );\n}\n\n/** The user's roles for a tenant (cached). */\nexport function rolesFor(user: Auth, tenant?: string): Promise<string[]> {\n return cached(cacheKey(ROLES_PREFIX, user, tenant), () =>\n accessConfig.resolver().resolveRoles(user, tenant),\n );\n}\n\n/** Drop the cached role/permission sets for a user (after any out-of-band role change). */\nexport async function flush(user: Auth, tenant?: string): Promise<void> {\n try {\n await cache.remove(cacheKey(PERMISSIONS_PREFIX, user, tenant));\n await cache.remove(cacheKey(ROLES_PREFIX, user, tenant));\n } catch (error) {\n log.error(\"access\", \"cache\", error);\n }\n}\n\n/**\n * Surface a resource-scoped check that has no registered ABAC policy, and\n * report whether the check should now DENY.\n *\n * `strictPolicies` defaults to `true`: a forgotten `definePolicy` call (typo'd\n * permission name, or a missing `import \"./policies\"` side-effect — the\n * single most common mistake app teams make with this package, per the\n * `define-policies` skill) must never fail OPEN, so the check denies.\n *\n * `strictPolicies: false` explicitly restores the pre-5.13 behavior: fall\n * back to the RBAC grant alone (ALLOW), which is a plausible IDOR if the\n * permission was meant to be resource-scoped.\n *\n * Either way this warns once per permission (not once per call) so a hot\n * request path doesn't spam logs — mirrors cascade's\n * `warnUndeclaredSensitiveFields`.\n */\nfunction unpolicedShouldDeny(permission: string): boolean {\n const strict = accessConfig.strictPolicies();\n\n if (!warnedUnpolicedPermissions.has(permission)) {\n warnedUnpolicedPermissions.add(permission);\n\n log.warn(\n \"access\",\n \"unpoliced-check\",\n strict\n ? `Instance-level check for permission \"${permission}\" has no registered ABAC policy — ` +\n `DENYING (strictPolicies defaults to true as of 5.13). Register one with ` +\n `definePolicy(\"${permission}\", ...), or make sure the module that calls definePolicy is ` +\n `actually imported. Set strictPolicies: false in the access config to restore the pre-5.13 ` +\n `RBAC-only fallback (ALLOWS — not recommended).`\n : `Instance-level check for permission \"${permission}\" has no registered ABAC policy — falling ` +\n `back to the RBAC grant alone (strictPolicies: false), so any holder of \"${permission}\" can ` +\n `act on ANY resource. If this permission is meant to be resource-scoped, register a policy ` +\n `with definePolicy(\"${permission}\", ...) (and confirm the module that calls it is ` +\n `imported), or remove strictPolicies: false to deny by default.`,\n );\n }\n\n return strict;\n}\n\n/**\n * The core decision: does the user hold `permission`, and — when a `resource`\n * is supplied — does its policy pass too?\n *\n * Fails CLOSED: any error while resolving the decision denies (and logs). An\n * instance-level check for a permission with no registered policy also fails\n * CLOSED by default ({@link AccessConfigurations.strictPolicies}) rather than\n * falling back to the RBAC grant alone.\n */\nexport async function check(\n user: Auth,\n permission: string,\n context: AccessContext = {},\n): Promise<boolean> {\n try {\n const tenant = tenantFor(user, context);\n const permissions = await permissionsFor(user, tenant);\n\n if (!matchesPermission(permissions, permission)) return false;\n\n const policy = getPolicy(permission);\n\n // A policy only runs on an instance check (a resource was supplied);\n // class-level checks (`gate`) stop at the grant.\n if (context.resource !== undefined && policy === undefined) {\n if (unpolicedShouldDeny(permission)) return false;\n }\n\n if (policy === undefined || context.resource === undefined) return true;\n\n const roles = await rolesFor(user, tenant);\n\n return Boolean(\n await policy(user, context.resource, {\n ...context,\n tenant,\n hasRole: (role) => roles.includes(role),\n hasPermission: (perm) => matchesPermission(permissions, perm),\n }),\n );\n } catch (error) {\n if (error instanceof AccessConfigError) throw error; // misconfig is loud, not a silent deny\n\n log.error(\"access\", \"check\", error);\n\n return false;\n }\n}\n"],"mappings":";;;;;;;;AASA,MAAM,qBAAqB;AAC3B,MAAM,eAAe;;;;;AAMrB,MAAM,6CAA6B,IAAI,IAAY;;AAQnD,SAAS,UAAU,MAAY,SAA4C;CACzE,OAAO,QAAQ,UAAU,aAAa,OAAO,IAAI;AACnD;AAEA,SAAS,SAAS,QAAgB,MAAY,QAAyB;CAGrE,MAAM,WAAW,UAAmB,mBAAmB,OAAO,KAAK,CAAC;CAEpE,OAAO,GAAG,SAAS,QAAQ,KAAK,QAAQ,EAAE,GAAG,QAAQ,KAAK,EAAE,EAAE,GAAG,QAAQ,UAAU,GAAG;AACxF;;;;;;;;AASA,eAAe,OACb,KACA,QACmB;CACnB,IAAI;EACF,MAAM,MAAM,MAAM,MAAM,IAAc,GAAG;EAEzC,IAAI,KAAK,OAAO;CAClB,SAAS,OAAO;EACd,IAAI,MAAM,UAAU,SAAS,KAAK;CACpC;CAEA,MAAM,QAAQ,MAAM,OAAO;CAE3B,IAAI;EACF,MAAM,MAAM,IAAI,KAAK,OAAO,EAAE,KAAK,aAAa,SAAS,EAAE,CAAC;CAC9D,SAAS,OAAO;EACd,IAAI,MAAM,UAAU,SAAS,KAAK;CACpC;CAEA,OAAO;AACT;;AAGA,SAAgB,eAAe,MAAY,QAAoC;CAC7E,OAAO,OAAO,SAAS,oBAAoB,MAAM,MAAM,SACrD,aAAa,SAAS,CAAC,CAAC,mBAAmB,MAAM,MAAM,CACzD;AACF;;AAGA,SAAgB,SAAS,MAAY,QAAoC;CACvE,OAAO,OAAO,SAAS,cAAc,MAAM,MAAM,SAC/C,aAAa,SAAS,CAAC,CAAC,aAAa,MAAM,MAAM,CACnD;AACF;;AAGA,eAAsB,MAAM,MAAY,QAAgC;CACtE,IAAI;EACF,MAAM,MAAM,OAAO,SAAS,oBAAoB,MAAM,MAAM,CAAC;EAC7D,MAAM,MAAM,OAAO,SAAS,cAAc,MAAM,MAAM,CAAC;CACzD,SAAS,OAAO;EACd,IAAI,MAAM,UAAU,SAAS,KAAK;CACpC;AACF;;;;;;;;;;;;;;;;;;AAmBA,SAAS,oBAAoB,YAA6B;CACxD,MAAM,SAAS,aAAa,eAAe;CAE3C,IAAI,CAAC,2BAA2B,IAAI,UAAU,GAAG;EAC/C,2BAA2B,IAAI,UAAU;EAEzC,IAAI,KACF,UACA,mBACA,SACI,wCAAwC,WAAW,0HAEhC,WAAW,wMAG9B,wCAAwC,WAAW,oHAC0B,WAAW,qHAEhE,WAAW,gHAEzC;CACF;CAEA,OAAO;AACT;;;;;;;;;;AAWA,eAAsB,MACpB,MACA,YACA,UAAyB,CAAC,GACR;CAClB,IAAI;EACF,MAAM,SAAS,UAAU,MAAM,OAAO;EACtC,MAAM,cAAc,MAAM,eAAe,MAAM,MAAM;EAErD,IAAI,CAAC,kBAAkB,aAAa,UAAU,GAAG,OAAO;EAExD,MAAM,SAAS,UAAU,UAAU;EAInC,IAAI,QAAQ,aAAa,UAAa,WAAW,QAC/C;OAAI,oBAAoB,UAAU,GAAG,OAAO;EAAK;EAGnD,IAAI,WAAW,UAAa,QAAQ,aAAa,QAAW,OAAO;EAEnE,MAAM,QAAQ,MAAM,SAAS,MAAM,MAAM;EAEzC,OAAO,QACL,MAAM,OAAO,MAAM,QAAQ,UAAU;GACnC,GAAG;GACH;GACA,UAAU,SAAS,MAAM,SAAS,IAAI;GACtC,gBAAgB,SAAS,kBAAkB,aAAa,IAAI;EAC9D,CAAC,CACH;CACF,SAAS,OAAO;EACd,IAAI,iBAAiB,mBAAmB,MAAM;EAE9C,IAAI,MAAM,UAAU,SAAS,KAAK;EAElC,OAAO;CACT;AACF"}
@@ -1,4 +1,4 @@
1
- //#region ../access/src/services/matcher.ts
1
+ //#region ../../packages/access/src/services/matcher.ts
2
2
  /**
3
3
  * Whether a set of granted permission patterns covers a requested permission.
4
4
  *
@@ -1 +1 @@
1
- {"version":3,"file":"matcher.mjs","names":[],"sources":["../../../../../../../access/src/services/matcher.ts"],"sourcesContent":["/**\n * Whether a set of granted permission patterns covers a requested permission.\n *\n * Supports three forms:\n * - exact — `\"orders.update\"` covers `\"orders.update\"`\n * - global — `\"*\"` covers everything (the super-grant)\n * - prefix wildcard — `\"orders.*\"` covers `\"orders.update\"` and `\"orders.update.status\"`\n *\n * `\"orders.*\"` does NOT cover the bare `\"orders\"` (no trailing segment).\n */\nexport function matchesPermission(granted: string[], permission: string): boolean {\n for (const pattern of granted) {\n if (pattern === \"*\" || pattern === permission) {\n return true;\n }\n\n // \"orders.*\" → prefix \"orders.\" — matches any nested permission.\n if (pattern.endsWith(\".*\") && permission.startsWith(pattern.slice(0, -1))) {\n return true;\n }\n }\n\n return false;\n}\n"],"mappings":";;;;;;;;;;;AAUA,SAAgB,kBAAkB,SAAmB,YAA6B;CAChF,KAAK,MAAM,WAAW,SAAS;EAC7B,IAAI,YAAY,OAAO,YAAY,YACjC,OAAO;EAIT,IAAI,QAAQ,SAAS,IAAI,KAAK,WAAW,WAAW,QAAQ,MAAM,GAAG,EAAE,CAAC,GACtE,OAAO;CAEX;CAEA,OAAO;AACT"}
1
+ {"version":3,"file":"matcher.mjs","names":[],"sources":["../../../../../../../../packages/access/src/services/matcher.ts"],"sourcesContent":["/**\n * Whether a set of granted permission patterns covers a requested permission.\n *\n * Supports three forms:\n * - exact — `\"orders.update\"` covers `\"orders.update\"`\n * - global — `\"*\"` covers everything (the super-grant)\n * - prefix wildcard — `\"orders.*\"` covers `\"orders.update\"` and `\"orders.update.status\"`\n *\n * `\"orders.*\"` does NOT cover the bare `\"orders\"` (no trailing segment).\n */\nexport function matchesPermission(granted: string[], permission: string): boolean {\n for (const pattern of granted) {\n if (pattern === \"*\" || pattern === permission) {\n return true;\n }\n\n // \"orders.*\" → prefix \"orders.\" — matches any nested permission.\n if (pattern.endsWith(\".*\") && permission.startsWith(pattern.slice(0, -1))) {\n return true;\n }\n }\n\n return false;\n}\n"],"mappings":";;;;;;;;;;;AAUA,SAAgB,kBAAkB,SAAmB,YAA6B;CAChF,KAAK,MAAM,WAAW,SAAS;EAC7B,IAAI,YAAY,OAAO,YAAY,YACjC,OAAO;EAIT,IAAI,QAAQ,SAAS,IAAI,KAAK,WAAW,WAAW,QAAQ,MAAM,GAAG,EAAE,CAAC,GACtE,OAAO;CAEX;CAEA,OAAO;AACT"}
@@ -1,6 +1,6 @@
1
1
  import { PolicyFn } from "../contracts/types.mjs";
2
2
 
3
- //#region ../access/src/services/policies.d.ts
3
+ //#region ../../packages/access/src/services/policies.d.ts
4
4
  /**
5
5
  * Register an ABAC condition for a permission. It runs ON TOP of the RBAC grant
6
6
  * whenever an authorization check carries a `resource` — letting you express
@@ -1 +1 @@
1
- {"version":3,"file":"policies.d.mts","names":[],"sources":["../../../../../../../access/src/services/policies.ts"],"mappings":";;;;;AAYA;;;;;;iBAAgB,YAAA,CAAa,UAAA,UAAoB,MAAA,EAAQ,QAAQ;AAUjE;AAAA,iBAAgB,aAAA"}
1
+ {"version":3,"file":"policies.d.mts","names":[],"sources":["../../../../../../../../packages/access/src/services/policies.ts"],"mappings":";;;;;AAYA;;;;;;iBAAgB,YAAA,CAAa,UAAA,UAAoB,MAAA,EAAQ,QAAQ;AAUjE;AAAA,iBAAgB,aAAA"}
@@ -1,4 +1,4 @@
1
- //#region ../access/src/services/policies.ts
1
+ //#region ../../packages/access/src/services/policies.ts
2
2
  const policies = /* @__PURE__ */ new Map();
3
3
  /**
4
4
  * Register an ABAC condition for a permission. It runs ON TOP of the RBAC grant
@@ -1 +1 @@
1
- {"version":3,"file":"policies.mjs","names":[],"sources":["../../../../../../../access/src/services/policies.ts"],"sourcesContent":["import type { PolicyFn } from \"../contracts/types\";\n\nconst policies = new Map<string, PolicyFn>();\n\n/**\n * Register an ABAC condition for a permission. It runs ON TOP of the RBAC grant\n * whenever an authorization check carries a `resource` — letting you express\n * \"...but only their own / only in their tenant / only while pending\".\n *\n * @example\n * definePolicy(\"orders.update\", (user, order) => order.get(\"customer_id\") === user.id);\n */\nexport function definePolicy(permission: string, policy: PolicyFn): void {\n policies.set(permission, policy);\n}\n\n/** The policy registered for a permission, if any. */\nexport function getPolicy(permission: string): PolicyFn | undefined {\n return policies.get(permission);\n}\n\n/** Remove every registered policy. Primarily for tests. */\nexport function clearPolicies(): void {\n policies.clear();\n}\n"],"mappings":";AAEA,MAAM,2BAAW,IAAI,IAAsB;;;;;;;;;AAU3C,SAAgB,aAAa,YAAoB,QAAwB;CACvE,SAAS,IAAI,YAAY,MAAM;AACjC;;AAGA,SAAgB,UAAU,YAA0C;CAClE,OAAO,SAAS,IAAI,UAAU;AAChC;;AAGA,SAAgB,gBAAsB;CACpC,SAAS,MAAM;AACjB"}
1
+ {"version":3,"file":"policies.mjs","names":[],"sources":["../../../../../../../../packages/access/src/services/policies.ts"],"sourcesContent":["import type { PolicyFn } from \"../contracts/types\";\n\nconst policies = new Map<string, PolicyFn>();\n\n/**\n * Register an ABAC condition for a permission. It runs ON TOP of the RBAC grant\n * whenever an authorization check carries a `resource` — letting you express\n * \"...but only their own / only in their tenant / only while pending\".\n *\n * @example\n * definePolicy(\"orders.update\", (user, order) => order.get(\"customer_id\") === user.id);\n */\nexport function definePolicy(permission: string, policy: PolicyFn): void {\n policies.set(permission, policy);\n}\n\n/** The policy registered for a permission, if any. */\nexport function getPolicy(permission: string): PolicyFn | undefined {\n return policies.get(permission);\n}\n\n/** Remove every registered policy. Primarily for tests. */\nexport function clearPolicies(): void {\n policies.clear();\n}\n"],"mappings":";AAEA,MAAM,2BAAW,IAAI,IAAsB;;;;;;;;;AAU3C,SAAgB,aAAa,YAAoB,QAAwB;CACvE,SAAS,IAAI,YAAY,MAAM;AACjC;;AAGA,SAAgB,UAAU,YAA0C;CAClE,OAAO,SAAS,IAAI,UAAU;AAChC;;AAGA,SAAgB,gBAAsB;CACpC,SAAS,MAAM;AACjB"}
@@ -1,4 +1,4 @@
1
- //#region ../access/src/utils/access-config-error.d.ts
1
+ //#region ../../packages/access/src/utils/access-config-error.d.ts
2
2
  /**
3
3
  * Thrown when `@warlock.js/access` is misconfigured (e.g. no resolver). Unlike a
4
4
  * runtime resolution failure — which fails CLOSED (denies) — a config error is a
@@ -1 +1 @@
1
- {"version":3,"file":"access-config-error.d.mts","names":[],"sources":["../../../../../../../access/src/utils/access-config-error.ts"],"mappings":";;AAKA;;;;cAAa,iBAAA,SAA0B,KAAK;cACvB,OAAA;AAAA"}
1
+ {"version":3,"file":"access-config-error.d.mts","names":[],"sources":["../../../../../../../../packages/access/src/utils/access-config-error.ts"],"mappings":";;AAKA;;;;cAAa,iBAAA,SAA0B,KAAK;cACvB,OAAA;AAAA"}
@@ -1,4 +1,4 @@
1
- //#region ../access/src/utils/access-config-error.ts
1
+ //#region ../../packages/access/src/utils/access-config-error.ts
2
2
  /**
3
3
  * Thrown when `@warlock.js/access` is misconfigured (e.g. no resolver). Unlike a
4
4
  * runtime resolution failure — which fails CLOSED (denies) — a config error is a
@@ -1 +1 @@
1
- {"version":3,"file":"access-config-error.mjs","names":[],"sources":["../../../../../../../access/src/utils/access-config-error.ts"],"sourcesContent":["/**\n * Thrown when `@warlock.js/access` is misconfigured (e.g. no resolver). Unlike a\n * runtime resolution failure — which fails CLOSED (denies) — a config error is a\n * developer mistake, so the engine re-throws it LOUD instead of silently denying.\n */\nexport class AccessConfigError extends Error {\n public constructor(message: string) {\n super(message);\n this.name = \"AccessConfigError\";\n }\n}\n"],"mappings":";;;;;;AAKA,IAAa,oBAAb,cAAuC,MAAM;CAC3C,AAAO,YAAY,SAAiB;EAClC,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF"}
1
+ {"version":3,"file":"access-config-error.mjs","names":[],"sources":["../../../../../../../../packages/access/src/utils/access-config-error.ts"],"sourcesContent":["/**\n * Thrown when `@warlock.js/access` is misconfigured (e.g. no resolver). Unlike a\n * runtime resolution failure — which fails CLOSED (denies) — a config error is a\n * developer mistake, so the engine re-throws it LOUD instead of silently denying.\n */\nexport class AccessConfigError extends Error {\n public constructor(message: string) {\n super(message);\n this.name = \"AccessConfigError\";\n }\n}\n"],"mappings":";;;;;;AAKA,IAAa,oBAAb,cAAuC,MAAM;CAC3C,AAAO,YAAY,SAAiB;EAClC,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF"}
@@ -1,4 +1,4 @@
1
- //#region ../access/src/utils/access-error-codes.d.ts
1
+ //#region ../../packages/access/src/utils/access-error-codes.d.ts
2
2
  /**
3
3
  * Wire-stable error codes emitted by `@warlock.js/access`. Map these in your
4
4
  * error transformer the way you map `AuthErrorCodes`. A value change here is a
@@ -1 +1 @@
1
- {"version":3,"file":"access-error-codes.d.mts","names":[],"sources":["../../../../../../../access/src/utils/access-error-codes.ts"],"mappings":";;AAKA;;;;aAAY,gBAAA;;EAEV,SAAS;AAAA"}
1
+ {"version":3,"file":"access-error-codes.d.mts","names":[],"sources":["../../../../../../../../packages/access/src/utils/access-error-codes.ts"],"mappings":";;AAKA;;;;aAAY,gBAAA;;EAEV,SAAS;AAAA"}
@@ -1,4 +1,4 @@
1
- //#region ../access/src/utils/access-error-codes.ts
1
+ //#region ../../packages/access/src/utils/access-error-codes.ts
2
2
  /**
3
3
  * Wire-stable error codes emitted by `@warlock.js/access`. Map these in your
4
4
  * error transformer the way you map `AuthErrorCodes`. A value change here is a
@@ -1 +1 @@
1
- {"version":3,"file":"access-error-codes.mjs","names":[],"sources":["../../../../../../../access/src/utils/access-error-codes.ts"],"sourcesContent":["/**\n * Wire-stable error codes emitted by `@warlock.js/access`. Map these in your\n * error transformer the way you map `AuthErrorCodes`. A value change here is a\n * breaking change for any client that reacts to the code.\n */\nexport enum AccessErrorCodes {\n /** EC100 — authenticated, but missing the required permission. */\n Forbidden = \"EC100\",\n}\n"],"mappings":";;;;;;AAKA,IAAY,mBAAL;;CAEL;;AACF"}
1
+ {"version":3,"file":"access-error-codes.mjs","names":[],"sources":["../../../../../../../../packages/access/src/utils/access-error-codes.ts"],"sourcesContent":["/**\n * Wire-stable error codes emitted by `@warlock.js/access`. Map these in your\n * error transformer the way you map `AuthErrorCodes`. A value change here is a\n * breaking change for any client that reacts to the code.\n */\nexport enum AccessErrorCodes {\n /** EC100 — authenticated, but missing the required permission. */\n Forbidden = \"EC100\",\n}\n"],"mappings":";;;;;;AAKA,IAAY,mBAAL;;CAEL;;AACF"}
package/llms-full.txt CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  ---
10
10
  name: check-permissions
11
- description: 'Check permissions in `@warlock.js/access` — `can` / `cannot` / `canAll` / `canAny` (boolean), `authorize` / `authorizeAll` / `authorizeAny` (throw 403), and the `gate` / `gateAny` / `gateAll` route middleware. Class-level vs instance-level (pass a `resource` to run its policy). TRIGGER: `can(`, `authorize(`, `canAll`, `canAny`, `gate`, `gateAny`, `gateAll`, "check a permission", "protect route by permission", "403 forbidden", "any vs all permissions". Skip: ownership / tenant conditions — `@warlock.js/access/define-policies/SKILL.md`; role checks — `@warlock.js/access/manage-roles/SKILL.md`.'
11
+ description: 'Check permissions in `@warlock.js/access` — `can` / `cannot` / `canAll` / `canAny` (boolean), `authorize` / `authorizeAll` / `authorizeAny` (throw 403), and the `gate` / `gateAny` / `gateAll` route middleware. Class-level vs instance-level (pass a `resource` to run its policy). TRIGGER: `can(`, `authorize(`, `canAll`, `canAny`, `gate`, `gateAny`, `gateAll`, "check a permission", "protect route by permission", "403 forbidden", "any vs all permissions". Skip: ownership / tenant conditions — the `define-policies` topic; role checks — the `manage-roles` topic.'
12
12
  ---
13
13
 
14
14
  # Check permissions
@@ -52,7 +52,7 @@ await authorizeAll(user, ["orders.update", "orders.viewCost"]);
52
52
 
53
53
  ## Instance-level — "this resource"
54
54
 
55
- Pass a `resource` and the permission's policy runs on top of the grant (see [`define-policies`](@warlock.js/access/define-policies/SKILL.md)):
55
+ Pass a `resource` and the permission's policy runs on top of the grant (see the `define-policies` topic):
56
56
 
57
57
  ```ts
58
58
  const order = await Order.find(orderId);
@@ -75,15 +75,15 @@ await authorize(user, "orders.update", { resource: order, tenant }); // grant AN
75
75
 
76
76
  ## See also
77
77
 
78
- - [`@warlock.js/access/define-policies/SKILL.md`](@warlock.js/access/define-policies/SKILL.md) — the `{ resource }` conditions.
79
- - [`@warlock.js/auth/protect-routes/SKILL.md`](@warlock.js/auth/protect-routes/SKILL.md) — `authMiddleware`, which runs first.
78
+ - The `define-policies` topic — the `{ resource }` conditions.
79
+ - The `protect-routes` topic of the `warlock-js-auth` skill — `authMiddleware`, which runs first.
80
80
 
81
81
 
82
82
  ## configure-access `@warlock.js/access/configure-access/SKILL.md`
83
83
 
84
84
  ---
85
85
  name: configure-access
86
- description: 'Configure `@warlock.js/access` — the REQUIRED `resolver` (the quickstart `DefaultAccessResolver` over a fixed code map vs the ejected, DB-backed `DatabaseAccessResolver`), `cache: { ttl }`, and the ambient tenant via the resolver''s optional `resolveTenant()`. TRIGGER: `AccessConfigurations`, `config.access`, `src/config/access.ts`, "set up permissions", "DefaultAccessResolver", "DatabaseAccessResolver", "access cache ttl", "access tenant". Skip: checking permissions — `@warlock.js/access/check-permissions/SKILL.md`; custom storage — `@warlock.js/access/implement-resolver/SKILL.md`.'
86
+ description: 'Configure `@warlock.js/access` — the REQUIRED `resolver` (the quickstart `DefaultAccessResolver` over a fixed code map vs the ejected, DB-backed `DatabaseAccessResolver`), `cache: { ttl }`, and the ambient tenant via the resolver''s optional `resolveTenant()`. TRIGGER: `AccessConfigurations`, `config.access`, `src/config/access.ts`, "set up permissions", "DefaultAccessResolver", "DatabaseAccessResolver", "access cache ttl", "access tenant". Skip: checking permissions — the `check-permissions` topic; custom storage — the `implement-resolver` topic.'
87
87
  ---
88
88
 
89
89
  # Configure access
@@ -127,7 +127,7 @@ const access: AccessConfigurations = {
127
127
  export default access;
128
128
  ```
129
129
 
130
- Assign roles via the ejected `UserRole.assign(user, "editor", tenant)` — it flushes that user's cached set for you (see [`manage-roles`](@warlock.js/access/manage-roles/SKILL.md)).
130
+ Assign roles via the ejected `UserRole.assign(user, "editor", tenant)` — it flushes that user's cached set for you (see the `manage-roles` topic).
131
131
 
132
132
  ## The full shape
133
133
 
@@ -145,7 +145,7 @@ type AccessConfigurations = {
145
145
 
146
146
  An instance check (`{ resource }`) for a permission with no registered ABAC policy **denies by default** (`strictPolicies: true`) — a forgotten policy must never fail open. The miss is logged once per permission (`log.warn`) either way.
147
147
 
148
- Set `strictPolicies: false` to explicitly restore the pre-5.13 behavior: fall back to the RBAC grant alone (ALLOWS — any holder of the permission can act on ANY resource, a plausible IDOR). See [`define-policies`](@warlock.js/access/define-policies/SKILL.md) for the full decision table.
148
+ Set `strictPolicies: false` to explicitly restore the pre-5.13 behavior: fall back to the RBAC grant alone (ALLOWS — any holder of the permission can act on ANY resource, a plausible IDOR). See the `define-policies` topic for the full decision table.
149
149
 
150
150
  > **Breaking in 5.13:** `strictPolicies` now defaults to `true` and DENIES (it used to default to `false` and ALLOW). Set `strictPolicies: false` to keep the old fail-open fallback.
151
151
 
@@ -167,8 +167,8 @@ The engine caches each user's resolved set (`cache.ttl`, default `10m`). Roles c
167
167
 
168
168
  ## See also
169
169
 
170
- - [`@warlock.js/access/implement-resolver/SKILL.md`](@warlock.js/access/implement-resolver/SKILL.md) — write a resolver for your own storage.
171
- - [`@warlock.js/access/manage-roles/SKILL.md`](@warlock.js/access/manage-roles/SKILL.md) — assign / revoke with the ejected table.
170
+ - The `implement-resolver` topic — write a resolver for your own storage.
171
+ - The `manage-roles` topic — assign / revoke with the ejected table.
172
172
  ```
173
173
 
174
174
 
@@ -176,7 +176,7 @@ The engine caches each user's resolved set (`cache.ttl`, default `10m`). Roles c
176
176
 
177
177
  ---
178
178
  name: define-policies
179
- description: 'ABAC conditions in `@warlock.js/access` — `definePolicy(permission, (user, resource, ctx) => boolean)` adds an instance-level rule (ownership / tenant / state) on top of the RBAC grant, evaluated when an authorization check carries a `resource`. TRIGGER: `definePolicy`, "ownership check", "only their own", "can edit this specific record", "ABAC", "policy", "resource-level permission", `authorize(user, perm, { resource })`. Skip: plain grant checks — `@warlock.js/access/check-permissions/SKILL.md`.'
179
+ description: 'ABAC conditions in `@warlock.js/access` — `definePolicy(permission, (user, resource, ctx) => boolean)` adds an instance-level rule (ownership / tenant / state) on top of the RBAC grant, evaluated when an authorization check carries a `resource`. TRIGGER: `definePolicy`, "ownership check", "only their own", "can edit this specific record", "ABAC", "policy", "resource-level permission", `authorize(user, perm, { resource })`. Skip: plain grant checks — the `check-permissions` topic.'
180
180
  ---
181
181
 
182
182
  # Define policies (ABAC)
@@ -212,7 +212,7 @@ So `gate("orders.update")` (a route gate, no resource) checks the grant; the per
212
212
  ## The decision = grant AND policy
213
213
 
214
214
  - No grant → denied (the policy never runs).
215
- - Grant + no policy registered → **denied by default** (`strictPolicies: true`, the 5.13+ default) — a typo'd permission or a forgotten `import "./policies"` must never silently degrade an instance check to a class-level one. Logs a one-time `log.warn` naming the permission either way. Set `strictPolicies: false` in the access config (see [`configure-access`](@warlock.js/access/configure-access/SKILL.md)) to explicitly restore the pre-5.13 RBAC-only fallback (allowed).
215
+ - Grant + no policy registered → **denied by default** (`strictPolicies: true`, the 5.13+ default) — a typo'd permission or a forgotten `import "./policies"` must never silently degrade an instance check to a class-level one. Logs a one-time `log.warn` naming the permission either way. Set `strictPolicies: false` in the access config (see the `configure-access` topic) to explicitly restore the pre-5.13 RBAC-only fallback (allowed).
216
216
  - Grant + policy → the policy decides.
217
217
 
218
218
  Policies **deny**, they don't grant — a policy can't let a user past a permission they don't hold.
@@ -241,14 +241,14 @@ definePolicy("orders.refund", (user, order, ctx) =>
241
241
 
242
242
  ## See also
243
243
 
244
- - [`@warlock.js/access/check-permissions/SKILL.md`](@warlock.js/access/check-permissions/SKILL.md) — `authorize(…, { resource })`.
244
+ - The `check-permissions` topic — `authorize(…, { resource })`.
245
245
 
246
246
 
247
247
  ## implement-resolver `@warlock.js/access/implement-resolver/SKILL.md`
248
248
 
249
249
  ---
250
250
  name: implement-resolver
251
- description: 'Connect `@warlock.js/access` to your role/permission storage by implementing the `AccessResolver` contract (`resolveRoles` / `resolvePermissions`, optional `resolveTenant`) — for a DB-backed catalog, a user column, a pivot table, a token claim, or an external directory. The engine owns matching / caching / policies; the resolver only fetches. TRIGGER: `AccessResolver`, `DatabaseAccessResolver`, `resolveTenant`, "custom resolver", "where do roles come from", "roles in a token claim", "permissions from an external API", "implement resolver". Skip: the quickstart `DefaultAccessResolver` — `@warlock.js/access/configure-access/SKILL.md`.'
251
+ description: 'Connect `@warlock.js/access` to your role/permission storage by implementing the `AccessResolver` contract (`resolveRoles` / `resolvePermissions`, optional `resolveTenant`) — for a DB-backed catalog, a user column, a pivot table, a token claim, or an external directory. The engine owns matching / caching / policies; the resolver only fetches. TRIGGER: `AccessResolver`, `DatabaseAccessResolver`, `resolveTenant`, "custom resolver", "where do roles come from", "roles in a token claim", "permissions from an external API", "implement resolver". Skip: the quickstart `DefaultAccessResolver` — the `configure-access` topic.'
252
252
  ---
253
253
 
254
254
  # Implement a resolver
@@ -350,7 +350,7 @@ class DirectResolver implements AccessResolver {
350
350
 
351
351
  ## See also
352
352
 
353
- - [`@warlock.js/access/configure-access/SKILL.md`](@warlock.js/access/configure-access/SKILL.md) — the quickstart `DefaultAccessResolver`.
353
+ - The `configure-access` topic — the quickstart `DefaultAccessResolver`.
354
354
  ```
355
355
 
356
356
 
@@ -358,7 +358,7 @@ class DirectResolver implements AccessResolver {
358
358
 
359
359
  ---
360
360
  name: manage-roles
361
- description: 'Assign and read roles in `@warlock.js/access` — the ejected `UserRole.assign` / `UserRole.revoke` (the `user_roles` table) followed by `access.flush`, plus `hasRole` / `hasAnyRole` / `hasAllRoles`. The role→permission catalog is the ejected `Role` table (dynamic). TRIGGER: `UserRole.assign`, `UserRole.revoke`, `access.flush`, `hasRole`, `hasAnyRole`, `hasAllRoles`, `Role` table, "give a user a role", "assign role", "check a user role", "roles per tenant". Skip: permission checks — `@warlock.js/access/check-permissions/SKILL.md`; resolver choice — `@warlock.js/access/configure-access/SKILL.md`.'
361
+ description: 'Assign and read roles in `@warlock.js/access` — the ejected `UserRole.assign` / `UserRole.revoke` (the `user_roles` table) followed by `access.flush`, plus `hasRole` / `hasAnyRole` / `hasAllRoles`. The role→permission catalog is the ejected `Role` table (dynamic). TRIGGER: `UserRole.assign`, `UserRole.revoke`, `access.flush`, `hasRole`, `hasAnyRole`, `hasAllRoles`, `Role` table, "give a user a role", "assign role", "check a user role", "roles per tenant". Skip: permission checks — the `check-permissions` topic; resolver choice — the `configure-access` topic.'
362
362
  ---
363
363
 
364
364
  # Manage roles
@@ -420,12 +420,12 @@ The ejected `UserRole` is a thin cascade model — `{ user_id, user_type, role,
420
420
 
421
421
  ## Roles stored elsewhere?
422
422
 
423
- If roles live on the user (a `roles` column) or in a token claim, you don't use `UserRole` — you manage assignment your way and a resolver reads them. See [`implement-resolver`](@warlock.js/access/implement-resolver/SKILL.md).
423
+ If roles live on the user (a `roles` column) or in a token claim, you don't use `UserRole` — you manage assignment your way and a resolver reads them. See the `implement-resolver` topic.
424
424
 
425
425
  ## See also
426
426
 
427
- - [`@warlock.js/access/configure-access/SKILL.md`](@warlock.js/access/configure-access/SKILL.md) — choosing the resolver.
428
- - [`@warlock.js/access/implement-resolver/SKILL.md`](@warlock.js/access/implement-resolver/SKILL.md) — non-table role storage.
427
+ - The `configure-access` topic — choosing the resolver.
428
+ - The `implement-resolver` topic — non-table role storage.
429
429
  ```
430
430
 
431
431
 
@@ -461,11 +461,11 @@ Skip if you only need "is the user an admin" — `authMiddleware("admin")` from
461
461
 
462
462
  ## Skills index
463
463
 
464
- - [`configure-access`](@warlock.js/access/configure-access/SKILL.md) — the required resolver (`DefaultAccessResolver` vs the ejected `DatabaseAccessResolver`), cache + tenant.
465
- - [`check-permissions`](@warlock.js/access/check-permissions/SKILL.md) — `can` / `cannot` / `canAll` / `canAny` / `authorize*` + the `gate*` middleware.
466
- - [`define-policies`](@warlock.js/access/define-policies/SKILL.md) — `definePolicy` for ownership / tenant / state conditions (ABAC).
467
- - [`manage-roles`](@warlock.js/access/manage-roles/SKILL.md) — assign / revoke via the ejected `UserRole` + `access.flush`, `hasRole` / `hasAnyRole` / `hasAllRoles`.
468
- - [`implement-resolver`](@warlock.js/access/implement-resolver/SKILL.md) — connect the engine to your storage (a DB catalog, a column, a pivot, a token claim).
464
+ - `configure-access` — the required resolver (`DefaultAccessResolver` vs the ejected `DatabaseAccessResolver`), cache + tenant.
465
+ - `check-permissions` — `can` / `cannot` / `canAll` / `canAny` / `authorize*` + the `gate*` middleware.
466
+ - `define-policies` — `definePolicy` for ownership / tenant / state conditions (ABAC).
467
+ - `manage-roles` — assign / revoke via the ejected `UserRole` + `access.flush`, `hasRole` / `hasAnyRole` / `hasAllRoles`.
468
+ - `implement-resolver` — connect the engine to your storage (a DB catalog, a column, a pivot, a token claim).
469
469
 
470
470
  ## What it deliberately doesn't do
471
471
 
@@ -475,7 +475,7 @@ Skip if you only need "is the user an admin" — `authMiddleware("admin")` from
475
475
 
476
476
  ## See also
477
477
 
478
- - [`@warlock.js/auth/overview/SKILL.md`](@warlock.js/auth/overview/SKILL.md) — authentication, the layer below.
479
- - [`@warlock.js/auth/protect-routes/SKILL.md`](@warlock.js/auth/protect-routes/SKILL.md) — `authMiddleware`; stack `gate` after it.
478
+ - The `overview` topic of the `warlock-js-auth` skill — authentication, the layer below.
479
+ - The `protect-routes` topic of the `warlock-js-auth` skill — `authMiddleware`; stack `gate` after it.
480
480
 
481
481
 
package/llms.txt CHANGED
@@ -6,9 +6,9 @@
6
6
 
7
7
  ## Skills
8
8
 
9
- - [check-permissions](@warlock.js/access/check-permissions/SKILL.md): Check permissions in `@warlock.js/access` — `can` / `cannot` / `canAll` / `canAny` (boolean), `authorize` / `authorizeAll` / `authorizeAny` (throw 403), and the `gate` / `gateAny` / `gateAll` route middleware. Class-level vs instance-level (pass a `resource` to run its policy). TRIGGER: `can(`, `authorize(`, `canAll`, `canAny`, `gate`, `gateAny`, `gateAll`, "check a permission", "protect route by permission", "403 forbidden", "any vs all permissions". Skip: ownership / tenant conditions — `@warlock.js/access/define-policies/SKILL.md`; role checks — `@warlock.js/access/manage-roles/SKILL.md`.
10
- - [configure-access](@warlock.js/access/configure-access/SKILL.md): Configure `@warlock.js/access` — the REQUIRED `resolver` (the quickstart `DefaultAccessResolver` over a fixed code map vs the ejected, DB-backed `DatabaseAccessResolver`), `cache: { ttl }`, and the ambient tenant via the resolver's optional `resolveTenant()`. TRIGGER: `AccessConfigurations`, `config.access`, `src/config/access.ts`, "set up permissions", "DefaultAccessResolver", "DatabaseAccessResolver", "access cache ttl", "access tenant". Skip: checking permissions — `@warlock.js/access/check-permissions/SKILL.md`; custom storage — `@warlock.js/access/implement-resolver/SKILL.md`.
11
- - [define-policies](@warlock.js/access/define-policies/SKILL.md): ABAC conditions in `@warlock.js/access` — `definePolicy(permission, (user, resource, ctx) => boolean)` adds an instance-level rule (ownership / tenant / state) on top of the RBAC grant, evaluated when an authorization check carries a `resource`. TRIGGER: `definePolicy`, "ownership check", "only their own", "can edit this specific record", "ABAC", "policy", "resource-level permission", `authorize(user, perm, { resource })`. Skip: plain grant checks — `@warlock.js/access/check-permissions/SKILL.md`.
12
- - [implement-resolver](@warlock.js/access/implement-resolver/SKILL.md): Connect `@warlock.js/access` to your role/permission storage by implementing the `AccessResolver` contract (`resolveRoles` / `resolvePermissions`, optional `resolveTenant`) — for a DB-backed catalog, a user column, a pivot table, a token claim, or an external directory. The engine owns matching / caching / policies; the resolver only fetches. TRIGGER: `AccessResolver`, `DatabaseAccessResolver`, `resolveTenant`, "custom resolver", "where do roles come from", "roles in a token claim", "permissions from an external API", "implement resolver". Skip: the quickstart `DefaultAccessResolver` — `@warlock.js/access/configure-access/SKILL.md`.
13
- - [manage-roles](@warlock.js/access/manage-roles/SKILL.md): Assign and read roles in `@warlock.js/access` — the ejected `UserRole.assign` / `UserRole.revoke` (the `user_roles` table) followed by `access.flush`, plus `hasRole` / `hasAnyRole` / `hasAllRoles`. The role→permission catalog is the ejected `Role` table (dynamic). TRIGGER: `UserRole.assign`, `UserRole.revoke`, `access.flush`, `hasRole`, `hasAnyRole`, `hasAllRoles`, `Role` table, "give a user a role", "assign role", "check a user role", "roles per tenant". Skip: permission checks — `@warlock.js/access/check-permissions/SKILL.md`; resolver choice — `@warlock.js/access/configure-access/SKILL.md`.
9
+ - [check-permissions](@warlock.js/access/check-permissions/SKILL.md): Check permissions in `@warlock.js/access` — `can` / `cannot` / `canAll` / `canAny` (boolean), `authorize` / `authorizeAll` / `authorizeAny` (throw 403), and the `gate` / `gateAny` / `gateAll` route middleware. Class-level vs instance-level (pass a `resource` to run its policy). TRIGGER: `can(`, `authorize(`, `canAll`, `canAny`, `gate`, `gateAny`, `gateAll`, "check a permission", "protect route by permission", "403 forbidden", "any vs all permissions". Skip: ownership / tenant conditions — the `define-policies` topic; role checks — the `manage-roles` topic.
10
+ - [configure-access](@warlock.js/access/configure-access/SKILL.md): Configure `@warlock.js/access` — the REQUIRED `resolver` (the quickstart `DefaultAccessResolver` over a fixed code map vs the ejected, DB-backed `DatabaseAccessResolver`), `cache: { ttl }`, and the ambient tenant via the resolver's optional `resolveTenant()`. TRIGGER: `AccessConfigurations`, `config.access`, `src/config/access.ts`, "set up permissions", "DefaultAccessResolver", "DatabaseAccessResolver", "access cache ttl", "access tenant". Skip: checking permissions — the `check-permissions` topic; custom storage — the `implement-resolver` topic.
11
+ - [define-policies](@warlock.js/access/define-policies/SKILL.md): ABAC conditions in `@warlock.js/access` — `definePolicy(permission, (user, resource, ctx) => boolean)` adds an instance-level rule (ownership / tenant / state) on top of the RBAC grant, evaluated when an authorization check carries a `resource`. TRIGGER: `definePolicy`, "ownership check", "only their own", "can edit this specific record", "ABAC", "policy", "resource-level permission", `authorize(user, perm, { resource })`. Skip: plain grant checks — the `check-permissions` topic.
12
+ - [implement-resolver](@warlock.js/access/implement-resolver/SKILL.md): Connect `@warlock.js/access` to your role/permission storage by implementing the `AccessResolver` contract (`resolveRoles` / `resolvePermissions`, optional `resolveTenant`) — for a DB-backed catalog, a user column, a pivot table, a token claim, or an external directory. The engine owns matching / caching / policies; the resolver only fetches. TRIGGER: `AccessResolver`, `DatabaseAccessResolver`, `resolveTenant`, "custom resolver", "where do roles come from", "roles in a token claim", "permissions from an external API", "implement resolver". Skip: the quickstart `DefaultAccessResolver` — the `configure-access` topic.
13
+ - [manage-roles](@warlock.js/access/manage-roles/SKILL.md): Assign and read roles in `@warlock.js/access` — the ejected `UserRole.assign` / `UserRole.revoke` (the `user_roles` table) followed by `access.flush`, plus `hasRole` / `hasAnyRole` / `hasAllRoles`. The role→permission catalog is the ejected `Role` table (dynamic). TRIGGER: `UserRole.assign`, `UserRole.revoke`, `access.flush`, `hasRole`, `hasAnyRole`, `hasAllRoles`, `Role` table, "give a user a role", "assign role", "check a user role", "roles per tenant". Skip: permission checks — the `check-permissions` topic; resolver choice — the `configure-access` topic.
14
14
  - [overview](@warlock.js/access/overview/SKILL.md): @warlock.js/access overview for orientation and choosing the focused skill for the task.
package/package.json CHANGED
@@ -5,16 +5,17 @@
5
5
  "environment": "server"
6
6
  },
7
7
  "peerDependencies": {
8
- "@warlock.js/auth": "5.28.0",
9
- "@warlock.js/cache": "5.28.0",
10
- "@warlock.js/cascade": "5.28.0",
11
- "@warlock.js/core": "5.28.0",
12
- "@warlock.js/logger": "5.28.0",
13
- "@warlock.js/seal": "5.28.0"
8
+ "@warlock.js/auth": "5.30.0",
9
+ "@warlock.js/cache": "5.30.0",
10
+ "@warlock.js/cascade": "5.30.0",
11
+ "@warlock.js/core": "5.30.0",
12
+ "@warlock.js/logger": "5.30.0",
13
+ "@warlock.js/seal": "5.30.0"
14
14
  },
15
15
  "repository": {
16
16
  "type": "git",
17
- "url": "https://github.com/warlockjs/access"
17
+ "url": "git+https://github.com/warlockjs/framework.git",
18
+ "directory": "packages/access"
18
19
  },
19
20
  "keywords": [
20
21
  "nodejs",
@@ -27,7 +28,7 @@
27
28
  ],
28
29
  "author": "hassanzohdy",
29
30
  "license": "MIT",
30
- "version": "5.28.0",
31
+ "version": "5.30.0",
31
32
  "main": "./cjs/index.cjs",
32
33
  "module": "./esm/index.mjs",
33
34
  "types": "./esm/index.d.mts",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: check-permissions
3
- description: 'Check permissions in `@warlock.js/access` — `can` / `cannot` / `canAll` / `canAny` (boolean), `authorize` / `authorizeAll` / `authorizeAny` (throw 403), and the `gate` / `gateAny` / `gateAll` route middleware. Class-level vs instance-level (pass a `resource` to run its policy). TRIGGER: `can(`, `authorize(`, `canAll`, `canAny`, `gate`, `gateAny`, `gateAll`, "check a permission", "protect route by permission", "403 forbidden", "any vs all permissions". Skip: ownership / tenant conditions — `@warlock.js/access/define-policies/SKILL.md`; role checks — `@warlock.js/access/manage-roles/SKILL.md`.'
3
+ description: 'Check permissions in `@warlock.js/access` — `can` / `cannot` / `canAll` / `canAny` (boolean), `authorize` / `authorizeAll` / `authorizeAny` (throw 403), and the `gate` / `gateAny` / `gateAll` route middleware. Class-level vs instance-level (pass a `resource` to run its policy). TRIGGER: `can(`, `authorize(`, `canAll`, `canAny`, `gate`, `gateAny`, `gateAll`, "check a permission", "protect route by permission", "403 forbidden", "any vs all permissions". Skip: ownership / tenant conditions — the `define-policies` topic; role checks — the `manage-roles` topic.'
4
4
  ---
5
5
 
6
6
  # Check permissions
@@ -44,7 +44,7 @@ await authorizeAll(user, ["orders.update", "orders.viewCost"]);
44
44
 
45
45
  ## Instance-level — "this resource"
46
46
 
47
- Pass a `resource` and the permission's policy runs on top of the grant (see [`define-policies`](@warlock.js/access/define-policies/SKILL.md)):
47
+ Pass a `resource` and the permission's policy runs on top of the grant (see the `define-policies` topic):
48
48
 
49
49
  ```ts
50
50
  const order = await Order.find(orderId);
@@ -67,5 +67,5 @@ await authorize(user, "orders.update", { resource: order, tenant }); // grant AN
67
67
 
68
68
  ## See also
69
69
 
70
- - [`@warlock.js/access/define-policies/SKILL.md`](@warlock.js/access/define-policies/SKILL.md) — the `{ resource }` conditions.
71
- - [`@warlock.js/auth/protect-routes/SKILL.md`](@warlock.js/auth/protect-routes/SKILL.md) — `authMiddleware`, which runs first.
70
+ - The `define-policies` topic — the `{ resource }` conditions.
71
+ - The `protect-routes` topic of the `warlock-js-auth` skill — `authMiddleware`, which runs first.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: configure-access
3
- description: 'Configure `@warlock.js/access` — the REQUIRED `resolver` (the quickstart `DefaultAccessResolver` over a fixed code map vs the ejected, DB-backed `DatabaseAccessResolver`), `cache: { ttl }`, and the ambient tenant via the resolver''s optional `resolveTenant()`. TRIGGER: `AccessConfigurations`, `config.access`, `src/config/access.ts`, "set up permissions", "DefaultAccessResolver", "DatabaseAccessResolver", "access cache ttl", "access tenant". Skip: checking permissions — `@warlock.js/access/check-permissions/SKILL.md`; custom storage — `@warlock.js/access/implement-resolver/SKILL.md`.'
3
+ description: 'Configure `@warlock.js/access` — the REQUIRED `resolver` (the quickstart `DefaultAccessResolver` over a fixed code map vs the ejected, DB-backed `DatabaseAccessResolver`), `cache: { ttl }`, and the ambient tenant via the resolver''s optional `resolveTenant()`. TRIGGER: `AccessConfigurations`, `config.access`, `src/config/access.ts`, "set up permissions", "DefaultAccessResolver", "DatabaseAccessResolver", "access cache ttl", "access tenant". Skip: checking permissions — the `check-permissions` topic; custom storage — the `implement-resolver` topic.'
4
4
  ---
5
5
 
6
6
  # Configure access
@@ -44,7 +44,7 @@ const access: AccessConfigurations = {
44
44
  export default access;
45
45
  ```
46
46
 
47
- Assign roles via the ejected `UserRole.assign(user, "editor", tenant)` — it flushes that user's cached set for you (see [`manage-roles`](@warlock.js/access/manage-roles/SKILL.md)).
47
+ Assign roles via the ejected `UserRole.assign(user, "editor", tenant)` — it flushes that user's cached set for you (see the `manage-roles` topic).
48
48
 
49
49
  ## The full shape
50
50
 
@@ -62,7 +62,7 @@ type AccessConfigurations = {
62
62
 
63
63
  An instance check (`{ resource }`) for a permission with no registered ABAC policy **denies by default** (`strictPolicies: true`) — a forgotten policy must never fail open. The miss is logged once per permission (`log.warn`) either way.
64
64
 
65
- Set `strictPolicies: false` to explicitly restore the pre-5.13 behavior: fall back to the RBAC grant alone (ALLOWS — any holder of the permission can act on ANY resource, a plausible IDOR). See [`define-policies`](@warlock.js/access/define-policies/SKILL.md) for the full decision table.
65
+ Set `strictPolicies: false` to explicitly restore the pre-5.13 behavior: fall back to the RBAC grant alone (ALLOWS — any holder of the permission can act on ANY resource, a plausible IDOR). See the `define-policies` topic for the full decision table.
66
66
 
67
67
  > **Breaking in 5.13:** `strictPolicies` now defaults to `true` and DENIES (it used to default to `false` and ALLOW). Set `strictPolicies: false` to keep the old fail-open fallback.
68
68
 
@@ -84,6 +84,6 @@ The engine caches each user's resolved set (`cache.ttl`, default `10m`). Roles c
84
84
 
85
85
  ## See also
86
86
 
87
- - [`@warlock.js/access/implement-resolver/SKILL.md`](@warlock.js/access/implement-resolver/SKILL.md) — write a resolver for your own storage.
88
- - [`@warlock.js/access/manage-roles/SKILL.md`](@warlock.js/access/manage-roles/SKILL.md) — assign / revoke with the ejected table.
87
+ - The `implement-resolver` topic — write a resolver for your own storage.
88
+ - The `manage-roles` topic — assign / revoke with the ejected table.
89
89
  ```
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: define-policies
3
- description: 'ABAC conditions in `@warlock.js/access` — `definePolicy(permission, (user, resource, ctx) => boolean)` adds an instance-level rule (ownership / tenant / state) on top of the RBAC grant, evaluated when an authorization check carries a `resource`. TRIGGER: `definePolicy`, "ownership check", "only their own", "can edit this specific record", "ABAC", "policy", "resource-level permission", `authorize(user, perm, { resource })`. Skip: plain grant checks — `@warlock.js/access/check-permissions/SKILL.md`.'
3
+ description: 'ABAC conditions in `@warlock.js/access` — `definePolicy(permission, (user, resource, ctx) => boolean)` adds an instance-level rule (ownership / tenant / state) on top of the RBAC grant, evaluated when an authorization check carries a `resource`. TRIGGER: `definePolicy`, "ownership check", "only their own", "can edit this specific record", "ABAC", "policy", "resource-level permission", `authorize(user, perm, { resource })`. Skip: plain grant checks — the `check-permissions` topic.'
4
4
  ---
5
5
 
6
6
  # Define policies (ABAC)
@@ -36,7 +36,7 @@ So `gate("orders.update")` (a route gate, no resource) checks the grant; the per
36
36
  ## The decision = grant AND policy
37
37
 
38
38
  - No grant → denied (the policy never runs).
39
- - Grant + no policy registered → **denied by default** (`strictPolicies: true`, the 5.13+ default) — a typo'd permission or a forgotten `import "./policies"` must never silently degrade an instance check to a class-level one. Logs a one-time `log.warn` naming the permission either way. Set `strictPolicies: false` in the access config (see [`configure-access`](@warlock.js/access/configure-access/SKILL.md)) to explicitly restore the pre-5.13 RBAC-only fallback (allowed).
39
+ - Grant + no policy registered → **denied by default** (`strictPolicies: true`, the 5.13+ default) — a typo'd permission or a forgotten `import "./policies"` must never silently degrade an instance check to a class-level one. Logs a one-time `log.warn` naming the permission either way. Set `strictPolicies: false` in the access config (see the `configure-access` topic) to explicitly restore the pre-5.13 RBAC-only fallback (allowed).
40
40
  - Grant + policy → the policy decides.
41
41
 
42
42
  Policies **deny**, they don't grant — a policy can't let a user past a permission they don't hold.
@@ -65,4 +65,4 @@ definePolicy("orders.refund", (user, order, ctx) =>
65
65
 
66
66
  ## See also
67
67
 
68
- - [`@warlock.js/access/check-permissions/SKILL.md`](@warlock.js/access/check-permissions/SKILL.md) — `authorize(…, { resource })`.
68
+ - The `check-permissions` topic — `authorize(…, { resource })`.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: implement-resolver
3
- description: 'Connect `@warlock.js/access` to your role/permission storage by implementing the `AccessResolver` contract (`resolveRoles` / `resolvePermissions`, optional `resolveTenant`) — for a DB-backed catalog, a user column, a pivot table, a token claim, or an external directory. The engine owns matching / caching / policies; the resolver only fetches. TRIGGER: `AccessResolver`, `DatabaseAccessResolver`, `resolveTenant`, "custom resolver", "where do roles come from", "roles in a token claim", "permissions from an external API", "implement resolver". Skip: the quickstart `DefaultAccessResolver` — `@warlock.js/access/configure-access/SKILL.md`.'
3
+ description: 'Connect `@warlock.js/access` to your role/permission storage by implementing the `AccessResolver` contract (`resolveRoles` / `resolvePermissions`, optional `resolveTenant`) — for a DB-backed catalog, a user column, a pivot table, a token claim, or an external directory. The engine owns matching / caching / policies; the resolver only fetches. TRIGGER: `AccessResolver`, `DatabaseAccessResolver`, `resolveTenant`, "custom resolver", "where do roles come from", "roles in a token claim", "permissions from an external API", "implement resolver". Skip: the quickstart `DefaultAccessResolver` — the `configure-access` topic.'
4
4
  ---
5
5
 
6
6
  # Implement a resolver
@@ -102,5 +102,5 @@ class DirectResolver implements AccessResolver {
102
102
 
103
103
  ## See also
104
104
 
105
- - [`@warlock.js/access/configure-access/SKILL.md`](@warlock.js/access/configure-access/SKILL.md) — the quickstart `DefaultAccessResolver`.
105
+ - The `configure-access` topic — the quickstart `DefaultAccessResolver`.
106
106
  ```
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: manage-roles
3
- description: 'Assign and read roles in `@warlock.js/access` — the ejected `UserRole.assign` / `UserRole.revoke` (the `user_roles` table) followed by `access.flush`, plus `hasRole` / `hasAnyRole` / `hasAllRoles`. The role→permission catalog is the ejected `Role` table (dynamic). TRIGGER: `UserRole.assign`, `UserRole.revoke`, `access.flush`, `hasRole`, `hasAnyRole`, `hasAllRoles`, `Role` table, "give a user a role", "assign role", "check a user role", "roles per tenant". Skip: permission checks — `@warlock.js/access/check-permissions/SKILL.md`; resolver choice — `@warlock.js/access/configure-access/SKILL.md`.'
3
+ description: 'Assign and read roles in `@warlock.js/access` — the ejected `UserRole.assign` / `UserRole.revoke` (the `user_roles` table) followed by `access.flush`, plus `hasRole` / `hasAnyRole` / `hasAllRoles`. The role→permission catalog is the ejected `Role` table (dynamic). TRIGGER: `UserRole.assign`, `UserRole.revoke`, `access.flush`, `hasRole`, `hasAnyRole`, `hasAllRoles`, `Role` table, "give a user a role", "assign role", "check a user role", "roles per tenant". Skip: permission checks — the `check-permissions` topic; resolver choice — the `configure-access` topic.'
4
4
  ---
5
5
 
6
6
  # Manage roles
@@ -62,10 +62,10 @@ The ejected `UserRole` is a thin cascade model — `{ user_id, user_type, role,
62
62
 
63
63
  ## Roles stored elsewhere?
64
64
 
65
- If roles live on the user (a `roles` column) or in a token claim, you don't use `UserRole` — you manage assignment your way and a resolver reads them. See [`implement-resolver`](@warlock.js/access/implement-resolver/SKILL.md).
65
+ If roles live on the user (a `roles` column) or in a token claim, you don't use `UserRole` — you manage assignment your way and a resolver reads them. See the `implement-resolver` topic.
66
66
 
67
67
  ## See also
68
68
 
69
- - [`@warlock.js/access/configure-access/SKILL.md`](@warlock.js/access/configure-access/SKILL.md) — choosing the resolver.
70
- - [`@warlock.js/access/implement-resolver/SKILL.md`](@warlock.js/access/implement-resolver/SKILL.md) — non-table role storage.
69
+ - The `configure-access` topic — choosing the resolver.
70
+ - The `implement-resolver` topic — non-table role storage.
71
71
  ```
@@ -28,11 +28,11 @@ Skip if you only need "is the user an admin" — `authMiddleware("admin")` from
28
28
 
29
29
  ## Skills index
30
30
 
31
- - [`configure-access`](@warlock.js/access/configure-access/SKILL.md) — the required resolver (`DefaultAccessResolver` vs the ejected `DatabaseAccessResolver`), cache + tenant.
32
- - [`check-permissions`](@warlock.js/access/check-permissions/SKILL.md) — `can` / `cannot` / `canAll` / `canAny` / `authorize*` + the `gate*` middleware.
33
- - [`define-policies`](@warlock.js/access/define-policies/SKILL.md) — `definePolicy` for ownership / tenant / state conditions (ABAC).
34
- - [`manage-roles`](@warlock.js/access/manage-roles/SKILL.md) — assign / revoke via the ejected `UserRole` + `access.flush`, `hasRole` / `hasAnyRole` / `hasAllRoles`.
35
- - [`implement-resolver`](@warlock.js/access/implement-resolver/SKILL.md) — connect the engine to your storage (a DB catalog, a column, a pivot, a token claim).
31
+ - `configure-access` — the required resolver (`DefaultAccessResolver` vs the ejected `DatabaseAccessResolver`), cache + tenant.
32
+ - `check-permissions` — `can` / `cannot` / `canAll` / `canAny` / `authorize*` + the `gate*` middleware.
33
+ - `define-policies` — `definePolicy` for ownership / tenant / state conditions (ABAC).
34
+ - `manage-roles` — assign / revoke via the ejected `UserRole` + `access.flush`, `hasRole` / `hasAnyRole` / `hasAllRoles`.
35
+ - `implement-resolver` — connect the engine to your storage (a DB catalog, a column, a pivot, a token claim).
36
36
 
37
37
  ## What it deliberately doesn't do
38
38
 
@@ -42,5 +42,5 @@ Skip if you only need "is the user an admin" — `authMiddleware("admin")` from
42
42
 
43
43
  ## See also
44
44
 
45
- - [`@warlock.js/auth/overview/SKILL.md`](@warlock.js/auth/overview/SKILL.md) — authentication, the layer below.
46
- - [`@warlock.js/auth/protect-routes/SKILL.md`](@warlock.js/auth/protect-routes/SKILL.md) — `authMiddleware`; stack `gate` after it.
45
+ - The `overview` topic of the `warlock-js-auth` skill — authentication, the layer below.
46
+ - The `protect-routes` topic of the `warlock-js-auth` skill — `authMiddleware`; stack `gate` after it.