@astram/identity 0.2.1 → 0.3.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 (56) 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.map +1 -1
  20. package/dist/features/identity-feature.module.js +136 -4
  21. package/dist/features/membership/membership-checker.d.ts +43 -0
  22. package/dist/features/membership/membership-checker.d.ts.map +1 -0
  23. package/dist/features/membership/membership-checker.js +25 -0
  24. package/dist/features/policy/policy.controller.d.ts +46 -0
  25. package/dist/features/policy/policy.controller.d.ts.map +1 -0
  26. package/dist/features/policy/policy.controller.js +150 -0
  27. package/dist/features/policy/policy.service.d.ts +129 -0
  28. package/dist/features/policy/policy.service.d.ts.map +1 -0
  29. package/dist/features/policy/policy.service.js +142 -0
  30. package/dist/features/scope/scope-closure.service.d.ts +82 -0
  31. package/dist/features/scope/scope-closure.service.d.ts.map +1 -0
  32. package/dist/features/scope/scope-closure.service.js +222 -0
  33. package/dist/features/scope/scope-subtree.resolver.d.ts +47 -0
  34. package/dist/features/scope/scope-subtree.resolver.d.ts.map +1 -0
  35. package/dist/features/scope/scope-subtree.resolver.js +24 -0
  36. package/dist/features/share/share.controller.d.ts +35 -0
  37. package/dist/features/share/share.controller.d.ts.map +1 -0
  38. package/dist/features/share/share.controller.js +143 -0
  39. package/dist/features/share/share.service.d.ts +90 -0
  40. package/dist/features/share/share.service.d.ts.map +1 -0
  41. package/dist/features/share/share.service.js +58 -0
  42. package/dist/index.d.ts +17 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +20 -1
  45. package/dist/membership-posting.object.d.ts.map +1 -1
  46. package/dist/membership-posting.object.js +10 -0
  47. package/dist/objects.d.ts.map +1 -1
  48. package/dist/objects.js +2 -1
  49. package/dist/record-share.object.d.ts +24 -0
  50. package/dist/record-share.object.d.ts.map +1 -0
  51. package/dist/record-share.object.js +60 -0
  52. package/dist/role.object.d.ts.map +1 -1
  53. package/dist/role.object.js +10 -0
  54. package/dist/user.object.d.ts.map +1 -1
  55. package/dist/user.object.js +10 -0
  56. 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");
@@ -91,6 +96,12 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
91
96
  // postingVersionBump closure pattern above).
92
97
  const accessBundleStore = new access_bundle_store_js_1.AccessBundleStore(authzPool);
93
98
  const grantAdminService = new grant_admin_service_js_1.GrantAdminService(authzPool, bumper, accessBundleStore);
99
+ // Scope closure (Task 1.2 of the scoped-hierarchy program): the reconciler + node
100
+ // move/delete maintenance, over the SAME shared authzPool (it reaches BOTH shield_app —
101
+ // companies — and shield_authz — scope_closure). Reuses the SAME `bumper` instance so a
102
+ // moveNode's session-invalidation joins the move's transaction. reconcileClosure() is
103
+ // kicked post-boot by the CatalogBootstrapProvider (mirrors the catalog reconcile).
104
+ const scopeClosureService = new scope_closure_service_js_1.ScopeClosureService(authzPool, bumper);
94
105
  // The core ModuleRegistry the catalog reconcile job needs, built from the identity
95
106
  // object registrations (guard's bridge reflects decorated classes → registry).
96
107
  const identityRegistry = (0, guard_1.buildIdentityModuleRegistry)([...objects_js_1.identityObjects]);
@@ -113,6 +124,14 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
113
124
  // same record-authorization inputs (matches astram3): the route guard's own decide() is the
114
125
  // first; this one backs the writer's server-side re-verification.
115
126
  const crossTenantAuthority = (0, guard_1.buildRecordAuthorization)((0, record_authorization_js_1.buildIdentityRecordAuthorization)(authzPool));
127
+ // Record-level LIST SCOPE (Task 3.1, Invariant #8): the engine-facing fn the engine's
128
+ // `list()` ANDs into BOTH the data SELECT and the COUNT — the in-query visible-set filter.
129
+ // Built HERE, over the SAME shared authzPool (guard constructs the un-overridable scope
130
+ // adapter inside `buildListScopeFn`, mirroring the four record-tier read adapters). Wired
131
+ // out-of-the-box into `EngineModule.forRoot({ listScope })` below (ADR-0003 injection seam —
132
+ // the engine never imports guard). Fail-closed: a store outage propagates and `list()` fails
133
+ // rather than returning unscoped rows.
134
+ const listScope = (0, guard_1.buildListScopeFn)((0, record_authorization_js_1.buildIdentityRecordAuthorization)(authzPool));
116
135
  // The writer's `GrantAuthorizeFn`: decide()-backed, fail-closed (no try/catch — a throw
117
136
  // propagates and the writer maps it to a retryable 503). isRoot ← the SAME rootIds the
118
137
  // consumer passes to ShieldModule.forRoot (root-correctness for control-plane grants).
@@ -168,6 +187,101 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
168
187
  // The read side (inbox/mine/incoming/offerable-types) over the same pool + the merged object
169
188
  // set (offerable = tenantScoped types only) + the per-type action vocabulary.
170
189
  const crossTenantGrantReadService = new cross_tenant_grant_read_service_js_1.CrossTenantGrantReadService(authzPool, [...objects_js_1.identityObjects], guard_1.actionsFor);
190
+ // ── GUIDED RULE-BUILDER (list-scoping engine Phase 5, §9.1) ──────────────────────────────
191
+ // The policy-authoring surface over the guard `MariaDbPolicyStore` + core `makeAuthorPolicy`.
192
+ // Reuses the SAME `identityRegistry` + `crossTenantAuthority` (a DecisionAuthority over the
193
+ // shared authzPool — the delegated-admin ceiling's decide()) as the cross-tenant writer. The
194
+ // core AuditSink is one-arg (`record(event)`); the engine `MariaDbAuditSink` is two-arg
195
+ // (`record(event, query)`) — so bind the query to the shared authzPool (mirrors the
196
+ // `grantAudit` bridge above). Best-effort audit (a sink throw must not fail the author).
197
+ const policyAuditSink = new engine_1.MariaDbAuditSink({}); // shield_audit.audit_log defaults
198
+ const policyAudit = {
199
+ record: async (event) => {
200
+ try {
201
+ await policyAuditSink.record(event, (sql, params) => authzPool.query(sql, params));
202
+ }
203
+ catch {
204
+ // Best-effort: an audit failure must not fail a legitimate policy author.
205
+ }
206
+ },
207
+ };
208
+ const policyStore = new guard_1.MariaDbPolicyStore(authzPool);
209
+ // The guided-builder SCHEMA source — the authorable field/action metadata, reflected from the
210
+ // registered identity object types (engine `reflectObjectType` yields fields WITH their
211
+ // `type`, which the core registry descriptor drops). Only tenantScoped, owned DATA-plane types
212
+ // are meaningfully policy-authorable in v1; but we expose every registered type's fields and
213
+ // let the delegated-admin ceiling + validateGuidedPolicy gate what a caller may actually
214
+ // author. A consumer's own types (extraObjects) would extend this the same way (future).
215
+ const schemaSource = [...objects_js_1.identityObjects].map((input) => {
216
+ const reg = typeof input === 'function' ? (0, engine_1.reflectObjectType)(input) : input;
217
+ return {
218
+ typeKey: reg.typeKey,
219
+ label: reg.typeKey,
220
+ actions: (0, guard_1.actionsFor)(reg.typeKey),
221
+ fields: reg.fields.map((f) => ({ key: f.key, type: f.type })),
222
+ };
223
+ });
224
+ const policyService = new policy_service_js_1.PolicyService({
225
+ registry: identityRegistry,
226
+ policyStore,
227
+ decide: crossTenantAuthority,
228
+ audit: policyAudit,
229
+ schemaSource,
230
+ });
231
+ // ── OWNER-SHARE surface (list-scoping engine Phase 6, §9.2) ──────────────────────────────
232
+ // The framework-owned RecordShareWriter (the who-may-share bound) + a ShareTypeResolver over
233
+ // the reflected identity object types (so guard never imports @astram/engine — the consumer
234
+ // resolves the physical share info here). Only `owned` types are shareable; the resolver
235
+ // surfaces `owned`/table/schema/pk/owner/tenant columns via the engine helpers. The writer's
236
+ // audit reuses the SAME shield_audit sink pattern (best-effort). The ShareService adds
237
+ // name-resolution (no raw uuid) via the existing DirectoryLabelResolver (`auditLabels`).
238
+ const shareTypeInfoByKey = new Map();
239
+ for (const input of objects_js_1.identityObjects) {
240
+ const reg = typeof input === 'function' ? (0, engine_1.reflectObjectType)(input) : input;
241
+ const owner = (0, engine_1.ownerColumn)(reg);
242
+ const pk = reg.primaryKey.kind === 'uuid' ? (0, engine_1.uuidPkColumn)(reg.primaryKey) : null;
243
+ // Only OWNED types with a uuid PK + an owner column are shareable; others are omitted (the
244
+ // writer rejects an unknown/non-owned type anyway — this map only lists real candidates).
245
+ if (!(0, engine_1.isOwned)(reg) || owner === null || pk === null)
246
+ continue;
247
+ const tenantCol = (0, engine_1.tenantColumnOf)(reg);
248
+ shareTypeInfoByKey.set(reg.typeKey, {
249
+ table: reg.table,
250
+ ...(reg.schema ? { schema: reg.schema } : {}),
251
+ pkColumn: pk,
252
+ ownerColumn: owner,
253
+ ...(tenantCol ? { tenantColumn: tenantCol } : {}),
254
+ owned: true,
255
+ });
256
+ }
257
+ const shareTypeResolver = {
258
+ resolve: (typeKey) => shareTypeInfoByKey.get(typeKey),
259
+ };
260
+ const shareAuditSink = new engine_1.MariaDbAuditSink({}); // shield_audit.audit_log
261
+ const recordShareWriter = new guard_1.RecordShareWriter({
262
+ db: authzPool,
263
+ types: shareTypeResolver,
264
+ audit: {
265
+ record: async (event) => {
266
+ try {
267
+ await shareAuditSink.record({
268
+ // A share/unshare is access administration → 'admin_action' (a valid AuditEventType;
269
+ // 'record_share' is the writer's own event tag, carried in `action`/attributes).
270
+ type: 'admin_action',
271
+ occurredAtMs: event.occurredAtMs,
272
+ actorId: event.actorId,
273
+ tenantId: event.tenantId,
274
+ action: { key: event.action }, // 'share.created' | 'share.removed'
275
+ attributes: { ...event.attributes, eventType: 'record_share' },
276
+ }, (sql, params) => authzPool.query(sql, params));
277
+ }
278
+ catch {
279
+ // Best-effort — an audit failure must never fail a legitimate share/unshare.
280
+ }
281
+ },
282
+ },
283
+ });
284
+ const shareService = new share_service_js_1.ShareService(recordShareWriter, auditLabels);
171
285
  return {
172
286
  module: IdentityFeatureModule_1,
173
287
  imports: [
@@ -179,7 +293,11 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
179
293
  // snapshot is empty and no bundles reconcile into shield_authz.access_bundle.
180
294
  engine_1.EngineModule.forFeature([], { accessBundles: index_js_1.identityAccessBundles }),
181
295
  // The engine's runtime CRUD pipe + the identity object registrations.
182
- engine_1.EngineModule.forRoot({ db: engineDbFromEnv(), registrations: [...objects_js_1.identityObjects] }),
296
+ engine_1.EngineModule.forRoot({
297
+ db: engineDbFromEnv(),
298
+ registrations: [...objects_js_1.identityObjects],
299
+ listScope,
300
+ }),
183
301
  // NestJS DiscoveryService/MetadataScanner — the boot bootstrap provider reflects the
184
302
  // live controllers through this to build the OperationInventory.
185
303
  core_1.DiscoveryModule,
@@ -200,6 +318,10 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
200
318
  // controller's own method order keeps the static sub-paths (/inbox, /offerable-types)
201
319
  // ahead of `/:uuid/*`. Appended after the existing controllers.
202
320
  cross_tenant_grant_controller_js_1.CrossTenantGrantController,
321
+ // Guided rule-builder (Phase 5) — `record-policies/*` collides with nothing. Appended last.
322
+ policy_controller_js_1.PolicyController,
323
+ // Owner shares (Phase 6) — `record-shares` collides with nothing. Appended last.
324
+ share_controller_js_1.ShareController,
203
325
  ],
204
326
  providers: [
205
327
  company_service_js_1.CompanyService,
@@ -221,6 +343,12 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
221
343
  // wiring (the out-of-the-box acceptance bar).
222
344
  { provide: guard_1.SHIELD_CROSS_TENANT_GRANT_WRITER, useValue: crossTenantGrantWriter },
223
345
  { provide: cross_tenant_grant_read_service_js_1.CrossTenantGrantReadService, useValue: crossTenantGrantReadService },
346
+ // Guided rule-builder (Phase 5) — the PolicyService over the shared authzPool + registry
347
+ // + the SAME DecisionAuthority (delegated-admin ceiling). useValue like GrantAdminService.
348
+ { provide: policy_service_js_1.PolicyService, useValue: policyService },
349
+ // Owner shares (Phase 6) — the ShareService over the framework RecordShareWriter + the
350
+ // DirectoryLabelResolver (name resolution). useValue, all over the shared authzPool.
351
+ { provide: share_service_js_1.ShareService, useValue: shareService },
224
352
  // The mutating half of auto-catalog: a single CatalogReconcileJob (post-boot pass +
225
353
  // cron), over the shared pool + the identity registry.
226
354
  { provide: catalog_reconcile_job_js_1.CatalogReconcileJob, useValue: new catalog_reconcile_job_js_1.CatalogReconcileJob(authzPool, identityRegistry) },
@@ -231,10 +359,14 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
231
359
  useFactory: (bundles) => new access_bundle_reconcile_job_js_1.AccessBundleReconcileJob(authzPool, bundles),
232
360
  inject: [engine_1.ENGINE_ALL_ACCESS_BUNDLES],
233
361
  },
362
+ // Scope closure reconciler + node move/delete maintenance (Task 1.2), built at forRoot()
363
+ // over the shared authzPool + the shared bumper, provided useValue so the bootstrap
364
+ // provider (and any future controller) injects the SAME instance.
365
+ { provide: scope_closure_service_js_1.ScopeClosureService, useValue: scopeClosureService },
234
366
  // 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).
367
+ // THEN bundles, THEN the scope-closure reconcile) + schedules them (DISABLE_ALL_CRONS-
368
+ // gated). Injects DiscoveryService/MetadataScanner/Reflector (from DiscoveryModule
369
+ // above), the jobs, the ScopeClosureService, and ENGINE_ALL_REGISTRATIONS.
238
370
  catalog_bootstrap_provider_js_1.CatalogBootstrapProvider,
239
371
  ],
240
372
  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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"}