@astram/identity 0.16.0 → 0.17.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 (128) hide show
  1. package/dist/branch.object.d.ts +9 -1
  2. package/dist/branch.object.d.ts.map +1 -1
  3. package/dist/branch.object.js +14 -1
  4. package/dist/company-branch.object.d.ts +5 -4
  5. package/dist/company-branch.object.d.ts.map +1 -1
  6. package/dist/company-branch.object.js +6 -4
  7. package/dist/company-department.object.d.ts +6 -6
  8. package/dist/company-department.object.d.ts.map +1 -1
  9. package/dist/company-department.object.js +7 -6
  10. package/dist/department.object.d.ts +9 -1
  11. package/dist/department.object.d.ts.map +1 -1
  12. package/dist/department.object.js +14 -1
  13. package/dist/features/access/access-admin.access.d.ts.map +1 -1
  14. package/dist/features/access/access-admin.access.js +8 -1
  15. package/dist/features/access/index.d.ts +1 -0
  16. package/dist/features/access/index.d.ts.map +1 -1
  17. package/dist/features/access/index.js +10 -2
  18. package/dist/features/access/org-structure.access.d.ts +3 -0
  19. package/dist/features/access/org-structure.access.d.ts.map +1 -0
  20. package/dist/features/access/org-structure.access.js +41 -0
  21. package/dist/features/access/user-management.access.d.ts.map +1 -1
  22. package/dist/features/access/user-management.access.js +5 -0
  23. package/dist/features/audit/audit-read.service.d.ts +32 -1
  24. package/dist/features/audit/audit-read.service.d.ts.map +1 -1
  25. package/dist/features/audit/audit-read.service.js +67 -14
  26. package/dist/features/audit/auth-audit-sink.d.ts +19 -0
  27. package/dist/features/audit/auth-audit-sink.d.ts.map +1 -0
  28. package/dist/features/audit/auth-audit-sink.js +24 -0
  29. package/dist/features/audit/directory-label-resolver.d.ts +10 -1
  30. package/dist/features/audit/directory-label-resolver.d.ts.map +1 -1
  31. package/dist/features/audit/directory-label-resolver.js +28 -1
  32. package/dist/features/authz/grant-admin.controller.d.ts +4 -4
  33. package/dist/features/authz/grant-admin.controller.js +4 -4
  34. package/dist/features/authz/grant-admin.service.d.ts +9 -8
  35. package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
  36. package/dist/features/authz/grant-admin.service.js +20 -24
  37. package/dist/features/authz/grant-ceiling.d.ts +35 -16
  38. package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
  39. package/dist/features/authz/grant-ceiling.js +44 -29
  40. package/dist/features/authz/role-binding.d.ts +25 -24
  41. package/dist/features/authz/role-binding.d.ts.map +1 -1
  42. package/dist/features/authz/role-binding.js +31 -44
  43. package/dist/features/identity-feature.module.d.ts +0 -11
  44. package/dist/features/identity-feature.module.d.ts.map +1 -1
  45. package/dist/features/identity-feature.module.js +90 -1
  46. package/dist/features/membership/membership.controller.d.ts.map +1 -1
  47. package/dist/features/membership/membership.controller.js +20 -6
  48. package/dist/features/membership/membership.service.d.ts +16 -10
  49. package/dist/features/membership/membership.service.d.ts.map +1 -1
  50. package/dist/features/membership/membership.service.js +61 -21
  51. package/dist/features/membership/posting.service.d.ts +25 -6
  52. package/dist/features/membership/posting.service.d.ts.map +1 -1
  53. package/dist/features/membership/posting.service.js +63 -16
  54. package/dist/features/org-bootstrap/org-bootstrap-file.d.ts +6 -2
  55. package/dist/features/org-bootstrap/org-bootstrap-file.d.ts.map +1 -1
  56. package/dist/features/org-bootstrap/org-bootstrap-file.js +54 -11
  57. package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts +30 -11
  58. package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts.map +1 -1
  59. package/dist/features/org-bootstrap/org-bootstrap-plan.js +172 -68
  60. package/dist/features/org-bootstrap/org-state.reader.d.ts.map +1 -1
  61. package/dist/features/org-bootstrap/org-state.reader.js +56 -49
  62. package/dist/features/org-bootstrap/run-org-bootstrap.d.ts +6 -1
  63. package/dist/features/org-bootstrap/run-org-bootstrap.d.ts.map +1 -1
  64. package/dist/features/org-bootstrap/run-org-bootstrap.js +81 -92
  65. package/dist/features/org-structure/org-catalog.controller.d.ts +29 -6
  66. package/dist/features/org-structure/org-catalog.controller.d.ts.map +1 -1
  67. package/dist/features/org-structure/org-catalog.controller.js +105 -17
  68. package/dist/features/org-structure/org-catalog.service.d.ts +99 -18
  69. package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -1
  70. package/dist/features/org-structure/org-catalog.service.js +327 -51
  71. package/dist/features/org-structure/org-unit-name.d.ts +31 -0
  72. package/dist/features/org-structure/org-unit-name.d.ts.map +1 -0
  73. package/dist/features/org-structure/org-unit-name.js +50 -0
  74. package/dist/features/role/role.controller.d.ts +28 -13
  75. package/dist/features/role/role.controller.d.ts.map +1 -1
  76. package/dist/features/role/role.controller.js +54 -14
  77. package/dist/features/role/role.service.d.ts +76 -21
  78. package/dist/features/role/role.service.d.ts.map +1 -1
  79. package/dist/features/role/role.service.js +204 -27
  80. package/dist/features/security-policy/security-policy-version-bump.d.ts +41 -0
  81. package/dist/features/security-policy/security-policy-version-bump.d.ts.map +1 -0
  82. package/dist/features/security-policy/security-policy-version-bump.js +65 -0
  83. package/dist/features/security-policy/security-policy.controller.d.ts +51 -0
  84. package/dist/features/security-policy/security-policy.controller.d.ts.map +1 -0
  85. package/dist/features/security-policy/security-policy.controller.js +121 -0
  86. package/dist/features/security-policy/security-policy.service.d.ts +94 -0
  87. package/dist/features/security-policy/security-policy.service.d.ts.map +1 -0
  88. package/dist/features/security-policy/security-policy.service.js +429 -0
  89. package/dist/features/service-account/service-account.service.d.ts +13 -3
  90. package/dist/features/service-account/service-account.service.d.ts.map +1 -1
  91. package/dist/features/service-account/service-account.service.js +48 -10
  92. package/dist/features/service-account/service-account.store.d.ts +11 -1
  93. package/dist/features/service-account/service-account.store.d.ts.map +1 -1
  94. package/dist/features/service-account/service-account.store.js +20 -1
  95. package/dist/features/user/signin-admin.tokens.d.ts +11 -0
  96. package/dist/features/user/signin-admin.tokens.d.ts.map +1 -0
  97. package/dist/features/user/signin-admin.tokens.js +13 -0
  98. package/dist/features/user/user-mfa-status.d.ts +38 -0
  99. package/dist/features/user/user-mfa-status.d.ts.map +1 -0
  100. package/dist/features/user/user-mfa-status.js +47 -0
  101. package/dist/features/user/user-signin-admin.service.d.ts +48 -0
  102. package/dist/features/user/user-signin-admin.service.d.ts.map +1 -0
  103. package/dist/features/user/user-signin-admin.service.js +232 -0
  104. package/dist/features/user/user.controller.d.ts +29 -2
  105. package/dist/features/user/user.controller.d.ts.map +1 -1
  106. package/dist/features/user/user.controller.js +71 -3
  107. package/dist/features/user/user.service.d.ts +40 -14
  108. package/dist/features/user/user.service.d.ts.map +1 -1
  109. package/dist/features/user/user.service.js +143 -10
  110. package/dist/index.d.ts +1 -0
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +3 -1
  113. package/dist/objects.d.ts.map +1 -1
  114. package/dist/objects.js +2 -0
  115. package/dist/role.object.d.ts +1 -1
  116. package/dist/role.object.d.ts.map +1 -1
  117. package/dist/role.object.js +6 -15
  118. package/dist/security-policy.object.d.ts +43 -0
  119. package/dist/security-policy.object.d.ts.map +1 -0
  120. package/dist/security-policy.object.js +107 -0
  121. package/dist/user.object.d.ts +6 -0
  122. package/dist/user.object.d.ts.map +1 -1
  123. package/dist/user.object.js +13 -1
  124. package/migrations/1710000000103-AddUserLoginName.cjs +87 -0
  125. package/migrations/1710000000104-CreateSecurityPolicy.cjs +90 -0
  126. package/migrations/1710000000105-OwnDepartmentsAndBranches.cjs +327 -0
  127. package/migrations/1710000000106-OwnRolesPerCompany.cjs +622 -0
  128. package/package.json +4 -4
@@ -12,11 +12,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.Role = void 0;
13
13
  const engine_1 = require("@astram/engine");
14
14
  const schema_1 = require("./schema");
15
- /** roles — global catalog. */
15
+ /** roles — each role is owned by one company (G-013); names are unique per company. */
16
16
  let Role = class Role {
17
17
  uuid;
18
18
  name;
19
- // The company a role was created for (G-009 T14, SPEC R13); NULL = shared (root-created). Create-only
19
+ // The company a role was created for (G-009 T14, SPEC R13); NULL = ownerless legacy row, root only. Create-only
20
20
  // and server-set (RoleService stamps it from the actor); NOT `trusted`, so the engine never
21
21
  // derives it and root's UI-created roles stay unowned. Changed later only by `transitionRoleOwner`.
22
22
  ownerCompanyUuid;
@@ -45,19 +45,10 @@ exports.Role = Role = __decorate([
45
45
  table: 'roles',
46
46
  schema: schema_1.SCHEMA,
47
47
  access: { plane: 'control', system: true },
48
- // Read-scope wall (ADR-0027, Task 4.1, OQ-2 v1 "bound-in-subtree"): a role is in-scope iff
49
- // it is BOUND (via a membership_posting) within the caller's reachable subtree — the SAME
50
- // "a role in my subtree" derivation the Task-4.2 grant-admin predicate uses (one consistent
51
- // notion). FIXED framework template; the ONLY dynamic values are the `?`-bound subtree node
52
- // ids. DECOUPLED from writes. Root spans all.
53
- // create:'unscoped' (ADR-0034): a NEW role is bound by no posting yet, so the post-insert
54
- // create wall would deny every create; the posting that binds it IS walled.
55
- readScope: {
56
- kind: 'subtreeSubquery',
57
- create: 'unscoped',
58
- pkColumn: 'r_uuid',
59
- subquerySql: `SELECT mp.mp_role_uuid FROM \`${schema_1.SCHEMA}\`.\`membership_posting\` mp JOIN \`${schema_1.SCHEMA}\`.\`company_membership\` cm ON cm.cm_uuid = mp.mp_membership_uuid WHERE cm.cm_company_uuid IN (${engine_1.READ_SCOPE_NODES_TOKEN})`,
60
- },
48
+ // Read-scope wall (G-013 spec §5.C, ADR-0027): a role is in scope iff its OWNER company is in
49
+ // the caller's reachable subtree. Walled create (ADR-0034, no `create` key): the owner is a
50
+ // reached company, so the post-insert wall holds. NULL owner ⇒ root only. Root spans all.
51
+ readScope: { kind: 'nodeColumn', column: 'r_owner_company_uuid' },
61
52
  }),
62
53
  (0, engine_1.PrimaryKey)({ kind: 'uuid', column: 'r_uuid' })
63
54
  ], Role);
@@ -0,0 +1,43 @@
1
+ /**
2
+ * security_policy — one mandatory-2FA policy (auth capabilities R2.1, R2.2, R2.9). Table DDL:
3
+ * `migrations/1710000000104-CreateSecurityPolicy.cjs` (columns mirror the fields below).
4
+ *
5
+ * A policy requires 2FA of everyone in a company subtree (`tenant`), of the holders of a role
6
+ * (`role`, target = role uuid), or of the holders of any key a permission key or grant wildcard
7
+ * covers (`permission`). `platform: true` rows are the root-written floor (no node, apply
8
+ * everywhere). Written only through `SecurityPolicyService` (R2.6 authority, audit, version bump);
9
+ * guard reads the table on the request path recompute (B-1, task 14).
10
+ *
11
+ * `plane: 'control'` — who must use 2FA is security administration, like `user`/`role`; a non-root
12
+ * admin needs `identity:security_policy:*`. `readScope: nodeColumn(sp_scope_node_uuid)` walls a
13
+ * non-root list/read/update/delete to policies whose node is in the caller's subtree (a platform
14
+ * row has no node, so only root sees it), and the create wall re-checks a non-root create.
15
+ *
16
+ * Create-only: the node, the creator's node (D-12), the platform flag and the kind (changing them is a delete plus a create,
17
+ * each with its own authority check). `createdBy` is stamped from the session by the engine.
18
+ */
19
+ export declare class SecurityPolicy {
20
+ uuid: string;
21
+ /** The company node the policy covers (with its subtree); null only for a platform row. */
22
+ scopeNodeUuid: string | null;
23
+ /** Platform floor (root only, R2.2). */
24
+ platform: boolean;
25
+ /** `tenant` | `role` | `permission`. */
26
+ targetKind: string;
27
+ /** Role uuid or permission key / wildcard; null for `tenant`. */
28
+ targetValue: string | null;
29
+ /** 0–14 days (R2.4). */
30
+ graceDays: number;
31
+ /**
32
+ * The node the creator acted at (D-12), stamped by the service from the session's active node
33
+ * (never the body); null for a platform row. Lowering or deleting needs that node or an ancestor.
34
+ */
35
+ createdAtNode: string | null;
36
+ /** Server-stamped author (engine overwrites from the session; never client). */
37
+ createdBy: string;
38
+ /** UTC epoch ms, set by the service. */
39
+ createdAtMs: number;
40
+ /** UTC epoch ms, set by the service on every write. */
41
+ updatedAtMs: number;
42
+ }
43
+ //# sourceMappingURL=security-policy.object.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security-policy.object.d.ts","sourceRoot":"","sources":["../src/security-policy.object.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAQa,cAAc;IAEzB,IAAI,EAAG,MAAM,CAAC;IAEd,2FAA2F;IAE3F,aAAa,EAAG,MAAM,GAAG,IAAI,CAAC;IAE9B,wCAAwC;IAExC,QAAQ,EAAG,OAAO,CAAC;IAEnB,wCAAwC;IAExC,UAAU,EAAG,MAAM,CAAC;IAEpB,iEAAiE;IAEjE,WAAW,EAAG,MAAM,GAAG,IAAI,CAAC;IAE5B,wBAAwB;IAExB,SAAS,EAAG,MAAM,CAAC;IAEnB;;;OAGG;IAEH,aAAa,EAAG,MAAM,GAAG,IAAI,CAAC;IAE9B,gFAAgF;IAEhF,SAAS,EAAG,MAAM,CAAC;IAEnB,wCAAwC;IAExC,WAAW,EAAG,MAAM,CAAC;IAErB,uDAAuD;IAEvD,WAAW,EAAG,MAAM,CAAC;CACtB"}
@@ -0,0 +1,107 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.SecurityPolicy = void 0;
13
+ const engine_1 = require("@astram/engine");
14
+ const schema_1 = require("./schema");
15
+ /**
16
+ * security_policy — one mandatory-2FA policy (auth capabilities R2.1, R2.2, R2.9). Table DDL:
17
+ * `migrations/1710000000104-CreateSecurityPolicy.cjs` (columns mirror the fields below).
18
+ *
19
+ * A policy requires 2FA of everyone in a company subtree (`tenant`), of the holders of a role
20
+ * (`role`, target = role uuid), or of the holders of any key a permission key or grant wildcard
21
+ * covers (`permission`). `platform: true` rows are the root-written floor (no node, apply
22
+ * everywhere). Written only through `SecurityPolicyService` (R2.6 authority, audit, version bump);
23
+ * guard reads the table on the request path recompute (B-1, task 14).
24
+ *
25
+ * `plane: 'control'` — who must use 2FA is security administration, like `user`/`role`; a non-root
26
+ * admin needs `identity:security_policy:*`. `readScope: nodeColumn(sp_scope_node_uuid)` walls a
27
+ * non-root list/read/update/delete to policies whose node is in the caller's subtree (a platform
28
+ * row has no node, so only root sees it), and the create wall re-checks a non-root create.
29
+ *
30
+ * Create-only: the node, the creator's node (D-12), the platform flag and the kind (changing them is a delete plus a create,
31
+ * each with its own authority check). `createdBy` is stamped from the session by the engine.
32
+ */
33
+ let SecurityPolicy = class SecurityPolicy {
34
+ uuid;
35
+ /** The company node the policy covers (with its subtree); null only for a platform row. */
36
+ scopeNodeUuid;
37
+ /** Platform floor (root only, R2.2). */
38
+ platform;
39
+ /** `tenant` | `role` | `permission`. */
40
+ targetKind;
41
+ /** Role uuid or permission key / wildcard; null for `tenant`. */
42
+ targetValue;
43
+ /** 0–14 days (R2.4). */
44
+ graceDays;
45
+ /**
46
+ * The node the creator acted at (D-12), stamped by the service from the session's active node
47
+ * (never the body); null for a platform row. Lowering or deleting needs that node or an ancestor.
48
+ */
49
+ createdAtNode;
50
+ /** Server-stamped author (engine overwrites from the session; never client). */
51
+ createdBy;
52
+ /** UTC epoch ms, set by the service. */
53
+ createdAtMs;
54
+ /** UTC epoch ms, set by the service on every write. */
55
+ updatedAtMs;
56
+ };
57
+ exports.SecurityPolicy = SecurityPolicy;
58
+ __decorate([
59
+ (0, engine_1.Field)({ column: 'sp_uuid', type: 'uuid', role: 'primaryKey' }),
60
+ __metadata("design:type", String)
61
+ ], SecurityPolicy.prototype, "uuid", void 0);
62
+ __decorate([
63
+ (0, engine_1.Field)({ column: 'sp_scope_node_uuid', type: 'uuid', immutable: true }),
64
+ __metadata("design:type", Object)
65
+ ], SecurityPolicy.prototype, "scopeNodeUuid", void 0);
66
+ __decorate([
67
+ (0, engine_1.Field)({ column: 'sp_platform', type: 'boolean', immutable: true }),
68
+ __metadata("design:type", Boolean)
69
+ ], SecurityPolicy.prototype, "platform", void 0);
70
+ __decorate([
71
+ (0, engine_1.Field)({ column: 'sp_target_kind', type: 'string', required: true, immutable: true }),
72
+ __metadata("design:type", String)
73
+ ], SecurityPolicy.prototype, "targetKind", void 0);
74
+ __decorate([
75
+ (0, engine_1.Field)({ column: 'sp_target_value', type: 'string' }),
76
+ __metadata("design:type", Object)
77
+ ], SecurityPolicy.prototype, "targetValue", void 0);
78
+ __decorate([
79
+ (0, engine_1.Field)({ column: 'sp_grace_days', type: 'number', required: true }),
80
+ __metadata("design:type", Number)
81
+ ], SecurityPolicy.prototype, "graceDays", void 0);
82
+ __decorate([
83
+ (0, engine_1.Field)({ column: 'sp_created_at_node', type: 'uuid', immutable: true }),
84
+ __metadata("design:type", Object)
85
+ ], SecurityPolicy.prototype, "createdAtNode", void 0);
86
+ __decorate([
87
+ (0, engine_1.Field)({ column: 'sp_created_by', type: 'uuid', trusted: 'actorId' }),
88
+ __metadata("design:type", String)
89
+ ], SecurityPolicy.prototype, "createdBy", void 0);
90
+ __decorate([
91
+ (0, engine_1.Field)({ column: 'sp_created_at_ms', type: 'number', required: true }),
92
+ __metadata("design:type", Number)
93
+ ], SecurityPolicy.prototype, "createdAtMs", void 0);
94
+ __decorate([
95
+ (0, engine_1.Field)({ column: 'sp_updated_at_ms', type: 'number', required: true }),
96
+ __metadata("design:type", Number)
97
+ ], SecurityPolicy.prototype, "updatedAtMs", void 0);
98
+ exports.SecurityPolicy = SecurityPolicy = __decorate([
99
+ (0, engine_1.ObjectType)({
100
+ typeKey: 'security_policy',
101
+ table: 'security_policy',
102
+ schema: schema_1.SCHEMA,
103
+ access: { plane: 'control', system: true },
104
+ readScope: { kind: 'nodeColumn', column: 'sp_scope_node_uuid' },
105
+ }),
106
+ (0, engine_1.PrimaryKey)({ kind: 'uuid', column: 'sp_uuid' })
107
+ ], SecurityPolicy);
@@ -7,6 +7,12 @@ export declare class User {
7
7
  uuid: string;
8
8
  username: string;
9
9
  email: string;
10
+ /**
11
+ * The sign-in handle (auth capabilities R1.2, migration 103): lower-case `[a-z0-9._-]{3,64}`,
12
+ * globally unique, NULL = none (sign in by email only). Set only by an admin (dominance,
13
+ * `UserService`) or `bootstrap:org`; never by `/account/*`. Not the display name (`username`).
14
+ */
15
+ loginName?: string | null;
10
16
  status: string;
11
17
  createdAt: string;
12
18
  }
@@ -1 +1 @@
1
- {"version":3,"file":"user.object.d.ts","sourceRoot":"","sources":["../src/user.object.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,qBA0Ba,IAAI;IAEf,IAAI,EAAG,MAAM,CAAC;IAGd,QAAQ,EAAG,MAAM,CAAC;IAGlB,KAAK,EAAG,MAAM,CAAC;IAGf,MAAM,EAAG,MAAM,CAAC;IAGhB,SAAS,EAAG,MAAM,CAAC;CACpB"}
1
+ {"version":3,"file":"user.object.d.ts","sourceRoot":"","sources":["../src/user.object.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,qBA4Ba,IAAI;IAEf,IAAI,EAAG,MAAM,CAAC;IAGd,QAAQ,EAAG,MAAM,CAAC;IAGlB,KAAK,EAAG,MAAM,CAAC;IAEf;;;;OAIG;IAEH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAG1B,MAAM,EAAG,MAAM,CAAC;IAGhB,SAAS,EAAG,MAAM,CAAC;CACpB"}
@@ -21,6 +21,12 @@ let User = class User {
21
21
  uuid;
22
22
  username;
23
23
  email;
24
+ /**
25
+ * The sign-in handle (auth capabilities R1.2, migration 103): lower-case `[a-z0-9._-]{3,64}`,
26
+ * globally unique, NULL = none (sign in by email only). Set only by an admin (dominance,
27
+ * `UserService`) or `bootstrap:org`; never by `/account/*`. Not the display name (`username`).
28
+ */
29
+ loginName;
24
30
  status;
25
31
  createdAt;
26
32
  };
@@ -37,6 +43,10 @@ __decorate([
37
43
  (0, engine_1.Field)({ column: 'u_email', type: 'string' }),
38
44
  __metadata("design:type", String)
39
45
  ], User.prototype, "email", void 0);
46
+ __decorate([
47
+ (0, engine_1.Field)({ column: 'u_login_name', type: 'string' }),
48
+ __metadata("design:type", Object)
49
+ ], User.prototype, "loginName", void 0);
40
50
  __decorate([
41
51
  (0, engine_1.Field)({ column: 'u_status', type: 'string', role: 'status' }),
42
52
  __metadata("design:type", String)
@@ -56,7 +66,9 @@ exports.User = User = __decorate([
56
66
  access: { plane: 'control', system: true },
57
67
  // G-010 R11 (ADR-0038): `invite` (`identity:user:invite`, POST /identity/users/:uuid/invite) next
58
68
  // to CRUD. Roles holding `identity:*` or `identity:user:*` gain it on upgrade; exact keys do not.
59
- actions: ['invite'],
69
+ // Auth capabilities R11.2 (task 18): the admin sign-in controls `reset_mfa`, `unlock`,
70
+ // `end_sessions` (POST /identity/users/:uuid/{reset-mfa,unlock,end-sessions}), same rule.
71
+ actions: ['invite', 'reset_mfa', 'unlock', 'end_sessions'],
60
72
  // Read-scope wall (ADR-0027, Task 4.1): a user has no company column (they belong to many
61
73
  // companies via `company_membership`) — a user is in-scope iff they have a membership in
62
74
  // the caller's reachable subtree. A multi-tenant user correctly appears for each subtree
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Framework identity migration — `users.u_login_name` (auth capabilities R1.2, R1.4; AC-5).
3
+ *
4
+ * WHY: sign-in by username needs a login handle that is not the display name (`u_username` stays
5
+ * a display name, D-1). The handle is `[a-z0-9._-]{3,64}`, stored lower-case, never containing
6
+ * `@` (so it can never equal an email). It is globally unique, like `u_email`. Login resolves a
7
+ * handle by joining `users` to `identity_credential` on `ic_principal_id`, which had no index.
8
+ *
9
+ * Steps (idempotent; a half-applied state is completed, nothing is dropped or overwritten):
10
+ * 1. ADD `u_login_name VARCHAR(64) NULL` (collation `utf8mb4_unicode_ci`, as the table) when
11
+ * absent. Every existing row starts NULL, so no existing data can clash (R1.9).
12
+ * 2. ADD UNIQUE `uq_users_login_name` when absent (NULLs may repeat). A non-unique index under
13
+ * that name is refused rather than trusted.
14
+ * 3. ADD `idx_identity_credential_principal` on `identity_credential (ic_principal_id)` when
15
+ * absent (non-unique; "one credential per account" stays the provisioner's locked check).
16
+ *
17
+ * `down` drops both indexes, then the column, each with IF EXISTS (a second run, or a database
18
+ * that never had them, does nothing).
19
+ *
20
+ * Database names come from `ctx.schemas` (namespace or single layout); no literal names, no ids.
21
+ */
22
+
23
+ const { frameworkSchemasForMigration } = require('@astram/core');
24
+
25
+ const MIGRATION = '1710000000103-AddUserLoginName';
26
+ const COLUMN = 'u_login_name';
27
+ const UNIQUE_INDEX = 'uq_users_login_name';
28
+ const PRINCIPAL_INDEX = 'idx_identity_credential_principal';
29
+
30
+ /** `null` = no index of that name; otherwise whether it is UNIQUE. */
31
+ async function indexState(query, schema, table, index) {
32
+ const rows = await query(
33
+ `SELECT NON_UNIQUE AS non_unique FROM information_schema.STATISTICS
34
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND INDEX_NAME = ? LIMIT 1`,
35
+ [schema, table, index],
36
+ );
37
+ if (!Array.isArray(rows) || rows.length === 0) return null;
38
+ return { unique: Number(rows[0].non_unique) === 0 };
39
+ }
40
+
41
+ async function up(ctx) {
42
+ const { query } = ctx;
43
+ const schemas = frameworkSchemasForMigration(ctx, process.env);
44
+ const APP = schemas.app;
45
+ const IDENTITY = schemas.identity;
46
+
47
+ const columns = await query(
48
+ `SELECT 1 AS n FROM information_schema.COLUMNS
49
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'users' AND COLUMN_NAME = ? LIMIT 1`,
50
+ [APP, COLUMN],
51
+ );
52
+ if (!Array.isArray(columns) || columns.length === 0) {
53
+ await query(
54
+ `ALTER TABLE \`${APP}\`.\`users\`
55
+ ADD COLUMN ${COLUMN} VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL`,
56
+ );
57
+ }
58
+
59
+ const unique = await indexState(query, APP, 'users', UNIQUE_INDEX);
60
+ if (unique === null) {
61
+ await query(`CREATE UNIQUE INDEX ${UNIQUE_INDEX} ON \`${APP}\`.\`users\` (${COLUMN})`);
62
+ } else if (!unique.unique) {
63
+ throw new Error(
64
+ `${MIGRATION}: ${APP}.users has a NON-unique index named ${UNIQUE_INDEX}; ` +
65
+ 'drop or rename it, then re-run astram-migrate.',
66
+ );
67
+ }
68
+
69
+ const principal = await indexState(query, IDENTITY, 'identity_credential', PRINCIPAL_INDEX);
70
+ if (principal === null) {
71
+ await query(
72
+ `CREATE INDEX ${PRINCIPAL_INDEX} ON \`${IDENTITY}\`.\`identity_credential\` (ic_principal_id)`,
73
+ );
74
+ }
75
+ }
76
+
77
+ async function down(ctx) {
78
+ const { query } = ctx;
79
+ const schemas = frameworkSchemasForMigration(ctx, process.env);
80
+ await query(
81
+ `DROP INDEX IF EXISTS ${PRINCIPAL_INDEX} ON \`${schemas.identity}\`.\`identity_credential\``,
82
+ );
83
+ await query(`DROP INDEX IF EXISTS ${UNIQUE_INDEX} ON \`${schemas.app}\`.\`users\``);
84
+ await query(`ALTER TABLE \`${schemas.app}\`.\`users\` DROP COLUMN IF EXISTS ${COLUMN}`);
85
+ }
86
+
87
+ module.exports = { up, down };
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Framework identity migration — mandatory 2FA policy storage (auth capabilities R2.4, R2.9, D-2;
3
+ * AC-8, AC-10).
4
+ *
5
+ * 1. `<app>.security_policy` — one row per policy, managed only through
6
+ * `/identity/security-policies` (identity object type `security_policy`, engine pipe). It lives
7
+ * in the app database with every other identity object type (the engine's `FRAMEWORK_SCHEMA.app`
8
+ * token); guard reads it with its runtime account, which already holds DML there.
9
+ * - sp_scope_node_uuid: the company (scope node) the policy applies to, with its subtree.
10
+ * NULL only for a platform-floor row (sp_platform = 1, root only, R2.2), which applies
11
+ * everywhere. The CHECK holds the pairing; a deleted company takes its policies with it.
12
+ * - sp_target_kind: 'tenant' (everyone in the subtree) | 'role' | 'permission'.
13
+ * - sp_target_value: the role uuid or the permission key (wildcards allowed, e.g.
14
+ * `identity:*`); NULL exactly when the kind is 'tenant'.
15
+ * - sp_grace_days: 0–14 (R2.4), default 0.
16
+ * - sp_created_at_node: the node the creator acted at (D-12), stamped from the session; NULL
17
+ * for a platform row (or a row written before the column, judged at its scope node). No
18
+ * FK: a deleted creator node leaves no actor "at or above" it, so only root can lower it.
19
+ * Indexes serve guard's reads (B-1): by scope node, by target, and the platform floor.
20
+ * 2. `<identity>.mfa_requirement_state` — the per-user grace clock (R2.4): when the user was first
21
+ * found required and not enrolled. Deleted when they enrol or stop being required. Credential
22
+ * adjacent, written by guard at lifecycle events; a deleted user takes the row with it.
23
+ * 3. `<identity>.session.s_restriction` — the restricted-session reason (D-2: 'reenroll' |
24
+ * 'mfa_enrollment'), NULL = unrestricted. `s_must_reenroll` (migration 019) is kept and still
25
+ * read, so an upgrade needs no back-fill.
26
+ *
27
+ * Idempotent (CREATE TABLE IF NOT EXISTS / ADD COLUMN IF NOT EXISTS); `down` drops the column and
28
+ * both tables with IF EXISTS. No rows are written, no ids are hard-coded. Database names come from
29
+ * `ctx.schemas` (namespace or single layout, ADR-0037/ADR-0041); both tables are listed in core
30
+ * `FRAMEWORK_TABLES`.
31
+ */
32
+
33
+ const { frameworkSchemasForMigration } = require('@astram/core');
34
+
35
+ async function up(ctx) {
36
+ const { query } = ctx;
37
+ const { app: APP, identity: IDENTITY } = frameworkSchemasForMigration(ctx, process.env);
38
+
39
+ await query(
40
+ `CREATE TABLE IF NOT EXISTS \`${APP}\`.\`security_policy\` (
41
+ sp_uuid VARCHAR(36) NOT NULL,
42
+ sp_scope_node_uuid VARCHAR(36) NULL,
43
+ sp_platform TINYINT(1) NOT NULL DEFAULT 0,
44
+ sp_target_kind VARCHAR(16) NOT NULL,
45
+ sp_target_value VARCHAR(255) NULL,
46
+ sp_grace_days TINYINT UNSIGNED NOT NULL DEFAULT 0,
47
+ sp_created_at_node VARCHAR(36) NULL,
48
+ sp_created_by VARCHAR(36) NOT NULL,
49
+ sp_created_at_ms BIGINT NOT NULL,
50
+ sp_updated_at_ms BIGINT NOT NULL,
51
+ PRIMARY KEY (sp_uuid),
52
+ INDEX idx_security_policy_scope (sp_scope_node_uuid),
53
+ INDEX idx_security_policy_target (sp_target_kind, sp_target_value),
54
+ INDEX idx_security_policy_platform (sp_platform),
55
+ CONSTRAINT ck_security_policy_grace CHECK (sp_grace_days BETWEEN 0 AND 14),
56
+ CONSTRAINT ck_security_policy_kind CHECK (sp_target_kind IN ('tenant', 'role', 'permission')),
57
+ CONSTRAINT ck_security_policy_scope CHECK ((sp_platform = 1 AND sp_scope_node_uuid IS NULL) OR (sp_platform = 0 AND sp_scope_node_uuid IS NOT NULL)),
58
+ CONSTRAINT ck_security_policy_target CHECK ((sp_target_kind = 'tenant' AND sp_target_value IS NULL) OR (sp_target_kind <> 'tenant' AND sp_target_value IS NOT NULL)),
59
+ CONSTRAINT FK_security_policy_scope_node
60
+ FOREIGN KEY (sp_scope_node_uuid) REFERENCES \`${APP}\`.\`companies\` (c_uuid)
61
+ ON DELETE CASCADE
62
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
63
+ );
64
+
65
+ await query(
66
+ `CREATE TABLE IF NOT EXISTS \`${IDENTITY}\`.\`mfa_requirement_state\` (
67
+ mrs_principal_id VARCHAR(36) NOT NULL,
68
+ mrs_required_since_ms BIGINT NOT NULL,
69
+ PRIMARY KEY (mrs_principal_id),
70
+ CONSTRAINT FK_mfa_requirement_state_user
71
+ FOREIGN KEY (mrs_principal_id) REFERENCES \`${APP}\`.\`users\` (u_uuid)
72
+ ON DELETE CASCADE
73
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
74
+ );
75
+
76
+ await query(
77
+ `ALTER TABLE \`${IDENTITY}\`.\`session\`
78
+ ADD COLUMN IF NOT EXISTS \`s_restriction\` VARCHAR(32) NULL`,
79
+ );
80
+ }
81
+
82
+ async function down(ctx) {
83
+ const { query } = ctx;
84
+ const { app: APP, identity: IDENTITY } = frameworkSchemasForMigration(ctx, process.env);
85
+ await query(`ALTER TABLE \`${IDENTITY}\`.\`session\` DROP COLUMN IF EXISTS \`s_restriction\``);
86
+ await query(`DROP TABLE IF EXISTS \`${IDENTITY}\`.\`mfa_requirement_state\``);
87
+ await query(`DROP TABLE IF EXISTS \`${APP}\`.\`security_policy\``);
88
+ }
89
+
90
+ module.exports = { up, down };