@generazioneai/authz 0.0.3 → 0.0.5

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 (68) hide show
  1. package/dist/enforce/index.d.ts +4 -0
  2. package/dist/enforce/index.d.ts.map +1 -0
  3. package/dist/enforce/index.js +21 -0
  4. package/dist/enforce/index.js.map +1 -0
  5. package/dist/enforce/prisma-extension.d.ts +25 -0
  6. package/dist/enforce/prisma-extension.d.ts.map +1 -0
  7. package/dist/enforce/prisma-extension.js +79 -0
  8. package/dist/enforce/prisma-extension.js.map +1 -0
  9. package/dist/enforce/run-unscoped.d.ts +2 -0
  10. package/dist/enforce/run-unscoped.d.ts.map +1 -0
  11. package/dist/enforce/run-unscoped.js +14 -0
  12. package/dist/enforce/run-unscoped.js.map +1 -0
  13. package/dist/enforce/scoped-repository.d.ts +10 -0
  14. package/dist/enforce/scoped-repository.d.ts.map +1 -0
  15. package/dist/enforce/scoped-repository.js +26 -0
  16. package/dist/enforce/scoped-repository.js.map +1 -0
  17. package/dist/nest/authorize.decorator.d.ts +3 -0
  18. package/dist/nest/authorize.decorator.d.ts.map +1 -0
  19. package/dist/nest/authorize.decorator.js +15 -0
  20. package/dist/nest/authorize.decorator.js.map +1 -0
  21. package/dist/nest/authz-context.interceptor.d.ts.map +1 -1
  22. package/dist/nest/authz-context.interceptor.js +4 -1
  23. package/dist/nest/authz-context.interceptor.js.map +1 -1
  24. package/dist/nest/global-authz.guard.d.ts +34 -0
  25. package/dist/nest/global-authz.guard.d.ts.map +1 -0
  26. package/dist/nest/global-authz.guard.js +82 -0
  27. package/dist/nest/global-authz.guard.js.map +1 -0
  28. package/dist/nest/index.d.ts +3 -0
  29. package/dist/nest/index.d.ts.map +1 -1
  30. package/dist/nest/index.js +3 -0
  31. package/dist/nest/index.js.map +1 -1
  32. package/dist/nest/internal-auth.interceptor.d.ts +20 -0
  33. package/dist/nest/internal-auth.interceptor.d.ts.map +1 -1
  34. package/dist/nest/internal-auth.interceptor.js +38 -2
  35. package/dist/nest/internal-auth.interceptor.js.map +1 -1
  36. package/dist/nest/public.decorator.d.ts +3 -0
  37. package/dist/nest/public.decorator.d.ts.map +1 -0
  38. package/dist/nest/public.decorator.js +10 -0
  39. package/dist/nest/public.decorator.js.map +1 -0
  40. package/dist/snapshot/ability-builder.d.ts +23 -0
  41. package/dist/snapshot/ability-builder.d.ts.map +1 -0
  42. package/dist/snapshot/ability-builder.js +40 -0
  43. package/dist/snapshot/ability-builder.js.map +1 -0
  44. package/dist/snapshot/distributed-lock.d.ts +19 -0
  45. package/dist/snapshot/distributed-lock.d.ts.map +1 -0
  46. package/dist/snapshot/distributed-lock.js +37 -0
  47. package/dist/snapshot/distributed-lock.js.map +1 -0
  48. package/dist/snapshot/index.d.ts +7 -0
  49. package/dist/snapshot/index.d.ts.map +1 -0
  50. package/dist/snapshot/index.js +26 -0
  51. package/dist/snapshot/index.js.map +1 -0
  52. package/dist/snapshot/l1-cache.d.ts +16 -0
  53. package/dist/snapshot/l1-cache.d.ts.map +1 -0
  54. package/dist/snapshot/l1-cache.js +43 -0
  55. package/dist/snapshot/l1-cache.js.map +1 -0
  56. package/dist/snapshot/perm-hash.d.ts +3 -0
  57. package/dist/snapshot/perm-hash.d.ts.map +1 -0
  58. package/dist/snapshot/perm-hash.js +33 -0
  59. package/dist/snapshot/perm-hash.js.map +1 -0
  60. package/dist/snapshot/snapshot.envelope.d.ts +36 -0
  61. package/dist/snapshot/snapshot.envelope.d.ts.map +1 -0
  62. package/dist/snapshot/snapshot.envelope.js +23 -0
  63. package/dist/snapshot/snapshot.envelope.js.map +1 -0
  64. package/dist/snapshot/snapshot.store.d.ts +24 -0
  65. package/dist/snapshot/snapshot.store.d.ts.map +1 -0
  66. package/dist/snapshot/snapshot.store.js +78 -0
  67. package/dist/snapshot/snapshot.store.js.map +1 -0
  68. package/package.json +19 -4
@@ -0,0 +1,23 @@
1
+ import { type PrismaAbility } from '@casl/prisma';
2
+ import type { AuthzContext } from '../context/authz-context';
3
+ import type { ResourceRegistry } from '../resource-registry';
4
+ import type { AbilityRule } from './snapshot.envelope';
5
+ export type Scope = 'GLOBAL' | 'TENANT' | 'OWN' | 'CONNECTED' | 'PROVIDER' | 'CUSTOMER';
6
+ export interface Grant {
7
+ action: string;
8
+ /** subject literal, or 'all' for the CASL wildcard (system tier). */
9
+ subject: string;
10
+ scope: Scope;
11
+ /** field-level allow-list; empty/absent = all fields. */
12
+ fields?: string[];
13
+ inverted?: boolean;
14
+ }
15
+ /**
16
+ * Grants → serialized CASL rules. GLOBAL scope (and the 'all' wildcard) emit no
17
+ * conditions (unrestricted within the grant); every other scope pulls the manifest's
18
+ * `scopes[scope]` template and substitutes the $ctx placeholders against `ctx`.
19
+ */
20
+ export declare function buildRulesFromGrants(grants: Grant[], registry: ResourceRegistry, ctx: AuthzContext): AbilityRule[];
21
+ /** Rehydrate a PrismaAbility from serialized rules (snapshot → runtime). */
22
+ export declare function hydrateAbility(rules: AbilityRule[]): PrismaAbility<any>;
23
+ //# sourceMappingURL=ability-builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ability-builder.d.ts","sourceRoot":"","sources":["../../src/snapshot/ability-builder.ts"],"names":[],"mappings":"AAIA,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAEvE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,MAAM,MAAM,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,GAAG,WAAW,GAAG,UAAU,GAAG,UAAU,CAAC;AAExF,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,KAAK,CAAC;IACb,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,KAAK,EAAE,EACf,QAAQ,EAAE,gBAAgB,EAC1B,GAAG,EAAE,YAAY,GAChB,WAAW,EAAE,CAiBf;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,aAAa,CAAC,GAAG,CAAC,CAEvE"}
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildRulesFromGrants = buildRulesFromGrants;
4
+ exports.hydrateAbility = hydrateAbility;
5
+ // Step 4 builder core — turn a user's role-permission GRANTS into CASL rules, then a
6
+ // PrismaAbility. A grant = (action × subject × scope [× fields]); the scope is resolved
7
+ // to a Prisma where via the resource manifest's scope template + substituteContext.
8
+ // (skillID flattens its 3-tier RBAC into grants and calls this.)
9
+ const prisma_1 = require("@casl/prisma");
10
+ const scope_substitute_1 = require("../scope-substitute");
11
+ /**
12
+ * Grants → serialized CASL rules. GLOBAL scope (and the 'all' wildcard) emit no
13
+ * conditions (unrestricted within the grant); every other scope pulls the manifest's
14
+ * `scopes[scope]` template and substitutes the $ctx placeholders against `ctx`.
15
+ */
16
+ function buildRulesFromGrants(grants, registry, ctx) {
17
+ const rules = [];
18
+ for (const g of grants) {
19
+ const rule = { action: g.action, subject: g.subject };
20
+ if (g.fields && g.fields.length)
21
+ rule.fields = g.fields;
22
+ if (g.inverted)
23
+ rule.inverted = true;
24
+ if (g.scope === 'GLOBAL' || g.subject === 'all') {
25
+ rules.push(rule);
26
+ continue;
27
+ }
28
+ const template = registry.get(g.subject)?.scopes?.[g.scope];
29
+ if (!template)
30
+ continue; // scope not declared for this subject (authz:check guards this)
31
+ rule.conditions = (0, scope_substitute_1.substituteContext)(template, ctx);
32
+ rules.push(rule);
33
+ }
34
+ return rules;
35
+ }
36
+ /** Rehydrate a PrismaAbility from serialized rules (snapshot → runtime). */
37
+ function hydrateAbility(rules) {
38
+ return (0, prisma_1.createPrismaAbility)(rules);
39
+ }
40
+ //# sourceMappingURL=ability-builder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ability-builder.js","sourceRoot":"","sources":["../../src/snapshot/ability-builder.ts"],"names":[],"mappings":";;AA2BA,oDAqBC;AAGD,wCAEC;AArDD,qFAAqF;AACrF,wFAAwF;AACxF,oFAAoF;AACpF,iEAAiE;AACjE,yCAAuE;AACvE,0DAAwD;AAiBxD;;;;GAIG;AACH,SAAgB,oBAAoB,CAClC,MAAe,EACf,QAA0B,EAC1B,GAAiB;IAEjB,MAAM,KAAK,GAAkB,EAAE,CAAC;IAChC,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,MAAM,IAAI,GAAgB,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;QACnE,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,CAAC,MAAM;YAAE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC;QACxD,IAAI,CAAC,CAAC,QAAQ;YAAE,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QAErC,IAAI,CAAC,CAAC,KAAK,KAAK,QAAQ,IAAI,CAAC,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;YAChD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjB,SAAS;QACX,CAAC;QACD,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAC5D,IAAI,CAAC,QAAQ;YAAE,SAAS,CAAC,gEAAgE;QACzF,IAAI,CAAC,UAAU,GAAG,IAAA,oCAAiB,EAAC,QAAQ,EAAE,GAAG,CAA4B,CAAC;QAC9E,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,4EAA4E;AAC5E,SAAgB,cAAc,CAAC,KAAoB;IACjD,OAAO,IAAA,4BAAmB,EAAC,KAAkD,CAAuB,CAAC;AACvG,CAAC"}
@@ -0,0 +1,19 @@
1
+ import type Redis from 'ioredis';
2
+ export declare const DEFAULT_LOCK_PREFIX = "authz:lock:";
3
+ export declare const DEFAULT_LOCK_TTL_SEC = 10;
4
+ export declare const DEFAULT_BACKOFF_MS: number[];
5
+ export declare class DistributedLock {
6
+ private readonly redis;
7
+ private readonly prefix;
8
+ constructor(redis: Redis, prefix?: string);
9
+ /** SET key owner EX ttl NX → true if acquired. */
10
+ acquire(key: string, owner: string, ttlSec?: number): Promise<boolean>;
11
+ /** Release only if still owned (compare-and-delete, avoids deleting a re-acquired lock). */
12
+ release(key: string, owner: string): Promise<void>;
13
+ /**
14
+ * Lock loser path: poll `read` with backoff, returning the first non-null result.
15
+ * Returns null if all attempts are exhausted (→ caller falls back to a direct build).
16
+ */
17
+ pollForResult<T>(read: () => Promise<T | null>, backoffMs?: number[]): Promise<T | null>;
18
+ }
19
+ //# sourceMappingURL=distributed-lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"distributed-lock.d.ts","sourceRoot":"","sources":["../../src/snapshot/distributed-lock.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,SAAS,CAAC;AAIjC,eAAO,MAAM,mBAAmB,gBAAgB,CAAC;AACjD,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,eAAO,MAAM,kBAAkB,UAAmB,CAAC;AAEnD,qBAAa,eAAe;IAExB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,KAAK,EAAE,KAAK,EACZ,MAAM,GAAE,MAA4B;IAGvD,kDAAkD;IAC5C,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,SAAuB,GAAG,OAAO,CAAC,OAAO,CAAC;IAI1F,4FAA4F;IACtF,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMxD;;;OAGG;IACG,aAAa,CAAC,CAAC,EACnB,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,EAC7B,SAAS,GAAE,MAAM,EAAuB,GACvC,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;CAQrB"}
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DistributedLock = exports.DEFAULT_BACKOFF_MS = exports.DEFAULT_LOCK_TTL_SEC = exports.DEFAULT_LOCK_PREFIX = void 0;
4
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
5
+ exports.DEFAULT_LOCK_PREFIX = 'authz:lock:';
6
+ exports.DEFAULT_LOCK_TTL_SEC = 10;
7
+ exports.DEFAULT_BACKOFF_MS = [5, 15, 50, 150];
8
+ class DistributedLock {
9
+ constructor(redis, prefix = exports.DEFAULT_LOCK_PREFIX) {
10
+ this.redis = redis;
11
+ this.prefix = prefix;
12
+ }
13
+ /** SET key owner EX ttl NX → true if acquired. */
14
+ async acquire(key, owner, ttlSec = exports.DEFAULT_LOCK_TTL_SEC) {
15
+ return (await this.redis.set(this.prefix + key, owner, 'EX', ttlSec, 'NX')) === 'OK';
16
+ }
17
+ /** Release only if still owned (compare-and-delete, avoids deleting a re-acquired lock). */
18
+ async release(key, owner) {
19
+ const lua = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
20
+ await this.redis.eval(lua, 1, this.prefix + key, owner);
21
+ }
22
+ /**
23
+ * Lock loser path: poll `read` with backoff, returning the first non-null result.
24
+ * Returns null if all attempts are exhausted (→ caller falls back to a direct build).
25
+ */
26
+ async pollForResult(read, backoffMs = exports.DEFAULT_BACKOFF_MS) {
27
+ for (const wait of backoffMs) {
28
+ await sleep(wait);
29
+ const v = await read();
30
+ if (v != null)
31
+ return v;
32
+ }
33
+ return null;
34
+ }
35
+ }
36
+ exports.DistributedLock = DistributedLock;
37
+ //# sourceMappingURL=distributed-lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"distributed-lock.js","sourceRoot":"","sources":["../../src/snapshot/distributed-lock.ts"],"names":[],"mappings":";;;AAKA,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAE7D,QAAA,mBAAmB,GAAG,aAAa,CAAC;AACpC,QAAA,oBAAoB,GAAG,EAAE,CAAC;AAC1B,QAAA,kBAAkB,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;AAEnD,MAAa,eAAe;IAC1B,YACmB,KAAY,EACZ,SAAiB,2BAAmB;QADpC,UAAK,GAAL,KAAK,CAAO;QACZ,WAAM,GAAN,MAAM,CAA8B;IACpD,CAAC;IAEJ,kDAAkD;IAClD,KAAK,CAAC,OAAO,CAAC,GAAW,EAAE,KAAa,EAAE,MAAM,GAAG,4BAAoB;QACrE,OAAO,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC;IACvF,CAAC;IAED,4FAA4F;IAC5F,KAAK,CAAC,OAAO,CAAC,GAAW,EAAE,KAAa;QACtC,MAAM,GAAG,GACP,mGAAmG,CAAC;QACtG,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,IAAI,CAAC,MAAM,GAAG,GAAG,EAAE,KAAK,CAAC,CAAC;IAC1D,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,aAAa,CACjB,IAA6B,EAC7B,YAAsB,0BAAkB;QAExC,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;YAC7B,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC;YAClB,MAAM,CAAC,GAAG,MAAM,IAAI,EAAE,CAAC;YACvB,IAAI,CAAC,IAAI,IAAI;gBAAE,OAAO,CAAC,CAAC;QAC1B,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;CACF;AAjCD,0CAiCC"}
@@ -0,0 +1,7 @@
1
+ export * from './snapshot.envelope';
2
+ export * from './ability-builder';
3
+ export * from './perm-hash';
4
+ export * from './l1-cache';
5
+ export * from './distributed-lock';
6
+ export * from './snapshot.store';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/snapshot/index.ts"],"names":[],"mappings":"AAGA,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC"}
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ // @generazioneai/authz/snapshot — Step 4 permission snapshot primitives.
18
+ // Pure core (envelope, perm-hash) + Redis-backed pieces (store, lock) + in-process L1.
19
+ // The NestJS resolver/subscriber + skillID builder/emitter are wired in the services.
20
+ __exportStar(require("./snapshot.envelope"), exports);
21
+ __exportStar(require("./ability-builder"), exports);
22
+ __exportStar(require("./perm-hash"), exports);
23
+ __exportStar(require("./l1-cache"), exports);
24
+ __exportStar(require("./distributed-lock"), exports);
25
+ __exportStar(require("./snapshot.store"), exports);
26
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/snapshot/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,yEAAyE;AACzE,uFAAuF;AACvF,sFAAsF;AACtF,sDAAoC;AACpC,oDAAkC;AAClC,8CAA4B;AAC5B,6CAA2B;AAC3B,qDAAmC;AACnC,mDAAiC"}
@@ -0,0 +1,16 @@
1
+ import type { SnapshotEnvelope } from './snapshot.envelope';
2
+ export interface L1Options {
3
+ max?: number;
4
+ ttlMs?: number;
5
+ }
6
+ export declare class L1SnapshotCache {
7
+ private readonly cache;
8
+ constructor(opts?: L1Options);
9
+ private key;
10
+ get(userId: string, actingJuridicalId?: string): SnapshotEnvelope | undefined;
11
+ set(env: SnapshotEnvelope): void;
12
+ /** Evict one tenant entry, or ALL entries for a user when actingJuridicalId is omitted. */
13
+ evict(userId: string, actingJuridicalId?: string): number;
14
+ clear(): void;
15
+ }
16
+ //# sourceMappingURL=l1-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"l1-cache.d.ts","sourceRoot":"","sources":["../../src/snapshot/l1-cache.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqC;gBAE/C,IAAI,GAAE,SAAc;IAOhC,OAAO,CAAC,GAAG;IAIX,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,iBAAiB,CAAC,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS;IAI7E,GAAG,CAAC,GAAG,EAAE,gBAAgB,GAAG,IAAI;IAIhC,2FAA2F;IAC3F,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,iBAAiB,CAAC,EAAE,MAAM,GAAG,MAAM;IAezD,KAAK,IAAI,IAAI;CAGd"}
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.L1SnapshotCache = void 0;
4
+ // Step 4 DEC-S4.5/10 — in-process L1 snapshot cache (TTL 30s = Redis TTL / 2, so an L1
5
+ // hit never outlives a revoke that already hit Redis). Keyed by (userId, actingJuridicalId).
6
+ const lru_cache_1 = require("lru-cache");
7
+ class L1SnapshotCache {
8
+ constructor(opts = {}) {
9
+ this.cache = new lru_cache_1.LRUCache({
10
+ max: opts.max ?? 2000,
11
+ ttl: opts.ttlMs ?? 30_000,
12
+ });
13
+ }
14
+ key(userId, actingJuridicalId) {
15
+ return `${userId}:${actingJuridicalId ?? ''}`;
16
+ }
17
+ get(userId, actingJuridicalId) {
18
+ return this.cache.get(this.key(userId, actingJuridicalId));
19
+ }
20
+ set(env) {
21
+ this.cache.set(this.key(env.userId, env.actingJuridicalId), env);
22
+ }
23
+ /** Evict one tenant entry, or ALL entries for a user when actingJuridicalId is omitted. */
24
+ evict(userId, actingJuridicalId) {
25
+ if (actingJuridicalId !== undefined) {
26
+ return this.cache.delete(this.key(userId, actingJuridicalId)) ? 1 : 0;
27
+ }
28
+ const prefix = `${userId}:`;
29
+ let n = 0;
30
+ for (const k of this.cache.keys()) {
31
+ if (k.startsWith(prefix)) {
32
+ this.cache.delete(k);
33
+ n++;
34
+ }
35
+ }
36
+ return n;
37
+ }
38
+ clear() {
39
+ this.cache.clear();
40
+ }
41
+ }
42
+ exports.L1SnapshotCache = L1SnapshotCache;
43
+ //# sourceMappingURL=l1-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"l1-cache.js","sourceRoot":"","sources":["../../src/snapshot/l1-cache.ts"],"names":[],"mappings":";;;AAAA,uFAAuF;AACvF,6FAA6F;AAC7F,yCAAqC;AAQrC,MAAa,eAAe;IAG1B,YAAY,OAAkB,EAAE;QAC9B,IAAI,CAAC,KAAK,GAAG,IAAI,oBAAQ,CAA2B;YAClD,GAAG,EAAE,IAAI,CAAC,GAAG,IAAI,IAAI;YACrB,GAAG,EAAE,IAAI,CAAC,KAAK,IAAI,MAAM;SAC1B,CAAC,CAAC;IACL,CAAC;IAEO,GAAG,CAAC,MAAc,EAAE,iBAA0B;QACpD,OAAO,GAAG,MAAM,IAAI,iBAAiB,IAAI,EAAE,EAAE,CAAC;IAChD,CAAC;IAED,GAAG,CAAC,MAAc,EAAE,iBAA0B;QAC5C,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAAC;IAC7D,CAAC;IAED,GAAG,CAAC,GAAqB;QACvB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,iBAAiB,CAAC,EAAE,GAAG,CAAC,CAAC;IACnE,CAAC;IAED,2FAA2F;IAC3F,KAAK,CAAC,MAAc,EAAE,iBAA0B;QAC9C,IAAI,iBAAiB,KAAK,SAAS,EAAE,CAAC;YACpC,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACxE,CAAC;QACD,MAAM,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC;QAC5B,IAAI,CAAC,GAAG,CAAC,CAAC;QACV,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;YAClC,IAAI,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;gBACzB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;gBACrB,CAAC,EAAE,CAAC;YACN,CAAC;QACH,CAAC;QACD,OAAO,CAAC,CAAC;IACX,CAAC;IAED,KAAK;QACH,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;CACF;AAzCD,0CAyCC"}
@@ -0,0 +1,3 @@
1
+ import type { AbilityRule } from './snapshot.envelope';
2
+ export declare function computePermHash(rules: AbilityRule[]): string;
3
+ //# sourceMappingURL=perm-hash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"perm-hash.d.ts","sourceRoot":"","sources":["../../src/snapshot/perm-hash.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AA2BvD,wBAAgB,eAAe,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,MAAM,CAM5D"}
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.computePermHash = computePermHash;
7
+ // Step 4 DEC-S4.7/8/9 — deterministic permission hash (128-bit, 32 hex).
8
+ //
9
+ // Two users with the same roles but different tenantId get DIFFERENT permHashes
10
+ // (conditions are post-substitution → tenant-specific) — that is correct. The hash is
11
+ // cross-pod deterministic: rules are normalized + sorted, then json-stable-stringify'd.
12
+ const node_crypto_1 = require("node:crypto");
13
+ const json_stable_stringify_1 = __importDefault(require("json-stable-stringify"));
14
+ const asSortedArray = (v) => Array.isArray(v) ? [...v].sort() : v;
15
+ const ruleKey = (r) => `${JSON.stringify(r.subject)}|${JSON.stringify(r.action)}|${r.inverted ? 1 : 0}`;
16
+ /** Canonical, hash-relevant projection of a rule (excludes `reason`/metadata). */
17
+ function normalize(r) {
18
+ return {
19
+ action: asSortedArray(r.action),
20
+ subject: asSortedArray(r.subject),
21
+ fields: r.fields && r.fields.length ? [...r.fields].sort() : undefined,
22
+ conditions: r.conditions ?? undefined,
23
+ inverted: r.inverted ?? false,
24
+ };
25
+ }
26
+ function computePermHash(rules) {
27
+ const normalized = rules
28
+ .map(normalize)
29
+ .sort((a, b) => ruleKey(a).localeCompare(ruleKey(b)));
30
+ const canonical = (0, json_stable_stringify_1.default)(normalized) ?? '[]';
31
+ return (0, node_crypto_1.createHash)('sha256').update(canonical).digest('hex').slice(0, 32);
32
+ }
33
+ //# sourceMappingURL=perm-hash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"perm-hash.js","sourceRoot":"","sources":["../../src/snapshot/perm-hash.ts"],"names":[],"mappings":";;;;;AAkCA,0CAMC;AAxCD,yEAAyE;AACzE,EAAE;AACF,gFAAgF;AAChF,sFAAsF;AACtF,wFAAwF;AACxF,6CAAyC;AACzC,kFAAoD;AAWpD,MAAM,aAAa,GAAG,CAAC,CAAoB,EAAqB,EAAE,CAChE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AAEvC,MAAM,OAAO,GAAG,CAAC,CAAiB,EAAU,EAAE,CAC5C,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAEnF,kFAAkF;AAClF,SAAS,SAAS,CAAC,CAAc;IAC/B,OAAO;QACL,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC;QAC/B,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC;QACjC,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS;QACtE,UAAU,EAAE,CAAC,CAAC,UAAU,IAAI,SAAS;QACrC,QAAQ,EAAE,CAAC,CAAC,QAAQ,IAAI,KAAK;KAC9B,CAAC;AACJ,CAAC;AAED,SAAgB,eAAe,CAAC,KAAoB;IAClD,MAAM,UAAU,GAAG,KAAK;SACrB,GAAG,CAAC,SAAS,CAAC;SACd,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACxD,MAAM,SAAS,GAAG,IAAA,+BAAe,EAAC,UAAU,CAAC,IAAI,IAAI,CAAC;IACtD,OAAO,IAAA,wBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAC3E,CAAC"}
@@ -0,0 +1,36 @@
1
+ import type { AccreditedAs, ConnectedEdges } from '../context/authz-context';
2
+ export declare const SNAPSHOT_SCHEMA_VERSION = 1;
3
+ /** A serialized CASL rule (post $ctx-substitution). Shape accepted by createPrismaAbility. */
4
+ export interface AbilityRule {
5
+ action: string | string[];
6
+ subject: string | string[];
7
+ /** Field-level allow-list (Step 1 fields[]). Empty/absent = all fields. */
8
+ fields?: string[];
9
+ /** Prisma WhereInput-shaped conditions (already substituted; no $ctx left). */
10
+ conditions?: Record<string, unknown>;
11
+ /** CASL `cannot` rule. */
12
+ inverted?: boolean;
13
+ /** Audit-only; excluded from permHash. */
14
+ reason?: string;
15
+ }
16
+ export interface SnapshotEnvelope {
17
+ schemaVersion: number;
18
+ snapId: string;
19
+ userId: string;
20
+ actingJuridicalId?: string;
21
+ /** sha256(canonical(sortedRules)) truncated to 32 hex (Step 4 DEC-S4.7/9). */
22
+ permHash: string;
23
+ /** epoch ms — drives refresh-ahead (DEC-S4.30). */
24
+ builtAt: number;
25
+ rules: AbilityRule[];
26
+ connected: ConnectedEdges;
27
+ accreditedAs: AccreditedAs;
28
+ /** Set when the rules blob was lz4-compressed (DEC-S4.27). Day-1: null. */
29
+ compressed?: 'lz4' | null;
30
+ }
31
+ /**
32
+ * Opaque snapshot id (DEC-S4.4): doesn't leak userId, idempotent per 1s build bucket.
33
+ * blake3 in the plan; sha256 here keeps zero extra deps (opacity is what matters).
34
+ */
35
+ export declare function computeSnapId(userId: string, actingJuridicalId: string | undefined, builtAtMs: number): string;
36
+ //# sourceMappingURL=snapshot.envelope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.envelope.d.ts","sourceRoot":"","sources":["../../src/snapshot/snapshot.envelope.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE7E,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,8FAA8F;AAC9F,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC1B,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,0BAA0B;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,SAAS,EAAE,cAAc,CAAC;IAC1B,YAAY,EAAE,YAAY,CAAC;IAC3B,2EAA2E;IAC3E,UAAU,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EACd,iBAAiB,EAAE,MAAM,GAAG,SAAS,EACrC,SAAS,EAAE,MAAM,GAChB,MAAM,CAMR"}
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SNAPSHOT_SCHEMA_VERSION = void 0;
4
+ exports.computeSnapId = computeSnapId;
5
+ // Step 4 DEC-S4.1/3 — the permission snapshot envelope cached in Redis.
6
+ //
7
+ // skillID builds it (rules already $ctx-substituted), stores it under an opaque snapId;
8
+ // the gateway/downstream fetch it by snapId (carried in the Step 2 JWT `snap` claim) and
9
+ // rehydrate the CASL ability via createPrismaAbility(rules).
10
+ const node_crypto_1 = require("node:crypto");
11
+ exports.SNAPSHOT_SCHEMA_VERSION = 1;
12
+ /**
13
+ * Opaque snapshot id (DEC-S4.4): doesn't leak userId, idempotent per 1s build bucket.
14
+ * blake3 in the plan; sha256 here keeps zero extra deps (opacity is what matters).
15
+ */
16
+ function computeSnapId(userId, actingJuridicalId, builtAtMs) {
17
+ const bucket = Math.floor(builtAtMs / 1000);
18
+ return (0, node_crypto_1.createHash)('sha256')
19
+ .update(`${userId}:${actingJuridicalId ?? ''}:${bucket}`)
20
+ .digest('hex')
21
+ .slice(0, 22);
22
+ }
23
+ //# sourceMappingURL=snapshot.envelope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.envelope.js","sourceRoot":"","sources":["../../src/snapshot/snapshot.envelope.ts"],"names":[],"mappings":";;;AA4CA,sCAUC;AAtDD,wEAAwE;AACxE,EAAE;AACF,wFAAwF;AACxF,yFAAyF;AACzF,6DAA6D;AAC7D,6CAAyC;AAG5B,QAAA,uBAAuB,GAAG,CAAC,CAAC;AAgCzC;;;GAGG;AACH,SAAgB,aAAa,CAC3B,MAAc,EACd,iBAAqC,EACrC,SAAiB;IAEjB,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAC5C,OAAO,IAAA,wBAAU,EAAC,QAAQ,CAAC;SACxB,MAAM,CAAC,GAAG,MAAM,IAAI,iBAAiB,IAAI,EAAE,IAAI,MAAM,EAAE,CAAC;SACxD,MAAM,CAAC,KAAK,CAAC;SACb,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAClB,CAAC"}
@@ -0,0 +1,24 @@
1
+ import type Redis from 'ioredis';
2
+ import type { SnapshotEnvelope } from './snapshot.envelope';
3
+ export declare const SNAP_PREFIX = "authz:snap:";
4
+ export declare const CURRENT_PREFIX = "authz:user:";
5
+ export declare const ROLE_PREFIX = "authz:role:";
6
+ export declare const DEFAULT_SNAPSHOT_TTL_SEC = 60;
7
+ export declare class RedisSnapshotStore {
8
+ private readonly redis;
9
+ constructor(redis: Redis);
10
+ /** Store the envelope + point the user's `current` pointer at it (atomic pipeline). */
11
+ put(env: SnapshotEnvelope, ttlSec?: number): Promise<void>;
12
+ getBySnapId(snapId: string): Promise<SnapshotEnvelope | null>;
13
+ /** Resolve the current snapId for (user, tenant) then load the envelope (pipelined). */
14
+ getCurrent(userId: string, actingJur?: string): Promise<SnapshotEnvelope | null>;
15
+ indexRoleMember(tier: string, roleId: string, userId: string): Promise<void>;
16
+ usersForRole(tier: string, roleId: string): Promise<string[]>;
17
+ /** Evict every snapshot of a user across tenants (SCAN the `current` pointers). */
18
+ revokeUser(userId: string): Promise<number>;
19
+ /** Evict one (user, tenant) snapshot. */
20
+ revokeUserTenant(userId: string, actingJur: string): Promise<void>;
21
+ /** Cascade a role revoke to all its members (reverse-index lookup → per-user evict). */
22
+ revokeRole(tier: string, roleId: string): Promise<number>;
23
+ }
24
+ //# sourceMappingURL=snapshot.store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.store.d.ts","sourceRoot":"","sources":["../../src/snapshot/snapshot.store.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,KAAK,MAAM,SAAS,CAAC;AACjC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,eAAO,MAAM,WAAW,gBAAgB,CAAC;AACzC,eAAO,MAAM,cAAc,gBAAgB,CAAC;AAC5C,eAAO,MAAM,WAAW,gBAAgB,CAAC;AACzC,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAM3C,qBAAa,kBAAkB;IACjB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,KAAK;IAEzC,uFAAuF;IACjF,GAAG,CAAC,GAAG,EAAE,gBAAgB,EAAE,MAAM,SAA2B,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ5E,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAKnE,wFAAwF;IAClF,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAMhF,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAG5E,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAKnE,mFAAmF;IAC7E,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmBjD,yCAAyC;IACnC,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASxE,wFAAwF;IAClF,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAMhE"}
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RedisSnapshotStore = exports.DEFAULT_SNAPSHOT_TTL_SEC = exports.ROLE_PREFIX = exports.CURRENT_PREFIX = exports.SNAP_PREFIX = void 0;
4
+ exports.SNAP_PREFIX = 'authz:snap:';
5
+ exports.CURRENT_PREFIX = 'authz:user:';
6
+ exports.ROLE_PREFIX = 'authz:role:';
7
+ exports.DEFAULT_SNAPSHOT_TTL_SEC = 60;
8
+ const currentKey = (userId, actingJur) => `${exports.CURRENT_PREFIX}${userId}:${actingJur ?? ''}:current`;
9
+ const roleKey = (tier, roleId) => `${exports.ROLE_PREFIX}${tier}:${roleId}:users`;
10
+ class RedisSnapshotStore {
11
+ constructor(redis) {
12
+ this.redis = redis;
13
+ }
14
+ /** Store the envelope + point the user's `current` pointer at it (atomic pipeline). */
15
+ async put(env, ttlSec = exports.DEFAULT_SNAPSHOT_TTL_SEC) {
16
+ await this.redis
17
+ .pipeline()
18
+ .set(exports.SNAP_PREFIX + env.snapId, JSON.stringify(env), 'EX', ttlSec)
19
+ .set(currentKey(env.userId, env.actingJuridicalId), env.snapId, 'EX', ttlSec)
20
+ .exec();
21
+ }
22
+ async getBySnapId(snapId) {
23
+ const raw = await this.redis.get(exports.SNAP_PREFIX + snapId);
24
+ return raw ? JSON.parse(raw) : null;
25
+ }
26
+ /** Resolve the current snapId for (user, tenant) then load the envelope (pipelined). */
27
+ async getCurrent(userId, actingJur) {
28
+ const snapId = await this.redis.get(currentKey(userId, actingJur));
29
+ return snapId ? this.getBySnapId(snapId) : null;
30
+ }
31
+ // ---- reverse-index (role-cascade revocation, DEC-S4.15) --------------------
32
+ async indexRoleMember(tier, roleId, userId) {
33
+ await this.redis.sadd(roleKey(tier, roleId), userId);
34
+ }
35
+ async usersForRole(tier, roleId) {
36
+ return this.redis.smembers(roleKey(tier, roleId));
37
+ }
38
+ // ---- revocation (DEC-S4.13) ------------------------------------------------
39
+ /** Evict every snapshot of a user across tenants (SCAN the `current` pointers). */
40
+ async revokeUser(userId) {
41
+ const pattern = `${exports.CURRENT_PREFIX}${userId}:*:current`;
42
+ let cursor = '0';
43
+ let evicted = 0;
44
+ do {
45
+ const [next, keys] = await this.redis.scan(cursor, 'MATCH', pattern, 'COUNT', 200);
46
+ cursor = next;
47
+ if (keys.length) {
48
+ const snapIds = (await this.redis.mget(...keys)).filter(Boolean);
49
+ const pipe = this.redis.pipeline();
50
+ keys.forEach((k) => pipe.del(k));
51
+ snapIds.forEach((id) => pipe.del(exports.SNAP_PREFIX + id));
52
+ await pipe.exec();
53
+ evicted += keys.length;
54
+ }
55
+ } while (cursor !== '0');
56
+ return evicted;
57
+ }
58
+ /** Evict one (user, tenant) snapshot. */
59
+ async revokeUserTenant(userId, actingJur) {
60
+ const ck = currentKey(userId, actingJur);
61
+ const snapId = await this.redis.get(ck);
62
+ const pipe = this.redis.pipeline();
63
+ pipe.del(ck);
64
+ if (snapId)
65
+ pipe.del(exports.SNAP_PREFIX + snapId);
66
+ await pipe.exec();
67
+ }
68
+ /** Cascade a role revoke to all its members (reverse-index lookup → per-user evict). */
69
+ async revokeRole(tier, roleId) {
70
+ const users = await this.usersForRole(tier, roleId);
71
+ let n = 0;
72
+ for (const u of users)
73
+ n += await this.revokeUser(u);
74
+ return n;
75
+ }
76
+ }
77
+ exports.RedisSnapshotStore = RedisSnapshotStore;
78
+ //# sourceMappingURL=snapshot.store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.store.js","sourceRoot":"","sources":["../../src/snapshot/snapshot.store.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@generazioneai/authz",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Runtime authz + autoquery for Skillera microservices",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -44,6 +44,14 @@
44
44
  "./nest": {
45
45
  "types": "./dist/nest/index.d.ts",
46
46
  "default": "./dist/nest/index.js"
47
+ },
48
+ "./snapshot": {
49
+ "types": "./dist/snapshot/index.d.ts",
50
+ "default": "./dist/snapshot/index.js"
51
+ },
52
+ "./enforce": {
53
+ "types": "./dist/enforce/index.d.ts",
54
+ "default": "./dist/enforce/index.js"
47
55
  }
48
56
  },
49
57
  "typesVersions": {
@@ -56,6 +64,12 @@
56
64
  ],
57
65
  "internal": [
58
66
  "dist/internal/index.d.ts"
67
+ ],
68
+ "snapshot": [
69
+ "dist/snapshot/index.d.ts"
70
+ ],
71
+ "enforce": [
72
+ "dist/enforce/index.d.ts"
59
73
  ]
60
74
  }
61
75
  },
@@ -79,13 +93,14 @@
79
93
  }
80
94
  },
81
95
  "dependencies": {
82
- "jose": "^6.0.10",
83
96
  "ioredis": "^5.4.0",
84
- "json-stable-stringify": "^1.1.1"
97
+ "jose": "^6.0.10",
98
+ "json-stable-stringify": "^1.1.1",
99
+ "lru-cache": "^10.4.0"
85
100
  },
86
101
  "devDependencies": {
87
102
  "@casl/ability": "^6.7.3",
88
- "@casl/prisma": "^1.5.0",
103
+ "@casl/prisma": "^1.6.2",
89
104
  "@nestjs/core": "^11.1.24",
90
105
  "@nestjs/microservices": "^11.1.24",
91
106
  "@types/json-stable-stringify": "^1.0.36",