@astram/identity 0.2.1 → 0.3.1

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 (57) hide show
  1. package/dist/company-membership.object.d.ts.map +1 -1
  2. package/dist/company-membership.object.js +7 -0
  3. package/dist/company.object.d.ts.map +1 -1
  4. package/dist/company.object.js +5 -0
  5. package/dist/features/authz/catalog-bootstrap.provider.d.ts +6 -1
  6. package/dist/features/authz/catalog-bootstrap.provider.d.ts.map +1 -1
  7. package/dist/features/authz/catalog-bootstrap.provider.js +28 -4
  8. package/dist/features/authz/grant-admin.controller.d.ts +4 -3
  9. package/dist/features/authz/grant-admin.controller.d.ts.map +1 -1
  10. package/dist/features/authz/grant-admin.controller.js +22 -9
  11. package/dist/features/authz/grant-admin.service.d.ts +19 -3
  12. package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
  13. package/dist/features/authz/grant-admin.service.js +43 -5
  14. package/dist/features/authz/record-authorization.d.ts +11 -2
  15. package/dist/features/authz/record-authorization.d.ts.map +1 -1
  16. package/dist/features/authz/record-authorization.js +11 -3
  17. package/dist/features/cross-tenant/grant-authorize.d.ts.map +1 -1
  18. package/dist/features/cross-tenant/grant-authorize.js +3 -0
  19. package/dist/features/identity-feature.module.d.ts +9 -1
  20. package/dist/features/identity-feature.module.d.ts.map +1 -1
  21. package/dist/features/identity-feature.module.js +147 -5
  22. package/dist/features/membership/membership-checker.d.ts +43 -0
  23. package/dist/features/membership/membership-checker.d.ts.map +1 -0
  24. package/dist/features/membership/membership-checker.js +25 -0
  25. package/dist/features/policy/policy.controller.d.ts +46 -0
  26. package/dist/features/policy/policy.controller.d.ts.map +1 -0
  27. package/dist/features/policy/policy.controller.js +150 -0
  28. package/dist/features/policy/policy.service.d.ts +129 -0
  29. package/dist/features/policy/policy.service.d.ts.map +1 -0
  30. package/dist/features/policy/policy.service.js +142 -0
  31. package/dist/features/scope/scope-closure.service.d.ts +82 -0
  32. package/dist/features/scope/scope-closure.service.d.ts.map +1 -0
  33. package/dist/features/scope/scope-closure.service.js +222 -0
  34. package/dist/features/scope/scope-subtree.resolver.d.ts +47 -0
  35. package/dist/features/scope/scope-subtree.resolver.d.ts.map +1 -0
  36. package/dist/features/scope/scope-subtree.resolver.js +24 -0
  37. package/dist/features/share/share.controller.d.ts +35 -0
  38. package/dist/features/share/share.controller.d.ts.map +1 -0
  39. package/dist/features/share/share.controller.js +143 -0
  40. package/dist/features/share/share.service.d.ts +90 -0
  41. package/dist/features/share/share.service.d.ts.map +1 -0
  42. package/dist/features/share/share.service.js +58 -0
  43. package/dist/index.d.ts +17 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +20 -1
  46. package/dist/membership-posting.object.d.ts.map +1 -1
  47. package/dist/membership-posting.object.js +10 -0
  48. package/dist/objects.d.ts.map +1 -1
  49. package/dist/objects.js +2 -1
  50. package/dist/record-share.object.d.ts +24 -0
  51. package/dist/record-share.object.d.ts.map +1 -0
  52. package/dist/record-share.object.js +60 -0
  53. package/dist/role.object.d.ts.map +1 -1
  54. package/dist/role.object.js +10 -0
  55. package/dist/user.object.d.ts.map +1 -1
  56. package/dist/user.object.js +10 -0
  57. package/package.json +4 -4
@@ -33,9 +33,14 @@ const db_permission_facts_adapter_js_1 = require("./authz/db-permission-facts.ad
33
33
  const access_bundle_store_js_1 = require("./authz/access-bundle-store.js");
34
34
  const grant_admin_service_js_1 = require("./authz/grant-admin.service.js");
35
35
  const grant_admin_controller_js_1 = require("./authz/grant-admin.controller.js");
36
+ const policy_service_js_1 = require("./policy/policy.service.js");
37
+ const policy_controller_js_1 = require("./policy/policy.controller.js");
38
+ const share_service_js_1 = require("./share/share.service.js");
39
+ const share_controller_js_1 = require("./share/share.controller.js");
36
40
  const catalog_reconcile_job_js_1 = require("./authz/catalog-reconcile.job.js");
37
41
  const access_bundle_reconcile_job_js_1 = require("./authz/access-bundle-reconcile.job.js");
38
42
  const catalog_bootstrap_provider_js_1 = require("./authz/catalog-bootstrap.provider.js");
43
+ const scope_closure_service_js_1 = require("./scope/scope-closure.service.js");
39
44
  const mariadb_audit_reader_js_1 = require("./audit/mariadb-audit-reader.js");
40
45
  const directory_label_resolver_js_1 = require("./audit/directory-label-resolver.js");
41
46
  const audit_read_service_js_1 = require("./audit/audit-read.service.js");
@@ -76,8 +81,14 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
76
81
  * by the writer's own `decide()` (a route-allows/writer-denies split). Omitted ⇒
77
82
  * `isRoot` is always false (a non-root consumer works; only a root offering
78
83
  * cross-tenant grants needs it). See ADR-0026 + the template wiring.
84
+ * @param extraObjects the consumer's OWN registered object types — pass the SAME array
85
+ * given to `buildIdentityRecordAuthorization(pool, extraObjects)`. Without them, the
86
+ * guided rule-builder SCHEMA (Phase 5) and the OWNER-SHARE resolver (Phase 6) only know
87
+ * the framework identity types, so a consumer's own OWNED type (e.g. `note`) is NOT
88
+ * policy-authorable and NOT shareable ("type '…' is not registered — cannot share").
89
+ * Passing them here lets an owner share their own records + author policies over them.
79
90
  */
80
- static forRoot({ rootIds } = {}) {
91
+ static forRoot({ rootIds, extraObjects = [], } = {}) {
81
92
  // ONE shared shield_authz pool for the whole grant-admin stack (ADR-0011): the
82
93
  // permission-version bumper, the AccessBundleStore, the GrantAdminService, and the two
83
94
  // reconcile jobs all run over this single pool (don't open several). Opened once at
@@ -91,6 +102,12 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
91
102
  // postingVersionBump closure pattern above).
92
103
  const accessBundleStore = new access_bundle_store_js_1.AccessBundleStore(authzPool);
93
104
  const grantAdminService = new grant_admin_service_js_1.GrantAdminService(authzPool, bumper, accessBundleStore);
105
+ // Scope closure (Task 1.2 of the scoped-hierarchy program): the reconciler + node
106
+ // move/delete maintenance, over the SAME shared authzPool (it reaches BOTH shield_app —
107
+ // companies — and shield_authz — scope_closure). Reuses the SAME `bumper` instance so a
108
+ // moveNode's session-invalidation joins the move's transaction. reconcileClosure() is
109
+ // kicked post-boot by the CatalogBootstrapProvider (mirrors the catalog reconcile).
110
+ const scopeClosureService = new scope_closure_service_js_1.ScopeClosureService(authzPool, bumper);
94
111
  // The core ModuleRegistry the catalog reconcile job needs, built from the identity
95
112
  // object registrations (guard's bridge reflects decorated classes → registry).
96
113
  const identityRegistry = (0, guard_1.buildIdentityModuleRegistry)([...objects_js_1.identityObjects]);
@@ -113,6 +130,14 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
113
130
  // same record-authorization inputs (matches astram3): the route guard's own decide() is the
114
131
  // first; this one backs the writer's server-side re-verification.
115
132
  const crossTenantAuthority = (0, guard_1.buildRecordAuthorization)((0, record_authorization_js_1.buildIdentityRecordAuthorization)(authzPool));
133
+ // Record-level LIST SCOPE (Task 3.1, Invariant #8): the engine-facing fn the engine's
134
+ // `list()` ANDs into BOTH the data SELECT and the COUNT — the in-query visible-set filter.
135
+ // Built HERE, over the SAME shared authzPool (guard constructs the un-overridable scope
136
+ // adapter inside `buildListScopeFn`, mirroring the four record-tier read adapters). Wired
137
+ // out-of-the-box into `EngineModule.forRoot({ listScope })` below (ADR-0003 injection seam —
138
+ // the engine never imports guard). Fail-closed: a store outage propagates and `list()` fails
139
+ // rather than returning unscoped rows.
140
+ const listScope = (0, guard_1.buildListScopeFn)((0, record_authorization_js_1.buildIdentityRecordAuthorization)(authzPool));
116
141
  // The writer's `GrantAuthorizeFn`: decide()-backed, fail-closed (no try/catch — a throw
117
142
  // propagates and the writer maps it to a retryable 503). isRoot ← the SAME rootIds the
118
143
  // consumer passes to ShieldModule.forRoot (root-correctness for control-plane grants).
@@ -168,6 +193,105 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
168
193
  // The read side (inbox/mine/incoming/offerable-types) over the same pool + the merged object
169
194
  // set (offerable = tenantScoped types only) + the per-type action vocabulary.
170
195
  const crossTenantGrantReadService = new cross_tenant_grant_read_service_js_1.CrossTenantGrantReadService(authzPool, [...objects_js_1.identityObjects], guard_1.actionsFor);
196
+ // ── GUIDED RULE-BUILDER (list-scoping engine Phase 5, §9.1) ──────────────────────────────
197
+ // The policy-authoring surface over the guard `MariaDbPolicyStore` + core `makeAuthorPolicy`.
198
+ // Reuses the SAME `identityRegistry` + `crossTenantAuthority` (a DecisionAuthority over the
199
+ // shared authzPool — the delegated-admin ceiling's decide()) as the cross-tenant writer. The
200
+ // core AuditSink is one-arg (`record(event)`); the engine `MariaDbAuditSink` is two-arg
201
+ // (`record(event, query)`) — so bind the query to the shared authzPool (mirrors the
202
+ // `grantAudit` bridge above). Best-effort audit (a sink throw must not fail the author).
203
+ const policyAuditSink = new engine_1.MariaDbAuditSink({}); // shield_audit.audit_log defaults
204
+ const policyAudit = {
205
+ record: async (event) => {
206
+ try {
207
+ await policyAuditSink.record(event, (sql, params) => authzPool.query(sql, params));
208
+ }
209
+ catch {
210
+ // Best-effort: an audit failure must not fail a legitimate policy author.
211
+ }
212
+ },
213
+ };
214
+ const policyStore = new guard_1.MariaDbPolicyStore(authzPool);
215
+ // The guided-builder SCHEMA source — the authorable field/action metadata, reflected from the
216
+ // registered identity object types (engine `reflectObjectType` yields fields WITH their
217
+ // `type`, which the core registry descriptor drops). Only tenantScoped, owned DATA-plane types
218
+ // are meaningfully policy-authorable in v1; but we expose every registered type's fields and
219
+ // let the delegated-admin ceiling + validateGuidedPolicy gate what a caller may actually
220
+ // author. A consumer's own types (extraObjects) would extend this the same way (future).
221
+ // Include the consumer's `extraObjects` so their OWNED types are policy-authorable too.
222
+ const schemaSource = [...objects_js_1.identityObjects, ...extraObjects].map((input) => {
223
+ const reg = typeof input === 'function' ? (0, engine_1.reflectObjectType)(input) : input;
224
+ return {
225
+ typeKey: reg.typeKey,
226
+ label: reg.typeKey,
227
+ actions: (0, guard_1.actionsFor)(reg.typeKey),
228
+ fields: reg.fields.map((f) => ({ key: f.key, type: f.type })),
229
+ };
230
+ });
231
+ const policyService = new policy_service_js_1.PolicyService({
232
+ registry: identityRegistry,
233
+ policyStore,
234
+ decide: crossTenantAuthority,
235
+ audit: policyAudit,
236
+ schemaSource,
237
+ });
238
+ // ── OWNER-SHARE surface (list-scoping engine Phase 6, §9.2) ──────────────────────────────
239
+ // The framework-owned RecordShareWriter (the who-may-share bound) + a ShareTypeResolver over
240
+ // the reflected identity object types (so guard never imports @astram/engine — the consumer
241
+ // resolves the physical share info here). Only `owned` types are shareable; the resolver
242
+ // surfaces `owned`/table/schema/pk/owner/tenant columns via the engine helpers. The writer's
243
+ // audit reuses the SAME shield_audit sink pattern (best-effort). The ShareService adds
244
+ // name-resolution (no raw uuid) via the existing DirectoryLabelResolver (`auditLabels`).
245
+ // Include the consumer's `extraObjects` so their OWNED types are shareable (a `note` a user
246
+ // owns can be shared) — without them the resolver only knows the framework identity types
247
+ // and the writer rejects "type 'note' is not registered — cannot share." (Live-acceptance fix.)
248
+ const shareTypeInfoByKey = new Map();
249
+ for (const input of [...objects_js_1.identityObjects, ...extraObjects]) {
250
+ const reg = typeof input === 'function' ? (0, engine_1.reflectObjectType)(input) : input;
251
+ const owner = (0, engine_1.ownerColumn)(reg);
252
+ const pk = reg.primaryKey.kind === 'uuid' ? (0, engine_1.uuidPkColumn)(reg.primaryKey) : null;
253
+ // Only OWNED types with a uuid PK + an owner column are shareable; others are omitted (the
254
+ // writer rejects an unknown/non-owned type anyway — this map only lists real candidates).
255
+ if (!(0, engine_1.isOwned)(reg) || owner === null || pk === null)
256
+ continue;
257
+ const tenantCol = (0, engine_1.tenantColumnOf)(reg);
258
+ shareTypeInfoByKey.set(reg.typeKey, {
259
+ table: reg.table,
260
+ ...(reg.schema ? { schema: reg.schema } : {}),
261
+ pkColumn: pk,
262
+ ownerColumn: owner,
263
+ ...(tenantCol ? { tenantColumn: tenantCol } : {}),
264
+ owned: true,
265
+ });
266
+ }
267
+ const shareTypeResolver = {
268
+ resolve: (typeKey) => shareTypeInfoByKey.get(typeKey),
269
+ };
270
+ const shareAuditSink = new engine_1.MariaDbAuditSink({}); // shield_audit.audit_log
271
+ const recordShareWriter = new guard_1.RecordShareWriter({
272
+ db: authzPool,
273
+ types: shareTypeResolver,
274
+ audit: {
275
+ record: async (event) => {
276
+ try {
277
+ await shareAuditSink.record({
278
+ // A share/unshare is access administration → 'admin_action' (a valid AuditEventType;
279
+ // 'record_share' is the writer's own event tag, carried in `action`/attributes).
280
+ type: 'admin_action',
281
+ occurredAtMs: event.occurredAtMs,
282
+ actorId: event.actorId,
283
+ tenantId: event.tenantId,
284
+ action: { key: event.action }, // 'share.created' | 'share.removed'
285
+ attributes: { ...event.attributes, eventType: 'record_share' },
286
+ }, (sql, params) => authzPool.query(sql, params));
287
+ }
288
+ catch {
289
+ // Best-effort — an audit failure must never fail a legitimate share/unshare.
290
+ }
291
+ },
292
+ },
293
+ });
294
+ const shareService = new share_service_js_1.ShareService(recordShareWriter, auditLabels);
171
295
  return {
172
296
  module: IdentityFeatureModule_1,
173
297
  imports: [
@@ -179,7 +303,11 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
179
303
  // snapshot is empty and no bundles reconcile into shield_authz.access_bundle.
180
304
  engine_1.EngineModule.forFeature([], { accessBundles: index_js_1.identityAccessBundles }),
181
305
  // The engine's runtime CRUD pipe + the identity object registrations.
182
- engine_1.EngineModule.forRoot({ db: engineDbFromEnv(), registrations: [...objects_js_1.identityObjects] }),
306
+ engine_1.EngineModule.forRoot({
307
+ db: engineDbFromEnv(),
308
+ registrations: [...objects_js_1.identityObjects],
309
+ listScope,
310
+ }),
183
311
  // NestJS DiscoveryService/MetadataScanner — the boot bootstrap provider reflects the
184
312
  // live controllers through this to build the OperationInventory.
185
313
  core_1.DiscoveryModule,
@@ -200,6 +328,10 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
200
328
  // controller's own method order keeps the static sub-paths (/inbox, /offerable-types)
201
329
  // ahead of `/:uuid/*`. Appended after the existing controllers.
202
330
  cross_tenant_grant_controller_js_1.CrossTenantGrantController,
331
+ // Guided rule-builder (Phase 5) — `record-policies/*` collides with nothing. Appended last.
332
+ policy_controller_js_1.PolicyController,
333
+ // Owner shares (Phase 6) — `record-shares` collides with nothing. Appended last.
334
+ share_controller_js_1.ShareController,
203
335
  ],
204
336
  providers: [
205
337
  company_service_js_1.CompanyService,
@@ -221,6 +353,12 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
221
353
  // wiring (the out-of-the-box acceptance bar).
222
354
  { provide: guard_1.SHIELD_CROSS_TENANT_GRANT_WRITER, useValue: crossTenantGrantWriter },
223
355
  { provide: cross_tenant_grant_read_service_js_1.CrossTenantGrantReadService, useValue: crossTenantGrantReadService },
356
+ // Guided rule-builder (Phase 5) — the PolicyService over the shared authzPool + registry
357
+ // + the SAME DecisionAuthority (delegated-admin ceiling). useValue like GrantAdminService.
358
+ { provide: policy_service_js_1.PolicyService, useValue: policyService },
359
+ // Owner shares (Phase 6) — the ShareService over the framework RecordShareWriter + the
360
+ // DirectoryLabelResolver (name resolution). useValue, all over the shared authzPool.
361
+ { provide: share_service_js_1.ShareService, useValue: shareService },
224
362
  // The mutating half of auto-catalog: a single CatalogReconcileJob (post-boot pass +
225
363
  // cron), over the shared pool + the identity registry.
226
364
  { provide: catalog_reconcile_job_js_1.CatalogReconcileJob, useValue: new catalog_reconcile_job_js_1.CatalogReconcileJob(authzPool, identityRegistry) },
@@ -231,10 +369,14 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
231
369
  useFactory: (bundles) => new access_bundle_reconcile_job_js_1.AccessBundleReconcileJob(authzPool, bundles),
232
370
  inject: [engine_1.ENGINE_ALL_ACCESS_BUNDLES],
233
371
  },
372
+ // Scope closure reconciler + node move/delete maintenance (Task 1.2), built at forRoot()
373
+ // over the shared authzPool + the shared bumper, provided useValue so the bootstrap
374
+ // provider (and any future controller) injects the SAME instance.
375
+ { provide: scope_closure_service_js_1.ScopeClosureService, useValue: scopeClosureService },
234
376
  // Boot: read-only verifyGatable (fail-closed). Post-boot: kicks the jobs once (catalog
235
- // THEN bundles) + schedules them (DISABLE_ALL_CRONS-gated). Injects DiscoveryService/
236
- // MetadataScanner/Reflector (from DiscoveryModule above), the two jobs, and
237
- // ENGINE_ALL_REGISTRATIONS (exported by EngineModule).
377
+ // THEN bundles, THEN the scope-closure reconcile) + schedules them (DISABLE_ALL_CRONS-
378
+ // gated). Injects DiscoveryService/MetadataScanner/Reflector (from DiscoveryModule
379
+ // above), the jobs, the ScopeClosureService, and ENGINE_ALL_REGISTRATIONS.
238
380
  catalog_bootstrap_provider_js_1.CatalogBootstrapProvider,
239
381
  ],
240
382
  exports: [company_service_js_1.CompanyService, role_service_js_1.RoleService, user_service_js_1.UserService],
@@ -0,0 +1,43 @@
1
+ /**
2
+ * `DbMembershipChecker` — the identity-side implementation of the guard-defined
3
+ * `MembershipChecker` port (Task 5.2 of the scoped-hierarchy tenancy program,
4
+ * ADR-0027 / ADR-0024). It answers ONE question the scope-switch gate asks: does a
5
+ * principal HOLD an ACTIVE membership at a target node? A `true` is the ONLY thing
6
+ * that lets `POST /auth/scope-switch` re-bind the session to that node; anything else
7
+ * (a `false`, or a thrown store outage the guard swallows) is a fail-closed DENY.
8
+ *
9
+ * The answer is a SINGLE existence read over `shield_app.company_membership`:
10
+ *
11
+ * SELECT 1 FROM shield_app.company_membership
12
+ * WHERE cm_user_uuid = ? AND cm_company_uuid = ?
13
+ * AND cm_is_active = 1 AND cm_status = 'active' LIMIT 1
14
+ *
15
+ * `cm_is_active = 1` is the first-class active-membership flag (a user has at most one
16
+ * active membership per company); `cm_status = 'active'` is the lifecycle status. BOTH
17
+ * are required — a de-activated OR non-active-status membership is NOT a valid switch
18
+ * target (least privilege: the switch confers the node's subtree + authority, so the
19
+ * binding must be genuinely live).
20
+ *
21
+ * ── LAYERING ────────────────────────────────────────────────────────────────────────
22
+ * The PORT (`MembershipChecker`) is defined in `@astram/guard`; this IMPLEMENTATION
23
+ * lives in `@astram/identity` (which owns the app schema + `company_membership`). The
24
+ * consumer injects an instance through `ShieldModule.forRoot({ membershipChecker })` —
25
+ * the same sanctioned direction as `DbScopeSubtreeResolver`. Guard never imports identity.
26
+ *
27
+ * ── FAIL-CLOSED ─────────────────────────────────────────────────────────────────────
28
+ * Returns `true` ONLY when the existence row is present. A store OUTAGE THROWS (the
29
+ * guard's scope-switch handler catches it → DENY): a check failure NEVER silently allows
30
+ * a switch. All SQL values are `?`-bound (never interpolated).
31
+ */
32
+ import type { Pool } from 'mariadb';
33
+ export declare class DbMembershipChecker {
34
+ private readonly pool;
35
+ constructor(pool: Pool);
36
+ /**
37
+ * Does `principalId` hold an ACTIVE membership at `nodeId`? A single existence read —
38
+ * `true` iff a row with `cm_is_active = 1 AND cm_status = 'active'` exists. Throws on a
39
+ * store outage (the guard's scope-switch gate catches it → fail-closed DENY).
40
+ */
41
+ hasActiveMembership(principalId: string, nodeId: string): Promise<boolean>;
42
+ }
43
+ //# sourceMappingURL=membership-checker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"membership-checker.d.ts","sourceRoot":"","sources":["../../../src/features/membership/membership-checker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAKpC,qBAAa,mBAAmB;IAClB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,IAAI;IAEvC;;;;OAIG;IACG,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAWjF"}
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DbMembershipChecker = void 0;
4
+ /** Schema the membership table lives in (the app schema). */
5
+ const APP = 'shield_app';
6
+ class DbMembershipChecker {
7
+ pool;
8
+ constructor(pool) {
9
+ this.pool = pool;
10
+ }
11
+ /**
12
+ * Does `principalId` hold an ACTIVE membership at `nodeId`? A single existence read —
13
+ * `true` iff a row with `cm_is_active = 1 AND cm_status = 'active'` exists. Throws on a
14
+ * store outage (the guard's scope-switch gate catches it → fail-closed DENY).
15
+ */
16
+ async hasActiveMembership(principalId, nodeId) {
17
+ const rows = (await this.pool.query(`SELECT 1 AS ok
18
+ FROM \`${APP}\`.\`company_membership\`
19
+ WHERE cm_user_uuid = ? AND cm_company_uuid = ?
20
+ AND cm_is_active = 1 AND cm_status = 'active'
21
+ LIMIT 1`, [principalId, nodeId]));
22
+ return rows.length > 0;
23
+ }
24
+ }
25
+ exports.DbMembershipChecker = DbMembershipChecker;
@@ -0,0 +1,46 @@
1
+ import type { GuidedPolicy, Principal } from '@astram/core';
2
+ import { PolicyService } from './policy.service.js';
3
+ /** The request shape after the enforcement guard attaches the resolved principal. */
4
+ interface AuthedRequest {
5
+ principal: Principal;
6
+ }
7
+ export declare class PolicyController {
8
+ private readonly policies;
9
+ constructor(policies: PolicyService);
10
+ /**
11
+ * The guided-builder SCHEMA for a type — its authorable fields (each with the operators the
12
+ * builder should offer, split into array/nullary families) + the type's actions.
13
+ * `GET /identity/record-policies/schema?typeKey=<key>`. Gated `role:read`. Server-derived so the
14
+ * builder's dropdowns never drift from enforcement. Unknown/non-authorable type → 404.
15
+ *
16
+ * ⚠️ Registered BEFORE `record-policies` (the `/schema` static sub-path) — Nest matches the
17
+ * first, and `record-policies/schema` is more specific than the bare `record-policies` GET.
18
+ */
19
+ schema(typeKey?: string): Promise<import("./policy.service.js").PolicyTypeSchema>;
20
+ /**
21
+ * List the record-access policies governing a type in the caller's tenant.
22
+ * `GET /identity/record-policies?typeKey=<key>`. Gated `role:read`.
23
+ */
24
+ list(req: AuthedRequest, typeKey?: string): Promise<{
25
+ policies: readonly import("@astram/core").Policy[];
26
+ }>;
27
+ /**
28
+ * Author a guided policy. `POST /identity/record-policies` with a {@link GuidedPolicy} body
29
+ * (typeKey, action, effect, structured conditions — NO raw expressions). Gated `role:update`;
30
+ * the delegated-admin ceiling + structural validation run inside the service. The target tenant
31
+ * is `req.principal.tenantId` — a body tenant is ignored.
32
+ */
33
+ author(req: AuthedRequest, body: GuidedPolicy): Promise<{
34
+ policy: import("@astram/core").Policy;
35
+ }>;
36
+ /**
37
+ * Delete a policy the caller owns. `DELETE /identity/record-policies/:id`. Gated `role:update`.
38
+ * TENANT-SCOPED: a foreign tenant's policy id → NotFound (the service can never delete another
39
+ * tenant's policy).
40
+ */
41
+ delete(req: AuthedRequest, id: string): Promise<{
42
+ success: boolean;
43
+ }>;
44
+ }
45
+ export {};
46
+ //# sourceMappingURL=policy.controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.controller.d.ts","sourceRoot":"","sources":["../../../src/features/policy/policy.controller.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EACL,aAAa,EAId,MAAM,qBAAqB,CAAC;AAE7B,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,qBACa,gBAAgB;IACf,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,aAAa;IAEpD;;;;;;;;OAQG;IAGG,MAAM,CAAmB,OAAO,CAAC,EAAE,MAAM;IAY/C;;;OAGG;IAGG,IAAI,CAAQ,GAAG,EAAE,aAAa,EAAoB,OAAO,CAAC,EAAE,MAAM;;;IAQxE;;;;;OAKG;IAGG,MAAM,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE,YAAY;;;IAWlE;;;;OAIG;IAGG,MAAM,CAAQ,GAAG,EAAE,aAAa,EAAe,EAAE,EAAE,MAAM;;;CAShE"}
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.PolicyController = void 0;
16
+ /**
17
+ * `PolicyController` — the §9.1 GUIDED RULE-BUILDER authoring API (list-scoping engine Phase 5).
18
+ *
19
+ * Three routes over {@link PolicyService}; gated purely by `@Classified` + `decide()` (no
20
+ * handler-level root check). Authoring a record-access policy IS access administration — the same
21
+ * authority as managing grants — so it keys to the `role` object type (chosen 2026-07-23): read
22
+ * routes need `identity:role:read`, mutating routes need `identity:role:update`. `role` is a
23
+ * CONTROL-plane type, so the capped root is allowed without a grant; a non-root admin needs it.
24
+ * On TOP of the route gate, the delegated-admin ceiling (only-author-what-you-can-grant) is
25
+ * enforced INSIDE the service (core `makeAuthorPolicy` → `checkDelegatedAdmin`).
26
+ *
27
+ * ⚠️ Path is `record-policies` (NOT under `roles/...`) so it never collides with
28
+ * IdentityController's `roles/:uuid` or GrantAdminController's `access/*` routes.
29
+ *
30
+ * Authority is ALWAYS `req.principal` (the enforcement guard attached it) — a tenant in the body
31
+ * is IGNORED by the service (the target is `actor.tenantId`), so cross-tenant authoring is
32
+ * impossible by construction.
33
+ */
34
+ const common_1 = require("@nestjs/common");
35
+ const guard_1 = require("@astram/guard");
36
+ const policy_service_js_1 = require("./policy.service.js");
37
+ let PolicyController = class PolicyController {
38
+ policies;
39
+ constructor(policies) {
40
+ this.policies = policies;
41
+ }
42
+ /**
43
+ * The guided-builder SCHEMA for a type — its authorable fields (each with the operators the
44
+ * builder should offer, split into array/nullary families) + the type's actions.
45
+ * `GET /identity/record-policies/schema?typeKey=<key>`. Gated `role:read`. Server-derived so the
46
+ * builder's dropdowns never drift from enforcement. Unknown/non-authorable type → 404.
47
+ *
48
+ * ⚠️ Registered BEFORE `record-policies` (the `/schema` static sub-path) — Nest matches the
49
+ * first, and `record-policies/schema` is more specific than the bare `record-policies` GET.
50
+ */
51
+ async schema(typeKey) {
52
+ if (!typeKey || typeof typeKey !== 'string') {
53
+ throw new common_1.BadRequestException('typeKey required');
54
+ }
55
+ try {
56
+ return this.policies.schema(typeKey);
57
+ }
58
+ catch (e) {
59
+ if (e instanceof policy_service_js_1.PolicyNotFoundError)
60
+ throw new common_1.NotFoundException(e.message);
61
+ throw e;
62
+ }
63
+ }
64
+ /**
65
+ * List the record-access policies governing a type in the caller's tenant.
66
+ * `GET /identity/record-policies?typeKey=<key>`. Gated `role:read`.
67
+ */
68
+ async list(req, typeKey) {
69
+ if (!typeKey || typeof typeKey !== 'string') {
70
+ throw new common_1.BadRequestException('typeKey required');
71
+ }
72
+ const policies = await this.policies.list(req.principal, typeKey);
73
+ return { policies };
74
+ }
75
+ /**
76
+ * Author a guided policy. `POST /identity/record-policies` with a {@link GuidedPolicy} body
77
+ * (typeKey, action, effect, structured conditions — NO raw expressions). Gated `role:update`;
78
+ * the delegated-admin ceiling + structural validation run inside the service. The target tenant
79
+ * is `req.principal.tenantId` — a body tenant is ignored.
80
+ */
81
+ async author(req, body) {
82
+ try {
83
+ const policy = await this.policies.author(req.principal, body);
84
+ return { policy };
85
+ }
86
+ catch (e) {
87
+ if (e instanceof policy_service_js_1.InvalidPolicyError)
88
+ throw new common_1.BadRequestException(e.message);
89
+ if (e instanceof policy_service_js_1.PolicyAuthorDeniedError)
90
+ throw new common_1.ForbiddenException(e.message);
91
+ throw e; // transient (store/authority outage) → propagates → 503
92
+ }
93
+ }
94
+ /**
95
+ * Delete a policy the caller owns. `DELETE /identity/record-policies/:id`. Gated `role:update`.
96
+ * TENANT-SCOPED: a foreign tenant's policy id → NotFound (the service can never delete another
97
+ * tenant's policy).
98
+ */
99
+ async delete(req, id) {
100
+ try {
101
+ await this.policies.delete(req.principal, id);
102
+ return { success: true };
103
+ }
104
+ catch (e) {
105
+ if (e instanceof policy_service_js_1.PolicyNotFoundError)
106
+ throw new common_1.NotFoundException(e.message);
107
+ throw e;
108
+ }
109
+ }
110
+ };
111
+ exports.PolicyController = PolicyController;
112
+ __decorate([
113
+ (0, common_1.Get)('record-policies/schema'),
114
+ (0, guard_1.Classified)({ typeKey: 'role', action: 'read' }),
115
+ __param(0, (0, common_1.Query)('typeKey')),
116
+ __metadata("design:type", Function),
117
+ __metadata("design:paramtypes", [String]),
118
+ __metadata("design:returntype", Promise)
119
+ ], PolicyController.prototype, "schema", null);
120
+ __decorate([
121
+ (0, common_1.Get)('record-policies'),
122
+ (0, guard_1.Classified)({ typeKey: 'role', action: 'read' }),
123
+ __param(0, (0, common_1.Req)()),
124
+ __param(1, (0, common_1.Query)('typeKey')),
125
+ __metadata("design:type", Function),
126
+ __metadata("design:paramtypes", [Object, String]),
127
+ __metadata("design:returntype", Promise)
128
+ ], PolicyController.prototype, "list", null);
129
+ __decorate([
130
+ (0, common_1.Post)('record-policies'),
131
+ (0, guard_1.Classified)({ typeKey: 'role', action: 'update' }),
132
+ __param(0, (0, common_1.Req)()),
133
+ __param(1, (0, common_1.Body)()),
134
+ __metadata("design:type", Function),
135
+ __metadata("design:paramtypes", [Object, Object]),
136
+ __metadata("design:returntype", Promise)
137
+ ], PolicyController.prototype, "author", null);
138
+ __decorate([
139
+ (0, common_1.Delete)('record-policies/:id'),
140
+ (0, guard_1.Classified)({ typeKey: 'role', action: 'update' }),
141
+ __param(0, (0, common_1.Req)()),
142
+ __param(1, (0, common_1.Param)('id')),
143
+ __metadata("design:type", Function),
144
+ __metadata("design:paramtypes", [Object, String]),
145
+ __metadata("design:returntype", Promise)
146
+ ], PolicyController.prototype, "delete", null);
147
+ exports.PolicyController = PolicyController = __decorate([
148
+ (0, common_1.Controller)('identity'),
149
+ __metadata("design:paramtypes", [policy_service_js_1.PolicyService])
150
+ ], PolicyController);
@@ -0,0 +1,129 @@
1
+ /**
2
+ * `PolicyService` — the backend of the §9.1 GUIDED RULE-BUILDER (list-scoping engine Phase 5).
3
+ *
4
+ * A thin authoring surface over the core `makeAuthorPolicy` command + the guard
5
+ * `MariaDbPolicyStore`. The core command already provides — and this service NEVER re-implements
6
+ * — validation (`validateGuidedPolicy`: known field, operator-valid-for-field-type, well-formed
7
+ * value; NO raw expressions), the delegated-admin ceiling (`checkDelegatedAdmin`: a non-root
8
+ * admin may author a policy ONLY over an `(action, typeKey)` they THEMSELVES hold — reusing the
9
+ * ONE decision authority), and the immutable audit record. This service:
10
+ * • builds the `AdminMutation` from the SERVER-resolved principal — the target tenant is
11
+ * ALWAYS `actor.tenantId`, NEVER the request body (a body tenant is ignored → a cross-tenant
12
+ * author is impossible by construction);
13
+ * • maps the core `CommandResult` deny reasons to typed errors the controller renders as
14
+ * 4xx/5xx (invalid → 400, denied/exceeds-authority → 403, outage → 503);
15
+ * • lists policies TENANT-SCOPED (`listPolicies(typeKey, actor.tenantId)`);
16
+ * • deletes policies TENANT-SCOPED (`deletePolicyForTenant` — a foreign tenant's policy id can
17
+ * NEVER be deleted; absent/foreign → NotFound, never a false success).
18
+ *
19
+ * FAIL-CLOSED: a transient store/authority throw PROPAGATES (the controller maps it to 503) —
20
+ * NEVER swallowed into a success. The delegated-admin check and validation run BEFORE any write.
21
+ */
22
+ import type { AuditSink, DecisionAuthority, GuidedPolicy, ModuleRegistry, Policy, Principal } from '@astram/core';
23
+ import type { MariaDbPolicyStore } from '@astram/guard';
24
+ /** A guided policy could not be authored because it is structurally invalid (→ 400). */
25
+ export declare class InvalidPolicyError extends Error {
26
+ constructor(message?: string);
27
+ }
28
+ /** The acting admin exceeds their authority (delegated-admin ceiling) — author denied (→ 403). */
29
+ export declare class PolicyAuthorDeniedError extends Error {
30
+ constructor(message?: string);
31
+ }
32
+ /** A policy id was not found in the acting admin's tenant (absent OR another tenant's) (→ 404). */
33
+ export declare class PolicyNotFoundError extends Error {
34
+ constructor(message?: string);
35
+ }
36
+ /** The minimal store surface the service uses (the concrete guard adapter satisfies it). */
37
+ export interface PolicyStoreLike {
38
+ savePolicy(tenantId: string, policy: GuidedPolicy): Promise<Policy>;
39
+ listPolicies(typeKey: string, tenantId: string): Promise<readonly Policy[]>;
40
+ deletePolicyForTenant(policyId: string, tenantId: string): Promise<number>;
41
+ }
42
+ /** The physical/logical type of a field, as the engine declares it. */
43
+ export type FieldTypeName = 'string' | 'number' | 'boolean' | 'uuid' | 'timestamp' | 'json';
44
+ /** One authorable field of a type, with the operators the guided builder should OFFER. */
45
+ export interface PolicyFieldSchema {
46
+ readonly key: string;
47
+ readonly label: string;
48
+ readonly type: FieldTypeName;
49
+ /** The operators VALID for this field type (the UI's dropdown; server validation is authority). */
50
+ readonly validOperators: readonly PolicyOperatorName[];
51
+ /** Operators requiring an array operand (in/not_in) — the UI renders a multi-value input. */
52
+ readonly arrayOperators: readonly PolicyOperatorName[];
53
+ /** Operators taking NO operand (is_null/is_not_null) — the UI hides the value input. */
54
+ readonly nullaryOperators: readonly PolicyOperatorName[];
55
+ }
56
+ /** The guided-builder schema for a type: its authorable fields + the type's actions. */
57
+ export interface PolicyTypeSchema {
58
+ readonly typeKey: string;
59
+ readonly label: string;
60
+ readonly actions: readonly string[];
61
+ readonly fields: readonly PolicyFieldSchema[];
62
+ }
63
+ /** The closed operator vocabulary — mirrors core `PolicyOperator`. */
64
+ export type PolicyOperatorName = 'eq' | 'ne' | 'in' | 'not_in' | 'gt' | 'lt' | 'gte' | 'lte' | 'contains' | 'not_contains' | 'is_null' | 'is_not_null';
65
+ /** A field descriptor the schema source hands the service (from the reflected object type). */
66
+ export interface FieldDef {
67
+ readonly key: string;
68
+ readonly type: FieldTypeName;
69
+ }
70
+ /** Per-type field + action metadata the service reads to build the guided-builder schema. */
71
+ export interface TypeSchemaSource {
72
+ readonly typeKey: string;
73
+ readonly label: string;
74
+ readonly actions: readonly string[];
75
+ readonly fields: readonly FieldDef[];
76
+ }
77
+ /**
78
+ * The operators the guided builder offers for a FIELD TYPE. Equality + null apply to every type;
79
+ * relational (gt/lt/…) only to numeric + timestamp (the recorded v1 limit — lexical vs numeric
80
+ * ordering on a VARCHAR diverges, so relational is NOT offered on string/uuid); text
81
+ * (contains/…) only to string. boolean/json get equality + null only. This is INTENTIONALLY a
82
+ * subset the builder shows; the server accepts anything `operatorValidForField` allows.
83
+ */
84
+ export declare function validOperatorsForFieldType(type: FieldTypeName): readonly PolicyOperatorName[];
85
+ /** The dependencies the service wires (all constructed in the identity module). */
86
+ export interface PolicyServiceDeps {
87
+ readonly registry: ModuleRegistry;
88
+ readonly policyStore: MariaDbPolicyStore | PolicyStoreLike;
89
+ readonly decide: DecisionAuthority;
90
+ readonly audit: AuditSink;
91
+ /**
92
+ * The authorable-type field/action metadata (built in the module from the reflected object
93
+ * types). Drives `schema()` — the guided builder's field + operator dropdowns. A type absent
94
+ * here is NOT authorable via the builder (schema → NotFound).
95
+ */
96
+ readonly schemaSource?: readonly TypeSchemaSource[];
97
+ readonly now?: () => number;
98
+ }
99
+ export declare class PolicyService {
100
+ private readonly store;
101
+ private readonly authorPolicy;
102
+ private readonly schemaByType;
103
+ constructor(deps: PolicyServiceDeps);
104
+ /**
105
+ * The guided-builder SCHEMA for a type: its authorable fields (each with its type + the
106
+ * operators the builder offers, split into array/nullary families the UI renders differently)
107
+ * + the type's actions. Server-derived from the registered object type so the builder's
108
+ * dropdowns are ONE source of truth with enforcement (no client-side operator table to drift).
109
+ * A type not in the schema source → PolicyNotFoundError (not authorable via the builder).
110
+ */
111
+ schema(typeKey: string): PolicyTypeSchema;
112
+ /**
113
+ * Author a guided policy in the acting admin's OWN tenant. The target tenant is
114
+ * `actor.tenantId` — a `tenantId` in the body is IGNORED (cross-tenant author impossible).
115
+ * Runs the core pipeline (delegated-admin ceiling → validate → write → audit). Maps deny
116
+ * reasons to typed errors; a transient throw PROPAGATES (→ 503).
117
+ */
118
+ author(actor: Principal, policy: GuidedPolicy): Promise<Policy>;
119
+ /** List the policies governing `typeKey` in the acting admin's tenant (tenant-scoped read). */
120
+ list(actor: Principal, typeKey: string): Promise<readonly Policy[]>;
121
+ /**
122
+ * Delete a policy the acting admin owns. TENANT-SCOPED: `deletePolicyForTenant` removes rows
123
+ * only when they belong to `actor.tenantId`, so a foreign tenant's policy id can never be
124
+ * deleted. Zero rows removed ⇒ the policy is absent OR another tenant's ⇒ NotFound (never a
125
+ * false "deleted someone else's").
126
+ */
127
+ delete(actor: Principal, policyId: string): Promise<void>;
128
+ }
129
+ //# sourceMappingURL=policy.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.service.d.ts","sourceRoot":"","sources":["../../../src/features/policy/policy.service.ts"],"names":[],"mappings":"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"}