@jini-ai/cms 0.3.11 → 0.4.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 (258) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +47 -19
  3. package/dist/content-types/agent-tools.d.ts +3 -56
  4. package/dist/content-types/agent-tools.js +22 -6
  5. package/dist/content-types/cleanup.d.ts +18 -13
  6. package/dist/content-types/cleanup.js +16 -11
  7. package/dist/content-types/errors.d.ts +57 -24
  8. package/dist/content-types/errors.js +56 -39
  9. package/dist/content-types/field-defs.d.ts +8 -4
  10. package/dist/content-types/field-defs.js +19 -14
  11. package/dist/content-types/index-provisioning.d.ts +61 -47
  12. package/dist/content-types/index-provisioning.js +90 -66
  13. package/dist/content-types/index.d.ts +4 -3
  14. package/dist/content-types/lifecycle-dispatch.d.ts +11 -5
  15. package/dist/content-types/lifecycle-dispatch.js +5 -4
  16. package/dist/content-types/lifecycle.d.ts +23 -18
  17. package/dist/content-types/lifecycle.js +71 -64
  18. package/dist/content-types/repo.memory.d.ts +5 -4
  19. package/dist/content-types/repo.memory.js +3 -2
  20. package/dist/content-types/tool-registrations.d.ts +6 -8
  21. package/dist/content-types/tool-registrations.js +24 -28
  22. package/dist/content-types/types.d.ts +17 -13
  23. package/dist/content-types/types.js +11 -5
  24. package/dist/content-types/write-service.d.ts +20 -22
  25. package/dist/content-types/write-service.js +78 -75
  26. package/dist/core/authorization.d.ts +15 -0
  27. package/dist/core/authorization.js +2 -0
  28. package/dist/core/commands/change-set.d.ts +15 -7
  29. package/dist/core/commands/command.d.ts +45 -26
  30. package/dist/core/commands/command.js +73 -63
  31. package/dist/core/entity-liveness.d.ts +11 -7
  32. package/dist/core/entity-liveness.js +5 -4
  33. package/dist/core/index.d.ts +12 -5
  34. package/dist/core/index.js +10 -4
  35. package/dist/core/ports.d.ts +25 -24
  36. package/dist/core/ports.js +1 -0
  37. package/dist/core/tools/args.d.ts +49 -0
  38. package/dist/core/tools/args.js +46 -0
  39. package/dist/core/tools/index.d.ts +3 -0
  40. package/dist/core/tools/index.js +3 -0
  41. package/dist/entries/agent-tools.d.ts +1 -73
  42. package/dist/entries/agent-tools.js +19 -13
  43. package/dist/entries/errors.d.ts +28 -14
  44. package/dist/entries/errors.js +17 -11
  45. package/dist/entries/field-validation.d.ts +5 -3
  46. package/dist/entries/field-validation.js +7 -5
  47. package/dist/entries/index.d.ts +2 -2
  48. package/dist/entries/lifecycle-dispatch.d.ts +5 -2
  49. package/dist/entries/lifecycle-dispatch.js +2 -1
  50. package/dist/entries/list.d.ts +4 -2
  51. package/dist/entries/list.js +4 -3
  52. package/dist/entries/repo.memory.d.ts +12 -10
  53. package/dist/entries/repo.memory.js +11 -10
  54. package/dist/entries/tool-registrations.d.ts +6 -8
  55. package/dist/entries/tool-registrations.js +42 -41
  56. package/dist/entries/types.d.ts +0 -7
  57. package/dist/entries/write-service.d.ts +28 -29
  58. package/dist/entries/write-service.js +114 -105
  59. package/dist/http/settings/cms-adapter.d.ts +20 -0
  60. package/dist/http/settings/cms-adapter.js +124 -0
  61. package/dist/http/settings/contracts.d.ts +135 -0
  62. package/dist/http/settings/contracts.js +2 -0
  63. package/dist/http/settings/http.d.ts +10 -0
  64. package/dist/http/settings/http.js +211 -0
  65. package/dist/http/settings/index.d.ts +5 -0
  66. package/dist/http/settings/index.js +4 -0
  67. package/dist/http/settings/shared.d.ts +68 -0
  68. package/dist/http/settings/shared.js +54 -0
  69. package/dist/http/settings/stream.d.ts +24 -0
  70. package/dist/http/settings/stream.js +147 -0
  71. package/dist/media/agent-tools.d.ts +1 -66
  72. package/dist/media/agent-tools.js +5 -1
  73. package/dist/media/blob-gc-lock.d.ts +6 -2
  74. package/dist/media/blob-gc-lock.js +4 -2
  75. package/dist/media/blob-gc.d.ts +64 -51
  76. package/dist/media/blob-gc.js +62 -52
  77. package/dist/media/blob-key.d.ts +1 -1
  78. package/dist/media/content-type-sniffer.d.ts +3 -1
  79. package/dist/media/content-type-sniffer.js +1 -1
  80. package/dist/media/html-attributes.d.ts +9 -5
  81. package/dist/media/html-attributes.js +8 -6
  82. package/dist/media/image-transformer.js +1 -1
  83. package/dist/media/image-transformer.sharp.d.ts +6 -0
  84. package/dist/media/image-transformer.sharp.js +12 -6
  85. package/dist/media/import/fetch-image.d.ts +24 -0
  86. package/dist/media/import/fetch-image.js +137 -0
  87. package/dist/media/import/index.d.ts +3 -0
  88. package/dist/media/import/index.js +3 -0
  89. package/dist/media/import/ports.d.ts +38 -0
  90. package/dist/media/index.d.ts +7 -1
  91. package/dist/media/index.js +1 -0
  92. package/dist/media/media-service.d.ts +14 -10
  93. package/dist/media/media-service.js +57 -52
  94. package/dist/media/ports.d.ts +3 -2
  95. package/dist/media/rendition-service.d.ts +3 -3
  96. package/dist/media/rendition-service.js +38 -37
  97. package/dist/media/repo.memory.d.ts +18 -7
  98. package/dist/media/repo.memory.js +12 -6
  99. package/dist/media/tool-registrations.d.ts +16 -15
  100. package/dist/media/tool-registrations.js +84 -72
  101. package/dist/media/transform-lock.d.ts +22 -12
  102. package/dist/media/transform-lock.js +14 -13
  103. package/dist/media/transform-registry.d.ts +3 -3
  104. package/dist/media/transform-registry.js +19 -18
  105. package/dist/media/transform-types.d.ts +8 -3
  106. package/dist/media/transform-types.js +10 -5
  107. package/dist/media/types.d.ts +21 -4
  108. package/dist/media/types.js +15 -2
  109. package/dist/navigation/agent-tools.d.ts +1 -80
  110. package/dist/navigation/agent-tools.js +15 -3
  111. package/dist/navigation/contracts.d.ts +1 -1
  112. package/dist/navigation/index.d.ts +2 -2
  113. package/dist/navigation/menu-service.d.ts +57 -40
  114. package/dist/navigation/menu-service.js +80 -69
  115. package/dist/navigation/ports.d.ts +1 -1
  116. package/dist/navigation/read-model.d.ts +3 -1
  117. package/dist/navigation/read-model.js +2 -2
  118. package/dist/navigation/reconcile.d.ts +2 -2
  119. package/dist/navigation/reconcile.js +2 -1
  120. package/dist/navigation/repo.memory.d.ts +7 -3
  121. package/dist/navigation/repo.memory.js +4 -2
  122. package/dist/navigation/resolver.d.ts +4 -1
  123. package/dist/navigation/resolver.js +1 -1
  124. package/dist/navigation/tool-registrations.d.ts +6 -8
  125. package/dist/navigation/tool-registrations.js +44 -42
  126. package/dist/navigation/types.d.ts +1 -1
  127. package/dist/presentation/presentation.d.ts +11 -3
  128. package/dist/presentation/presentation.js +13 -6
  129. package/dist/presentation/repo.memory.d.ts +6 -2
  130. package/dist/presentation/repo.memory.js +3 -2
  131. package/dist/settings/agent-tools.d.ts +2 -75
  132. package/dist/settings/agent-tools.js +49 -45
  133. package/dist/settings/agent-value-write-tools.d.ts +22 -0
  134. package/dist/settings/agent-value-write-tools.js +154 -0
  135. package/dist/settings/agent-writable-preferences.js +8 -20
  136. package/dist/settings/agent-write-denylist.d.ts +19 -0
  137. package/dist/settings/agent-write-denylist.js +15 -0
  138. package/dist/settings/change-feed.d.ts +11 -2
  139. package/dist/settings/change-feed.js +4 -4
  140. package/dist/settings/definitions-dispatch.d.ts +8 -3
  141. package/dist/settings/definitions-dispatch.js +5 -5
  142. package/dist/settings/dictionaries/index.d.ts +5 -2
  143. package/dist/settings/dictionaries/index.js +2 -2
  144. package/dist/settings/dictionaries/settings-dialog.ar.js +0 -1
  145. package/dist/settings/dictionaries/settings-dialog.bn.js +0 -1
  146. package/dist/settings/dictionaries/settings-dialog.de.js +0 -1
  147. package/dist/settings/dictionaries/settings-dialog.es.js +0 -1
  148. package/dist/settings/dictionaries/settings-dialog.fa.js +0 -1
  149. package/dist/settings/dictionaries/settings-dialog.fr.js +0 -1
  150. package/dist/settings/dictionaries/settings-dialog.hi.js +0 -1
  151. package/dist/settings/dictionaries/settings-dialog.hu.js +0 -1
  152. package/dist/settings/dictionaries/settings-dialog.id.js +0 -1
  153. package/dist/settings/dictionaries/settings-dialog.it.js +0 -1
  154. package/dist/settings/dictionaries/settings-dialog.ja.js +0 -1
  155. package/dist/settings/dictionaries/settings-dialog.ko.js +0 -1
  156. package/dist/settings/dictionaries/settings-dialog.pl.js +0 -1
  157. package/dist/settings/dictionaries/settings-dialog.pt-BR.js +0 -1
  158. package/dist/settings/dictionaries/settings-dialog.ru.js +0 -1
  159. package/dist/settings/dictionaries/settings-dialog.th.js +0 -1
  160. package/dist/settings/dictionaries/settings-dialog.tr.js +0 -1
  161. package/dist/settings/dictionaries/settings-dialog.uk.js +0 -1
  162. package/dist/settings/dictionaries/settings-dialog.ur.js +0 -1
  163. package/dist/settings/dictionaries/settings-dialog.zh-CN.js +0 -1
  164. package/dist/settings/dictionaries/settings-dialog.zh-TW.js +0 -1
  165. package/dist/settings/ensure-definitions.d.ts +5 -5
  166. package/dist/settings/errors.d.ts +7 -1
  167. package/dist/settings/errors.js +7 -1
  168. package/dist/settings/index.d.ts +4 -1
  169. package/dist/settings/index.js +1 -0
  170. package/dist/settings/ports.d.ts +6 -5
  171. package/dist/settings/principal-lookup.d.ts +9 -0
  172. package/dist/settings/principal-lookup.js +2 -0
  173. package/dist/settings/purge-service.d.ts +5 -4
  174. package/dist/settings/purge-service.js +3 -2
  175. package/dist/settings/settings.d.ts +22 -7
  176. package/dist/settings/settings.js +47 -26
  177. package/dist/settings/tool-registrations.d.ts +25 -32
  178. package/dist/settings/tool-registrations.js +66 -56
  179. package/dist/settings/types.d.ts +1 -1
  180. package/dist/settings/ui-tab-definitions.d.ts +1 -1
  181. package/dist/settings/write-service.d.ts +32 -26
  182. package/dist/settings/write-service.js +26 -20
  183. package/dist/taxonomy/agent-tools.d.ts +1 -66
  184. package/dist/taxonomy/agent-tools.js +25 -15
  185. package/dist/taxonomy/content-lookup.js +1 -1
  186. package/dist/taxonomy/index.d.ts +2 -2
  187. package/dist/taxonomy/merge-term.d.ts +7 -4
  188. package/dist/taxonomy/merge-term.js +7 -5
  189. package/dist/taxonomy/repo.memory.d.ts +42 -19
  190. package/dist/taxonomy/repo.memory.js +14 -12
  191. package/dist/taxonomy/validation-chain.d.ts +39 -18
  192. package/dist/taxonomy/validation-chain.js +35 -25
  193. package/dist/taxonomy/write-service.d.ts +103 -44
  194. package/dist/taxonomy/write-service.js +221 -179
  195. package/dist/trash/adapters/record-store.d.ts +52 -0
  196. package/dist/trash/adapters/record-store.js +36 -0
  197. package/dist/trash/cursor.d.ts +20 -0
  198. package/dist/trash/cursor.js +36 -0
  199. package/dist/trash/follow-ups.d.ts +76 -0
  200. package/dist/trash/follow-ups.js +46 -0
  201. package/dist/trash/index.d.ts +8 -0
  202. package/dist/trash/index.js +8 -0
  203. package/dist/trash/ports.d.ts +308 -0
  204. package/dist/trash/ports.js +2 -0
  205. package/dist/trash/repo.memory.d.ts +50 -0
  206. package/dist/trash/repo.memory.js +114 -0
  207. package/dist/trash/sweeper.d.ts +77 -0
  208. package/dist/trash/sweeper.js +127 -0
  209. package/dist/trash/write-service.d.ts +100 -0
  210. package/dist/trash/write-service.js +242 -0
  211. package/dist/workspace/agent-tools.d.ts +1 -59
  212. package/dist/workspace/agent-tools.js +27 -12
  213. package/dist/workspace/create.d.ts +31 -6
  214. package/dist/workspace/create.js +18 -5
  215. package/dist/workspace/delete.d.ts +18 -13
  216. package/dist/workspace/delete.js +20 -12
  217. package/dist/workspace/index.d.ts +1 -1
  218. package/dist/workspace/repo.memory.d.ts +19 -4
  219. package/dist/workspace/repo.memory.js +26 -4
  220. package/dist/workspace/tool-registrations.d.ts +2 -2
  221. package/dist/workspace/tool-registrations.js +13 -13
  222. package/dist/workspace/update.d.ts +6 -5
  223. package/dist/workspace/update.js +9 -8
  224. package/package.json +44 -38
  225. package/dist/identity/admin-crud-service.d.ts +0 -280
  226. package/dist/identity/admin-crud-service.js +0 -468
  227. package/dist/identity/agent-tool-input.d.ts +0 -58
  228. package/dist/identity/agent-tool-input.js +0 -126
  229. package/dist/identity/agent-tools.d.ts +0 -101
  230. package/dist/identity/agent-tools.js +0 -290
  231. package/dist/identity/auth-service.d.ts +0 -118
  232. package/dist/identity/auth-service.js +0 -170
  233. package/dist/identity/authorize.d.ts +0 -90
  234. package/dist/identity/authorize.js +0 -94
  235. package/dist/identity/grant-service.d.ts +0 -198
  236. package/dist/identity/grant-service.js +0 -361
  237. package/dist/identity/hasher.d.ts +0 -39
  238. package/dist/identity/hasher.js +0 -39
  239. package/dist/identity/index.d.ts +0 -62
  240. package/dist/identity/index.js +0 -54
  241. package/dist/identity/password-policy.d.ts +0 -57
  242. package/dist/identity/password-policy.js +0 -66
  243. package/dist/identity/permission-migrations.d.ts +0 -86
  244. package/dist/identity/permission-migrations.js +0 -67
  245. package/dist/identity/permissions.d.ts +0 -35
  246. package/dist/identity/permissions.js +0 -603
  247. package/dist/identity/ports.d.ts +0 -201
  248. package/dist/identity/repo.memory.d.ts +0 -164
  249. package/dist/identity/repo.memory.js +0 -224
  250. package/dist/identity/seed.d.ts +0 -54
  251. package/dist/identity/seed.js +0 -429
  252. package/dist/identity/tool-registrations.d.ts +0 -50
  253. package/dist/identity/tool-registrations.js +0 -419
  254. package/dist/identity/types.d.ts +0 -185
  255. package/dist/identity/types.js +0 -64
  256. package/dist/identity/username.d.ts +0 -12
  257. package/dist/identity/username.js +0 -14
  258. /package/dist/{identity → media/import}/ports.js +0 -0
@@ -1,170 +0,0 @@
1
- import { createHash, randomBytes } from "node:crypto";
2
- import { resolveEffectivePermissions } from "./authorize.js";
3
- import { AuthInvalidCredentialsError } from "./types.js";
4
- import { normalizeUsername } from "./username.js";
5
- /**
6
- * @file Login / logout / session validation (REQ-06, state.spec `LOGIN`/`LOGOUT`).
7
- *
8
- * Purpose:
9
- * The credential-verification + session-lifecycle surface `middleware/dev-auth.ts`
10
- * wires into HTTP. Session tokens are hashed with SHA-256 before storage — the
11
- * same technique `members/write-service.ts` uses for magic-link/member-session
12
- * tokens (INV-05: the raw token is never persisted). Password verification is
13
- * argon2id (`hasher.ts`), never a raw comparison.
14
- *
15
- * `createSessionForPrincipal` (2026-09-06) is the one session minter; `login()` calls it after
16
- * verifying credentials, and a host that identifies a principal by some other means (e.g. a
17
- * loopback-only single-use boot token, which has nothing to verify a password against) calls it
18
- * directly instead of re-implementing session construction. Before this export existed, a
19
- * downstream host had duplicated this file's private `hashToken` byte-for-byte because
20
- * `login()` was the only minter and hard-requires a password — this closes that fork.
21
- *
22
- * Architectural role:
23
- * Ordinary core functions (like `updatePost`/`membersWriteService`), not a
24
- * port — session/credential logic has one implementation.
25
- */
26
- /** RT-004/behavior.spec §4: absolute session lifetime, fixed at creation. */
27
- export const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
28
- /** SHA-256 hex digest of a raw bearer token (mirrors `members/write-service.ts`'s `hashToken`). */
29
- function hashToken(rawToken) {
30
- return createHash("sha256").update(rawToken).digest("hex");
31
- }
32
- function newRawToken() {
33
- return randomBytes(32).toString("hex");
34
- }
35
- function isoPlusMs(nowIso, ms) {
36
- return new Date(new Date(nowIso).getTime() + ms).toISOString();
37
- }
38
- /**
39
- * Mint and persist a session for a principal the caller has ALREADY identified by some means
40
- * other than this function (password verification in `login()` below, or a caller-specific proof
41
- * such as a loopback-only single-use boot token). Factored out of `login()`'s former inline
42
- * session-construction block (2026-09-06) so both paths share one minter — a host with no
43
- * password to verify no longer has to hand-roll its own token hashing (and risk drifting from
44
- * this file's private `hashToken`) just to mint a session for a principal it has already proven
45
- * by other means.
46
- *
47
- * @complexity O(1) — one session write.
48
- * @overallScore 100
49
- */
50
- export async function createSessionForPrincipal(required) {
51
- const { deps, input } = required;
52
- const nowIso = deps.clock.nowIso();
53
- const rawToken = newRawToken();
54
- const session = {
55
- id: deps.idGen.newId(),
56
- workspaceId: input.workspaceId,
57
- principalId: input.principalId,
58
- tokenHash: hashToken(rawToken),
59
- createdAt: nowIso,
60
- expiresAt: isoPlusMs(nowIso, SESSION_TTL_MS),
61
- ip: input.ip,
62
- userAgent: input.userAgent,
63
- };
64
- await deps.repos.sessions.save(session);
65
- return { session, rawToken };
66
- }
67
- /**
68
- * Verify `username`+`password` for an `active` principal and mint a session
69
- * (AC-02, REQ-06). Constant-shaped failure: a nonexistent username, a
70
- * disabled principal, and a wrong password are all `AuthInvalidCredentialsError`
71
- * with the same message — no user-enumeration signal.
72
- *
73
- * @complexity O(1) — one username lookup, one principal lookup, one hash
74
- * verify, one session write (via `createSessionForPrincipal`), one `lastLoginAt` write.
75
- * @overallScore 100
76
- */
77
- export async function login(required) {
78
- const { deps, input } = required;
79
- const username = normalizeUsername(input.username);
80
- if (!username || !input.password) {
81
- throw new AuthInvalidCredentialsError("invalid username or password");
82
- }
83
- const userRow = await deps.repos.users.findByUsername({ workspaceId: input.workspaceId, username });
84
- if (!userRow) {
85
- throw new AuthInvalidCredentialsError("invalid username or password");
86
- }
87
- const principal = await deps.repos.principals.findById({
88
- workspaceId: input.workspaceId,
89
- id: userRow.principalId,
90
- });
91
- if (!principal || principal.status !== "active") {
92
- throw new AuthInvalidCredentialsError("invalid username or password");
93
- }
94
- const passwordOk = await deps.hasher.verify(userRow.passwordHash, input.password);
95
- if (!passwordOk) {
96
- throw new AuthInvalidCredentialsError("invalid username or password");
97
- }
98
- const { session, rawToken } = await createSessionForPrincipal({
99
- deps,
100
- input: { workspaceId: input.workspaceId, principalId: principal.id, ip: input.ip, userAgent: input.userAgent },
101
- });
102
- // Reuses the session's own `createdAt` rather than a second `clock.nowIso()` call — both are
103
- // meant to be the same instant, and reading one avoids two clock reads ever disagreeing.
104
- await deps.repos.users.save({ ...userRow, lastLoginAt: session.createdAt });
105
- return { principal, session, rawToken };
106
- }
107
- /**
108
- * Resolve a session cookie's raw token to its principal, applying every
109
- * fail-closed check server-side (EC-02/EC-03/EC-13, AC-05/AC-20): unknown
110
- * token, revoked, past absolute `expiresAt`, or a disabled bound principal
111
- * all resolve to `null` (the caller maps that to 401 `UNAUTHENTICATED` and
112
- * never reaches `authorize()`).
113
- *
114
- * @complexity O(1) — one token-hash lookup, one principal lookup.
115
- * @overallScore 100
116
- */
117
- export async function validateSession(required) {
118
- const { deps, input } = required;
119
- const nowIso = input.nowIso ?? deps.clock.nowIso();
120
- const session = await deps.repos.sessions.findByTokenHash({
121
- workspaceId: input.workspaceId,
122
- tokenHash: hashToken(input.rawToken),
123
- });
124
- if (!session)
125
- return null;
126
- if (session.revokedAt)
127
- return null;
128
- // EC-13/AC-20: past absolute expiry is treated as revoked, never reaching authorize().
129
- if (session.expiresAt <= nowIso)
130
- return null;
131
- const principal = await deps.repos.principals.findById({
132
- workspaceId: input.workspaceId,
133
- id: session.principalId,
134
- });
135
- // EC-02: a disabled principal's live session stops validating immediately.
136
- if (!principal || principal.status !== "active")
137
- return null;
138
- return { principal, session };
139
- }
140
- /** Revoke the session bound to `rawToken`. Idempotent — already-revoked/unknown is a no-op. */
141
- export async function logout(required) {
142
- const { deps, input } = required;
143
- const session = await deps.repos.sessions.findByTokenHash({
144
- workspaceId: input.workspaceId,
145
- tokenHash: hashToken(input.rawToken),
146
- });
147
- if (!session || session.revokedAt)
148
- return;
149
- await deps.repos.sessions.revoke({
150
- workspaceId: input.workspaceId,
151
- id: session.id,
152
- revokedAt: deps.clock.nowIso(),
153
- });
154
- }
155
- /** The introspection surface for `AUTH_ME` (REQ-07): dotted permission strings, `["*"]` for owner. */
156
- export async function getEffectivePermissions(required) {
157
- const rows = await resolveEffectivePermissions({
158
- deps: {
159
- principals: required.deps.principals,
160
- principalRoles: required.deps.principalRoles,
161
- rolePolicies: required.deps.rolePolicies,
162
- principalPolicies: required.deps.principalPolicies,
163
- policyPermissions: required.deps.policyPermissions,
164
- },
165
- principalId: required.input.principalId,
166
- workspaceId: required.input.workspaceId,
167
- });
168
- return [...new Set(rows.map((row) => row.permission))];
169
- }
170
- //# sourceMappingURL=auth-service.js.map
@@ -1,90 +0,0 @@
1
- import type { UUID } from "../core/ports.js";
2
- import type { PolicyPermissionRepoPort, PrincipalPolicyRepoPort, PrincipalRepoPort, PrincipalRoleRepoPort, RolePolicyRepoPort } from "./ports.js";
3
- import type { PolicyPermissionRecord } from "./types.js";
4
- /**
5
- * @file `authorize()` — the RBAC evaluator (REQ-04).
6
- *
7
- * Purpose:
8
- * Ordinary core function, deliberately NOT a port (one evaluator —
9
- * "no PolicyPort"). The command gateway calls this before every
10
- * mutation (REQ-05); it is deterministic and side-effect free given the
11
- * repo snapshot (state.spec §4).
12
- *
13
- * How it relates to the project:
14
- * - `command.ts`'s `executeCommand` calls a bound closure of this shape
15
- * (typed there as `AuthorizeFn`, kept generic so `core/commands` never
16
- * imports this feature-level library — see that file's header).
17
- * - Route handlers call it directly for mutations that do not flow through
18
- * the command gateway (this pass: `members` disable — see the handoff).
19
- *
20
- * Architectural role:
21
- * Implements the matcher precedence in behavior.spec §1.1 exactly:
22
- * disabled-principal short-circuit > owner wildcard > exact matching row >
23
- * fail-closed default. Fail-closed extensibility: a non-null
24
- * `constraintJson` is never treated as unconstrained, and a `resourceType`
25
- * mismatch/absence is never treated as a global grant.
26
- */
27
- /** The context an authorization decision is evaluated against (REQ-04). */
28
- export interface AuthorizeContext {
29
- workspaceId: UUID;
30
- entityType?: string;
31
- entityId?: UUID;
32
- }
33
- /** `authorize()`'s result — always fail-closed on `allowed: false` (INV-03). */
34
- export interface AuthorizeResult {
35
- allowed: boolean;
36
- /** Machine-readable reason: `principal_disabled | no_grant | resource_scope_mismatch | unconstrained_deny | owner_wildcard | matched`. */
37
- reason: string;
38
- }
39
- /** Repos `authorize()`/`resolveEffectivePermissions()` read. Not a port bag with alternate adapters — see file header. */
40
- export interface AuthorizeDeps {
41
- principals: PrincipalRepoPort;
42
- principalRoles: PrincipalRoleRepoPort;
43
- rolePolicies: RolePolicyRepoPort;
44
- principalPolicies: PrincipalPolicyRepoPort;
45
- policyPermissions: PolicyPermissionRepoPort;
46
- }
47
- /**
48
- * Resolve a principal's effective permission rows: the union of (a) its
49
- * roles' policies' permissions and (b) policies attached directly via
50
- * `principal_policies` (behavior.spec §1.2). Scoped to `workspaceId` by
51
- * construction (every repo call below is workspace-scoped).
52
- *
53
- * @complexity O(r*p + d) round trips, r = roles held, p = policies per role,
54
- * d = direct policy attachments — all expected small (a handful of rows per
55
- * principal). No caller-unbounded collection is walked.
56
- * @overallScore 100
57
- */
58
- export declare function resolveEffectivePermissions(required: {
59
- deps: AuthorizeDeps;
60
- principalId: UUID;
61
- workspaceId: UUID;
62
- }): Promise<PolicyPermissionRecord[]>;
63
- /**
64
- * The RBAC decision (REQ-04). Fail-closed: `allowed` defaults to `false` and
65
- * only ever flips to `true` on an explicit disabled-short-circuit-cleared,
66
- * matching row.
67
- *
68
- * Precedence (behavior.spec §1.1, highest to lowest):
69
- * 1. Disabled principal -> `false`, `principal_disabled` (overrides all grants).
70
- * 2. An unconstrained owner `"*"` row -> `true`, `owner_wildcard`.
71
- * 3. An exact matching row (permission + workspace + resource-scope + no
72
- * constraint) -> `true`, `matched`.
73
- * 4. Fail-closed default -> `false`, with the most specific diagnostic reason
74
- * available among same-permission candidates (`unconstrained_deny` over
75
- * `resource_scope_mismatch` over `no_grant` — an inferred, undocumented-but-
76
- * reasonable tie-break; the spec only pins each reason in isolation, see
77
- * AC-09/AC-14/EC-04).
78
- *
79
- * @complexity O(resolveEffectivePermissions) + O(k) local filtering, k = the
80
- * principal's effective row count (small, bounded by grants a workspace
81
- * administrator creates by hand).
82
- * @overallScore 100
83
- */
84
- export declare function authorize(required: {
85
- deps: AuthorizeDeps;
86
- principalId: UUID;
87
- permission: string;
88
- context: AuthorizeContext;
89
- }): Promise<AuthorizeResult>;
90
- //# sourceMappingURL=authorize.d.ts.map
@@ -1,94 +0,0 @@
1
- /**
2
- * Resolve a principal's effective permission rows: the union of (a) its
3
- * roles' policies' permissions and (b) policies attached directly via
4
- * `principal_policies` (behavior.spec §1.2). Scoped to `workspaceId` by
5
- * construction (every repo call below is workspace-scoped).
6
- *
7
- * @complexity O(r*p + d) round trips, r = roles held, p = policies per role,
8
- * d = direct policy attachments — all expected small (a handful of rows per
9
- * principal). No caller-unbounded collection is walked.
10
- * @overallScore 100
11
- */
12
- export async function resolveEffectivePermissions(required) {
13
- const { deps, principalId, workspaceId } = required;
14
- const roleLinks = await deps.principalRoles.listByPrincipalId({ workspaceId, principalId });
15
- const rolePermissionGroups = await Promise.all(roleLinks.map(async (roleLink) => {
16
- const rolePolicyLinks = await deps.rolePolicies.listByRoleId({
17
- workspaceId,
18
- roleId: roleLink.roleId,
19
- });
20
- const policyPermissionGroups = await Promise.all(rolePolicyLinks.map((rolePolicyLink) => deps.policyPermissions.listByPolicyId({ workspaceId, policyId: rolePolicyLink.policyId })));
21
- return policyPermissionGroups.flat();
22
- }));
23
- const directLinks = await deps.principalPolicies.listByPrincipalId({ workspaceId, principalId });
24
- const directPermissionGroups = await Promise.all(directLinks.map((directLink) => deps.policyPermissions.listByPolicyId({ workspaceId, policyId: directLink.policyId })));
25
- return [...rolePermissionGroups.flat(), ...directPermissionGroups.flat()];
26
- }
27
- /**
28
- * True iff `row` is an exact, unconstrained-or-scope-matching grant of
29
- * `permission` in `context` (state.spec §4 `matchesRow`). Assumes `row` has
30
- * already passed the owner-wildcard check upstream — this never special-cases
31
- * `"*"`.
32
- */
33
- function matchesRow(row, permission, context) {
34
- if (row.permission !== permission)
35
- return false;
36
- if (row.constraintJson != null)
37
- return false;
38
- if (row.resourceType != null && row.resourceType !== context.entityType)
39
- return false;
40
- return true;
41
- }
42
- /**
43
- * The RBAC decision (REQ-04). Fail-closed: `allowed` defaults to `false` and
44
- * only ever flips to `true` on an explicit disabled-short-circuit-cleared,
45
- * matching row.
46
- *
47
- * Precedence (behavior.spec §1.1, highest to lowest):
48
- * 1. Disabled principal -> `false`, `principal_disabled` (overrides all grants).
49
- * 2. An unconstrained owner `"*"` row -> `true`, `owner_wildcard`.
50
- * 3. An exact matching row (permission + workspace + resource-scope + no
51
- * constraint) -> `true`, `matched`.
52
- * 4. Fail-closed default -> `false`, with the most specific diagnostic reason
53
- * available among same-permission candidates (`unconstrained_deny` over
54
- * `resource_scope_mismatch` over `no_grant` — an inferred, undocumented-but-
55
- * reasonable tie-break; the spec only pins each reason in isolation, see
56
- * AC-09/AC-14/EC-04).
57
- *
58
- * @complexity O(resolveEffectivePermissions) + O(k) local filtering, k = the
59
- * principal's effective row count (small, bounded by grants a workspace
60
- * administrator creates by hand).
61
- * @overallScore 100
62
- */
63
- export async function authorize(required) {
64
- const { deps, principalId, permission, context } = required;
65
- const principal = await deps.principals.findById({ workspaceId: context.workspaceId, id: principalId });
66
- if (!principal || principal.status === "disabled") {
67
- return { allowed: false, reason: "principal_disabled" };
68
- }
69
- const effectiveRows = await resolveEffectivePermissions({
70
- deps,
71
- principalId,
72
- workspaceId: context.workspaceId,
73
- });
74
- const hasUnconstrainedWildcard = effectiveRows.some((row) => row.permission === "*" && row.resourceType == null && row.constraintJson == null);
75
- if (hasUnconstrainedWildcard) {
76
- return { allowed: true, reason: "owner_wildcard" };
77
- }
78
- const candidates = effectiveRows.filter((row) => row.permission === permission);
79
- if (candidates.length === 0) {
80
- return { allowed: false, reason: "no_grant" };
81
- }
82
- const matched = candidates.some((row) => matchesRow(row, permission, context));
83
- if (matched) {
84
- return { allowed: true, reason: "matched" };
85
- }
86
- if (candidates.some((row) => row.constraintJson != null)) {
87
- return { allowed: false, reason: "unconstrained_deny" };
88
- }
89
- if (candidates.some((row) => row.resourceType != null)) {
90
- return { allowed: false, reason: "resource_scope_mismatch" };
91
- }
92
- return { allowed: false, reason: "no_grant" };
93
- }
94
- //# sourceMappingURL=authorize.js.map
@@ -1,198 +0,0 @@
1
- import type { UUID } from "../core/ports.js";
2
- import { type AuthorizeDeps } from "./authorize.js";
3
- import type { AuthServiceDeps } from "./auth-service.js";
4
- import type { IdentityRepos } from "./ports.js";
5
- import { type PolicyRecord, type PrincipalPolicyRecord, type PrincipalRecord, type PrincipalRoleRecord, type RoleRecord, type UserRecord } from "./types.js";
6
- /**
7
- * @file Human grant-writing transitions (state.spec.md §3): `CREATE_USER`,
8
- * `CREATE_ROLE`, `CREATE_POLICY`, `ASSIGN_ROLE`, `ATTACH_POLICY`.
9
- *
10
- * Purpose:
11
- * The only usable identity principal before this file was the seeded owner —
12
- * there was no API path to create a second user or grant/change permissions.
13
- * These five functions are that path, following `auth-service.ts`'s exact
14
- * shape (`{ deps: AuthServiceDeps, input: {...} }`, one exported async
15
- * function per transition, plain `Error` subclasses for control flow).
16
- *
17
- * Out of scope (deferred, see the dispatch directive): `ISSUE_API_KEY`,
18
- * `CREATE_PRINCIPAL` (api_key/agent principal creation), agent/api_key
19
- * delegation, `WRITE_POLICY_PERMISSION`, `DISABLE_PRINCIPAL`.
20
- *
21
- * Architectural role:
22
- * Ordinary core functions, not ports (mirrors `auth-service.ts`'s reasoning —
23
- * grant-writing logic has one implementation). Each function calls
24
- * `authorize()`/`resolveEffectivePermissions()` from `authorize.ts` itself
25
- * (rather than pushing the gate into the route layer, the way
26
- * `members/disable.ts`'s route does) so the caller-permission gate and the
27
- * INV-07 clamp are exercised directly by unit tests, no HTTP harness needed.
28
- */
29
- /**
30
- * Assemble the `AuthorizeDeps` bag `authorize()`/`resolveEffectivePermissions()` expect from the
31
- * flat `IdentityRepos` bag. Exported so `admin-crud-service.ts`'s new transitions
32
- * (`ENABLE_PRINCIPAL`/`UPDATE_USER`/`RESET_USER_PASSWORD`/`UPDATE_ROLE`/`UPDATE_POLICY`/
33
- * `DELETE_ROLE`/`DELETE_POLICY`/`WRITE_POLICY_PERMISSION`/`DISABLE_PRINCIPAL`) reuse the identical
34
- * gate/clamp logic rather than duplicating it — same file-split reasoning `auth-service.ts` and
35
- * `grant-service.ts` already use (one shared helper set, two transition files).
36
- */
37
- export declare function authorizeDepsFrom(repos: IdentityRepos): AuthorizeDeps;
38
- /**
39
- * Fail-closed caller-permission gate shared by every transition in this file.
40
- * Throws `IdentityForbiddenError` (403 `FORBIDDEN`) unless the caller holds
41
- * at least one of `permissions` (an OR gate — `CREATE_USER` accepts either
42
- * `user.manage` or `member.manage`, state.spec §3).
43
- *
44
- * @complexity O(p) `authorize()` calls, p = `permissions.length` (1 or 2 in
45
- * every caller this pass) — each itself O(resolveEffectivePermissions), see
46
- * that function's doc.
47
- * @overallScore 100
48
- */
49
- export declare function assertCallerHasAnyPermission(required: {
50
- deps: AuthServiceDeps;
51
- workspaceId: UUID;
52
- callerPrincipalId: UUID;
53
- permissions: readonly string[];
54
- }): Promise<void>;
55
- /**
56
- * The INV-07 grant-authority clamp (feature.spec.md, state.spec §3
57
- * `ASSIGN_ROLE`/`ATTACH_POLICY` rows): every permission in `conferredPermissions`
58
- * must be a matching effective row the caller holds unconstrained
59
- * (`resourceType`/`constraintJson` both null; owner's `*` qualifies for all).
60
- * A permission held only conditionally (resource-scoped or constraint-bound)
61
- * cannot be delegated. Throws `GrantExceedsIssuerError` (403
62
- * `GRANT_EXCEEDS_ISSUER`) naming every offending permission, and writes no
63
- * grant row (caller does the write only after this resolves).
64
- *
65
- * @complexity O(resolveEffectivePermissions) + O(k) local filtering, k =
66
- * `conferredPermissions.length` (bounded by one role's/policy's permission
67
- * rows — a handful, per `identity/INFO.md`'s scale assumptions).
68
- * @overallScore 100
69
- */
70
- export declare function assertGrantClamp(required: {
71
- deps: AuthServiceDeps;
72
- workspaceId: UUID;
73
- callerPrincipalId: UUID;
74
- conferredPermissions: readonly string[];
75
- }): Promise<void>;
76
- /**
77
- * `CREATE_USER` (state.spec §3, REQ-01/MF-1). Atomically mints a NEW
78
- * `kind='user'` principal and its `users` row — the `principalId` is always
79
- * generated here, never caller-supplied, so this transition can never attach
80
- * a credential to a pre-existing principal (the MF-1 privilege-escalation
81
- * class: an earlier draft let an admin bind a password to the seeded owner
82
- * principal and log in as owner). Gated by `user.manage` OR `member.manage`
83
- * (admin onboarding, AC-22).
84
- *
85
- * "Atomically" here means structurally, not via a DB transaction — the
86
- * in-memory adapters have no cross-repo transaction (same disclosed gap as
87
- * `seed.ts`/`executeCommand`'s compensating-rollback path). The invariant
88
- * that actually matters (no credential ever attaches to an existing
89
- * principal) holds regardless, because `principalId` is generated inline and
90
- * the password is hashed before either row is written, so a hashing failure
91
- * leaves no orphan principal. A future SQLite adapter wraps both writes in
92
- * one real transaction (port/adapter rule-of-two) without changing this contract.
93
- *
94
- * @complexity O(1) — one permission check, one username lookup, one hash,
95
- * two saves.
96
- * @overallScore 100
97
- */
98
- export declare function createUser(required: {
99
- deps: AuthServiceDeps;
100
- input: {
101
- workspaceId: UUID;
102
- callerPrincipalId: UUID;
103
- username: string;
104
- email?: string | undefined;
105
- password: string;
106
- };
107
- }): Promise<{
108
- principal: PrincipalRecord;
109
- user: UserRecord;
110
- }>;
111
- /**
112
- * `CREATE_ROLE` (state.spec §3). Gated by `role.manage`. Always mints
113
- * `isBuiltin=false` — built-ins are SEED-only and immutable (INV-06).
114
- *
115
- * Deviation from the state.spec §3 table (disclosed): that row lists
116
- * `name`, `description?` jointly for `CREATE_ROLE`/`CREATE_POLICY`, but
117
- * `RoleRecord` (types.ts, frozen before this task) has no `description`
118
- * field — only `PolicyRecord` does. `createRole` therefore takes `name`
119
- * only; `createPolicy` below takes both.
120
- *
121
- * @complexity O(1) — one permission check, one save.
122
- * @overallScore 100
123
- */
124
- export declare function createRole(required: {
125
- deps: AuthServiceDeps;
126
- input: {
127
- workspaceId: UUID;
128
- callerPrincipalId: UUID;
129
- name: string;
130
- };
131
- }): Promise<{
132
- role: RoleRecord;
133
- }>;
134
- /**
135
- * `CREATE_POLICY` (state.spec §3). Gated by `role.manage`. Always mints
136
- * `isBuiltin=false` and `isFrozen=false` — only the (out-of-scope this pass)
137
- * `ISSUE_API_KEY` transition ever mints `isFrozen=true` policies.
138
- *
139
- * @complexity O(1) — one permission check, one save.
140
- * @overallScore 100
141
- */
142
- export declare function createPolicy(required: {
143
- deps: AuthServiceDeps;
144
- input: {
145
- workspaceId: UUID;
146
- callerPrincipalId: UUID;
147
- name: string;
148
- description?: string | undefined;
149
- };
150
- }): Promise<{
151
- policy: PolicyRecord;
152
- }>;
153
- /**
154
- * `ASSIGN_ROLE` (state.spec §3, REQ-02, AC-24/AC-25). Gated by `role.manage`;
155
- * target must be a `kind='user'` principal (AC-25b); the INV-07 clamp must
156
- * pass over every permission carried by the role's policies (AC-24) — so
157
- * assigning the built-in `owner` role requires the caller to already hold
158
- * the wildcard `*` (owner-only). No row is written on any rejection.
159
- *
160
- * @complexity O(resolveEffectivePermissions) + O(role's rolePolicy count)
161
- * round trips to gather conferred permissions — both bounded by the small,
162
- * hand-authored grant graph this system assumes (see `identity/INFO.md`).
163
- * @overallScore 100
164
- */
165
- export declare function assignRole(required: {
166
- deps: AuthServiceDeps;
167
- input: {
168
- workspaceId: UUID;
169
- callerPrincipalId: UUID;
170
- principalId: UUID;
171
- roleId: UUID;
172
- };
173
- }): Promise<{
174
- assignment: PrincipalRoleRecord;
175
- }>;
176
- /**
177
- * `ATTACH_POLICY` (state.spec §3, REQ-02, AC-24/AC-25). Gated by
178
- * `role.manage`; target must be a `kind='user'` principal (AC-25b); the
179
- * INV-07 clamp must pass over every permission the policy carries (AC-24) —
180
- * attaching the built-in `owner` policy requires the caller to already hold
181
- * `*` (owner-only). No row is written on any rejection.
182
- *
183
- * @complexity O(resolveEffectivePermissions) + O(policy's permission row
184
- * count) — both bounded, see `assignRole`'s doc.
185
- * @overallScore 100
186
- */
187
- export declare function attachPolicy(required: {
188
- deps: AuthServiceDeps;
189
- input: {
190
- workspaceId: UUID;
191
- callerPrincipalId: UUID;
192
- principalId: UUID;
193
- policyId: UUID;
194
- };
195
- }): Promise<{
196
- attachment: PrincipalPolicyRecord;
197
- }>;
198
- //# sourceMappingURL=grant-service.d.ts.map