@astram/identity 0.1.2 → 0.2.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 (139) hide show
  1. package/dist/auth-facts-keys.d.ts +3 -0
  2. package/dist/auth-facts-keys.d.ts.map +1 -0
  3. package/dist/auth-facts-keys.js +24 -0
  4. package/dist/cross-tenant-grant.object.d.ts +28 -0
  5. package/dist/cross-tenant-grant.object.d.ts.map +1 -0
  6. package/dist/cross-tenant-grant.object.js +64 -0
  7. package/dist/features/access/access-admin.access.d.ts +3 -0
  8. package/dist/features/access/access-admin.access.d.ts.map +1 -0
  9. package/dist/features/access/access-admin.access.js +27 -0
  10. package/dist/features/access/company-admin.access.d.ts +3 -0
  11. package/dist/features/access/company-admin.access.d.ts.map +1 -0
  12. package/dist/features/access/company-admin.access.js +31 -0
  13. package/dist/features/access/index.d.ts +15 -0
  14. package/dist/features/access/index.d.ts.map +1 -0
  15. package/dist/features/access/index.js +26 -0
  16. package/dist/features/access/user-management.access.d.ts +3 -0
  17. package/dist/features/access/user-management.access.d.ts.map +1 -0
  18. package/dist/features/access/user-management.access.js +35 -0
  19. package/dist/features/audit/audit-read.service.d.ts +92 -0
  20. package/dist/features/audit/audit-read.service.d.ts.map +1 -0
  21. package/dist/features/audit/audit-read.service.js +97 -0
  22. package/dist/features/audit/audit.controller.d.ts +35 -0
  23. package/dist/features/audit/audit.controller.d.ts.map +1 -0
  24. package/dist/features/audit/audit.controller.js +73 -0
  25. package/dist/features/audit/directory-label-resolver.d.ts +14 -0
  26. package/dist/features/audit/directory-label-resolver.d.ts.map +1 -0
  27. package/dist/features/audit/directory-label-resolver.js +46 -0
  28. package/dist/features/audit/mariadb-audit-reader.d.ts +46 -0
  29. package/dist/features/audit/mariadb-audit-reader.d.ts.map +1 -0
  30. package/dist/features/audit/mariadb-audit-reader.js +116 -0
  31. package/dist/features/authz/access-bundle-reconcile.job.d.ts +35 -0
  32. package/dist/features/authz/access-bundle-reconcile.job.d.ts.map +1 -0
  33. package/dist/features/authz/access-bundle-reconcile.job.js +84 -0
  34. package/dist/features/authz/access-bundle-store.d.ts +65 -0
  35. package/dist/features/authz/access-bundle-store.d.ts.map +1 -0
  36. package/dist/features/authz/access-bundle-store.js +154 -0
  37. package/dist/features/authz/catalog-bootstrap.provider.d.ts +28 -0
  38. package/dist/features/authz/catalog-bootstrap.provider.d.ts.map +1 -0
  39. package/dist/features/authz/catalog-bootstrap.provider.js +85 -0
  40. package/dist/features/authz/catalog-reconcile.job.d.ts +23 -0
  41. package/dist/features/authz/catalog-reconcile.job.d.ts.map +1 -0
  42. package/dist/features/authz/catalog-reconcile.job.js +47 -0
  43. package/dist/features/authz/db-permission-facts.adapter.d.ts +60 -0
  44. package/dist/features/authz/db-permission-facts.adapter.d.ts.map +1 -0
  45. package/dist/features/authz/db-permission-facts.adapter.js +116 -0
  46. package/dist/features/authz/grant-admin.controller.d.ts +96 -0
  47. package/dist/features/authz/grant-admin.controller.d.ts.map +1 -0
  48. package/dist/features/authz/grant-admin.controller.js +209 -0
  49. package/dist/features/authz/grant-admin.service.d.ts +107 -0
  50. package/dist/features/authz/grant-admin.service.d.ts.map +1 -0
  51. package/dist/features/authz/grant-admin.service.js +297 -0
  52. package/dist/features/authz/mariadb-catalog-store.d.ts +16 -0
  53. package/dist/features/authz/mariadb-catalog-store.d.ts.map +1 -0
  54. package/dist/features/authz/mariadb-catalog-store.js +44 -0
  55. package/dist/features/authz/permission-version.bumper.d.ts +47 -0
  56. package/dist/features/authz/permission-version.bumper.d.ts.map +1 -0
  57. package/dist/features/authz/permission-version.bumper.js +68 -0
  58. package/dist/features/authz/record-authorization.d.ts +31 -0
  59. package/dist/features/authz/record-authorization.d.ts.map +1 -0
  60. package/dist/features/authz/record-authorization.js +59 -0
  61. package/dist/features/authz/session-facts.reader.d.ts +41 -0
  62. package/dist/features/authz/session-facts.reader.d.ts.map +1 -0
  63. package/dist/features/authz/session-facts.reader.js +27 -0
  64. package/dist/features/company/company.controller.d.ts +37 -0
  65. package/dist/features/company/company.controller.d.ts.map +1 -0
  66. package/dist/features/company/company.controller.js +100 -0
  67. package/dist/features/company/company.service.d.ts +25 -0
  68. package/dist/features/company/company.service.d.ts.map +1 -0
  69. package/dist/features/company/company.service.js +53 -0
  70. package/dist/features/cross-tenant/cross-tenant-grant-read.service.d.ts +126 -0
  71. package/dist/features/cross-tenant/cross-tenant-grant-read.service.d.ts.map +1 -0
  72. package/dist/features/cross-tenant/cross-tenant-grant-read.service.js +276 -0
  73. package/dist/features/cross-tenant/cross-tenant-grant.controller.d.ts +65 -0
  74. package/dist/features/cross-tenant/cross-tenant-grant.controller.d.ts.map +1 -0
  75. package/dist/features/cross-tenant/cross-tenant-grant.controller.js +250 -0
  76. package/dist/features/cross-tenant/grant-authorize.d.ts +42 -0
  77. package/dist/features/cross-tenant/grant-authorize.d.ts.map +1 -0
  78. package/dist/features/cross-tenant/grant-authorize.js +17 -0
  79. package/dist/features/identity-feature.module.d.ts +26 -0
  80. package/dist/features/identity-feature.module.d.ts.map +1 -0
  81. package/dist/features/identity-feature.module.js +247 -0
  82. package/dist/features/membership/membership.controller.d.ts +77 -0
  83. package/dist/features/membership/membership.controller.d.ts.map +1 -0
  84. package/dist/features/membership/membership.controller.js +174 -0
  85. package/dist/features/membership/membership.service.d.ts +73 -0
  86. package/dist/features/membership/membership.service.d.ts.map +1 -0
  87. package/dist/features/membership/membership.service.js +173 -0
  88. package/dist/features/membership/posting-version-bump.d.ts +10 -0
  89. package/dist/features/membership/posting-version-bump.d.ts.map +1 -0
  90. package/dist/features/membership/posting-version-bump.js +4 -0
  91. package/dist/features/membership/posting.service.d.ts +59 -0
  92. package/dist/features/membership/posting.service.d.ts.map +1 -0
  93. package/dist/features/membership/posting.service.js +181 -0
  94. package/dist/features/org-structure/org-catalog.controller.d.ts +14 -0
  95. package/dist/features/org-structure/org-catalog.controller.d.ts.map +1 -0
  96. package/dist/features/org-structure/org-catalog.controller.js +62 -0
  97. package/dist/features/org-structure/org-catalog.service.d.ts +19 -0
  98. package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -0
  99. package/dist/features/org-structure/org-catalog.service.js +53 -0
  100. package/dist/features/role/role.controller.d.ts +37 -0
  101. package/dist/features/role/role.controller.d.ts.map +1 -0
  102. package/dist/features/role/role.controller.js +104 -0
  103. package/dist/features/role/role.service.d.ts +24 -0
  104. package/dist/features/role/role.service.d.ts.map +1 -0
  105. package/dist/features/role/role.service.js +53 -0
  106. package/dist/features/shared/http.d.ts +15 -0
  107. package/dist/features/shared/http.d.ts.map +1 -0
  108. package/dist/features/shared/http.js +25 -0
  109. package/dist/features/shared/list-query.d.ts +44 -0
  110. package/dist/features/shared/list-query.d.ts.map +1 -0
  111. package/dist/features/shared/list-query.js +53 -0
  112. package/dist/features/shared/named-option.d.ts +6 -0
  113. package/dist/features/shared/named-option.d.ts.map +1 -0
  114. package/dist/features/shared/named-option.js +2 -0
  115. package/dist/features/shared/record-resolver.d.ts +11 -0
  116. package/dist/features/shared/record-resolver.d.ts.map +1 -0
  117. package/dist/features/shared/record-resolver.js +32 -0
  118. package/dist/features/user/user.controller.d.ts +39 -0
  119. package/dist/features/user/user.controller.d.ts.map +1 -0
  120. package/dist/features/user/user.controller.js +104 -0
  121. package/dist/features/user/user.service.d.ts +26 -0
  122. package/dist/features/user/user.service.d.ts.map +1 -0
  123. package/dist/features/user/user.service.js +53 -0
  124. package/dist/index.d.ts +34 -3
  125. package/dist/index.d.ts.map +1 -1
  126. package/dist/index.js +66 -33
  127. package/dist/objects.d.ts +13 -0
  128. package/dist/objects.d.ts.map +1 -0
  129. package/dist/objects.js +20 -0
  130. package/dist/shared-resource.object.d.ts +28 -0
  131. package/dist/shared-resource.object.d.ts.map +1 -0
  132. package/dist/shared-resource.object.js +63 -0
  133. package/migrations/1710000000010-CreateAuditLog.cjs +42 -0
  134. package/migrations/1710000000011-CreateCrossTenantGrant.cjs +66 -0
  135. package/migrations/1710000000012-SeedCrossTenantGrantPerms.cjs +86 -0
  136. package/migrations/1710000000013-CreateSharedResource.cjs +30 -0
  137. package/migrations/1710000000099-SeedDevIdentity.cjs +130 -0
  138. package/migrations/1710000000100-SeedDevTenantBAndRoot.cjs +212 -0
  139. package/package.json +5 -4
@@ -0,0 +1,26 @@
1
+ import { type DynamicModule } from '@nestjs/common';
2
+ /**
3
+ * IdentityFeatureModule — the framework-owned identity FEATURE surface (company/user/role CRUD;
4
+ * more in later phases) as thin controllers over the engine's RecordsService. The CONSUMER imports
5
+ * `IdentityFeatureModule.forRoot()` alongside `ShieldModule.forRoot(...)` (ADR-0026) — @astram/guard
6
+ * never imports @astram/identity. `forRoot()` SELF-WIRES the engine (EngineModule.forRoot over the
7
+ * app's APP_DB_* env) + registers the identity objects + mounts the CRUD routes, so the consumer
8
+ * needs no separate engine setup.
9
+ *
10
+ * CONTROLLER ORDER: RoleController is kept ahead of any future grant-admin controller (Phase 4) with
11
+ * a conflicting `GET identity/roles/:uuid` path — a Nest route-precedence constraint.
12
+ */
13
+ export declare class IdentityFeatureModule {
14
+ /**
15
+ * @param rootIds the framework's root allowlist — pass the SAME array given to
16
+ * `ShieldModule.forRoot({ rootIds })`. It seeds the cross-tenant grant writer's
17
+ * `isRoot` closure so a root caller who passes the route guard is not then denied
18
+ * by the writer's own `decide()` (a route-allows/writer-denies split). Omitted ⇒
19
+ * `isRoot` is always false (a non-root consumer works; only a root offering
20
+ * cross-tenant grants needs it). See ADR-0026 + the template wiring.
21
+ */
22
+ static forRoot({ rootIds }?: {
23
+ rootIds?: readonly string[];
24
+ }): DynamicModule;
25
+ }
26
+ //# sourceMappingURL=identity-feature.module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity-feature.module.d.ts","sourceRoot":"","sources":["../../src/features/identity-feature.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAoE5D;;;;;;;;;;GAUG;AACH,qBACa,qBAAqB;IAChC;;;;;;;OAOG;IACH,MAAM,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,GAAE;QAAE,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;KAAO,GAAG,aAAa;CAkLjF"}
@@ -0,0 +1,247 @@
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 IdentityFeatureModule_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.IdentityFeatureModule = void 0;
11
+ const common_1 = require("@nestjs/common");
12
+ const core_1 = require("@nestjs/core");
13
+ const engine_1 = require("@astram/engine");
14
+ const guard_1 = require("@astram/guard");
15
+ const objects_js_1 = require("../objects.js");
16
+ const index_js_1 = require("./access/index.js");
17
+ const company_service_js_1 = require("./company/company.service.js");
18
+ const company_controller_js_1 = require("./company/company.controller.js");
19
+ const role_service_js_1 = require("./role/role.service.js");
20
+ const role_controller_js_1 = require("./role/role.controller.js");
21
+ const user_service_js_1 = require("./user/user.service.js");
22
+ const user_controller_js_1 = require("./user/user.controller.js");
23
+ const membership_controller_js_1 = require("./membership/membership.controller.js");
24
+ const membership_service_js_1 = require("./membership/membership.service.js");
25
+ const posting_service_js_1 = require("./membership/posting.service.js");
26
+ const org_catalog_controller_js_1 = require("./org-structure/org-catalog.controller.js");
27
+ const org_catalog_service_js_1 = require("./org-structure/org-catalog.service.js");
28
+ const record_resolver_js_1 = require("./shared/record-resolver.js");
29
+ const posting_version_bump_js_1 = require("./membership/posting-version-bump.js");
30
+ const permission_version_bumper_js_1 = require("./authz/permission-version.bumper.js");
31
+ const record_authorization_js_1 = require("./authz/record-authorization.js");
32
+ const db_permission_facts_adapter_js_1 = require("./authz/db-permission-facts.adapter.js");
33
+ const access_bundle_store_js_1 = require("./authz/access-bundle-store.js");
34
+ const grant_admin_service_js_1 = require("./authz/grant-admin.service.js");
35
+ const grant_admin_controller_js_1 = require("./authz/grant-admin.controller.js");
36
+ const catalog_reconcile_job_js_1 = require("./authz/catalog-reconcile.job.js");
37
+ const access_bundle_reconcile_job_js_1 = require("./authz/access-bundle-reconcile.job.js");
38
+ const catalog_bootstrap_provider_js_1 = require("./authz/catalog-bootstrap.provider.js");
39
+ const mariadb_audit_reader_js_1 = require("./audit/mariadb-audit-reader.js");
40
+ const directory_label_resolver_js_1 = require("./audit/directory-label-resolver.js");
41
+ const audit_read_service_js_1 = require("./audit/audit-read.service.js");
42
+ const audit_controller_js_1 = require("./audit/audit.controller.js");
43
+ const cross_tenant_grant_read_service_js_1 = require("./cross-tenant/cross-tenant-grant-read.service.js");
44
+ const cross_tenant_grant_controller_js_1 = require("./cross-tenant/cross-tenant-grant.controller.js");
45
+ const grant_authorize_js_1 = require("./cross-tenant/grant-authorize.js");
46
+ /** Map guard's env DbConfig → the engine's EngineDbConfig (adds the pool cap the engine needs). */
47
+ function engineDbFromEnv() {
48
+ const db = (0, guard_1.loadDbConfig)();
49
+ return {
50
+ host: db.host,
51
+ port: db.port,
52
+ user: db.user,
53
+ password: db.password,
54
+ database: db.database,
55
+ // The engine's runtime CRUD pool cap. A conservative default; the app can raise it later via
56
+ // an explicit EngineModule.forRoot if it needs more. (ADR-0018 budget lives app-side.)
57
+ connectionLimit: Number(process.env.APP_DB_MAX_CONNECTIONS ?? 10),
58
+ };
59
+ }
60
+ /**
61
+ * IdentityFeatureModule — the framework-owned identity FEATURE surface (company/user/role CRUD;
62
+ * more in later phases) as thin controllers over the engine's RecordsService. The CONSUMER imports
63
+ * `IdentityFeatureModule.forRoot()` alongside `ShieldModule.forRoot(...)` (ADR-0026) — @astram/guard
64
+ * never imports @astram/identity. `forRoot()` SELF-WIRES the engine (EngineModule.forRoot over the
65
+ * app's APP_DB_* env) + registers the identity objects + mounts the CRUD routes, so the consumer
66
+ * needs no separate engine setup.
67
+ *
68
+ * CONTROLLER ORDER: RoleController is kept ahead of any future grant-admin controller (Phase 4) with
69
+ * a conflicting `GET identity/roles/:uuid` path — a Nest route-precedence constraint.
70
+ */
71
+ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModule {
72
+ /**
73
+ * @param rootIds the framework's root allowlist — pass the SAME array given to
74
+ * `ShieldModule.forRoot({ rootIds })`. It seeds the cross-tenant grant writer's
75
+ * `isRoot` closure so a root caller who passes the route guard is not then denied
76
+ * by the writer's own `decide()` (a route-allows/writer-denies split). Omitted ⇒
77
+ * `isRoot` is always false (a non-root consumer works; only a root offering
78
+ * cross-tenant grants needs it). See ADR-0026 + the template wiring.
79
+ */
80
+ static forRoot({ rootIds } = {}) {
81
+ // ONE shared shield_authz pool for the whole grant-admin stack (ADR-0011): the
82
+ // permission-version bumper, the AccessBundleStore, the GrantAdminService, and the two
83
+ // reconcile jobs all run over this single pool (don't open several). Opened once at
84
+ // forRoot() (not import time) — mirrors the pre-existing bump-pool pattern.
85
+ const authzPool = (0, record_authorization_js_1.createIdentityAuthzPool)();
86
+ const bumper = new permission_version_bumper_js_1.PermissionVersionBumper();
87
+ const postingVersionBump = {
88
+ bumpTenant: (tenantUuid) => bumper.bumpForTenant(tenantUuid, authzPool),
89
+ };
90
+ // The grant-admin collaborators built at forRoot() over the shared pool (mirrors the
91
+ // postingVersionBump closure pattern above).
92
+ const accessBundleStore = new access_bundle_store_js_1.AccessBundleStore(authzPool);
93
+ const grantAdminService = new grant_admin_service_js_1.GrantAdminService(authzPool, bumper, accessBundleStore);
94
+ // The core ModuleRegistry the catalog reconcile job needs, built from the identity
95
+ // object registrations (guard's bridge reflects decorated classes → registry).
96
+ const identityRegistry = (0, guard_1.buildIdentityModuleRegistry)([...objects_js_1.identityObjects]);
97
+ // Audit viewer (Phase 5): the AuditReader + LabelResolver over the SAME shared authzPool
98
+ // (the reader queries shield_audit + the resolver/actor-lookup query shield_app, both
99
+ // reachable by that pool). `auditNameToId` is the actor-name -> uuid reverse lookup for the
100
+ // actor filter (astram3's exact closure). A mariadb Pool.query(sql, params) structurally
101
+ // satisfies the reader/resolver's `AuditQueryable` — pass authzPool directly. Built at
102
+ // forRoot() (not via Nest DI) because it needs the hand-built pool, then provided useValue.
103
+ const auditReader = new mariadb_audit_reader_js_1.MariaDbAuditReader(authzPool);
104
+ const auditLabels = new directory_label_resolver_js_1.DirectoryLabelResolver(authzPool);
105
+ const auditNameToId = async (name) => {
106
+ const rows = (await authzPool.query('SELECT u_uuid AS id FROM `shield_app`.`users` WHERE u_username = ? LIMIT 1', [name]));
107
+ return rows[0]?.id ?? null;
108
+ };
109
+ const auditReadService = new audit_read_service_js_1.AuditReadService(auditReader, auditLabels, auditNameToId);
110
+ // Cross-tenant grants (Phase 6): the ENTIRE offer/accept/revoke surface, SELF-WIRED here
111
+ // over the SAME shared authzPool (no consumer code — astram3 hand-built this in its consumer
112
+ // authz.module, the anti-pattern this replaces). A SECOND DecisionAuthority is built over the
113
+ // same record-authorization inputs (matches astram3): the route guard's own decide() is the
114
+ // first; this one backs the writer's server-side re-verification.
115
+ const crossTenantAuthority = (0, guard_1.buildRecordAuthorization)((0, record_authorization_js_1.buildIdentityRecordAuthorization)(authzPool));
116
+ // The writer's `GrantAuthorizeFn`: decide()-backed, fail-closed (no try/catch — a throw
117
+ // propagates and the writer maps it to a retryable 503). isRoot ← the SAME rootIds the
118
+ // consumer passes to ShieldModule.forRoot (root-correctness for control-plane grants).
119
+ const authorizeCrossTenantGrant = (0, grant_authorize_js_1.makeAuthorizeCrossTenantGrant)({
120
+ authority: crossTenantAuthority,
121
+ readRoleIds: (principalId, tenantId) => (0, db_permission_facts_adapter_js_1.readRoleIdsForTenant)(authzPool, principalId, tenantId),
122
+ isRoot: (principalId) => (rootIds ?? []).includes(principalId),
123
+ });
124
+ // Audit bridge: map guard's `GrantAuditEvent` → the engine's `MariaDbAuditSink` (the SAME
125
+ // shield_audit.audit_log sink Phase 5's reader reads). The engine sink's `record(event, query)`
126
+ // takes a SECOND query fn (its rows commit on the pipe's txn); guard's `GrantAuditSink.record`
127
+ // takes ONE arg — so we adapt, running the write on the shared pool. The engine AuditEvent's
128
+ // `action` is an `Action` object (`{ key }`), while guard's is a 'grant.*' string — wrap it.
129
+ // AUDIT-ON-BOTH (ADR-0009 / invariant #6): a cross-tenant grant lifecycle event is filed under
130
+ // BOTH the owning tenant's AND the grantee tenant's audit trail — one row each — so a
131
+ // tenant-scoped auditor on EITHER side sees the event (a grantee-tenant auditor must see a
132
+ // grant accepted INTO their tenant, not just the owner's trail). Same tenant id twice (should
133
+ // never happen for a cross-tenant grant) is de-duped to ONE row.
134
+ // BEST-EFFORT: a throw here must NEVER fail the lifecycle write (the writer already swallows a
135
+ // sink throw; we also guard here so a mapping/DB error is inert).
136
+ const grantAuditSink = new engine_1.MariaDbAuditSink({}); // defaults: shield_audit.audit_log
137
+ const grantAudit = {
138
+ record: async (event) => {
139
+ try {
140
+ const tenantIds = event.granteeTenantId && event.granteeTenantId !== event.owningTenantId
141
+ ? [event.owningTenantId, event.granteeTenantId]
142
+ : [event.owningTenantId];
143
+ for (const tenantId of tenantIds) {
144
+ await grantAuditSink.record({
145
+ type: 'authentication',
146
+ occurredAtMs: event.occurredAtMs,
147
+ actorId: event.actorId,
148
+ tenantId,
149
+ action: { key: event.action },
150
+ attributes: {
151
+ ...event.attributes,
152
+ owningTenantId: event.owningTenantId,
153
+ granteeTenantId: event.granteeTenantId,
154
+ },
155
+ }, (sql, params) => authzPool.query(sql, params));
156
+ }
157
+ }
158
+ catch {
159
+ // Best-effort: an audit failure must not fail a legitimate lifecycle operation.
160
+ }
161
+ },
162
+ };
163
+ const crossTenantGrantWriter = new guard_1.CrossTenantGrantWriter({
164
+ db: authzPool,
165
+ authorize: authorizeCrossTenantGrant,
166
+ audit: grantAudit,
167
+ });
168
+ // The read side (inbox/mine/incoming/offerable-types) over the same pool + the merged object
169
+ // set (offerable = tenantScoped types only) + the per-type action vocabulary.
170
+ const crossTenantGrantReadService = new cross_tenant_grant_read_service_js_1.CrossTenantGrantReadService(authzPool, [...objects_js_1.identityObjects], guard_1.actionsFor);
171
+ return {
172
+ module: IdentityFeatureModule_1,
173
+ imports: [
174
+ // Collect the 3 identity access bundles into ENGINE_ALL_ACCESS_BUNDLES: forRoot does
175
+ // NOT take `accessBundles` — only forFeature accumulates them (engine.module.ts). MUST
176
+ // run BEFORE EngineModule.forRoot() below: `imports` array elements evaluate
177
+ // left-to-right, and forRoot's body snapshots the accumulator SYNCHRONOUSLY at call
178
+ // time (not in a DI factory) — so forFeature has to push into it first, or the
179
+ // snapshot is empty and no bundles reconcile into shield_authz.access_bundle.
180
+ engine_1.EngineModule.forFeature([], { accessBundles: index_js_1.identityAccessBundles }),
181
+ // The engine's runtime CRUD pipe + the identity object registrations.
182
+ engine_1.EngineModule.forRoot({ db: engineDbFromEnv(), registrations: [...objects_js_1.identityObjects] }),
183
+ // NestJS DiscoveryService/MetadataScanner — the boot bootstrap provider reflects the
184
+ // live controllers through this to build the OperationInventory.
185
+ core_1.DiscoveryModule,
186
+ ],
187
+ // Route order: RoleController stays AHEAD of GrantAdminController (documented Nest
188
+ // route-precedence constraint — the summary route `access/role-grants` dodges
189
+ // `roles/:uuid`, but Role is registered first regardless).
190
+ controllers: [
191
+ company_controller_js_1.CompanyController,
192
+ role_controller_js_1.RoleController,
193
+ grant_admin_controller_js_1.GrantAdminController,
194
+ user_controller_js_1.UserController,
195
+ membership_controller_js_1.MembershipController,
196
+ org_catalog_controller_js_1.OrgCatalogController,
197
+ // `identity/audit` collides with no other route — order-independent; appended last.
198
+ audit_controller_js_1.AuditController,
199
+ // Cross-tenant grants — its `cross-tenant-grants/*` paths collide with nothing; the
200
+ // controller's own method order keeps the static sub-paths (/inbox, /offerable-types)
201
+ // ahead of `/:uuid/*`. Appended after the existing controllers.
202
+ cross_tenant_grant_controller_js_1.CrossTenantGrantController,
203
+ ],
204
+ providers: [
205
+ company_service_js_1.CompanyService,
206
+ role_service_js_1.RoleService,
207
+ user_service_js_1.UserService,
208
+ membership_service_js_1.MembershipService,
209
+ posting_service_js_1.PostingService,
210
+ org_catalog_service_js_1.OrgCatalogService,
211
+ record_resolver_js_1.RecordResolverService,
212
+ { provide: posting_version_bump_js_1.POSTING_VERSION_BUMP, useValue: postingVersionBump },
213
+ // Grant-admin stack — all over the ONE shared authzPool.
214
+ { provide: access_bundle_store_js_1.AccessBundleStore, useValue: accessBundleStore },
215
+ { provide: grant_admin_service_js_1.GrantAdminService, useValue: grantAdminService },
216
+ // Audit read stack — built at forRoot() over the ONE shared authzPool, provided as a
217
+ // useValue (like GrantAdminService) so it carries the hand-built reader/resolver/lookup.
218
+ { provide: audit_read_service_js_1.AuditReadService, useValue: auditReadService },
219
+ // Cross-tenant grant stack (Phase 6) — the writer + read service, both over the ONE
220
+ // shared authzPool, provided useValue so the controller injects them with no consumer
221
+ // wiring (the out-of-the-box acceptance bar).
222
+ { provide: guard_1.SHIELD_CROSS_TENANT_GRANT_WRITER, useValue: crossTenantGrantWriter },
223
+ { provide: cross_tenant_grant_read_service_js_1.CrossTenantGrantReadService, useValue: crossTenantGrantReadService },
224
+ // The mutating half of auto-catalog: a single CatalogReconcileJob (post-boot pass +
225
+ // cron), over the shared pool + the identity registry.
226
+ { provide: catalog_reconcile_job_js_1.CatalogReconcileJob, useValue: new catalog_reconcile_job_js_1.CatalogReconcileJob(authzPool, identityRegistry) },
227
+ // The bundle half: reconciles the merged AccessBundleSpec[] (ENGINE_ALL_ACCESS_BUNDLES,
228
+ // reflected by the engine from identityAccessBundles) into shield_authz.access_bundle.
229
+ {
230
+ provide: access_bundle_reconcile_job_js_1.AccessBundleReconcileJob,
231
+ useFactory: (bundles) => new access_bundle_reconcile_job_js_1.AccessBundleReconcileJob(authzPool, bundles),
232
+ inject: [engine_1.ENGINE_ALL_ACCESS_BUNDLES],
233
+ },
234
+ // 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).
238
+ catalog_bootstrap_provider_js_1.CatalogBootstrapProvider,
239
+ ],
240
+ exports: [company_service_js_1.CompanyService, role_service_js_1.RoleService, user_service_js_1.UserService],
241
+ };
242
+ }
243
+ };
244
+ exports.IdentityFeatureModule = IdentityFeatureModule;
245
+ exports.IdentityFeatureModule = IdentityFeatureModule = IdentityFeatureModule_1 = __decorate([
246
+ (0, common_1.Module)({})
247
+ ], IdentityFeatureModule);
@@ -0,0 +1,77 @@
1
+ import { MembershipService } from './membership.service.js';
2
+ import { PostingService, type PostingInput } from './posting.service.js';
3
+ import { RecordResolverService } from '../shared/record-resolver.js';
4
+ /** A membership card for the API, with EVERY uuid resolved to a display name. */
5
+ interface MembershipCardDto {
6
+ /** The membership id — used as a route param / react key, NEVER as a display field. */
7
+ membershipUuid: string;
8
+ companyUuid: string;
9
+ companyName: string;
10
+ isActive: boolean;
11
+ status: string;
12
+ postings: PostingCardDto[];
13
+ }
14
+ /** A posting for the API, with role/dept/branch uuids resolved to names. */
15
+ interface PostingCardDto {
16
+ postingUuid: string;
17
+ roleUuid: string;
18
+ roleName: string;
19
+ departmentUuid: string | null;
20
+ departmentName: string | null;
21
+ branchUuid: string | null;
22
+ branchName: string | null;
23
+ /** Effective-window bounds as epoch-ms (BIGINT storage), or null = open-ended. */
24
+ effectiveFrom: number | null;
25
+ effectiveUntil: number | null;
26
+ }
27
+ export declare class MembershipController {
28
+ private readonly memberships;
29
+ private readonly postings;
30
+ private readonly resolver;
31
+ constructor(memberships: MembershipService, postings: PostingService, resolver: RecordResolverService);
32
+ /**
33
+ * The user's membership cards + their postings, with EVERY uuid resolved to a display
34
+ * NAME (companyName / roleName / departmentName / branchName). Raw `*_uuid` values are
35
+ * returned ONLY as ids/params for the client to key/route on — never as a display
36
+ * field. This is the non-negotiable no-raw-UUID rule enforced server-side.
37
+ */
38
+ listUserMemberships(userUuid: string): Promise<{
39
+ memberships: MembershipCardDto[];
40
+ }>;
41
+ /** Add the user to a company (creates an inactive membership card). */
42
+ addUserMembership(userUuid: string, body: {
43
+ companyUuid?: string;
44
+ }): Promise<{
45
+ id: import("@astram/engine").RecordId;
46
+ }>;
47
+ /**
48
+ * Update a membership — `isActive` flips activation through `setActive` (the one-active
49
+ * invariant); `status` is a plain field update. Activating requires the owning user so
50
+ * the invariant can deactivate the user's OTHER memberships.
51
+ */
52
+ updateMembership(membershipUuid: string, body: {
53
+ isActive?: boolean;
54
+ status?: string;
55
+ userUuid?: string;
56
+ }): Promise<{
57
+ success: boolean;
58
+ }>;
59
+ /** Remove a membership card (its postings cascade at the DB). */
60
+ removeMembership(membershipUuid: string): Promise<{
61
+ success: boolean;
62
+ }>;
63
+ /** Atomically replace a membership's full posting set (the diff sync). */
64
+ replacePostings(membershipUuid: string, body: {
65
+ postings?: PostingInput[];
66
+ }): Promise<{
67
+ success: boolean;
68
+ }>;
69
+ /**
70
+ * Resolve a record's display name via the RecordsService facade (`resolver.getRecord`), falling back to
71
+ * `(unknown)` for a missing/deleted record — NEVER leaking the uuid as the label. Used
72
+ * only to build human-readable membership/posting cards (rule: no raw UUIDs in the UI).
73
+ */
74
+ private resolveName;
75
+ }
76
+ export {};
77
+ //# sourceMappingURL=membership.controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"membership.controller.d.ts","sourceRoot":"","sources":["../../../src/features/membership/membership.controller.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAGrE,iFAAiF;AACjF,UAAU,iBAAiB;IACzB,uFAAuF;IACvF,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC5B;AAED,4EAA4E;AAC5E,UAAU,cAAc;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,kFAAkF;IAClF,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AASD,qBACa,oBAAoB;IAE7B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAFR,WAAW,EAAE,iBAAiB,EAC9B,QAAQ,EAAE,cAAc,EACxB,QAAQ,EAAE,qBAAqB;IAGlD;;;;;OAKG;IAGG,mBAAmB,CAAoB,QAAQ,EAAE,MAAM;;;IA0C7D,uEAAuE;IAGjE,iBAAiB,CACF,QAAQ,EAAE,MAAM,EAC3B,IAAI,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAMxC;;;;OAIG;IAGG,gBAAgB,CACK,cAAc,EAAE,MAAM,EACvC,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAc1E,iEAAiE;IAG3D,gBAAgB,CAA0B,cAAc,EAAE,MAAM;;;IAKtE,0EAA0E;IAGpE,eAAe,CACM,cAAc,EAAE,MAAM,EACvC,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAA;KAAE;;;IAM7C;;;;OAIG;YACW,WAAW;CAM1B"}
@@ -0,0 +1,174 @@
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.MembershipController = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const guard_1 = require("@astram/guard");
18
+ const membership_service_js_1 = require("./membership.service.js");
19
+ const posting_service_js_1 = require("./posting.service.js");
20
+ const record_resolver_js_1 = require("../shared/record-resolver.js");
21
+ const http_js_1 = require("../shared/http.js");
22
+ // ── Memberships + postings ───────────────────────────────────────────────────
23
+ //
24
+ // Gated on the `company_membership` / `membership_posting` grants via `@Classified`
25
+ // + the engine's mandatory in-pipe gate. Both are DATA-plane types, so the capped root
26
+ // is denied them by construction — the membership admin surface is grant-driven, not
27
+ // root-driven. Real per-company-admin scoping is a later permission-engine concern.
28
+ let MembershipController = class MembershipController {
29
+ memberships;
30
+ postings;
31
+ resolver;
32
+ constructor(memberships, postings, resolver) {
33
+ this.memberships = memberships;
34
+ this.postings = postings;
35
+ this.resolver = resolver;
36
+ }
37
+ /**
38
+ * The user's membership cards + their postings, with EVERY uuid resolved to a display
39
+ * NAME (companyName / roleName / departmentName / branchName). Raw `*_uuid` values are
40
+ * returned ONLY as ids/params for the client to key/route on — never as a display
41
+ * field. This is the non-negotiable no-raw-UUID rule enforced server-side.
42
+ */
43
+ async listUserMemberships(userUuid) {
44
+ const rows = await this.memberships.listMemberships(userUuid);
45
+ const cards = [];
46
+ for (const m of rows) {
47
+ const membershipUuid = typeof m.id === 'string' ? m.id : String(m.id);
48
+ const companyUuid = (0, http_js_1.asUuid)(m.fields.companyUuid) ?? '';
49
+ const postingRows = await this.postings.listPostings(membershipUuid);
50
+ const postings = [];
51
+ for (const p of postingRows) {
52
+ const roleUuid = (0, http_js_1.asUuid)(p.fields.roleUuid) ?? '';
53
+ const departmentUuid = (0, http_js_1.asUuid)(p.fields.departmentUuid);
54
+ const branchUuid = (0, http_js_1.asUuid)(p.fields.branchUuid);
55
+ postings.push({
56
+ postingUuid: typeof p.id === 'string' ? p.id : String(p.id),
57
+ roleUuid,
58
+ roleName: await this.resolveName('role', roleUuid),
59
+ departmentUuid,
60
+ departmentName: departmentUuid
61
+ ? await this.resolveName('department', departmentUuid)
62
+ : null,
63
+ branchUuid,
64
+ branchName: branchUuid ? await this.resolveName('branch', branchUuid) : null,
65
+ effectiveFrom: (0, http_js_1.asEpochMs)(p.fields.effectiveFrom),
66
+ effectiveUntil: (0, http_js_1.asEpochMs)(p.fields.effectiveUntil),
67
+ });
68
+ }
69
+ cards.push({
70
+ membershipUuid,
71
+ companyUuid,
72
+ companyName: companyUuid ? await this.resolveName('company', companyUuid) : '(unknown)',
73
+ isActive: Boolean(m.fields.isActive),
74
+ status: typeof m.fields.status === 'string' ? m.fields.status : 'active',
75
+ postings,
76
+ });
77
+ }
78
+ return { memberships: cards };
79
+ }
80
+ /** Add the user to a company (creates an inactive membership card). */
81
+ async addUserMembership(userUuid, body) {
82
+ const id = await this.memberships.addMembership(userUuid, body?.companyUuid ?? '');
83
+ return { id };
84
+ }
85
+ /**
86
+ * Update a membership — `isActive` flips activation through `setActive` (the one-active
87
+ * invariant); `status` is a plain field update. Activating requires the owning user so
88
+ * the invariant can deactivate the user's OTHER memberships.
89
+ */
90
+ async updateMembership(membershipUuid, body) {
91
+ if (body?.isActive === true) {
92
+ if (!body.userUuid) {
93
+ throw new common_1.ForbiddenException('userUuid is required to activate a membership');
94
+ }
95
+ await this.memberships.setActive(membershipUuid, body.userUuid);
96
+ }
97
+ if (typeof body?.status === 'string') {
98
+ await this.memberships.updateMembership(membershipUuid, { status: body.status });
99
+ }
100
+ return { success: true };
101
+ }
102
+ /** Remove a membership card (its postings cascade at the DB). */
103
+ async removeMembership(membershipUuid) {
104
+ await this.memberships.removeMembership(membershipUuid);
105
+ return { success: true };
106
+ }
107
+ /** Atomically replace a membership's full posting set (the diff sync). */
108
+ async replacePostings(membershipUuid, body) {
109
+ await this.postings.replacePostings(membershipUuid, body?.postings ?? []);
110
+ return { success: true };
111
+ }
112
+ /**
113
+ * Resolve a record's display name via the RecordsService facade (`resolver.getRecord`), falling back to
114
+ * `(unknown)` for a missing/deleted record — NEVER leaking the uuid as the label. Used
115
+ * only to build human-readable membership/posting cards (rule: no raw UUIDs in the UI).
116
+ */
117
+ async resolveName(typeKey, uuid) {
118
+ if (!uuid)
119
+ return '(unknown)';
120
+ const view = await this.resolver.getRecord(typeKey, uuid);
121
+ const name = view?.fields?.name;
122
+ return typeof name === 'string' && name.length > 0 ? name : '(unknown)';
123
+ }
124
+ };
125
+ exports.MembershipController = MembershipController;
126
+ __decorate([
127
+ (0, common_1.Get)('users/:userUuid/memberships'),
128
+ (0, guard_1.Classified)({ typeKey: 'company_membership', action: 'read' }),
129
+ __param(0, (0, common_1.Param)('userUuid')),
130
+ __metadata("design:type", Function),
131
+ __metadata("design:paramtypes", [String]),
132
+ __metadata("design:returntype", Promise)
133
+ ], MembershipController.prototype, "listUserMemberships", null);
134
+ __decorate([
135
+ (0, common_1.Post)('users/:userUuid/memberships'),
136
+ (0, guard_1.Classified)({ typeKey: 'company_membership', action: 'create' }),
137
+ __param(0, (0, common_1.Param)('userUuid')),
138
+ __param(1, (0, common_1.Body)()),
139
+ __metadata("design:type", Function),
140
+ __metadata("design:paramtypes", [String, Object]),
141
+ __metadata("design:returntype", Promise)
142
+ ], MembershipController.prototype, "addUserMembership", null);
143
+ __decorate([
144
+ (0, common_1.Put)('memberships/:membershipUuid'),
145
+ (0, guard_1.Classified)({ typeKey: 'company_membership', action: 'update' }),
146
+ __param(0, (0, common_1.Param)('membershipUuid')),
147
+ __param(1, (0, common_1.Body)()),
148
+ __metadata("design:type", Function),
149
+ __metadata("design:paramtypes", [String, Object]),
150
+ __metadata("design:returntype", Promise)
151
+ ], MembershipController.prototype, "updateMembership", null);
152
+ __decorate([
153
+ (0, common_1.Delete)('memberships/:membershipUuid'),
154
+ (0, guard_1.Classified)({ typeKey: 'company_membership', action: 'delete' }),
155
+ __param(0, (0, common_1.Param)('membershipUuid')),
156
+ __metadata("design:type", Function),
157
+ __metadata("design:paramtypes", [String]),
158
+ __metadata("design:returntype", Promise)
159
+ ], MembershipController.prototype, "removeMembership", null);
160
+ __decorate([
161
+ (0, common_1.Put)('memberships/:membershipUuid/postings'),
162
+ (0, guard_1.Classified)({ typeKey: 'membership_posting', action: 'update' }),
163
+ __param(0, (0, common_1.Param)('membershipUuid')),
164
+ __param(1, (0, common_1.Body)()),
165
+ __metadata("design:type", Function),
166
+ __metadata("design:paramtypes", [String, Object]),
167
+ __metadata("design:returntype", Promise)
168
+ ], MembershipController.prototype, "replacePostings", null);
169
+ exports.MembershipController = MembershipController = __decorate([
170
+ (0, common_1.Controller)('identity'),
171
+ __metadata("design:paramtypes", [membership_service_js_1.MembershipService,
172
+ posting_service_js_1.PostingService,
173
+ record_resolver_js_1.RecordResolverService])
174
+ ], MembershipController);
@@ -0,0 +1,73 @@
1
+ /**
2
+ * MembershipService — the membership layer's write/read logic, built ENTIRELY on the
3
+ * injected RecordsService facade (which supplies the request principal and runs each op
4
+ * through the engine pipe). It never touches a pool, query runner, or raw SQL — every op
5
+ * is one-or-more facade calls, so the permission gate + anti-forgery write-context +
6
+ * audit + cross-tenant isolation are applied automatically.
7
+ *
8
+ * A `company_membership` is the first-class `(user, company)` scope with `is_active`
9
+ * (exactly one active per user) + `status`. This service owns the ONE invariant the DB
10
+ * cannot express — one-active-per-user — because there is no partial-unique on
11
+ * `cm_is_active`; `setActive` is the guard.
12
+ */
13
+ import { RecordsService, type RecordId, type RecordView } from '@astram/engine';
14
+ import { type PostingVersionBump } from './posting-version-bump.js';
15
+ export declare class MembershipService {
16
+ private readonly records;
17
+ private readonly versionBump?;
18
+ constructor(records: RecordsService, versionBump?: (PostingVersionBump | null) | undefined);
19
+ /**
20
+ * Read a membership's tenant (its `companyUuid`) through the pipe, or `null` when
21
+ * unresolvable (→ no bump; fail-safe, never guesses a tenant). Used to target the
22
+ * permission-version bump at the company whose coarse grants a mutation changed.
23
+ */
24
+ private tenantOf;
25
+ /**
26
+ * Bump the permission version for each DISTINCT tenant in `tenantUuids` (best-effort +
27
+ * fail-safe): the mutation already succeeded, so a bump hiccup must NOT surface as an
28
+ * error that hides a successful membership change — the cache is invalidated by the next
29
+ * legit bump / the facts' natural TTL instead. No collaborator wired → no-op (prior
30
+ * behavior). `null`/empty entries are skipped (never bump a guessed tenant).
31
+ */
32
+ private bumpTenants;
33
+ /**
34
+ * Add a membership card for `(userUuid, companyUuid)`. New memberships are created
35
+ * INACTIVE (`isActive:false`) — activation is an explicit `setActive` call so the
36
+ * one-active invariant is never violated by a fresh add. A duplicate `(user, company)`
37
+ * hits the DB's UNIQUE constraint and the engine's error surfaces to the caller (NOT
38
+ * swallowed here). `createdBy` is stamped server-side (actorId-trusted).
39
+ */
40
+ addMembership(userUuid: string, companyUuid: string): Promise<RecordId>;
41
+ /** List a user's membership cards (tenant-scoped by the pipe). */
42
+ listMemberships(userUuid: string): Promise<readonly RecordView[]>;
43
+ /**
44
+ * Flip which of the user's memberships is active — the ONE-ACTIVE invariant. The service
45
+ * is the guard because the DB has no partial-unique on `cm_is_active`.
46
+ *
47
+ * ⚠️ ORDER IS LOAD-BEARING (fail-safe against a LOGIN LOCKOUT). The engine's `list` has
48
+ * no ORDER BY (PKs are random uuids → arbitrary row order) and each `update` is its OWN
49
+ * transaction (no cross-call atomicity). So this MUST activate the TARGET FIRST, then
50
+ * deactivate the others:
51
+ * - Worst case of a mid-loop failure is then a transient TWO-active window, which is
52
+ * RECOVERABLE — the credential resolver reads `is_active=1 LIMIT 1`, so it still
53
+ * resolves a tenant and login works.
54
+ * - The forbidden alternative (deactivate-old-then-activate-target) can leave a
55
+ * ZERO-active window on a mid-failure → the resolver returns null → the user is
56
+ * LOCKED OUT of login until an admin re-activates. Never do that.
57
+ *
58
+ * Guard: if `membershipUuid` is NOT one of the user's memberships, throw — otherwise the
59
+ * loop would deactivate everything and activate nothing (also a zero-active lockout).
60
+ */
61
+ setActive(membershipUuid: string, userUuid: string): Promise<void>;
62
+ /**
63
+ * Update a membership's mutable non-activity fields (currently `status`). Activity is
64
+ * flipped only via `setActive` to preserve the one-active invariant, so `isActive` is
65
+ * intentionally NOT a patch key here.
66
+ */
67
+ updateMembership(membershipUuid: string, patch: {
68
+ status?: string;
69
+ }): Promise<void>;
70
+ /** Remove a membership card (its postings cascade at the DB via FK ON DELETE CASCADE). */
71
+ removeMembership(membershipUuid: string): Promise<void>;
72
+ }
73
+ //# sourceMappingURL=membership.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"membership.service.d.ts","sourceRoot":"","sources":["../../../src/features/membership/membership.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAChF,OAAO,EAAwB,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE1F,qBACa,iBAAiB;IAE1B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAQxB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;gBARZ,OAAO,EAAE,cAAc,EAQvB,WAAW,CAAC,GAAE,kBAAkB,GAAG,IAAI,aAAA;IAG1D;;;;OAIG;YACW,QAAQ;IAUtB;;;;;;OAMG;YACW,WAAW;IAYzB;;;;;;OAMG;IACH,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAMvE,kEAAkE;IAC5D,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,UAAU,EAAE,CAAC;IAKvE;;;;;;;;;;;;;;;;;OAiBG;IACG,SAAS,CAAC,cAAc,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAqCxE;;;;OAIG;IACH,gBAAgB,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAWnF,0FAA0F;IACpF,gBAAgB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAU9D"}