@astram/identity 0.1.1 → 0.2.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 (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 +4 -3
@@ -0,0 +1,130 @@
1
+ /**
2
+ * ⛔ DEV/TEST-ONLY coherent identity seed — a no-op in production.
3
+ *
4
+ * Creates ONE id-aligned chain so a fresh app has a real login that resolves end-to-end:
5
+ * company 'Astram Dev Co' (shield_app.companies)
6
+ * user 'admin' (shield_app.users) → the principal
7
+ * role 'Administrator' (shield_app.roles)
8
+ * membership (active) + posting (open window) → links user→company→role
9
+ * credential admin@astram.test / Astram@dev-123 (shield_identity.identity_credential)
10
+ * ic_principal_id = users.u_uuid, ic_tenant_id = companies.c_uuid, ic_roles = [role uuid]
11
+ * grant role → 'identity:*' (shield_authz.role_permission) → lights the admin nav
12
+ *
13
+ * The bug this fixes: the old alice/bob/carol fixtures seeded CREDENTIALS with SYNTHETIC ids
14
+ * (principal-alice/tenant-a) and NO org rows, so /auth/me + auth-facts had nothing to resolve.
15
+ * Here every id is generated once and reused across shield_app + shield_identity + shield_authz.
16
+ *
17
+ * GATE: runs only when NODE_ENV=development OR ASTRAM_SEED_DEV=1. Otherwise a no-op.
18
+ * IDEMPOTENT: every insert is INSERT ... SELECT ... WHERE NOT EXISTS on the BUSINESS key; a
19
+ * second run inserts ZERO rows. No hard-coded UUIDs (UUID() for new PKs; dependents resolved
20
+ * by business key). down() removes the chain by business key.
21
+ */
22
+
23
+ const APP = 'shield_app';
24
+ const IDENT = 'shield_identity';
25
+ const AUTHZ = 'shield_authz';
26
+
27
+ const COMPANY = 'Astram Dev Co';
28
+ const USERNAME = 'admin';
29
+ const EMAIL = 'admin@astram.test';
30
+ const ROLE = 'Administrator';
31
+ const HINT = 'admin@astram.test';
32
+ // Argon2id encoded hash of 'Astram@dev-123' (dev-only; generated via the framework hasher — Step 1;
33
+ // params match Argon2idPasswordHasher's DEFAULT_PARAMS: memoryCost 19456, timeCost 2, parallelism 1).
34
+ const PASSWORD_HASH = '$argon2id$v=19$m=19456,t=2,p=1$vwqMTNRLVkrwAp4vGlZrYA$U+/nIGzGy+w2KAlMG5epvHqIyjV2hxNaVYDxJcbO6uc';
35
+ const GRANT_KEY = 'identity:*';
36
+
37
+ function seedEnabled() {
38
+ return process.env.NODE_ENV === 'development' || process.env.ASTRAM_SEED_DEV === '1';
39
+ }
40
+
41
+ async function up({ query }) {
42
+ if (!seedEnabled()) return; // production / non-dev → no-op
43
+
44
+ // 1) company (tenant)
45
+ await query(
46
+ `INSERT INTO \`${APP}\`.\`companies\` (c_uuid, c_name, c_status)
47
+ SELECT UUID(), ?, 'active'
48
+ WHERE NOT EXISTS (SELECT 1 FROM \`${APP}\`.\`companies\` WHERE c_name = ?)`,
49
+ [COMPANY, COMPANY],
50
+ );
51
+ // 2) user (principal)
52
+ await query(
53
+ `INSERT INTO \`${APP}\`.\`users\` (u_uuid, u_username, u_email, u_status)
54
+ SELECT UUID(), ?, ?, 'active'
55
+ WHERE NOT EXISTS (SELECT 1 FROM \`${APP}\`.\`users\` WHERE u_username = ?)`,
56
+ [USERNAME, EMAIL, USERNAME],
57
+ );
58
+ // 3) role
59
+ await query(
60
+ `INSERT INTO \`${APP}\`.\`roles\` (r_uuid, r_name)
61
+ SELECT UUID(), ?
62
+ WHERE NOT EXISTS (SELECT 1 FROM \`${APP}\`.\`roles\` WHERE r_name = ?)`,
63
+ [ROLE, ROLE],
64
+ );
65
+
66
+ // Resolve the ids we just ensured (by business key — never hard-coded).
67
+ const [company] = await query(`SELECT c_uuid FROM \`${APP}\`.\`companies\` WHERE c_name = ? LIMIT 1`, [COMPANY]);
68
+ const [user] = await query(`SELECT u_uuid FROM \`${APP}\`.\`users\` WHERE u_username = ? LIMIT 1`, [USERNAME]);
69
+ const [role] = await query(`SELECT r_uuid FROM \`${APP}\`.\`roles\` WHERE r_name = ? LIMIT 1`, [ROLE]);
70
+ const companyId = company.c_uuid, userId = user.u_uuid, roleId = role.r_uuid;
71
+
72
+ // 4) membership (active tenant pointer)
73
+ await query(
74
+ `INSERT INTO \`${APP}\`.\`company_membership\` (cm_uuid, cm_user_uuid, cm_company_uuid, cm_is_active, cm_status)
75
+ SELECT UUID(), ?, ?, 1, 'active'
76
+ WHERE NOT EXISTS (SELECT 1 FROM \`${APP}\`.\`company_membership\` WHERE cm_user_uuid = ? AND cm_company_uuid = ?)`,
77
+ [userId, companyId, userId, companyId],
78
+ );
79
+ const [membership] = await query(
80
+ `SELECT cm_uuid FROM \`${APP}\`.\`company_membership\` WHERE cm_user_uuid = ? AND cm_company_uuid = ? LIMIT 1`,
81
+ [userId, companyId],
82
+ );
83
+ const membershipId = membership.cm_uuid;
84
+
85
+ // 5) posting (open window: from/until null → always in-effect)
86
+ await query(
87
+ `INSERT INTO \`${APP}\`.\`membership_posting\` (mp_uuid, mp_membership_uuid, mp_role_uuid, mp_effective_from_ms, mp_effective_until_ms)
88
+ SELECT UUID(), ?, ?, NULL, NULL
89
+ WHERE NOT EXISTS (SELECT 1 FROM \`${APP}\`.\`membership_posting\` WHERE mp_membership_uuid = ? AND mp_role_uuid = ?)`,
90
+ [membershipId, roleId, membershipId, roleId],
91
+ );
92
+
93
+ // 6) credential (shield_identity) — ids ALIGNED to the org rows above
94
+ const nowMs = 1_752_192_000_000; // fixed dev timestamp (no Date.now — keeps re-runs identical)
95
+ await query(
96
+ `INSERT INTO \`${IDENT}\`.\`identity_credential\`
97
+ (ic_identity_hint, ic_password_hash, ic_principal_id, ic_tenant_id, ic_roles, ic_created_at_ms, ic_updated_at_ms)
98
+ SELECT ?, ?, ?, ?, ?, ?, ?
99
+ WHERE NOT EXISTS (SELECT 1 FROM \`${IDENT}\`.\`identity_credential\` WHERE ic_identity_hint = ?)`,
100
+ [HINT, PASSWORD_HASH, userId, companyId, JSON.stringify([roleId]), nowMs, nowMs, HINT],
101
+ );
102
+
103
+ // 7) grant (shield_authz) — one wildcard row covers identity:company/user/role:read
104
+ await query(
105
+ `INSERT INTO \`${AUTHZ}\`.\`role_permission\` (rp_uuid, rp_role_uuid, rp_perm_key)
106
+ SELECT UUID(), ?, ?
107
+ WHERE NOT EXISTS (SELECT 1 FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? AND rp_perm_key = ?)`,
108
+ [roleId, GRANT_KEY, roleId, GRANT_KEY],
109
+ );
110
+ }
111
+
112
+ async function down({ query }) {
113
+ // Remove by business key (safe whether or not seed ran). Order: dependents first.
114
+ const [role] = await query(`SELECT r_uuid FROM \`${APP}\`.\`roles\` WHERE r_name = ? LIMIT 1`, [ROLE]);
115
+ if (role) {
116
+ await query(`DELETE FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? AND rp_perm_key = ?`, [role.r_uuid, GRANT_KEY]);
117
+ }
118
+ await query(`DELETE FROM \`${IDENT}\`.\`identity_credential\` WHERE ic_identity_hint = ?`, [HINT]);
119
+ // membership_posting + company_membership cascade on user/company delete; remove user/company/role.
120
+ await query(`DELETE mp FROM \`${APP}\`.\`membership_posting\` mp
121
+ JOIN \`${APP}\`.\`company_membership\` cm ON cm.cm_uuid = mp.mp_membership_uuid
122
+ JOIN \`${APP}\`.\`users\` u ON u.u_uuid = cm.cm_user_uuid WHERE u.u_username = ?`, [USERNAME]);
123
+ await query(`DELETE cm FROM \`${APP}\`.\`company_membership\` cm
124
+ JOIN \`${APP}\`.\`users\` u ON u.u_uuid = cm.cm_user_uuid WHERE u.u_username = ?`, [USERNAME]);
125
+ await query(`DELETE FROM \`${APP}\`.\`users\` WHERE u_username = ?`, [USERNAME]);
126
+ await query(`DELETE FROM \`${APP}\`.\`roles\` WHERE r_name = ?`, [ROLE]);
127
+ await query(`DELETE FROM \`${APP}\`.\`companies\` WHERE c_name = ?`, [COMPANY]);
128
+ }
129
+
130
+ module.exports = { up, down };
@@ -0,0 +1,212 @@
1
+ /**
2
+ * ⛔ DEV/TEST-ONLY seed — a no-op in production. Extends `...099-SeedDevIdentity.cjs` with a
3
+ * SECOND tenant + a platform root, so Phase 6's cross-tenant offer→accept→revoke flow can be
4
+ * exercised across two REAL tenants out of the box (tenant-a offers, tenant-b accepts).
5
+ *
6
+ * PART A — tenant-b (mirrors ALL 7 steps of `...099`, id-aligned to a second chain):
7
+ * company 'Astram Tenant B' (shield_app.companies)
8
+ * user 'admin-b' (shield_app.users) → the principal
9
+ * role ITS OWN 'Administrator B' role (created by THIS migration — NOT the shared
10
+ * 'Administrator' role `...099` created for tenant-a). Removes the shared-role
11
+ * fragility a Phase-6 review flagged: admin-b's tenant-b authority is now
12
+ * self-evident from its OWN role + membership, not merely a membership join
13
+ * reusing a global role. 'Administrator B' is granted `identity:*` directly
14
+ * (mirrors tenant-a's 'Administrator' → `identity:*` pattern), which covers
15
+ * `identity:cross_tenant_grant:accept` via the same wildcard expansion, so
16
+ * admin-b can ACCEPT a cross-tenant grant in tenant-b out of the box.
17
+ * membership (active) + posting (open window) → links admin-b→company-b→Administrator B
18
+ * credential admin-b@astram.test / Astram@dev-123 (SAME dev hash as `...099`'s admin)
19
+ * ic_principal_id = users.u_uuid, ic_tenant_id = companies.c_uuid, ic_roles = [Administrator B role uuid]
20
+ *
21
+ * PART B — root (mirrors astram3 `1720000000014-DesignateCappedRoot.js`):
22
+ * user 'root', u_is_platform_superuser = 1 (idempotent: INSERT-if-absent, then UPDATE the flag
23
+ * to 1 so re-runs always converge). Root gets NO data-plane wildcard grant — root's authority
24
+ * for control-plane object types (like `cross_tenant_grant`) comes from `isRoot`, not a role
25
+ * grant (mirrors astram3: root operates via the cap, not grants).
26
+ * credential root@astram.test / Astram@dev-123 (SAME dev hash), ic_tenant_id = tenant-a's
27
+ * company id (root still logs into a session scoped to SOME tenant), ic_roles = [] (root's
28
+ * power is the su flag, not a role).
29
+ *
30
+ * ⚠️ RUNTIME NOTE — `rootIds` allowlist (Task 9 / consumer-wiring concern, NOT this migration):
31
+ * The framework resolves `isRoot` at runtime from the `rootIds` list the CONSUMER app passes
32
+ * to `ShieldModule.forRoot({ rootIds })` / `IdentityFeatureModule.forRoot({ rootIds })` — NOT
33
+ * purely from `u_is_platform_superuser` in the DB. Seeding `u_is_platform_superuser = 1` here
34
+ * makes root a superuser IN THE DATABASE, but the consuming app (e.g. astram4) MUST ALSO add
35
+ * this root user's uuid to its `rootIds` config for `isRoot` to be true at request time. This
36
+ * migration CANNOT know that uuid ahead of time (it's `UUID()`-generated) — resolve it via:
37
+ * SELECT u_uuid FROM shield_app.users WHERE u_username = 'root';
38
+ * and wire the result into the consumer's `rootIds` array.
39
+ *
40
+ * GATE: runs only when NODE_ENV=development OR ASTRAM_SEED_DEV=1. Otherwise a no-op.
41
+ * IDEMPOTENT: every insert is INSERT ... SELECT ... WHERE NOT EXISTS on the BUSINESS key; the
42
+ * root flag UPDATE is naturally idempotent (converges to 1 every run). A second run inserts
43
+ * ZERO new rows. No hard-coded UUIDs (UUID() for new PKs; dependents resolved by business key).
44
+ * down() removes both chains by business key (including 'Administrator B' + its identity:*
45
+ * grant) — leaves the shared 'Administrator' role and tenant-a (from `...099`) untouched.
46
+ *
47
+ * Timestamp 1710000000100 sorts strictly after the existing dev seed (...099) and the ctg perm
48
+ * seed (...012) — runs LAST. It creates its own 'Administrator B' role (independent of the
49
+ * shared 'Administrator' role's grants), so it no longer depends on `...012`'s coarse-key
50
+ * expansion for admin-b's accept authority.
51
+ */
52
+
53
+ const APP = 'shield_app';
54
+ const IDENT = 'shield_identity';
55
+ const AUTHZ = 'shield_authz';
56
+
57
+ // --- Part A: tenant-b ---
58
+ const COMPANY_B = 'Astram Tenant B';
59
+ const USERNAME_B = 'admin-b';
60
+ const EMAIL_B = 'admin-b@astram.test';
61
+ const ROLE_B = 'Administrator B'; // tenant-b's OWN role (not the shared 'Administrator')
62
+ const HINT_B = 'admin-b@astram.test';
63
+ const IDENTITY_WILDCARD_KEY = 'identity:*';
64
+
65
+ // --- Part B: root ---
66
+ const ROOT_USERNAME = 'root';
67
+ const ROOT_EMAIL = 'root@astram.test';
68
+ const ROOT_HINT = 'root@astram.test';
69
+
70
+ // Argon2id encoded hash of 'Astram@dev-123' (dev-only; SAME hash as ...099's admin — reused
71
+ // verbatim so all dev credentials share one known password; params match
72
+ // Argon2idPasswordHasher's DEFAULT_PARAMS: memoryCost 19456, timeCost 2, parallelism 1).
73
+ const PASSWORD_HASH = '$argon2id$v=19$m=19456,t=2,p=1$vwqMTNRLVkrwAp4vGlZrYA$U+/nIGzGy+w2KAlMG5epvHqIyjV2hxNaVYDxJcbO6uc';
74
+
75
+ function seedEnabled() {
76
+ return process.env.NODE_ENV === 'development' || process.env.ASTRAM_SEED_DEV === '1';
77
+ }
78
+
79
+ async function up({ query }) {
80
+ if (!seedEnabled()) return; // production / non-dev → no-op
81
+
82
+ // ── PART A: tenant-b ────────────────────────────────────────────────────────────────────
83
+
84
+ // A1) company-b (tenant)
85
+ await query(
86
+ `INSERT INTO \`${APP}\`.\`companies\` (c_uuid, c_name, c_status)
87
+ SELECT UUID(), ?, 'active'
88
+ WHERE NOT EXISTS (SELECT 1 FROM \`${APP}\`.\`companies\` WHERE c_name = ?)`,
89
+ [COMPANY_B, COMPANY_B],
90
+ );
91
+ // A2) user admin-b (principal)
92
+ await query(
93
+ `INSERT INTO \`${APP}\`.\`users\` (u_uuid, u_username, u_email, u_status)
94
+ SELECT UUID(), ?, ?, 'active'
95
+ WHERE NOT EXISTS (SELECT 1 FROM \`${APP}\`.\`users\` WHERE u_username = ?)`,
96
+ [USERNAME_B, EMAIL_B, USERNAME_B],
97
+ );
98
+
99
+ // Resolve ids by business key (never hard-coded). 'Administrator B' is tenant-b's OWN role
100
+ // (created here, NOT reusing `...099`'s shared 'Administrator') — makes admin-b's isolation
101
+ // self-evident from its own role, not merely from the membership join.
102
+ await query(
103
+ `INSERT INTO \`${APP}\`.\`roles\` (r_uuid, r_name)
104
+ SELECT UUID(), ?
105
+ WHERE NOT EXISTS (SELECT 1 FROM \`${APP}\`.\`roles\` WHERE r_name = ?)`,
106
+ [ROLE_B, ROLE_B],
107
+ );
108
+
109
+ const [companyB] = await query(`SELECT c_uuid FROM \`${APP}\`.\`companies\` WHERE c_name = ? LIMIT 1`, [COMPANY_B]);
110
+ const [userB] = await query(`SELECT u_uuid FROM \`${APP}\`.\`users\` WHERE u_username = ? LIMIT 1`, [USERNAME_B]);
111
+ const [roleB] = await query(`SELECT r_uuid FROM \`${APP}\`.\`roles\` WHERE r_name = ? LIMIT 1`, [ROLE_B]);
112
+ const companyBId = companyB.c_uuid, userBId = userB.u_uuid, roleId = roleB.r_uuid;
113
+
114
+ // A3) membership (active tenant pointer, admin-b → company-b)
115
+ await query(
116
+ `INSERT INTO \`${APP}\`.\`company_membership\` (cm_uuid, cm_user_uuid, cm_company_uuid, cm_is_active, cm_status)
117
+ SELECT UUID(), ?, ?, 1, 'active'
118
+ WHERE NOT EXISTS (SELECT 1 FROM \`${APP}\`.\`company_membership\` WHERE cm_user_uuid = ? AND cm_company_uuid = ?)`,
119
+ [userBId, companyBId, userBId, companyBId],
120
+ );
121
+ const [membershipB] = await query(
122
+ `SELECT cm_uuid FROM \`${APP}\`.\`company_membership\` WHERE cm_user_uuid = ? AND cm_company_uuid = ? LIMIT 1`,
123
+ [userBId, companyBId],
124
+ );
125
+ const membershipBId = membershipB.cm_uuid;
126
+
127
+ // A4) posting (open window: from/until null → always in-effect) — admin-b holds Administrator B IN company-b
128
+ await query(
129
+ `INSERT INTO \`${APP}\`.\`membership_posting\` (mp_uuid, mp_membership_uuid, mp_role_uuid, mp_effective_from_ms, mp_effective_until_ms)
130
+ SELECT UUID(), ?, ?, NULL, NULL
131
+ WHERE NOT EXISTS (SELECT 1 FROM \`${APP}\`.\`membership_posting\` WHERE mp_membership_uuid = ? AND mp_role_uuid = ?)`,
132
+ [membershipBId, roleId, membershipBId, roleId],
133
+ );
134
+
135
+ // A5) credential (shield_identity) — ids ALIGNED to company-b/admin-b above
136
+ const nowMs = 1_752_192_000_000; // fixed dev timestamp (no Date.now — keeps re-runs identical)
137
+ await query(
138
+ `INSERT INTO \`${IDENT}\`.\`identity_credential\`
139
+ (ic_identity_hint, ic_password_hash, ic_principal_id, ic_tenant_id, ic_roles, ic_created_at_ms, ic_updated_at_ms)
140
+ SELECT ?, ?, ?, ?, ?, ?, ?
141
+ WHERE NOT EXISTS (SELECT 1 FROM \`${IDENT}\`.\`identity_credential\` WHERE ic_identity_hint = ?)`,
142
+ [HINT_B, PASSWORD_HASH, userBId, companyBId, JSON.stringify([roleId]), nowMs, nowMs, HINT_B],
143
+ );
144
+
145
+ // A6) grant 'Administrator B' the identity:* wildcard — mirrors tenant-a's 'Administrator'
146
+ // → identity:* pattern (`...099`), so admin-b is a full admin in tenant-b via its OWN role.
147
+ // This wildcard also covers `identity:cross_tenant_grant:accept` (same coarse-key expansion
148
+ // the shared 'Administrator' relied on), so admin-b can accept a cross-tenant grant.
149
+ await query(
150
+ `INSERT INTO \`${AUTHZ}\`.\`role_permission\` (rp_uuid, rp_role_uuid, rp_perm_key)
151
+ SELECT UUID(), ?, ?
152
+ WHERE NOT EXISTS (SELECT 1 FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? AND rp_perm_key = ?)`,
153
+ [roleId, IDENTITY_WILDCARD_KEY, roleId, IDENTITY_WILDCARD_KEY],
154
+ );
155
+
156
+ // ── PART B: root ────────────────────────────────────────────────────────────────────────
157
+
158
+ // B1) ensure a dedicated 'root' user exists (su=1). INSERT-if-absent, then UPDATE the flag to
159
+ // 1 so a pre-existing 'root' row (e.g. su=0) always converges on re-run.
160
+ await query(
161
+ `INSERT INTO \`${APP}\`.\`users\` (u_uuid, u_username, u_email, u_status, u_is_platform_superuser)
162
+ SELECT UUID(), ?, ?, 'active', 1
163
+ WHERE NOT EXISTS (SELECT 1 FROM \`${APP}\`.\`users\` WHERE u_username = ?)`,
164
+ [ROOT_USERNAME, ROOT_EMAIL, ROOT_USERNAME],
165
+ );
166
+ await query(`UPDATE \`${APP}\`.\`users\` SET u_is_platform_superuser = 1 WHERE u_username = ?`, [ROOT_USERNAME]);
167
+
168
+ const [rootUser] = await query(`SELECT u_uuid FROM \`${APP}\`.\`users\` WHERE u_username = ? LIMIT 1`, [ROOT_USERNAME]);
169
+ const rootUserId = rootUser.u_uuid;
170
+
171
+ // Root still logs into a session scoped to a tenant — anchor it to tenant-a (the original
172
+ // dev company from ...099), resolved by business key (never hard-coded).
173
+ const [companyA] = await query(`SELECT c_uuid FROM \`${APP}\`.\`companies\` WHERE c_name = ? LIMIT 1`, ['Astram Dev Co']);
174
+ const rootTenantId = companyA ? companyA.c_uuid : companyBId; // fall back to tenant-b if ...099 was skipped
175
+
176
+ // B2) credential (shield_identity) — ic_roles = [] (root's power is the su flag, not a role)
177
+ await query(
178
+ `INSERT INTO \`${IDENT}\`.\`identity_credential\`
179
+ (ic_identity_hint, ic_password_hash, ic_principal_id, ic_tenant_id, ic_roles, ic_created_at_ms, ic_updated_at_ms)
180
+ SELECT ?, ?, ?, ?, ?, ?, ?
181
+ WHERE NOT EXISTS (SELECT 1 FROM \`${IDENT}\`.\`identity_credential\` WHERE ic_identity_hint = ?)`,
182
+ [ROOT_HINT, PASSWORD_HASH, rootUserId, rootTenantId, JSON.stringify([]), nowMs, nowMs, ROOT_HINT],
183
+ );
184
+ }
185
+
186
+ async function down({ query }) {
187
+ // Reverse PART A (dependents first) — this migration's OWN 'Administrator B' role (+ its
188
+ // identity:* grant) is reversed too; the shared 'Administrator' role + tenant-a (from `...099`)
189
+ // are left untouched.
190
+ const [roleB] = await query(`SELECT r_uuid FROM \`${APP}\`.\`roles\` WHERE r_name = ? LIMIT 1`, [ROLE_B]);
191
+ if (roleB) {
192
+ await query(`DELETE FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? AND rp_perm_key = ?`, [roleB.r_uuid, IDENTITY_WILDCARD_KEY]);
193
+ }
194
+ await query(`DELETE FROM \`${IDENT}\`.\`identity_credential\` WHERE ic_identity_hint = ?`, [HINT_B]);
195
+ await query(`DELETE mp FROM \`${APP}\`.\`membership_posting\` mp
196
+ JOIN \`${APP}\`.\`company_membership\` cm ON cm.cm_uuid = mp.mp_membership_uuid
197
+ JOIN \`${APP}\`.\`users\` u ON u.u_uuid = cm.cm_user_uuid WHERE u.u_username = ?`, [USERNAME_B]);
198
+ await query(`DELETE cm FROM \`${APP}\`.\`company_membership\` cm
199
+ JOIN \`${APP}\`.\`users\` u ON u.u_uuid = cm.cm_user_uuid WHERE u.u_username = ?`, [USERNAME_B]);
200
+ await query(`DELETE FROM \`${APP}\`.\`users\` WHERE u_username = ?`, [USERNAME_B]);
201
+ await query(`DELETE FROM \`${APP}\`.\`companies\` WHERE c_name = ?`, [COMPANY_B]);
202
+ // Role deleted LAST (after the posting that references it, and after its own grant).
203
+ if (roleB) {
204
+ await query(`DELETE FROM \`${APP}\`.\`roles\` WHERE r_uuid = ?`, [roleB.r_uuid]);
205
+ }
206
+
207
+ // Reverse PART B — root's credential + the root user.
208
+ await query(`DELETE FROM \`${IDENT}\`.\`identity_credential\` WHERE ic_identity_hint = ?`, [ROOT_HINT]);
209
+ await query(`DELETE FROM \`${APP}\`.\`users\` WHERE u_username = ?`, [ROOT_USERNAME]);
210
+ }
211
+
212
+ module.exports = { up, down };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astram/identity",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "description": "Astram framework — the framework-owned identity + tenancy model. Hosts the identity @ObjectType classes (user, company, membership, posting), framework-authored identity migrations, and the identity resolver. Top of the dependency graph: depends on @astram/engine (for @ObjectType/@Field decorators) + @astram/guard / @astram/core (for the auth ports).",
6
6
  "type": "commonjs",
@@ -21,9 +21,10 @@
21
21
  "access": "public"
22
22
  },
23
23
  "dependencies": {
24
+ "mariadb": "^3.4.0",
24
25
  "@astram/core": "1.0.0",
25
- "@astram/guard": "0.4.0",
26
- "@astram/engine": "0.1.1"
26
+ "@astram/engine": "0.1.3",
27
+ "@astram/guard": "0.5.0"
27
28
  },
28
29
  "peerDependencies": {
29
30
  "@nestjs/common": "^11",