@nacre.work/core 0.0.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 (90) hide show
  1. package/README.md +3 -0
  2. package/dist/authz/cache.d.ts +54 -0
  3. package/dist/authz/cache.d.ts.map +1 -0
  4. package/dist/authz/cache.js +63 -0
  5. package/dist/authz/cache.js.map +1 -0
  6. package/dist/authz/filter.d.ts +60 -0
  7. package/dist/authz/filter.d.ts.map +1 -0
  8. package/dist/authz/filter.js +54 -0
  9. package/dist/authz/filter.js.map +1 -0
  10. package/dist/authz/index.d.ts +15 -0
  11. package/dist/authz/index.d.ts.map +1 -0
  12. package/dist/authz/index.js +9 -0
  13. package/dist/authz/index.js.map +1 -0
  14. package/dist/authz/permissions.d.ts +20 -0
  15. package/dist/authz/permissions.d.ts.map +1 -0
  16. package/dist/authz/permissions.js +43 -0
  17. package/dist/authz/permissions.js.map +1 -0
  18. package/dist/authz/principals.d.ts +34 -0
  19. package/dist/authz/principals.d.ts.map +1 -0
  20. package/dist/authz/principals.js +42 -0
  21. package/dist/authz/principals.js.map +1 -0
  22. package/dist/authz/reference.d.ts +25 -0
  23. package/dist/authz/reference.d.ts.map +1 -0
  24. package/dist/authz/reference.js +34 -0
  25. package/dist/authz/reference.js.map +1 -0
  26. package/dist/authz/resolve.d.ts +45 -0
  27. package/dist/authz/resolve.d.ts.map +1 -0
  28. package/dist/authz/resolve.js +98 -0
  29. package/dist/authz/resolve.js.map +1 -0
  30. package/dist/authz/scope-tree.d.ts +52 -0
  31. package/dist/authz/scope-tree.d.ts.map +1 -0
  32. package/dist/authz/scope-tree.js +56 -0
  33. package/dist/authz/scope-tree.js.map +1 -0
  34. package/dist/authz/store.d.ts +50 -0
  35. package/dist/authz/store.d.ts.map +1 -0
  36. package/dist/authz/store.js +115 -0
  37. package/dist/authz/store.js.map +1 -0
  38. package/dist/authz/tags.d.ts +40 -0
  39. package/dist/authz/tags.d.ts.map +1 -0
  40. package/dist/authz/tags.js +48 -0
  41. package/dist/authz/tags.js.map +1 -0
  42. package/dist/config.d.ts +73 -0
  43. package/dist/config.d.ts.map +1 -0
  44. package/dist/config.js +157 -0
  45. package/dist/config.js.map +1 -0
  46. package/dist/db/client.d.ts +38 -0
  47. package/dist/db/client.d.ts.map +1 -0
  48. package/dist/db/client.js +34 -0
  49. package/dist/db/client.js.map +1 -0
  50. package/dist/db/migrate.d.ts +32 -0
  51. package/dist/db/migrate.d.ts.map +1 -0
  52. package/dist/db/migrate.js +103 -0
  53. package/dist/db/migrate.js.map +1 -0
  54. package/dist/index.d.ts +16 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +11 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/metrics.d.ts +77 -0
  59. package/dist/metrics.d.ts.map +1 -0
  60. package/dist/metrics.js +179 -0
  61. package/dist/metrics.js.map +1 -0
  62. package/dist/migrate-main.d.ts +2 -0
  63. package/dist/migrate-main.d.ts.map +1 -0
  64. package/dist/migrate-main.js +34 -0
  65. package/dist/migrate-main.js.map +1 -0
  66. package/dist/migrations/0001_init.sql +226 -0
  67. package/dist/migrations/0002_rls_coverage.sql +128 -0
  68. package/dist/migrations/0003_groups_version.sql +48 -0
  69. package/dist/migrations/0004_acl_version.sql +26 -0
  70. package/dist/migrations/0005_grants_bump_version.sql +38 -0
  71. package/dist/migrations/0006_layer_slug_unique.sql +47 -0
  72. package/dist/migrations/0007_index_lease.sql +42 -0
  73. package/dist/observability.d.ts +11 -0
  74. package/dist/observability.d.ts.map +1 -0
  75. package/dist/observability.js +55 -0
  76. package/dist/observability.js.map +1 -0
  77. package/dist/tsconfig.tsbuildinfo +1 -0
  78. package/dist/types.d.ts +54 -0
  79. package/dist/types.d.ts.map +1 -0
  80. package/dist/types.js +15 -0
  81. package/dist/types.js.map +1 -0
  82. package/dist/vector/query.d.ts +140 -0
  83. package/dist/vector/query.d.ts.map +1 -0
  84. package/dist/vector/query.js +94 -0
  85. package/dist/vector/query.js.map +1 -0
  86. package/dist/vector/search.d.ts +62 -0
  87. package/dist/vector/search.d.ts.map +1 -0
  88. package/dist/vector/search.js +98 -0
  89. package/dist/vector/search.js.map +1 -0
  90. package/package.json +39 -0
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @nacre.work/core
2
+
3
+ Data model, permission resolver, shared types
@@ -0,0 +1,54 @@
1
+ import type { Principal, PrincipalRef } from '../types.js';
2
+ import { type GroupGraph } from './principals.js';
3
+ /**
4
+ * The effective-principals cache.
5
+ *
6
+ * Resolving a user's transitive groups is the one query on the search path that
7
+ * does not depend on the search, so it is the one worth caching. It is also the
8
+ * one where a stale answer is a leak that lasts exactly as long as the cache
9
+ * entry.
10
+ *
11
+ * **The key carries `groups_version`, so staleness is impossible rather than
12
+ * time-bounded.** A membership change increments the version in Postgres, by
13
+ * trigger, and every subsequent read composes a different key — the old entry
14
+ * is not invalidated, it is simply never asked for again. The TTL is a memory
15
+ * bound, not the correctness mechanism.
16
+ *
17
+ * That distinction is invariant I4. A TTL-only cache honours a 60-second SLA
18
+ * on average and misses it whenever the clock is unkind; a version-keyed cache
19
+ * cannot serve a revoked grant at all, because the request that would have hit
20
+ * the stale entry is looking somewhere else.
21
+ */
22
+ export interface CacheStore {
23
+ get(key: string): Promise<string | undefined>;
24
+ set(key: string, value: string, ttlSeconds: number): Promise<void>;
25
+ }
26
+ /** For a single process, and for tests. Redis is the shared implementation. */
27
+ export declare class MemoryCache implements CacheStore {
28
+ #private;
29
+ get(key: string): Promise<string | undefined>;
30
+ set(key: string, value: string, ttlSeconds: number): Promise<void>;
31
+ get size(): number;
32
+ }
33
+ export declare function principalsCacheKey(orgId: string, principal: Principal, groupsVersion: number): string;
34
+ export interface CachedPrincipalsOptions {
35
+ readonly orgId: string;
36
+ readonly principal: Principal;
37
+ /** Read from organizations.groups_version in the same transaction as the grants. */
38
+ readonly groupsVersion: number;
39
+ readonly ttlSeconds: number;
40
+ }
41
+ /**
42
+ * Effective principals, from cache when the version matches.
43
+ *
44
+ * The graph loader is only called on a miss. Note what is *not* here: no
45
+ * invalidation, no deletion, no pub/sub. A version bump changes the key, and
46
+ * the old entry ages out on its own.
47
+ *
48
+ * A cache failure is not a permission failure — it falls through to the graph
49
+ * rather than denying. Reading it as a denial would turn a Redis restart into
50
+ * an outage, and reading it as an allow is not on the table because there is
51
+ * nothing to allow: the fallback computes the same answer, slower.
52
+ */
53
+ export declare function cachedEffectivePrincipals(options: CachedPrincipalsOptions, cache: CacheStore, loadGraph: () => Promise<GroupGraph>): Promise<ReadonlySet<PrincipalRef>>;
54
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../authz/cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1D,OAAO,EAAuB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAEtE;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IAC7C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACnE;AAED,+EAA+E;AAC/E,qBAAa,WAAY,YAAW,UAAU;;IAGtC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAU7C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxE,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAErG;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAA;IAC7B,oFAAoF;IACpF,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC5B;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,uBAAuB,EAChC,KAAK,EAAE,UAAU,EACjB,SAAS,EAAE,MAAM,OAAO,CAAC,UAAU,CAAC,GACnC,OAAO,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC,CA2BpC"}
@@ -0,0 +1,63 @@
1
+ import { effectivePrincipals } from './principals.js';
2
+ /** For a single process, and for tests. Redis is the shared implementation. */
3
+ export class MemoryCache {
4
+ #entries = new Map();
5
+ async get(key) {
6
+ const entry = this.#entries.get(key);
7
+ if (entry === undefined)
8
+ return undefined;
9
+ if (entry.expires <= Date.now()) {
10
+ this.#entries.delete(key);
11
+ return undefined;
12
+ }
13
+ return entry.value;
14
+ }
15
+ async set(key, value, ttlSeconds) {
16
+ this.#entries.set(key, { value, expires: Date.now() + ttlSeconds * 1000 });
17
+ }
18
+ get size() {
19
+ return this.#entries.size;
20
+ }
21
+ }
22
+ export function principalsCacheKey(orgId, principal, groupsVersion) {
23
+ return `acl:principals:${orgId}:${principal.type}:${principal.id}:v${groupsVersion}`;
24
+ }
25
+ /**
26
+ * Effective principals, from cache when the version matches.
27
+ *
28
+ * The graph loader is only called on a miss. Note what is *not* here: no
29
+ * invalidation, no deletion, no pub/sub. A version bump changes the key, and
30
+ * the old entry ages out on its own.
31
+ *
32
+ * A cache failure is not a permission failure — it falls through to the graph
33
+ * rather than denying. Reading it as a denial would turn a Redis restart into
34
+ * an outage, and reading it as an allow is not on the table because there is
35
+ * nothing to allow: the fallback computes the same answer, slower.
36
+ */
37
+ export async function cachedEffectivePrincipals(options, cache, loadGraph) {
38
+ const key = principalsCacheKey(options.orgId, options.principal, options.groupsVersion);
39
+ try {
40
+ const hit = await cache.get(key);
41
+ if (hit !== undefined) {
42
+ const parsed = JSON.parse(hit);
43
+ if (Array.isArray(parsed) && parsed.every((p) => typeof p === 'string')) {
44
+ return new Set(parsed);
45
+ }
46
+ // Unparseable means somebody else wrote this key with a different shape.
47
+ // Recompute rather than guess; a malformed principal set is a wrong
48
+ // permission set.
49
+ }
50
+ }
51
+ catch {
52
+ // Cache unreachable. Fall through.
53
+ }
54
+ const principals = effectivePrincipals(options.principal, await loadGraph());
55
+ try {
56
+ await cache.set(key, JSON.stringify([...principals]), options.ttlSeconds);
57
+ }
58
+ catch {
59
+ // Writing is best effort. A cache that cannot be written is slow, not wrong.
60
+ }
61
+ return principals;
62
+ }
63
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../authz/cache.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAmB,MAAM,iBAAiB,CAAA;AA2BtE,+EAA+E;AAC/E,MAAM,OAAO,WAAW;IACb,QAAQ,GAAG,IAAI,GAAG,EAA8C,CAAA;IAEzE,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACpC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,SAAS,CAAA;QACzC,IAAI,KAAK,CAAC,OAAO,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YAChC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YACzB,OAAO,SAAS,CAAA;QAClB,CAAC;QACD,OAAO,KAAK,CAAC,KAAK,CAAA;IACpB,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,KAAa,EAAE,UAAkB;QACtD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,UAAU,GAAG,IAAI,EAAE,CAAC,CAAA;IAC5E,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAA;IAC3B,CAAC;CACF;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAa,EAAE,SAAoB,EAAE,aAAqB;IAC3F,OAAO,kBAAkB,KAAK,IAAI,SAAS,CAAC,IAAI,IAAI,SAAS,CAAC,EAAE,KAAK,aAAa,EAAE,CAAA;AACtF,CAAC;AAUD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,OAAgC,EAChC,KAAiB,EACjB,SAAoC;IAEpC,MAAM,GAAG,GAAG,kBAAkB,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,aAAa,CAAC,CAAA;IAEvF,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAChC,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtB,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YACvC,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,EAAE,CAAC;gBACxE,OAAO,IAAI,GAAG,CAAC,MAAwB,CAAC,CAAA;YAC1C,CAAC;YACD,yEAAyE;YACzE,oEAAoE;YACpE,kBAAkB;QACpB,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,mCAAmC;IACrC,CAAC;IAED,MAAM,UAAU,GAAG,mBAAmB,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,SAAS,EAAE,CAAC,CAAA;IAE5E,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAA;IAC3E,CAAC;IAAC,MAAM,CAAC;QACP,6EAA6E;IAC/E,CAAC;IAED,OAAO,UAAU,CAAA;AACnB,CAAC"}
@@ -0,0 +1,60 @@
1
+ import type { AccessPlan } from './resolve.js';
2
+ /**
3
+ * A Qdrant payload filter. Narrow on purpose — this module builds filters and
4
+ * nothing reads them back apart from the vector client.
5
+ */
6
+ export interface VectorFilter {
7
+ readonly must: readonly FilterClause[];
8
+ /**
9
+ * At least one of these must match.
10
+ *
11
+ * That is Qdrant's meaning, not Elasticsearch's: a `should` list is a
12
+ * constraint, and a point that matches none of them does not pass the filter.
13
+ * There is no scalar `min_should` to go with it — Qdrant's `min_should` is an
14
+ * object, `{ conditions, min_count }`, for the "at least N" case. The filter
15
+ * sketch in docs/authz.md carried `min_should: 1` as an integer, which the
16
+ * API rejects outright; every query built from it came back 400.
17
+ *
18
+ * Never emit an empty `should`. An empty list is not "match nothing" — it is
19
+ * no constraint at all, so the filter degrades to `must` alone and the caller
20
+ * sees every point in the collection.
21
+ */
22
+ readonly should?: readonly FilterClause[];
23
+ readonly must_not?: readonly FilterClause[];
24
+ }
25
+ export type FilterClause = {
26
+ readonly key: string;
27
+ readonly match: {
28
+ readonly value: string | boolean;
29
+ };
30
+ } | {
31
+ readonly key: string;
32
+ readonly match: {
33
+ readonly any: readonly string[];
34
+ };
35
+ };
36
+ /**
37
+ * Plans that can be turned into a query at all.
38
+ *
39
+ * `AccessPlan` includes `none`, and there is no correct filter for it — the
40
+ * caller must not run the query. Excluding it from this type means a caller who
41
+ * forgets gets a compile error rather than a filter that happens to match
42
+ * everything.
43
+ */
44
+ export type QueryablePlan = Exclude<AccessPlan, {
45
+ kind: 'none';
46
+ }>;
47
+ /**
48
+ * Build the pre-filter for a search.
49
+ *
50
+ * This goes into the `filter` field of the Qdrant query, where it is applied
51
+ * *inside* the HNSW traversal. That is what makes `top_k` return k permitted
52
+ * results rather than k results with some removed afterwards — invariant I2 —
53
+ * and it is the reason no part of this codebase filters a result set.
54
+ *
55
+ * In a hybrid query the same filter is repeated in every prefetch branch.
56
+ * Omitting it from one branch is a leak, and the branches are far enough apart
57
+ * in the query document for that to look like a formatting difference.
58
+ */
59
+ export declare function buildFilter(orgId: string, plan: QueryablePlan): VectorFilter;
60
+ //# sourceMappingURL=filter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../../authz/filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAE9C;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,YAAY,EAAE,CAAA;IACtC;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,YAAY,EAAE,CAAA;IACzC,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,YAAY,EAAE,CAAA;CAC5C;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAA;CAAE,GAC9E;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,CAAA;CAAE,CAAA;AAEjF;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,UAAU,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAEjE;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,YAAY,CA2C5E"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Build the pre-filter for a search.
3
+ *
4
+ * This goes into the `filter` field of the Qdrant query, where it is applied
5
+ * *inside* the HNSW traversal. That is what makes `top_k` return k permitted
6
+ * results rather than k results with some removed afterwards — invariant I2 —
7
+ * and it is the reason no part of this codebase filters a result set.
8
+ *
9
+ * In a hybrid query the same filter is repeated in every prefetch branch.
10
+ * Omitting it from one branch is a leak, and the branches are far enough apart
11
+ * in the query document for that to look like a formatting difference.
12
+ */
13
+ export function buildFilter(orgId, plan) {
14
+ // org_id and deleted are unconditional.
15
+ //
16
+ // org_id is duplicated in the payload even when each tenant has its own
17
+ // collection: it is the second line of defense behind picking the right
18
+ // collection, and it costs one clause.
19
+ //
20
+ // deleted is invariant I5. There is a real window between a delete and the
21
+ // garbage collector's sweep, and a query without this clause returns
22
+ // documents from inside it. Depending on GC timing is how that invariant
23
+ // gets broken.
24
+ const must = [
25
+ { key: 'org_id', match: { value: orgId } },
26
+ { key: 'deleted', match: { value: false } },
27
+ ];
28
+ if (plan.kind === 'all')
29
+ return { must };
30
+ // Only non-empty lists become clauses. An empty `any` adds nothing and an
31
+ // empty `should` removes the constraint entirely.
32
+ const should = [];
33
+ if (plan.layers.length > 0)
34
+ should.push({ key: 'layer_id', match: { any: plan.layers } });
35
+ if (plan.extraDocs.length > 0)
36
+ should.push({ key: 'doc_id', match: { any: plan.extraDocs } });
37
+ if (should.length === 0) {
38
+ // A scoped plan that reaches nothing. `resolve` never produces one — it
39
+ // returns `kind: 'none'` instead — so arriving here means the plan was
40
+ // assembled by hand. Refusing is rule I3 applied to the query builder: a
41
+ // permission set that cannot be expressed denies, and the alternative is a
42
+ // filter with no `should` at all, which returns the whole collection.
43
+ throw new Error('refusing to build a filter from a scoped plan that reaches no layer and ' +
44
+ 'no document — a plan like this must be kind: "none"');
45
+ }
46
+ return {
47
+ must,
48
+ should,
49
+ ...(plan.deniedDocs.length > 0
50
+ ? { must_not: [{ key: 'doc_id', match: { any: plan.deniedDocs } }] }
51
+ : {}),
52
+ };
53
+ }
54
+ //# sourceMappingURL=filter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter.js","sourceRoot":"","sources":["../../authz/filter.ts"],"names":[],"mappings":"AAwCA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa,EAAE,IAAmB;IAC5D,wCAAwC;IACxC,EAAE;IACF,wEAAwE;IACxE,wEAAwE;IACxE,uCAAuC;IACvC,EAAE;IACF,2EAA2E;IAC3E,qEAAqE;IACrE,yEAAyE;IACzE,eAAe;IACf,MAAM,IAAI,GAAmB;QAC3B,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;QAC1C,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;KAC5C,CAAA;IAED,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK;QAAE,OAAO,EAAE,IAAI,EAAE,CAAA;IAExC,0EAA0E;IAC1E,kDAAkD;IAClD,MAAM,MAAM,GAAmB,EAAE,CAAA;IACjC,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IACzF,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC,CAAA;IAE7F,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,wEAAwE;QACxE,uEAAuE;QACvE,yEAAyE;QACzE,2EAA2E;QAC3E,sEAAsE;QACtE,MAAM,IAAI,KAAK,CACb,0EAA0E;YACxE,qDAAqD,CACxD,CAAA;IACH,CAAC;IAED,OAAO;QACL,IAAI;QACJ,MAAM;QACN,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC;YAC5B,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,UAAU,EAAE,EAAE,CAAC,EAAE;YACpE,CAAC,CAAC,EAAE,CAAC;KACR,CAAA;AACH,CAAC"}
@@ -0,0 +1,15 @@
1
+ export { implied, satisfies } from './permissions.js';
2
+ export { effectivePrincipals, MemoryGroupGraph } from './principals.js';
3
+ export type { GroupGraph, GroupGraphData } from './principals.js';
4
+ export { resolve } from './resolve.js';
5
+ export type { AccessPlan, ResolveInput } from './resolve.js';
6
+ export { referenceAllows } from './reference.js';
7
+ export type { ReferenceInput } from './reference.js';
8
+ export { buildFilter } from './filter.js';
9
+ export type { FilterClause, QueryablePlan, VectorFilter } from './filter.js';
10
+ export { ancestry, MemoryScopeTree } from './scope-tree.js';
11
+ export type { ScopeTree, ScopeTreeData } from './scope-tree.js';
12
+ export { aclTags, principalTag, DEFAULT_TAG_BYTES } from './tags.js';
13
+ export { cachedEffectivePrincipals, MemoryCache, principalsCacheKey } from './cache.js';
14
+ export type { CacheStore, CachedPrincipalsOptions } from './cache.js';
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../authz/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AACvE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACjE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC5E,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAC3D,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/D,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AACpE,OAAO,EAAE,yBAAyB,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AACvF,YAAY,EAAE,UAAU,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAA"}
@@ -0,0 +1,9 @@
1
+ export { implied, satisfies } from './permissions.js';
2
+ export { effectivePrincipals, MemoryGroupGraph } from './principals.js';
3
+ export { resolve } from './resolve.js';
4
+ export { referenceAllows } from './reference.js';
5
+ export { buildFilter } from './filter.js';
6
+ export { ancestry, MemoryScopeTree } from './scope-tree.js';
7
+ export { aclTags, principalTag, DEFAULT_TAG_BYTES } from './tags.js';
8
+ export { cachedEffectivePrincipals, MemoryCache, principalsCacheKey } from './cache.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../authz/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAEvE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAEtC,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAEhD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAEzC,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAE3D,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AACpE,OAAO,EAAE,yBAAyB,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA"}
@@ -0,0 +1,20 @@
1
+ import type { Permission } from '../types.js';
2
+ /**
3
+ * The set of permissions `granted` satisfies.
4
+ *
5
+ * Returns a copy. Handing out the module's own set would let any caller widen
6
+ * everyone's permissions with one `add()` — a process-wide privilege
7
+ * escalation reachable from a typo. The sets hold at most three elements and
8
+ * this is not the hot path; `satisfies` is, and it never allocates.
9
+ */
10
+ export declare function implied(granted: Permission): ReadonlySet<Permission>;
11
+ /**
12
+ * Does holding `granted` satisfy a request for `requested`?
13
+ *
14
+ * This answers one grant against one request and nothing else. It knows
15
+ * nothing about scopes, inheritance, deny, or tenancy — resolve.ts composes
16
+ * those on top. Keeping it that narrow is what makes it cheap to state as a
17
+ * table and cheap to test exhaustively.
18
+ */
19
+ export declare function satisfies(granted: Permission, requested: Permission): boolean;
20
+ //# sourceMappingURL=permissions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../../authz/permissions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAuB7C;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,UAAU,GAAG,WAAW,CAAC,UAAU,CAAC,CAEpE;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,GAAG,OAAO,CAE7E"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Which permissions a single granted permission satisfies.
3
+ *
4
+ * Two rules from docs/authz.md section 3.2 meet here, and they pull in
5
+ * opposite directions:
6
+ *
7
+ * rule 6 — `write` does not imply `read`. A service account that only
8
+ * uploads documents must not be able to search them.
9
+ * rule 7 — `admin` implies `read` and `write` within its scope.
10
+ *
11
+ * The asymmetry is deliberate and is the single most common thing to get
12
+ * wrong here, because every other permission system in common use treats
13
+ * write as a superset of read. Invariant 6 exists precisely because this one
14
+ * does not.
15
+ */
16
+ const IMPLIES = {
17
+ read: new Set(['read']),
18
+ write: new Set(['write']),
19
+ admin: new Set(['read', 'write', 'admin']),
20
+ };
21
+ /**
22
+ * The set of permissions `granted` satisfies.
23
+ *
24
+ * Returns a copy. Handing out the module's own set would let any caller widen
25
+ * everyone's permissions with one `add()` — a process-wide privilege
26
+ * escalation reachable from a typo. The sets hold at most three elements and
27
+ * this is not the hot path; `satisfies` is, and it never allocates.
28
+ */
29
+ export function implied(granted) {
30
+ return new Set(IMPLIES[granted]);
31
+ }
32
+ /**
33
+ * Does holding `granted` satisfy a request for `requested`?
34
+ *
35
+ * This answers one grant against one request and nothing else. It knows
36
+ * nothing about scopes, inheritance, deny, or tenancy — resolve.ts composes
37
+ * those on top. Keeping it that narrow is what makes it cheap to state as a
38
+ * table and cheap to test exhaustively.
39
+ */
40
+ export function satisfies(granted, requested) {
41
+ return IMPLIES[granted].has(requested);
42
+ }
43
+ //# sourceMappingURL=permissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.js","sourceRoot":"","sources":["../../authz/permissions.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,GAA0D;IACrE,IAAI,EAAE,IAAI,GAAG,CAAa,CAAC,MAAM,CAAC,CAAC;IACnC,KAAK,EAAE,IAAI,GAAG,CAAa,CAAC,OAAO,CAAC,CAAC;IACrC,KAAK,EAAE,IAAI,GAAG,CAAa,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;CACvD,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO,CAAC,OAAmB;IACzC,OAAO,IAAI,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAA;AAClC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,SAAS,CAAC,OAAmB,EAAE,SAAqB;IAClE,OAAO,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;AACxC,CAAC"}
@@ -0,0 +1,34 @@
1
+ import type { Principal, PrincipalRef } from '../types.js';
2
+ /**
3
+ * Group membership for one organization. Groups nest, and the nesting is
4
+ * supplied by an external SCIM sync, which means it is not trusted to be
5
+ * acyclic — see T14.
6
+ */
7
+ export interface GroupGraph {
8
+ /** Groups the principal is a direct member of. */
9
+ directGroups(principal: PrincipalRef): readonly string[];
10
+ }
11
+ export interface GroupGraphData {
12
+ /** principal ref (`user:…` or `group:…`) → group ids it belongs to */
13
+ readonly membership: Readonly<Record<string, readonly string[]>>;
14
+ }
15
+ export declare class MemoryGroupGraph implements GroupGraph {
16
+ #private;
17
+ constructor(data: GroupGraphData);
18
+ directGroups(principal: PrincipalRef): readonly string[];
19
+ }
20
+ /**
21
+ * The principal plus every group it belongs to, transitively.
22
+ *
23
+ * Cycles terminate. A membership graph arriving from someone else's IdP over
24
+ * SCIM can contain A ⊂ B ⊂ A, and the correct response is to resolve the
25
+ * reachable set and move on — not to throw, and certainly not to recurse until
26
+ * the stack gives out. An exception here would surface as a permission
27
+ * evaluation failure, which denies access (rule I3), so an unrelated
28
+ * directory-hygiene problem in a customer's IdP would read as an outage.
29
+ *
30
+ * Iterative rather than recursive for the same reason: 10 000 principals is a
31
+ * documented case (T15), and that is well past a comfortable stack depth.
32
+ */
33
+ export declare function effectivePrincipals(principal: Principal, graph: GroupGraph): ReadonlySet<PrincipalRef>;
34
+ //# sourceMappingURL=principals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"principals.d.ts","sourceRoot":"","sources":["../../authz/principals.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAG1D;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,kDAAkD;IAClD,YAAY,CAAC,SAAS,EAAE,YAAY,GAAG,SAAS,MAAM,EAAE,CAAA;CACzD;AAED,MAAM,WAAW,cAAc;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAAA;CACjE;AAED,qBAAa,gBAAiB,YAAW,UAAU;;gBAGrC,IAAI,EAAE,cAAc;IAIhC,YAAY,CAAC,SAAS,EAAE,YAAY,GAAG,SAAS,MAAM,EAAE;CAGzD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,UAAU,GAChB,WAAW,CAAC,YAAY,CAAC,CAkB3B"}
@@ -0,0 +1,42 @@
1
+ import { principalRef } from '../types.js';
2
+ export class MemoryGroupGraph {
3
+ #membership;
4
+ constructor(data) {
5
+ this.#membership = new Map(Object.entries(data.membership));
6
+ }
7
+ directGroups(principal) {
8
+ return this.#membership.get(principal) ?? [];
9
+ }
10
+ }
11
+ /**
12
+ * The principal plus every group it belongs to, transitively.
13
+ *
14
+ * Cycles terminate. A membership graph arriving from someone else's IdP over
15
+ * SCIM can contain A ⊂ B ⊂ A, and the correct response is to resolve the
16
+ * reachable set and move on — not to throw, and certainly not to recurse until
17
+ * the stack gives out. An exception here would surface as a permission
18
+ * evaluation failure, which denies access (rule I3), so an unrelated
19
+ * directory-hygiene problem in a customer's IdP would read as an outage.
20
+ *
21
+ * Iterative rather than recursive for the same reason: 10 000 principals is a
22
+ * documented case (T15), and that is well past a comfortable stack depth.
23
+ */
24
+ export function effectivePrincipals(principal, graph) {
25
+ const self = principalRef(principal);
26
+ const seen = new Set([self]);
27
+ const queue = [self];
28
+ while (queue.length > 0) {
29
+ // Safe: the loop condition guarantees a value, but noUncheckedIndexedAccess
30
+ // does not know that.
31
+ const current = queue.pop();
32
+ for (const group of graph.directGroups(current)) {
33
+ const ref = `group:${group}`;
34
+ if (seen.has(ref))
35
+ continue;
36
+ seen.add(ref);
37
+ queue.push(ref);
38
+ }
39
+ }
40
+ return seen;
41
+ }
42
+ //# sourceMappingURL=principals.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"principals.js","sourceRoot":"","sources":["../../authz/principals.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAiB1C,MAAM,OAAO,gBAAgB;IAClB,WAAW,CAAgC;IAEpD,YAAY,IAAoB;QAC9B,IAAI,CAAC,WAAW,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAA;IAC7D,CAAC;IAED,YAAY,CAAC,SAAuB;QAClC,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,CAAA;IAC9C,CAAC;CACF;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,mBAAmB,CACjC,SAAoB,EACpB,KAAiB;IAEjB,MAAM,IAAI,GAAG,YAAY,CAAC,SAAS,CAAC,CAAA;IACpC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAe,CAAC,IAAI,CAAC,CAAC,CAAA;IAC1C,MAAM,KAAK,GAAmB,CAAC,IAAI,CAAC,CAAA;IAEpC,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,4EAA4E;QAC5E,sBAAsB;QACtB,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,EAAkB,CAAA;QAC3C,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,CAAC;YAChD,MAAM,GAAG,GAAiB,SAAS,KAAK,EAAE,CAAA;YAC1C,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,SAAQ;YAC3B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;YACb,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACjB,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC"}
@@ -0,0 +1,25 @@
1
+ import type { Grant, OrgRole, Permission, PrincipalRef, Scope } from '../types.js';
2
+ import { type ScopeTree } from './scope-tree.js';
3
+ /**
4
+ * The reference implementation: the rules of docs/authz.md 3.2, written out
5
+ * one at a time, for one scope at a time.
6
+ *
7
+ * **Do not optimize this file.** Its only value is being obviously correct by
8
+ * inspection, so that the property-based test can catch `resolve.ts` drifting
9
+ * away from it. An optimized reference agrees with the bug it was supposed to
10
+ * find, and the test goes quiet exactly when it matters.
11
+ *
12
+ * It answers one question — "may these principals do X to this one scope?" —
13
+ * and answers it in time proportional to the number of grants. `resolve.ts`
14
+ * answers a different question, "what is the whole permitted set?", which is
15
+ * what a pre-filter needs and what cannot be built by asking this per document.
16
+ */
17
+ export interface ReferenceInput {
18
+ readonly orgId: string;
19
+ readonly role: OrgRole;
20
+ readonly principals: ReadonlySet<PrincipalRef>;
21
+ readonly grants: readonly Grant[];
22
+ readonly tree: ScopeTree;
23
+ }
24
+ export declare function referenceAllows(input: ReferenceInput, scope: Scope, permission: Permission): boolean;
25
+ //# sourceMappingURL=reference.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reference.d.ts","sourceRoot":"","sources":["../../authz/reference.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAGlF,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAE1D;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAA;IAC9C,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAA;IACjC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CACzB;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE,cAAc,EACrB,KAAK,EAAE,KAAK,EACZ,UAAU,EAAE,UAAU,GACrB,OAAO,CAoCT"}
@@ -0,0 +1,34 @@
1
+ import { principalRef } from '../types.js';
2
+ import { satisfies } from './permissions.js';
3
+ import { ancestry } from './scope-tree.js';
4
+ export function referenceAllows(input, scope, permission) {
5
+ const { orgId, role, principals, grants, tree } = input;
6
+ // Rule 2. platform_admin administers organizations and reads no documents.
7
+ // Checked before anything else so no later rule can grant it access.
8
+ if (role === 'platform_admin')
9
+ return false;
10
+ // Rule 3. org_admin holds admin on every scope in its organization,
11
+ // implicitly. Rule 7 then makes that cover read and write.
12
+ if (role === 'org_admin')
13
+ return true;
14
+ // Rule 1 is a precondition, not part of the ACL: a grant from another tenant
15
+ // is not weighed against anything, it is discarded.
16
+ const mine = grants.filter((g) => g.orgId === orgId && principals.has(principalRef(g.principal)));
17
+ const chain = ancestry(scope, tree);
18
+ const covers = (granted) =>
19
+ // Rule 4. A grant reaches everything nested under it, so it applies when
20
+ // its scope is the target or any of the target's ancestors.
21
+ chain.some((s) => s.type === granted.type && s.id === granted.id);
22
+ const applicable = mine.filter(
23
+ // Rules 6 and 7 live entirely in `satisfies`: write does not imply read,
24
+ // admin implies both.
25
+ (g) => covers(g.scope) && satisfies(g.permission, permission));
26
+ // Rule 5. Any applicable deny beats any applicable allow, at any depth. Note
27
+ // what this is not: it is not "the most specific scope wins". Depth is never
28
+ // compared, which is why the check is a plain `some` over the whole set.
29
+ if (applicable.some((g) => g.effect === 'deny'))
30
+ return false;
31
+ // Rule 8. Default deny — an empty set of allows is no access.
32
+ return applicable.some((g) => g.effect === 'allow');
33
+ }
34
+ //# sourceMappingURL=reference.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reference.js","sourceRoot":"","sources":["../../authz/reference.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC5C,OAAO,EAAE,QAAQ,EAAkB,MAAM,iBAAiB,CAAA;AAwB1D,MAAM,UAAU,eAAe,CAC7B,KAAqB,EACrB,KAAY,EACZ,UAAsB;IAEtB,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAA;IAEvD,2EAA2E;IAC3E,qEAAqE;IACrE,IAAI,IAAI,KAAK,gBAAgB;QAAE,OAAO,KAAK,CAAA;IAE3C,oEAAoE;IACpE,2DAA2D;IAC3D,IAAI,IAAI,KAAK,WAAW;QAAE,OAAO,IAAI,CAAA;IAErC,6EAA6E;IAC7E,oDAAoD;IACpD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CACxB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,IAAI,UAAU,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CACtE,CAAA;IAED,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IACnC,MAAM,MAAM,GAAG,CAAC,OAAc,EAAW,EAAE;IACzC,yEAAyE;IACzE,4DAA4D;IAC5D,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,KAAK,OAAO,CAAC,EAAE,CAAC,CAAA;IAEnE,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM;IAC5B,yEAAyE;IACzE,sBAAsB;IACtB,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,UAAU,EAAE,UAAU,CAAC,CAC9D,CAAA;IAED,6EAA6E;IAC7E,6EAA6E;IAC7E,yEAAyE;IACzE,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC;QAAE,OAAO,KAAK,CAAA;IAE7D,8DAA8D;IAC9D,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,OAAO,CAAC,CAAA;AACrD,CAAC"}
@@ -0,0 +1,45 @@
1
+ import type { Grant, OrgRole, Permission, PrincipalRef } from '../types.js';
2
+ import type { ScopeTree } from './scope-tree.js';
3
+ /**
4
+ * What the caller may reach, in the shape a pre-filter needs.
5
+ *
6
+ * A discriminated union rather than a struct with an `all` flag, so that the
7
+ * "no access at all" case cannot be handed to the filter builder by accident.
8
+ * `buildFilter` does not accept `none`; forgetting to check is a type error
9
+ * rather than a query that quietly returns the whole index.
10
+ */
11
+ export type AccessPlan = {
12
+ readonly kind: 'none';
13
+ } | {
14
+ readonly kind: 'all';
15
+ } | {
16
+ readonly kind: 'scoped';
17
+ /** Layers the caller may reach in full. */
18
+ readonly layers: readonly string[];
19
+ /** Documents reachable outside those layers. */
20
+ readonly extraDocs: readonly string[];
21
+ /** Documents excluded even inside an allowed layer. */
22
+ readonly deniedDocs: readonly string[];
23
+ };
24
+ export interface ResolveInput {
25
+ readonly orgId: string;
26
+ readonly role: OrgRole;
27
+ readonly principals: ReadonlySet<PrincipalRef>;
28
+ /** Grants for these principals. Extra rows are tolerated and filtered here. */
29
+ readonly grants: readonly Grant[];
30
+ readonly tree: ScopeTree;
31
+ }
32
+ /**
33
+ * Turn grants into the set of things the caller may reach.
34
+ *
35
+ * Where this differs from `reference.ts`: the reference answers "may they touch
36
+ * this one scope?", which is the question the rules are written in. A search
37
+ * needs the inverse — the whole permitted set, up front, so it can go into the
38
+ * query as a pre-filter. Computing it by asking the reference per document
39
+ * would be a post-filter with extra steps.
40
+ *
41
+ * The property-based test exists because these two are different algorithms
42
+ * answering the same question, and only one of them is obviously correct.
43
+ */
44
+ export declare function resolve(input: ResolveInput, permission: Permission): AccessPlan;
45
+ //# sourceMappingURL=resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../authz/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAG3E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAEhD;;;;;;;GAOG;AACH,MAAM,MAAM,UAAU,GAClB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;CAAE,GACxB;IACE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,2CAA2C;IAC3C,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;IAClC,gDAAgD;IAChD,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;IACrC,uDAAuD;IACvD,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;CACvC,CAAA;AAEL,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAA;IAC9C,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAA;IACjC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,GAAG,UAAU,CA6E/E"}