@jini-ai/user-management 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 (190) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/LICENSE +201 -0
  3. package/README.md +104 -0
  4. package/dist/admin/adapters/http.d.ts +29 -0
  5. package/dist/admin/adapters/http.js +69 -0
  6. package/dist/admin/adapters/memory.d.ts +61 -0
  7. package/dist/admin/adapters/memory.js +217 -0
  8. package/dist/admin/auth.module.d.ts +30 -0
  9. package/dist/admin/auth.module.js +12 -0
  10. package/dist/admin/conformance/index.d.ts +56 -0
  11. package/dist/admin/conformance/index.js +156 -0
  12. package/dist/admin/controllers/auth.controller.d.ts +23 -0
  13. package/dist/admin/controllers/auth.controller.js +33 -0
  14. package/dist/admin/controllers/controller-store.d.ts +18 -0
  15. package/dist/admin/controllers/controller-store.js +23 -0
  16. package/dist/admin/controllers/members.controller.d.ts +33 -0
  17. package/dist/admin/controllers/members.controller.js +119 -0
  18. package/dist/admin/controllers/roles.controller.d.ts +47 -0
  19. package/dist/admin/controllers/roles.controller.js +282 -0
  20. package/dist/admin/controllers/users.controller.d.ts +53 -0
  21. package/dist/admin/controllers/users.controller.js +220 -0
  22. package/dist/admin/index.d.ts +21 -0
  23. package/dist/admin/index.js +15 -0
  24. package/dist/admin/members.module.d.ts +34 -0
  25. package/dist/admin/members.module.js +13 -0
  26. package/dist/admin/messages.en.d.ts +37 -0
  27. package/dist/admin/messages.en.js +21 -0
  28. package/dist/admin/models.d.ts +133 -0
  29. package/dist/admin/models.js +2 -0
  30. package/dist/admin/people.rules.d.ts +29 -0
  31. package/dist/admin/people.rules.js +51 -0
  32. package/dist/admin/ports.d.ts +187 -0
  33. package/dist/admin/ports.js +17 -0
  34. package/dist/admin/react/components/DestructiveDialog.d.ts +5 -0
  35. package/dist/admin/react/components/DestructiveDialog.js +6 -0
  36. package/dist/admin/react/components/MemberRow.d.ts +5 -0
  37. package/dist/admin/react/components/MemberRow.js +8 -0
  38. package/dist/admin/react/components/MembersConfirmation.d.ts +2 -0
  39. package/dist/admin/react/components/MembersConfirmation.js +8 -0
  40. package/dist/admin/react/components/PermissionPanel.d.ts +5 -0
  41. package/dist/admin/react/components/PermissionPanel.js +9 -0
  42. package/dist/admin/react/components/PolicyRow.d.ts +5 -0
  43. package/dist/admin/react/components/PolicyRow.js +11 -0
  44. package/dist/admin/react/components/RoleRow.d.ts +5 -0
  45. package/dist/admin/react/components/RoleRow.js +10 -0
  46. package/dist/admin/react/components/UserRow.d.ts +5 -0
  47. package/dist/admin/react/components/UserRow.js +8 -0
  48. package/dist/admin/react/components/UsersConfirmation.d.ts +3 -0
  49. package/dist/admin/react/components/UsersConfirmation.js +9 -0
  50. package/dist/admin/react/hooks/AuthBinding.hooks.d.ts +39 -0
  51. package/dist/admin/react/hooks/AuthBinding.hooks.js +21 -0
  52. package/dist/admin/react/hooks/LoginPage.hooks.d.ts +54 -0
  53. package/dist/admin/react/hooks/LoginPage.hooks.js +23 -0
  54. package/dist/admin/react/hooks/MembersBinding.hooks.d.ts +35 -0
  55. package/dist/admin/react/hooks/MembersBinding.hooks.js +21 -0
  56. package/dist/admin/react/hooks/MembersPage.hooks.d.ts +99 -0
  57. package/dist/admin/react/hooks/MembersPage.hooks.js +38 -0
  58. package/dist/admin/react/hooks/PeopleController.hooks.d.ts +18 -0
  59. package/dist/admin/react/hooks/PeopleController.hooks.js +20 -0
  60. package/dist/admin/react/hooks/PermissionPanel.hooks.d.ts +30 -0
  61. package/dist/admin/react/hooks/PermissionPanel.hooks.js +15 -0
  62. package/dist/admin/react/hooks/PoliciesTab.hooks.d.ts +47 -0
  63. package/dist/admin/react/hooks/PoliciesTab.hooks.js +26 -0
  64. package/dist/admin/react/hooks/RolesBinding.hooks.d.ts +39 -0
  65. package/dist/admin/react/hooks/RolesBinding.hooks.js +23 -0
  66. package/dist/admin/react/hooks/RolesController.hooks.d.ts +43 -0
  67. package/dist/admin/react/hooks/RolesController.hooks.js +25 -0
  68. package/dist/admin/react/hooks/RolesPage.hooks.d.ts +105 -0
  69. package/dist/admin/react/hooks/RolesPage.hooks.js +33 -0
  70. package/dist/admin/react/hooks/RolesTab.hooks.d.ts +35 -0
  71. package/dist/admin/react/hooks/RolesTab.hooks.js +19 -0
  72. package/dist/admin/react/hooks/UsersBinding.hooks.d.ts +36 -0
  73. package/dist/admin/react/hooks/UsersBinding.hooks.js +21 -0
  74. package/dist/admin/react/hooks/UsersPage.hooks.d.ts +106 -0
  75. package/dist/admin/react/hooks/UsersPage.hooks.js +22 -0
  76. package/dist/admin/react/hooks/UsersTab.hooks.d.ts +101 -0
  77. package/dist/admin/react/hooks/UsersTab.hooks.js +53 -0
  78. package/dist/admin/react/index.d.ts +203 -0
  79. package/dist/admin/react/index.js +32 -0
  80. package/dist/admin/react/pages/LoginPage.d.ts +3 -0
  81. package/dist/admin/react/pages/LoginPage.js +13 -0
  82. package/dist/admin/react/pages/MembersPage.d.ts +3 -0
  83. package/dist/admin/react/pages/MembersPage.js +19 -0
  84. package/dist/admin/react/pages/RolesPage.d.ts +3 -0
  85. package/dist/admin/react/pages/RolesPage.js +19 -0
  86. package/dist/admin/react/pages/UsersPage.d.ts +3 -0
  87. package/dist/admin/react/pages/UsersPage.js +19 -0
  88. package/dist/admin/react/tabs/AuthTab.d.ts +4 -0
  89. package/dist/admin/react/tabs/AuthTab.js +8 -0
  90. package/dist/admin/react/tabs/MembersTab.d.ts +4 -0
  91. package/dist/admin/react/tabs/MembersTab.js +9 -0
  92. package/dist/admin/react/tabs/PoliciesTab.d.ts +4 -0
  93. package/dist/admin/react/tabs/PoliciesTab.js +9 -0
  94. package/dist/admin/react/tabs/RolesTab.d.ts +4 -0
  95. package/dist/admin/react/tabs/RolesTab.js +9 -0
  96. package/dist/admin/react/tabs/UsersTab.d.ts +4 -0
  97. package/dist/admin/react/tabs/UsersTab.js +9 -0
  98. package/dist/admin/roles.module.d.ts +48 -0
  99. package/dist/admin/roles.module.js +15 -0
  100. package/dist/admin/rules.d.ts +32 -0
  101. package/dist/admin/rules.js +32 -0
  102. package/dist/admin/users.module.d.ts +39 -0
  103. package/dist/admin/users.module.js +14 -0
  104. package/dist/core/builtin-permissions.d.ts +10 -0
  105. package/dist/core/builtin-permissions.js +402 -0
  106. package/dist/core/index.d.ts +6 -0
  107. package/dist/core/index.js +9 -0
  108. package/dist/core/permissions.d.ts +40 -0
  109. package/dist/core/permissions.js +52 -0
  110. package/dist/core/ports.d.ts +237 -0
  111. package/dist/core/ports.js +2 -0
  112. package/dist/core/runtime-ports.d.ts +8 -0
  113. package/dist/core/runtime-ports.js +2 -0
  114. package/dist/core/types.d.ts +224 -0
  115. package/dist/core/types.js +93 -0
  116. package/dist/react/auth/Login.d.ts +9 -0
  117. package/dist/react/auth/Login.js +8 -0
  118. package/dist/react/auth/hooks/use-login.hooks.d.ts +22 -0
  119. package/dist/react/auth/hooks/use-login.hooks.js +24 -0
  120. package/dist/react/errors.d.ts +10 -0
  121. package/dist/react/errors.js +13 -0
  122. package/dist/react/index.d.ts +18 -0
  123. package/dist/react/index.js +9 -0
  124. package/dist/react/members/Members.d.ts +6 -0
  125. package/dist/react/members/Members.js +38 -0
  126. package/dist/react/members/hooks/use-members.hooks.d.ts +32 -0
  127. package/dist/react/members/hooks/use-members.hooks.js +120 -0
  128. package/dist/react/members/rules.d.ts +26 -0
  129. package/dist/react/members/rules.js +35 -0
  130. package/dist/react/models.d.ts +50 -0
  131. package/dist/react/models.js +2 -0
  132. package/dist/react/ports.d.ts +157 -0
  133. package/dist/react/ports.js +2 -0
  134. package/dist/react/roles/Roles.d.ts +100 -0
  135. package/dist/react/roles/Roles.hooks.d.ts +17 -0
  136. package/dist/react/roles/Roles.hooks.js +40 -0
  137. package/dist/react/roles/Roles.js +150 -0
  138. package/dist/react/roles/hooks/use-roles.hooks.d.ts +80 -0
  139. package/dist/react/roles/hooks/use-roles.hooks.js +303 -0
  140. package/dist/react/roles/rules.d.ts +35 -0
  141. package/dist/react/roles/rules.js +31 -0
  142. package/dist/react/roles/text.d.ts +27 -0
  143. package/dist/react/roles/text.js +13 -0
  144. package/dist/react/testing.d.ts +51 -0
  145. package/dist/react/testing.js +275 -0
  146. package/dist/react/users/Users.d.ts +72 -0
  147. package/dist/react/users/Users.js +129 -0
  148. package/dist/react/users/hooks/use-reset-password-fields.hooks.d.ts +16 -0
  149. package/dist/react/users/hooks/use-reset-password-fields.hooks.js +21 -0
  150. package/dist/react/users/hooks/use-users.hooks.d.ts +77 -0
  151. package/dist/react/users/hooks/use-users.hooks.js +286 -0
  152. package/dist/react/users/rules.d.ts +33 -0
  153. package/dist/react/users/rules.js +61 -0
  154. package/dist/server/admin-crud-service.d.ts +295 -0
  155. package/dist/server/admin-crud-service.js +561 -0
  156. package/dist/server/agent-tool-input.d.ts +58 -0
  157. package/dist/server/agent-tool-input.js +158 -0
  158. package/dist/server/agent-tool-registrations.d.ts +51 -0
  159. package/dist/server/agent-tool-registrations.js +417 -0
  160. package/dist/server/agent-tools.d.ts +88 -0
  161. package/dist/server/agent-tools.js +229 -0
  162. package/dist/server/auth-service.d.ts +107 -0
  163. package/dist/server/auth-service.js +170 -0
  164. package/dist/server/authorize.d.ts +92 -0
  165. package/dist/server/authorize.js +96 -0
  166. package/dist/server/grant-service.d.ts +219 -0
  167. package/dist/server/grant-service.js +419 -0
  168. package/dist/server/hasher.d.ts +64 -0
  169. package/dist/server/hasher.js +53 -0
  170. package/dist/server/identity-transaction.d.ts +9 -0
  171. package/dist/server/identity-transaction.js +8 -0
  172. package/dist/server/index.d.ts +18 -0
  173. package/dist/server/index.js +20 -0
  174. package/dist/server/messages.d.ts +7 -0
  175. package/dist/server/messages.js +5 -0
  176. package/dist/server/password-policy.d.ts +59 -0
  177. package/dist/server/password-policy.js +66 -0
  178. package/dist/server/permission-migrations.d.ts +84 -0
  179. package/dist/server/permission-migrations.js +77 -0
  180. package/dist/server/repo.memory-transactions.d.ts +16 -0
  181. package/dist/server/repo.memory-transactions.js +79 -0
  182. package/dist/server/repo.memory.d.ts +186 -0
  183. package/dist/server/repo.memory.js +230 -0
  184. package/dist/server/seed.d.ts +56 -0
  185. package/dist/server/seed.js +435 -0
  186. package/dist/server/session-tokens.d.ts +10 -0
  187. package/dist/server/session-tokens.js +12 -0
  188. package/dist/server/username.d.ts +14 -0
  189. package/dist/server/username.js +14 -0
  190. package/package.json +144 -0
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @file Password policy: no minimum length, no composition rules — presence and an upper bound only.
3
+ *
4
+ * Owner decision: the previous NIST SP 800-63B-aligned 12-character minimum has been removed.
5
+ * This module no longer enforces composition rules (mandatory uppercase, digit, or symbol) either
6
+ * — that predates this change and remains deliberate, since composition rules measurably push
7
+ * people toward predictable, guessable patterns ("Password1!") rather than genuinely stronger
8
+ * secrets. What remains is the minimum a credential system can enforce at all: a password must be
9
+ * non-empty, and is capped at `MAX_PASSWORD_LENGTH` as a defense-in-depth bound (see that
10
+ * constant's own doc).
11
+ *
12
+ * `createUser` (`grant-service.ts`) and `resetUserPassword` (`admin-crud-service.ts`) are the
13
+ * identity system's only two write paths that ever set a password, confirmed by tracing the
14
+ * whole call graph — both call this module.
15
+ *
16
+ * Two things this deliberately does NOT do, both load-bearing:
17
+ * - Never reaches `seed.ts`'s first-boot owner password. `seedIdentity` hashes it directly
18
+ * (`deps.hasher.hash(ownerPassword)`, `seed.ts:317`) and calls neither `createUser` nor
19
+ * `resetUserPassword` — confirmed before writing this module. That separation is intentional
20
+ * to preserve here: the host chooses the first-boot owner password (`SeedIdentityInput` requires
21
+ * it and supplies no default), and an existing installation is already authenticating with
22
+ * whatever it chose. Enforcing this policy on that path would lock out every such installation
23
+ * whose chosen value predates the policy.
24
+ * - Never re-validates at login. This is a write-time policy — `login`/`verify` never call it.
25
+ * An account whose password predates this policy (or was seeded) must keep authenticating.
26
+ */
27
+ /**
28
+ * Generous upper bound — not itself a security requirement (argon2id's cost is tuned by its
29
+ * `memoryCost`/`timeCost`/`parallelism` parameters, not by input size — see `hasher.ts`'s own
30
+ * `@complexity` note), but a defense-in-depth cap on how large a string this service will accept
31
+ * and pass to the hasher at all, independent of whatever limit (if any) sits in front of it at
32
+ * the HTTP layer. Comfortably above any realistic human-typed passphrase.
33
+ */
34
+ export const MAX_PASSWORD_LENGTH = 512;
35
+ /**
36
+ * Validates `password` against the presence-and-upper-bound-only policy.
37
+ *
38
+ * Measures length in Unicode CODE POINTS (`[...password].length`), not UTF-16 code units
39
+ * (`password.length`): a passphrase built from astral-plane characters (many emoji, some
40
+ * CJK/historic scripts) is represented as a surrogate pair per character in a JS string, so plain
41
+ * `.length` overcounts those characters relative to what a human typing them perceives — the same
42
+ * reasoning that applies to `MAX_PASSWORD_LENGTH` below applies to the emptiness check here, since
43
+ * both compare this same code-point count. No trimming, no character allowlist: every printable
44
+ * character, including leading/trailing spaces and full Unicode, is accepted as typed. Silently
45
+ * altering the input (trimming, stripping) would make the stored credential differ from the
46
+ * secret the operator actually chose.
47
+ *
48
+ * @returns `null` if `password` satisfies the policy, otherwise an operator-facing message
49
+ * stating the requirement plainly — used verbatim as the thrown `IdentityValidationError`'s
50
+ * message by both call sites, and rendered as-is through the admin's `describeApiError` layer.
51
+ *
52
+ * @complexity O(n) in the password's length — spreading a string into code points is a single
53
+ * linear pass, which is the minimum work a correct (non-UTF-16-code-unit) count requires.
54
+ * @overallScore 100
55
+ */
56
+ export function validatePasswordPolicy({ password }) {
57
+ const length = [...password].length;
58
+ if (length === 0) {
59
+ return "Password is required.";
60
+ }
61
+ if (length > MAX_PASSWORD_LENGTH) {
62
+ return `Password must be no more than ${MAX_PASSWORD_LENGTH} characters.`;
63
+ }
64
+ return null;
65
+ }
66
+ //# sourceMappingURL=password-policy.js.map
@@ -0,0 +1,84 @@
1
+ import { type PermissionMigration } from '../core/builtin-permissions.js';
2
+ import type { IdGenerator, UUID } from "@jini-ai/core/primitives";
3
+ import type { IdentityTransactionPort, PolicyPermissionRepoPort, PolicyRepoPort } from "../core/ports.js";
4
+ /**
5
+ * @file Shared deprecate-old/grant-new permission migration mechanism
6
+ * (generalized from the `settings.write` ->
7
+ * `settings.definitions.manage` precedent already shipped ad hoc in
8
+ * `identity/seed.ts`/`identity/permissions.ts`; see
9
+ * `docs/decisions/permission-catalog-migration.md`).
10
+ *
11
+ * Purpose:
12
+ * A permission-string rename/split after real grants exist is a breaking
13
+ * migration. This module is
14
+ * the one place that fan-out runs, so Menus/Members/Analytics/Integrations
15
+ * remediation efforts each register their own `{from, to}` pair here instead of
16
+ * hand-rolling a fourth divergent copy of the same fix.
17
+ *
18
+ * How it relates to the project:
19
+ * - `registerPermissionMigration`/`listPermissionMigrations` are a small,
20
+ * in-process registry — pure metadata, no I/O, mirrors
21
+ * `permissions.ts`'s `PermissionCatalog.register` overwrite semantics.
22
+ * - `migrateDeprecatedPermissionGrants` is the actual grant fan-out: for
23
+ * every registered pair, every policy holding `from` gets every string in
24
+ * `to` it doesn't already hold. It is deliberately, structurally
25
+ * additive-only — there is no delete/update call against
26
+ * `policy_permissions` anywhere in this file.
27
+ * - Feature-agnostic by design: this module has zero knowledge of
28
+ * `navigation`/`settings`/any specific feature. Callers (e.g.
29
+ * `identity/permissions.ts`) register their own pairs.
30
+ *
31
+ * Architectural role:
32
+ * Security-adjacent domain logic (a bug here could fail-open (grant more than
33
+ * intended) or fail-closed (a policy silently missing a needed grant) across every
34
+ * feature that reuses it). The additive-only design bounds the fail-closed
35
+ * direction to "a required new permission is missing," never "an existing
36
+ * permission is removed." See `__tests__/permission-migrations.test.ts` for
37
+ * the dedicated fixture-driven certification required before any
38
+ * caller wires this into a live boot path.
39
+ * See docs/decisions/DR-003-additive-permission-migration.md.
40
+ */
41
+ export type { PermissionMigration } from '../core/builtin-permissions.js';
42
+ export interface MigrateDeprecatedPermissionGrantsDeps {
43
+ /** Bound to the same storage as both repository ports; required before any migration work. */
44
+ transactions: IdentityTransactionPort;
45
+ policyPermissions: PolicyPermissionRepoPort;
46
+ policies: PolicyRepoPort;
47
+ idGen: IdGenerator;
48
+ workspaceId: UUID;
49
+ }
50
+ export interface MigrateDeprecatedPermissionGrantsResult {
51
+ /** Total new `policy_permissions` rows written across every policy and every registered pair. */
52
+ migratedGrantCount: number;
53
+ }
54
+ /**
55
+ * Register a `{from, to, reason}` rename/split pair for later fan-out.
56
+ * Idempotent: re-registering the same `from` overwrites the prior entry
57
+ * rather than duplicating it (matches `PermissionCatalog.register`'s
58
+ * existing overwrite semantics, `identity/permissions.ts`).
59
+ *
60
+ * @complexity O(1).
61
+ * @overallScore 100
62
+ */
63
+ export declare function registerPermissionMigration(migration: PermissionMigration): void;
64
+ /** Enumerate every registered migration pair. */
65
+ export declare function listPermissionMigrations(_required: Record<string, never>): PermissionMigration[];
66
+ /**
67
+ * For every registered `{from, to}` pair, for every policy in the workspace
68
+ * holding `from`, ensure every string in `to` is also granted — adding only
69
+ * the rows that are missing. Never removes or mutates `from` (or any other
70
+ * existing row) — additive-only by construction. The entire fan-out runs in
71
+ * one identity transaction, so a failed or partial run restores the previous state rather
72
+ * than committing some target grants while withholding others.
73
+ *
74
+ * Safe to call on every boot (idempotent): a rerun with nothing new to add
75
+ * returns `migratedGrantCount: 0` (mirrors `migrateLegacyPresentationSettings`'s
76
+ * boot-safety precedent).
77
+ *
78
+ * @complexity O(p * m) where p = policies in the workspace, m = registered
79
+ * migration pairs; both are small, bounded collections in this app's shape.
80
+ * @overallScore 100
81
+ * See docs/decisions/DR-003-additive-permission-migration.md.
82
+ */
83
+ export declare function migrateDeprecatedPermissionGrants(deps: MigrateDeprecatedPermissionGrantsDeps): Promise<MigrateDeprecatedPermissionGrantsResult>;
84
+ //# sourceMappingURL=permission-migrations.d.ts.map
@@ -0,0 +1,77 @@
1
+ import { inIdentityTransaction } from "./identity-transaction.js";
2
+ import { BUILTIN_PERMISSION_MIGRATIONS } from '../core/builtin-permissions.js';
3
+ /** Module-singleton registry — keyed by `from` so re-registration overwrites (idempotent, matches `PermissionCatalog.register`). */
4
+ const registry = new Map(BUILTIN_PERMISSION_MIGRATIONS.map((migration) => [migration.from, migration]));
5
+ /**
6
+ * Register a `{from, to, reason}` rename/split pair for later fan-out.
7
+ * Idempotent: re-registering the same `from` overwrites the prior entry
8
+ * rather than duplicating it (matches `PermissionCatalog.register`'s
9
+ * existing overwrite semantics, `identity/permissions.ts`).
10
+ *
11
+ * @complexity O(1).
12
+ * @overallScore 100
13
+ */
14
+ export function registerPermissionMigration(migration) {
15
+ registry.set(migration.from, migration);
16
+ }
17
+ /** Enumerate every registered migration pair. */
18
+ export function listPermissionMigrations(_required) {
19
+ return [...registry.values()];
20
+ }
21
+ /**
22
+ * For every registered `{from, to}` pair, for every policy in the workspace
23
+ * holding `from`, ensure every string in `to` is also granted — adding only
24
+ * the rows that are missing. Never removes or mutates `from` (or any other
25
+ * existing row) — additive-only by construction. The entire fan-out runs in
26
+ * one identity transaction, so a failed or partial run restores the previous state rather
27
+ * than committing some target grants while withholding others.
28
+ *
29
+ * Safe to call on every boot (idempotent): a rerun with nothing new to add
30
+ * returns `migratedGrantCount: 0` (mirrors `migrateLegacyPresentationSettings`'s
31
+ * boot-safety precedent).
32
+ *
33
+ * @complexity O(p * m) where p = policies in the workspace, m = registered
34
+ * migration pairs; both are small, bounded collections in this app's shape.
35
+ * @overallScore 100
36
+ * See docs/decisions/DR-003-additive-permission-migration.md.
37
+ */
38
+ export async function migrateDeprecatedPermissionGrants(deps) {
39
+ const migrations = listPermissionMigrations({});
40
+ if (migrations.length === 0)
41
+ return { migratedGrantCount: 0 };
42
+ return inIdentityTransaction({
43
+ transactions: deps.transactions,
44
+ workspaceId: deps.workspaceId,
45
+ execute: async () => {
46
+ const policies = await deps.policies.list({ workspaceId: deps.workspaceId });
47
+ let migratedGrantCount = 0;
48
+ for (const policy of policies) {
49
+ const existing = await deps.policyPermissions.listByPolicyId({
50
+ workspaceId: deps.workspaceId,
51
+ policyId: policy.id,
52
+ });
53
+ const held = new Set(existing.map((row) => row.permission));
54
+ for (const migration of migrations) {
55
+ if (!held.has(migration.from))
56
+ continue;
57
+ for (const toPermission of migration.to) {
58
+ if (held.has(toPermission))
59
+ continue;
60
+ await deps.policyPermissions.save({
61
+ id: deps.idGen.newId(),
62
+ workspaceId: deps.workspaceId,
63
+ policyId: policy.id,
64
+ permission: toPermission,
65
+ resourceType: null,
66
+ constraintJson: null,
67
+ });
68
+ held.add(toPermission);
69
+ migratedGrantCount += 1;
70
+ }
71
+ }
72
+ }
73
+ return { migratedGrantCount };
74
+ },
75
+ });
76
+ }
77
+ //# sourceMappingURL=permission-migrations.js.map
@@ -0,0 +1,16 @@
1
+ import type { IdentityRepos, IdentityTransactionPort } from '../core/ports.js';
2
+ /**
3
+ * Wrap a complete memory repository bag with one serialization queue and rollback snapshots.
4
+ * Use only the returned repositories after binding: retained raw handles bypass isolation.
5
+ * Every ordinary read/write also joins the queue, so an assignment outside a service cannot
6
+ * race a guarded delete and readers cannot observe uncommitted changes. This is a dev/test
7
+ * adapter; durable hosts supply their own transaction port over their own storage connection.
8
+ */
9
+ export declare function createTransactionalInMemoryIdentityRepos(required: {
10
+ repos: Omit<IdentityRepos, "transactions"> & {
11
+ transactions?: IdentityTransactionPort;
12
+ };
13
+ }): IdentityRepos & {
14
+ transactions: IdentityTransactionPort;
15
+ };
16
+ //# sourceMappingURL=repo.memory-transactions.d.ts.map
@@ -0,0 +1,79 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { InMemoryIdentityRows } from './repo.memory.js';
3
+ /**
4
+ * Wrap a complete memory repository bag with one serialization queue and rollback snapshots.
5
+ * Use only the returned repositories after binding: retained raw handles bypass isolation.
6
+ * Every ordinary read/write also joins the queue, so an assignment outside a service cannot
7
+ * race a guarded delete and readers cannot observe uncommitted changes. This is a dev/test
8
+ * adapter; durable hosts supply their own transaction port over their own storage connection.
9
+ */
10
+ export function createTransactionalInMemoryIdentityRepos(required) {
11
+ if (required.repos.transactions)
12
+ throw new Error('identity repositories are already bound to a transaction port');
13
+ const context = new AsyncLocalStorage();
14
+ let tail = Promise.resolve();
15
+ const stores = Object.entries(required.repos).filter(([key]) => key !== 'transactions');
16
+ for (const [, repo] of stores) {
17
+ if (!(repo instanceof InMemoryIdentityRows)) {
18
+ throw new Error('memory transactions require in-memory identity repositories');
19
+ }
20
+ }
21
+ /** Preserve queue progress after rejection; descendants of a completed body cannot bypass it. */
22
+ async function exclusive(execute) {
23
+ if (context.getStore()?.active)
24
+ return execute();
25
+ const previous = tail;
26
+ let release;
27
+ tail = new Promise(resolve => { release = resolve; });
28
+ await previous;
29
+ const scope = { active: true };
30
+ try {
31
+ return await context.run(scope, execute);
32
+ }
33
+ finally {
34
+ scope.active = false;
35
+ release();
36
+ }
37
+ }
38
+ const transactions = {
39
+ run: ({ execute }) => exclusive(async () => {
40
+ const snapshots = stores.map(([, repo]) => {
41
+ if (!(repo instanceof InMemoryIdentityRows))
42
+ throw new Error('memory transactions require in-memory identity repositories');
43
+ const rows = repo.snapshotRows({});
44
+ return { restore: () => repo.restoreRows({ rows }) };
45
+ });
46
+ try {
47
+ return await execute();
48
+ }
49
+ catch (error) {
50
+ for (const snapshot of snapshots)
51
+ snapshot.restore();
52
+ throw error;
53
+ }
54
+ }),
55
+ };
56
+ function wrap(repo) {
57
+ return new Proxy(repo, {
58
+ get(target, property, receiver) {
59
+ const member = Reflect.get(target, property, receiver);
60
+ if (typeof member !== 'function')
61
+ return member;
62
+ return (...args) => exclusive(async () => {
63
+ // Detach inputs/outputs so a row reference outside the queue cannot mutate stored state.
64
+ const result = await Reflect.apply(member, target, structuredClone(args));
65
+ return structuredClone(result);
66
+ });
67
+ },
68
+ });
69
+ }
70
+ const repos = {
71
+ principals: wrap(required.repos.principals), users: wrap(required.repos.users),
72
+ sessions: wrap(required.repos.sessions), roles: wrap(required.repos.roles),
73
+ policies: wrap(required.repos.policies), policyPermissions: wrap(required.repos.policyPermissions),
74
+ rolePolicies: wrap(required.repos.rolePolicies), principalRoles: wrap(required.repos.principalRoles),
75
+ principalPolicies: wrap(required.repos.principalPolicies),
76
+ };
77
+ return { ...repos, transactions };
78
+ }
79
+ //# sourceMappingURL=repo.memory-transactions.js.map
@@ -0,0 +1,186 @@
1
+ import type { PolicyPermissionRecord, PolicyRecord, PrincipalPolicyRecord, PrincipalRecord, PrincipalRoleRecord, RolePolicyRecord, RoleRecord, SessionRecord, UserRecord } from "../core/types.js";
2
+ import type { PolicyPermissionRepoPort, PolicyRepoPort, PrincipalPolicyRepoPort, PrincipalRepoPort, PrincipalRoleRepoPort, RolePolicyRepoPort, RoleRepoPort, SessionRepoPort, UserRepoPort } from "../core/ports.js";
3
+ /**
4
+ * @file In-memory adapters for the `user-management` repo ports.
5
+ *
6
+ * Purpose:
7
+ * Test/dev implementations of the nine identity repo ports. Mirrors the exact
8
+ * style of `src/features/post/repo.memory.ts` / `src/members/repo.memory.ts`:
9
+ * constructor takes seed rows, methods filter/mutate an internal array. No
10
+ * business rules live here — that belongs to `authorize.ts`/`auth-service.ts`/
11
+ * `seed.ts`.
12
+ *
13
+ * Architectural role:
14
+ * The only adapter this pass — a SQLite adapter is a later step (see
15
+ * `ports.ts` header and `server/deps.ts` comments), matching the disclosed
16
+ * precedent of members/navigation/integrations/analytics.
17
+ */
18
+ /** Snapshot seam used only by the memory transaction adapter; no business rules live here. */
19
+ export declare abstract class InMemoryIdentityRows<Row> {
20
+ protected rows: Row[];
21
+ constructor(required: Record<string, never>, optional?: {
22
+ initialRows?: Row[];
23
+ });
24
+ /** Detached state so rollback also restores in-place changes such as session revocation. */
25
+ snapshotRows(required: Record<string, never>): Row[];
26
+ /** Restore a detached snapshot after the transaction rejects. */
27
+ restoreRows(required: {
28
+ rows: Row[];
29
+ }): void;
30
+ }
31
+ export declare class InMemoryPrincipalRepo extends InMemoryIdentityRows<PrincipalRecord> implements PrincipalRepoPort {
32
+ constructor(_required: Record<string, never>, { initialRows }?: {
33
+ initialRows?: PrincipalRecord[];
34
+ });
35
+ findById(required: {
36
+ workspaceId: string;
37
+ id: string;
38
+ }): Promise<PrincipalRecord | null>;
39
+ list(required: {
40
+ workspaceId: string;
41
+ }): Promise<PrincipalRecord[]>;
42
+ save(record: PrincipalRecord): Promise<void>;
43
+ }
44
+ export declare class InMemoryUserRepo extends InMemoryIdentityRows<UserRecord> implements UserRepoPort {
45
+ constructor(_required: Record<string, never>, { initialRows }?: {
46
+ initialRows?: UserRecord[];
47
+ });
48
+ findByPrincipalId(required: {
49
+ workspaceId: string;
50
+ principalId: string;
51
+ }): Promise<UserRecord | null>;
52
+ findByUsername(required: {
53
+ workspaceId: string;
54
+ username: string;
55
+ }): Promise<UserRecord | null>;
56
+ list(required: {
57
+ workspaceId: string;
58
+ }): Promise<UserRecord[]>;
59
+ save(record: UserRecord): Promise<void>;
60
+ }
61
+ export declare class InMemorySessionRepo extends InMemoryIdentityRows<SessionRecord> implements SessionRepoPort {
62
+ constructor(_required: Record<string, never>, { initialRows }?: {
63
+ initialRows?: SessionRecord[];
64
+ });
65
+ findById(required: {
66
+ workspaceId: string;
67
+ id: string;
68
+ }): Promise<SessionRecord | null>;
69
+ findByTokenHash(required: {
70
+ workspaceId: string;
71
+ tokenHash: string;
72
+ }): Promise<SessionRecord | null>;
73
+ save(record: SessionRecord): Promise<void>;
74
+ revoke(required: {
75
+ workspaceId: string;
76
+ id: string;
77
+ revokedAt: string;
78
+ }): Promise<void>;
79
+ listByPrincipalId(required: {
80
+ workspaceId: string;
81
+ principalId: string;
82
+ }): Promise<SessionRecord[]>;
83
+ }
84
+ export declare class InMemoryRoleRepo extends InMemoryIdentityRows<RoleRecord> implements RoleRepoPort {
85
+ constructor(_required: Record<string, never>, { initialRows }?: {
86
+ initialRows?: RoleRecord[];
87
+ });
88
+ findById(required: {
89
+ workspaceId: string;
90
+ id: string;
91
+ }): Promise<RoleRecord | null>;
92
+ findByName(required: {
93
+ workspaceId: string;
94
+ name: string;
95
+ }): Promise<RoleRecord | null>;
96
+ list(required: {
97
+ workspaceId: string;
98
+ }): Promise<RoleRecord[]>;
99
+ save(record: RoleRecord): Promise<void>;
100
+ delete(required: {
101
+ workspaceId: string;
102
+ id: string;
103
+ }): Promise<void>;
104
+ }
105
+ export declare class InMemoryPolicyRepo extends InMemoryIdentityRows<PolicyRecord> implements PolicyRepoPort {
106
+ constructor(_required: Record<string, never>, { initialRows }?: {
107
+ initialRows?: PolicyRecord[];
108
+ });
109
+ findById(required: {
110
+ workspaceId: string;
111
+ id: string;
112
+ }): Promise<PolicyRecord | null>;
113
+ findByName(required: {
114
+ workspaceId: string;
115
+ name: string;
116
+ }): Promise<PolicyRecord | null>;
117
+ list(required: {
118
+ workspaceId: string;
119
+ }): Promise<PolicyRecord[]>;
120
+ save(record: PolicyRecord): Promise<void>;
121
+ delete(required: {
122
+ workspaceId: string;
123
+ id: string;
124
+ }): Promise<void>;
125
+ }
126
+ export declare class InMemoryPolicyPermissionRepo extends InMemoryIdentityRows<PolicyPermissionRecord> implements PolicyPermissionRepoPort {
127
+ constructor(_required: Record<string, never>, { initialRows }?: {
128
+ initialRows?: PolicyPermissionRecord[];
129
+ });
130
+ listByPolicyId(required: {
131
+ workspaceId: string;
132
+ policyId: string;
133
+ }): Promise<PolicyPermissionRecord[]>;
134
+ save(record: PolicyPermissionRecord): Promise<void>;
135
+ deleteByPolicyId(required: {
136
+ workspaceId: string;
137
+ policyId: string;
138
+ }): Promise<void>;
139
+ delete(required: {
140
+ workspaceId: string;
141
+ id: string;
142
+ }): Promise<void>;
143
+ }
144
+ export declare class InMemoryRolePolicyRepo extends InMemoryIdentityRows<RolePolicyRecord> implements RolePolicyRepoPort {
145
+ constructor(_required: Record<string, never>, { initialRows }?: {
146
+ initialRows?: RolePolicyRecord[];
147
+ });
148
+ listByRoleId(required: {
149
+ workspaceId: string;
150
+ roleId: string;
151
+ }): Promise<RolePolicyRecord[]>;
152
+ listByPolicyId(required: {
153
+ workspaceId: string;
154
+ policyId: string;
155
+ }): Promise<RolePolicyRecord[]>;
156
+ save(record: RolePolicyRecord): Promise<void>;
157
+ }
158
+ export declare class InMemoryPrincipalRoleRepo extends InMemoryIdentityRows<PrincipalRoleRecord> implements PrincipalRoleRepoPort {
159
+ constructor(_required: Record<string, never>, { initialRows }?: {
160
+ initialRows?: PrincipalRoleRecord[];
161
+ });
162
+ listByPrincipalId(required: {
163
+ workspaceId: string;
164
+ principalId: string;
165
+ }): Promise<PrincipalRoleRecord[]>;
166
+ listByRoleId(required: {
167
+ workspaceId: string;
168
+ roleId: string;
169
+ }): Promise<PrincipalRoleRecord[]>;
170
+ save(record: PrincipalRoleRecord): Promise<void>;
171
+ }
172
+ export declare class InMemoryPrincipalPolicyRepo extends InMemoryIdentityRows<PrincipalPolicyRecord> implements PrincipalPolicyRepoPort {
173
+ constructor(_required: Record<string, never>, { initialRows }?: {
174
+ initialRows?: PrincipalPolicyRecord[];
175
+ });
176
+ listByPrincipalId(required: {
177
+ workspaceId: string;
178
+ principalId: string;
179
+ }): Promise<PrincipalPolicyRecord[]>;
180
+ listByPolicyId(required: {
181
+ workspaceId: string;
182
+ policyId: string;
183
+ }): Promise<PrincipalPolicyRecord[]>;
184
+ save(record: PrincipalPolicyRecord): Promise<void>;
185
+ }
186
+ //# sourceMappingURL=repo.memory.d.ts.map