@bevel-software/platform-core-backend 0.19.0 → 0.20.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 (255) hide show
  1. package/dist/core/create-core-server.d.ts.map +1 -1
  2. package/dist/core/create-core-server.js +22 -0
  3. package/dist/core/create-core-server.js.map +1 -1
  4. package/dist/core/create-core-services.d.ts +3 -0
  5. package/dist/core/create-core-services.d.ts.map +1 -1
  6. package/dist/core/create-core-services.js +10 -1
  7. package/dist/core/create-core-services.js.map +1 -1
  8. package/dist/modules/audit/agent-audit.service.d.ts +54 -0
  9. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -0
  10. package/dist/modules/audit/agent-audit.service.js +290 -0
  11. package/dist/modules/audit/agent-audit.service.js.map +1 -0
  12. package/dist/modules/audit/agent-rest-audit.middleware.d.ts +40 -0
  13. package/dist/modules/audit/agent-rest-audit.middleware.d.ts.map +1 -0
  14. package/dist/modules/audit/agent-rest-audit.middleware.js +75 -0
  15. package/dist/modules/audit/agent-rest-audit.middleware.js.map +1 -0
  16. package/dist/modules/audit/audit.contract.d.ts +145 -0
  17. package/dist/modules/audit/audit.contract.d.ts.map +1 -0
  18. package/dist/modules/audit/audit.contract.js +35 -0
  19. package/dist/modules/audit/audit.contract.js.map +1 -0
  20. package/dist/modules/audit/audit.routes.d.ts +21 -0
  21. package/dist/modules/audit/audit.routes.d.ts.map +1 -0
  22. package/dist/modules/audit/audit.routes.js +157 -0
  23. package/dist/modules/audit/audit.routes.js.map +1 -0
  24. package/dist/modules/audit/event-classifier.d.ts +52 -0
  25. package/dist/modules/audit/event-classifier.d.ts.map +1 -0
  26. package/dist/modules/audit/event-classifier.js +123 -0
  27. package/dist/modules/audit/event-classifier.js.map +1 -0
  28. package/dist/modules/audit/index.d.ts +7 -0
  29. package/dist/modules/audit/index.d.ts.map +1 -0
  30. package/dist/modules/audit/index.js +7 -0
  31. package/dist/modules/audit/index.js.map +1 -0
  32. package/dist/modules/audit/request-audit.d.ts +57 -0
  33. package/dist/modules/audit/request-audit.d.ts.map +1 -0
  34. package/dist/modules/audit/request-audit.js +117 -0
  35. package/dist/modules/audit/request-audit.js.map +1 -0
  36. package/dist/modules/database/core-schema.d.ts +414 -0
  37. package/dist/modules/database/core-schema.d.ts.map +1 -1
  38. package/dist/modules/database/core-schema.js +99 -0
  39. package/dist/modules/database/core-schema.js.map +1 -1
  40. package/dist/modules/diff/diff.service.d.ts.map +1 -1
  41. package/dist/modules/diff/diff.service.js +6 -1
  42. package/dist/modules/diff/diff.service.js.map +1 -1
  43. package/dist/modules/kb-fs/git-guarded-filesystem.d.ts +9 -1
  44. package/dist/modules/kb-fs/git-guarded-filesystem.d.ts.map +1 -1
  45. package/dist/modules/kb-fs/git-guarded-filesystem.js +16 -3
  46. package/dist/modules/kb-fs/git-guarded-filesystem.js.map +1 -1
  47. package/dist/modules/mcp/mcp-auth.middleware.d.ts +1 -0
  48. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  49. package/dist/modules/mcp/mcp-auth.middleware.js +32 -0
  50. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  51. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  52. package/dist/modules/mcp/mcp.routes.js +8 -1
  53. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  54. package/dist/modules/mcp/mcp.service.d.ts +9 -1
  55. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  56. package/dist/modules/mcp/mcp.service.js +48 -5
  57. package/dist/modules/mcp/mcp.service.js.map +1 -1
  58. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +59 -0
  59. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  60. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +180 -8
  61. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  62. package/dist/modules/settings/deployment-settings.service.d.ts +10 -2
  63. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  64. package/dist/modules/settings/deployment-settings.service.js +32 -6
  65. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  66. package/dist/modules/tool-auth/external-api-key.interface.d.ts +21 -5
  67. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  68. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  69. package/dist/modules/tool-auth/external-api-key.service.d.ts +1 -0
  70. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  71. package/dist/modules/tool-auth/external-api-key.service.js +22 -9
  72. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  73. package/dist/modules/tool-auth/internal-token.service.d.ts +10 -0
  74. package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
  75. package/dist/modules/tool-auth/internal-token.service.js +1 -0
  76. package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
  77. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +8 -0
  78. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  79. package/dist/modules/tool-auth/tool-auth.middleware.js +9 -1
  80. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  81. package/dist/modules/workspace/workspace.service.d.ts +8 -0
  82. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  83. package/dist/modules/workspace/workspace.service.js +9 -0
  84. package/dist/modules/workspace/workspace.service.js.map +1 -1
  85. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  86. package/dist/modules/workspace/workspace.tools.js +49 -13
  87. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  88. package/dist/shared/git-internals.d.ts +7 -1
  89. package/dist/shared/git-internals.d.ts.map +1 -1
  90. package/dist/shared/git-internals.js +67 -9
  91. package/dist/shared/git-internals.js.map +1 -1
  92. package/migrations/0012_agent_audit.sql +41 -0
  93. package/migrations/0013_agent_identity_and_key_soft_delete.sql +29 -0
  94. package/migrations/meta/0012_snapshot.json +2247 -0
  95. package/migrations/meta/0013_snapshot.json +2259 -0
  96. package/migrations/meta/_journal.json +14 -0
  97. package/package.json +4 -4
  98. package/src/core/create-core-server.ts +28 -0
  99. package/src/core/create-core-services.ts +13 -0
  100. package/src/modules/audit/__tests__/agent-audit.service.test.ts +370 -0
  101. package/src/modules/audit/__tests__/agent-rest-audit.middleware.test.ts +113 -0
  102. package/src/modules/audit/__tests__/audit.routes.test.ts +201 -0
  103. package/src/modules/audit/__tests__/event-classifier.test.ts +121 -0
  104. package/src/modules/audit/agent-audit.service.ts +316 -0
  105. package/src/modules/audit/agent-rest-audit.middleware.ts +83 -0
  106. package/src/modules/audit/audit.contract.ts +164 -0
  107. package/src/modules/audit/audit.routes.ts +168 -0
  108. package/src/modules/audit/event-classifier.ts +155 -0
  109. package/src/modules/audit/index.ts +14 -0
  110. package/src/modules/audit/request-audit.ts +141 -0
  111. package/src/modules/database/core-schema.ts +104 -0
  112. package/src/modules/diff/diff.service.ts +6 -1
  113. package/src/modules/kb-fs/git-guarded-filesystem.ts +16 -3
  114. package/src/modules/mcp/__tests__/bevel-oauth-provider.test.ts +183 -13
  115. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -2
  116. package/src/modules/mcp/__tests__/mcp.routes.local-token.test.ts +24 -4
  117. package/src/modules/mcp/__tests__/mcp.routes.stateless.test.ts +1 -1
  118. package/src/modules/mcp/__tests__/mcp.service.test.ts +120 -3
  119. package/src/modules/mcp/mcp-auth.middleware.ts +31 -0
  120. package/src/modules/mcp/mcp.routes.ts +11 -1
  121. package/src/modules/mcp/mcp.service.ts +62 -4
  122. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +184 -6
  123. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +35 -0
  124. package/src/modules/settings/deployment-settings.service.ts +47 -8
  125. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +29 -8
  126. package/src/modules/tool-auth/__tests__/internal-token.service.test.ts +6 -0
  127. package/src/modules/tool-auth/external-api-key.interface.ts +22 -5
  128. package/src/modules/tool-auth/external-api-key.service.ts +25 -8
  129. package/src/modules/tool-auth/internal-token.service.ts +11 -0
  130. package/src/modules/tool-auth/tool-auth.middleware.ts +17 -1
  131. package/src/modules/workspace/__tests__/git-internals.security.test.ts +135 -44
  132. package/src/modules/workspace/workspace.service.ts +9 -0
  133. package/src/modules/workspace/workspace.tools.ts +47 -11
  134. package/src/shared/__tests__/git-internals.test.ts +41 -0
  135. package/src/shared/git-internals.ts +61 -6
  136. package/dist/modules/access/access-errors.d.ts +0 -34
  137. package/dist/modules/access/access-errors.d.ts.map +0 -1
  138. package/dist/modules/access/access-errors.js +0 -40
  139. package/dist/modules/access/access-errors.js.map +0 -1
  140. package/dist/modules/access/access-splice.d.ts +0 -140
  141. package/dist/modules/access/access-splice.d.ts.map +0 -1
  142. package/dist/modules/access/access-splice.js +0 -389
  143. package/dist/modules/access/access-splice.js.map +0 -1
  144. package/dist/modules/access/group-files.d.ts +0 -83
  145. package/dist/modules/access/group-files.d.ts.map +0 -1
  146. package/dist/modules/access/group-files.js +0 -167
  147. package/dist/modules/access/group-files.js.map +0 -1
  148. package/dist/modules/access/kb-read-filter.d.ts +0 -41
  149. package/dist/modules/access/kb-read-filter.d.ts.map +0 -1
  150. package/dist/modules/access/kb-read-filter.js +0 -60
  151. package/dist/modules/access/kb-read-filter.js.map +0 -1
  152. package/dist/modules/access/render-roles-yaml.d.ts +0 -22
  153. package/dist/modules/access/render-roles-yaml.d.ts.map +0 -1
  154. package/dist/modules/access/render-roles-yaml.js +0 -56
  155. package/dist/modules/access/render-roles-yaml.js.map +0 -1
  156. package/dist/modules/access/roles-yaml-guard.d.ts +0 -56
  157. package/dist/modules/access/roles-yaml-guard.d.ts.map +0 -1
  158. package/dist/modules/access/roles-yaml-guard.js +0 -79
  159. package/dist/modules/access/roles-yaml-guard.js.map +0 -1
  160. package/dist/modules/diff/diff-paths.d.ts +0 -2
  161. package/dist/modules/diff/diff-paths.d.ts.map +0 -1
  162. package/dist/modules/diff/diff-paths.js +0 -9
  163. package/dist/modules/diff/diff-paths.js.map +0 -1
  164. package/dist/modules/groups/group-provision.service.d.ts +0 -130
  165. package/dist/modules/groups/group-provision.service.d.ts.map +0 -1
  166. package/dist/modules/groups/group-provision.service.js +0 -288
  167. package/dist/modules/groups/group-provision.service.js.map +0 -1
  168. package/dist/modules/groups/groups.contract.d.ts +0 -106
  169. package/dist/modules/groups/groups.contract.d.ts.map +0 -1
  170. package/dist/modules/groups/groups.contract.js +0 -36
  171. package/dist/modules/groups/groups.contract.js.map +0 -1
  172. package/dist/modules/groups/groups.routes.d.ts +0 -42
  173. package/dist/modules/groups/groups.routes.d.ts.map +0 -1
  174. package/dist/modules/groups/groups.routes.js +0 -379
  175. package/dist/modules/groups/groups.routes.js.map +0 -1
  176. package/dist/modules/groups/groups.service.d.ts +0 -60
  177. package/dist/modules/groups/groups.service.d.ts.map +0 -1
  178. package/dist/modules/groups/groups.service.js +0 -172
  179. package/dist/modules/groups/groups.service.js.map +0 -1
  180. package/dist/modules/groups/index.d.ts +0 -7
  181. package/dist/modules/groups/index.d.ts.map +0 -1
  182. package/dist/modules/groups/index.js +0 -6
  183. package/dist/modules/groups/index.js.map +0 -1
  184. package/dist/modules/groups/join-proposals.d.ts +0 -53
  185. package/dist/modules/groups/join-proposals.d.ts.map +0 -1
  186. package/dist/modules/groups/join-proposals.js +0 -67
  187. package/dist/modules/groups/join-proposals.js.map +0 -1
  188. package/dist/modules/groups/join-requests.service.d.ts +0 -81
  189. package/dist/modules/groups/join-requests.service.d.ts.map +0 -1
  190. package/dist/modules/groups/join-requests.service.js +0 -135
  191. package/dist/modules/groups/join-requests.service.js.map +0 -1
  192. package/dist/modules/mcp/mcp-session-store.d.ts +0 -74
  193. package/dist/modules/mcp/mcp-session-store.d.ts.map +0 -1
  194. package/dist/modules/mcp/mcp-session-store.js +0 -131
  195. package/dist/modules/mcp/mcp-session-store.js.map +0 -1
  196. package/dist/modules/workflow/file-change-notifier.d.ts +0 -38
  197. package/dist/modules/workflow/file-change-notifier.d.ts.map +0 -1
  198. package/dist/modules/workflow/file-change-notifier.js +0 -22
  199. package/dist/modules/workflow/file-change-notifier.js.map +0 -1
  200. package/dist/modules/workflow/git/branch-name.d.ts +0 -10
  201. package/dist/modules/workflow/git/branch-name.d.ts.map +0 -1
  202. package/dist/modules/workflow/git/branch-name.js +0 -76
  203. package/dist/modules/workflow/git/branch-name.js.map +0 -1
  204. package/dist/modules/workflow/git/clone-config.d.ts +0 -61
  205. package/dist/modules/workflow/git/clone-config.d.ts.map +0 -1
  206. package/dist/modules/workflow/git/clone-config.js +0 -69
  207. package/dist/modules/workflow/git/clone-config.js.map +0 -1
  208. package/dist/modules/workflow/git/mutex.d.ts +0 -11
  209. package/dist/modules/workflow/git/mutex.d.ts.map +0 -1
  210. package/dist/modules/workflow/git/mutex.js +0 -23
  211. package/dist/modules/workflow/git/mutex.js.map +0 -1
  212. package/dist/modules/workflow/locking-filesystem.d.ts +0 -137
  213. package/dist/modules/workflow/locking-filesystem.d.ts.map +0 -1
  214. package/dist/modules/workflow/locking-filesystem.js +0 -553
  215. package/dist/modules/workflow/locking-filesystem.js.map +0 -1
  216. package/dist/modules/workflow/read-only-filesystem.d.ts +0 -24
  217. package/dist/modules/workflow/read-only-filesystem.d.ts.map +0 -1
  218. package/dist/modules/workflow/read-only-filesystem.js +0 -39
  219. package/dist/modules/workflow/read-only-filesystem.js.map +0 -1
  220. package/dist/modules/workflow/workflow.errors.d.ts +0 -197
  221. package/dist/modules/workflow/workflow.errors.d.ts.map +0 -1
  222. package/dist/modules/workflow/workflow.errors.js +0 -298
  223. package/dist/modules/workflow/workflow.errors.js.map +0 -1
  224. package/dist/modules/workspace/bevel-ignore.d.ts +0 -30
  225. package/dist/modules/workspace/bevel-ignore.d.ts.map +0 -1
  226. package/dist/modules/workspace/bevel-ignore.js +0 -61
  227. package/dist/modules/workspace/bevel-ignore.js.map +0 -1
  228. package/dist/modules/workspace/kb-seed.interface.d.ts +0 -36
  229. package/dist/modules/workspace/kb-seed.interface.d.ts.map +0 -1
  230. package/dist/modules/workspace/kb-seed.interface.js +0 -2
  231. package/dist/modules/workspace/kb-seed.interface.js.map +0 -1
  232. package/dist/modules/workspace/kb-seed.service.d.ts +0 -125
  233. package/dist/modules/workspace/kb-seed.service.d.ts.map +0 -1
  234. package/dist/modules/workspace/kb-seed.service.js +0 -535
  235. package/dist/modules/workspace/kb-seed.service.js.map +0 -1
  236. package/dist/modules/workspace/plugins-migration.d.ts +0 -50
  237. package/dist/modules/workspace/plugins-migration.d.ts.map +0 -1
  238. package/dist/modules/workspace/plugins-migration.js +0 -379
  239. package/dist/modules/workspace/plugins-migration.js.map +0 -1
  240. package/dist/shared/fs-errors.d.ts +0 -8
  241. package/dist/shared/fs-errors.d.ts.map +0 -1
  242. package/dist/shared/fs-errors.js +0 -11
  243. package/dist/shared/fs-errors.js.map +0 -1
  244. package/dist/shared/fs-walk.d.ts +0 -23
  245. package/dist/shared/fs-walk.d.ts.map +0 -1
  246. package/dist/shared/fs-walk.js +0 -42
  247. package/dist/shared/fs-walk.js.map +0 -1
  248. package/dist/shared/hash-email.d.ts +0 -11
  249. package/dist/shared/hash-email.d.ts.map +0 -1
  250. package/dist/shared/hash-email.js +0 -14
  251. package/dist/shared/hash-email.js.map +0 -1
  252. package/dist/shared/kb-walk.d.ts +0 -52
  253. package/dist/shared/kb-walk.d.ts.map +0 -1
  254. package/dist/shared/kb-walk.js +0 -76
  255. package/dist/shared/kb-walk.js.map +0 -1
@@ -1,389 +0,0 @@
1
- /**
2
- * Surgical text-splice editor for `access.md` frontmatter and node-frontmatter
3
- * access verbs.
4
- *
5
- * WHY a splice and not parse→model→emit: the resolver's parser
6
- * (`parseYamlSubset` / `extractFrontmatter` / `stripComment`) deliberately
7
- * throws away comments, blank lines, key order, AND the markdown body below the
8
- * closing `---`. Re-serialising from the parsed model would silently DELETE the
9
- * prose bodies the real `access.md` files carry (the repo-root one is a whole
10
- * README) — and a `parse(emit(x))` round-trip test would PASS anyway because
11
- * parse ignores the body. So every mutation here edits the RAW text: we locate
12
- * the verb's block, splice exactly one list line in or out, and leave every
13
- * other byte — comments, ordering, blank lines, the body — untouched.
14
- *
15
- * ---
16
- * write: (verb key line, indent 0)
17
- * - Admin (list item, indent 2) <- splice point
18
- * - Felix <f@x.eu> (list item)
19
- * # a comment (preserved verbatim)
20
- * owner: [] (inline-empty list)
21
- * ---
22
- * # Body prose (preserved verbatim)
23
- *
24
- * The same engine serves node-frontmatter, which additionally accepts a scalar
25
- * form (`owner: Felix <f@x.eu>`). Adding a second principal to a scalar verb
26
- * normalises it to the block-list form.
27
- */
28
- import { parseAccessEntry, canonicalEmail, canonicalRoleName, accessMdDeclaresBodyRules, } from './access-control.service.js';
29
- /** Thrown when the input can't be spliced safely (malformed frontmatter, etc.). */
30
- export class AccessSpliceError extends Error {
31
- constructor(message) {
32
- super(message);
33
- this.name = 'AccessSpliceError';
34
- }
35
- }
36
- /** Control chars (NUL..US) — newlines, tabs, etc. Never allowed in a principal. */
37
- // eslint-disable-next-line no-control-regex -- intentional control-char injection guard
38
- const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
39
- /**
40
- * Render a principal as the canonical access-entry line value the parser reads
41
- * back (`Name <email>` or a bare role display name). The caller is responsible
42
- * for having validated the principal first (see `validatePrincipal`).
43
- *
44
- * `deny` prefixes the entry with `deny ` so the resolver reads it as a denial
45
- * (the `deny-here` per-item override) — same line shape, opposite verdict.
46
- */
47
- export function renderEntry(p, deny = false) {
48
- const body = p.kind === 'user' ? `${p.displayName} <${p.email}>` : p.role;
49
- return deny ? `deny ${body}` : body;
50
- }
51
- /**
52
- * Validate a principal before it is spliced into a file. Rejects anything that
53
- * could break frontmatter or smuggle in extra YAML — newlines, control chars,
54
- * and emails / names that don't match the resolver's own regexes. This is the
55
- * injection guard: a principal that survives this is safe to render verbatim
56
- * into a ` - ` list line.
57
- *
58
- * Returns the canonical principal (lowercased email / canonical role) on
59
- * success, or throws `AccessSpliceError`.
60
- */
61
- export function validatePrincipal(p) {
62
- if (p.kind === 'user') {
63
- const displayName = p.displayName.trim();
64
- const email = canonicalEmail(p.email);
65
- if (!displayName)
66
- throw new AccessSpliceError('person grant needs a display name');
67
- // `#` is rejected because `uncomment()` truncates a whitespace-preceded `#`
68
- // on read-back, so a name containing it would not round-trip through blockEntries.
69
- if (CONTROL_CHARS.test(displayName) ||
70
- displayName.includes('<') ||
71
- displayName.includes('>') ||
72
- displayName.includes('#')) {
73
- throw new AccessSpliceError(`invalid display name: ${JSON.stringify(p.displayName)}`);
74
- }
75
- // Reuse the resolver's own email shape so a granted entry always parses back.
76
- if (!/^[^<>\s@]+@[^<>\s@]+\.[^<>\s@]+$/.test(email)) {
77
- throw new AccessSpliceError(`invalid email: ${JSON.stringify(p.email)}`);
78
- }
79
- // The rendered `Name <email>` line must round-trip through parseAccessEntry.
80
- const round = parseAccessEntry(`${displayName} <${email}>`);
81
- if (!round.ok || round.entry.kind !== 'user' || round.entry.deny) {
82
- throw new AccessSpliceError(`person grant does not round-trip: ${JSON.stringify(p)}`);
83
- }
84
- return { kind: 'user', email, displayName };
85
- }
86
- const role = p.role.trim();
87
- const canonical = canonicalRoleName(role);
88
- if (!canonical)
89
- throw new AccessSpliceError('role grant needs a name');
90
- if (CONTROL_CHARS.test(role) ||
91
- role.includes('<') ||
92
- role.includes('>') ||
93
- role.includes(':') ||
94
- role.includes('#')) {
95
- throw new AccessSpliceError(`invalid role name: ${JSON.stringify(p.role)}`);
96
- }
97
- // `deny` is never a grantee — it's the denial prefix. `everyone` IS grantable
98
- // (the built-in public role); the access route restricts it to the read verb.
99
- if (canonical === 'deny') {
100
- throw new AccessSpliceError(`'${role}' is a reserved name and cannot be granted as a role`);
101
- }
102
- const round = parseAccessEntry(role);
103
- if (!round.ok || round.entry.kind !== 'role' || round.entry.deny) {
104
- throw new AccessSpliceError(`role grant does not round-trip: ${JSON.stringify(p)}`);
105
- }
106
- return { kind: 'role', role };
107
- }
108
- /**
109
- * True when a parsed entry names the given principal (ignoring deny prefix).
110
- * `tokenMatch` (role-shaped principals only) selects exact-spelling vs
111
- * name-level matching — see {@link TokenMatch}.
112
- */
113
- function entryMatches(entry, p, tokenMatch) {
114
- if (p.kind === 'user')
115
- return entry.kind === 'user' && entry.email === canonicalEmail(p.email);
116
- if (entry.kind !== 'role')
117
- return false;
118
- const canonicalPrincipal = canonicalRoleName(p.role);
119
- if (tokenMatch === 'exact')
120
- return entry.role === canonicalPrincipal;
121
- const strip = (token) => token.startsWith('role/') ? canonicalRoleName(token.slice('role/'.length)) : token;
122
- return strip(entry.role) === strip(canonicalPrincipal);
123
- }
124
- /**
125
- * Split a file into [pre, frontmatter-lines, post] without losing a byte.
126
- * `hasFrontmatter` is false when the file has no `---` fence yet — callers that
127
- * grant into a fresh file synthesise one.
128
- */
129
- function splitFrontmatter(text) {
130
- const eol = text.includes('\r\n') ? '\r\n' : '\n';
131
- const lines = text.split(/\r?\n/);
132
- // Opening fence must be the very first line.
133
- if (lines[0]?.trim() !== '---') {
134
- return { pre: [], fm: [], post: lines, eol, hasFrontmatter: false };
135
- }
136
- for (let i = 1; i < lines.length; i++) {
137
- if (lines[i].trim() === '---') {
138
- return {
139
- pre: lines.slice(0, 1), // the opening ---
140
- fm: lines.slice(1, i),
141
- post: lines.slice(i), // closing --- onward (body preserved)
142
- eol,
143
- hasFrontmatter: true,
144
- };
145
- }
146
- }
147
- throw new AccessSpliceError('unterminated frontmatter — no closing `---`');
148
- }
149
- function joinFrontmatter(f) {
150
- return [...f.pre, ...f.fm, ...f.post].join(f.eol);
151
- }
152
- /**
153
- * The line region a mutation edits, per {@link SpliceTarget}. `lines` aliases
154
- * the region's array (edited in place, like `f.fm` always was); `join()`
155
- * reassembles the whole file around it.
156
- */
157
- function pickRegion(text, f, target) {
158
- if (target === 'folder' && f.hasFrontmatter && accessMdDeclaresBodyRules(text)) {
159
- // New format: post = [closing '---', ...body]. Edit the body lines.
160
- const closing = f.post[0];
161
- const body = f.post.slice(1);
162
- return {
163
- lines: body,
164
- join: () => [...f.pre, ...f.fm, closing, ...body].join(f.eol),
165
- };
166
- }
167
- return { lines: f.fm, join: () => joinFrontmatter(f) };
168
- }
169
- /** Leading-space count of a line. */
170
- function indentOf(line) {
171
- const m = line.match(/^( *)/);
172
- return m ? m[1].length : 0;
173
- }
174
- /** Strip a trailing `# comment` the way the resolver's tokeniser does. */
175
- function uncomment(line) {
176
- let inWs = true;
177
- for (let i = 0; i < line.length; i++) {
178
- const ch = line[i];
179
- if (ch === '#' && (inWs || (i > 0 && /\s/.test(line[i - 1]))))
180
- return line.slice(0, i);
181
- if (!/\s/.test(ch))
182
- inWs = false;
183
- }
184
- return line;
185
- }
186
- /**
187
- * Locate a verb's block within the frontmatter lines. Walks the raw lines (not
188
- * the parsed model) so we can address them for splicing. Recognises the three
189
- * shapes the resolver accepts: a block list, an inline `verb: []`, and (node
190
- * frontmatter only) an inline scalar `verb: value`.
191
- */
192
- function findVerbBlock(fm, verb) {
193
- const block = {
194
- keyLine: -1,
195
- keyIndent: 0,
196
- itemLines: [],
197
- itemIndent: -1,
198
- inlineEmpty: false,
199
- inlineScalarValue: null,
200
- };
201
- for (let i = 0; i < fm.length; i++) {
202
- const content = uncomment(fm[i]);
203
- if (!content.trim())
204
- continue;
205
- const indent = indentOf(content);
206
- const body = content.slice(indent);
207
- // A list item belonging to the current verb block.
208
- if ((body.startsWith('- ') || body === '-') &&
209
- block.keyLine !== -1 &&
210
- indent > block.keyIndent) {
211
- block.itemLines.push(i);
212
- if (block.itemIndent === -1)
213
- block.itemIndent = indent;
214
- continue;
215
- }
216
- const colon = body.indexOf(':');
217
- if (colon < 0)
218
- continue; // not a key line; ignore (the parser would too)
219
- const key = body.slice(0, colon).trim();
220
- const value = body.slice(colon + 1).trim();
221
- if (block.keyLine !== -1) {
222
- // We already found our verb; a new key at <= our indent ends the block.
223
- if (indent <= block.keyIndent)
224
- break;
225
- continue;
226
- }
227
- if (key === verb && indent === 0) {
228
- block.keyLine = i;
229
- block.keyIndent = indent;
230
- if (value === '[]')
231
- block.inlineEmpty = true;
232
- else if (value !== '')
233
- block.inlineScalarValue = value;
234
- }
235
- }
236
- if (block.itemIndent === -1)
237
- block.itemIndent = block.keyIndent + 2;
238
- return block;
239
- }
240
- /**
241
- * Parse the items currently under a verb block into ParsedEntry form, for
242
- * matching. Handles list items and the inline-scalar form.
243
- */
244
- function blockEntries(fm, block) {
245
- const out = [];
246
- if (block.inlineScalarValue !== null) {
247
- const r = parseAccessEntry(block.inlineScalarValue);
248
- if (r.ok)
249
- out.push({ entry: r.entry, line: block.keyLine });
250
- return out;
251
- }
252
- for (const li of block.itemLines) {
253
- const content = uncomment(fm[li]).trim();
254
- const value = content === '-' ? '' : content.slice(2).trim();
255
- if (!value)
256
- continue;
257
- const r = parseAccessEntry(value);
258
- if (r.ok)
259
- out.push({ entry: r.entry, line: li });
260
- }
261
- return out;
262
- }
263
- // ---------------------------------------------------------------------------
264
- // Public splice operations
265
- // ---------------------------------------------------------------------------
266
- /**
267
- * Add `principal` under `verb`. Idempotent: if an entry with the EXACT same
268
- * token spelling already exists with the SAME deny-ness (a grant when
269
- * granting, a deny when denying), returns `changed: false`. Idempotency is
270
- * deliberately exact-token, never name-level: a bare token and a `role/`
271
- * token are different principals (group-first precedence gives the bare key
272
- * to a same-named group), so a bare-`X` GROUP grant must go through even when
273
- * `role/X` is present, and vice versa. Creates the verb key (and the
274
- * frontmatter fence, for a fresh file) as needed. Preserves everything else
275
- * byte-for-byte.
276
- *
277
- * `allowScalar` controls whether a fresh single-entry verb may be written in
278
- * the node-frontmatter scalar form (`owner: Name <email>`). access.md always
279
- * uses the block-list form; node frontmatter prefers scalar for a lone entry.
280
- *
281
- * `deny` writes the entry as a `deny <principal>` denial instead of a grant
282
- * (the `deny-here` per-item override). The block/scalar/empty-list handling is
283
- * identical — only the rendered line and the idempotency check differ — so the
284
- * deny path reuses this engine rather than a parallel `spliceDeny`. NOTE: the
285
- * resolver lets a same-scope grant override a same-scope deny of the same
286
- * principal, so a caller adding a deny where a grant already exists must strip
287
- * that grant first (see `AccessMutationService`); this function only writes the
288
- * one entry it's asked to.
289
- */
290
- export function spliceGrant(text, verb, rawPrincipal, opts = {}) {
291
- const principal = validatePrincipal(rawPrincipal);
292
- const deny = opts.deny ?? false;
293
- const f = splitFrontmatter(text);
294
- if (!f.hasFrontmatter) {
295
- // Fresh file — synthesise a minimal frontmatter block, keep any body lines
296
- // the caller passed (usually none for a new access.md).
297
- const itemLine = opts.allowScalar
298
- ? `${verb}: ${renderEntry(principal, deny)}`
299
- : `${verb}:${f.eol} - ${renderEntry(principal, deny)}`;
300
- const fenced = ['---', itemLine, '---'].join(f.eol);
301
- const bodyText = f.post.length ? f.post.join(f.eol) : '';
302
- const text2 = bodyText ? `${fenced}${f.eol}${bodyText}` : `${fenced}${f.eol}`;
303
- return { text: text2, changed: true };
304
- }
305
- const region = pickRegion(text, f, opts.target ?? 'node');
306
- const block = findVerbBlock(region.lines, verb);
307
- const existing = blockEntries(region.lines, block);
308
- // Idempotency: an entry for this EXACT token with the same deny-ness is
309
- // already present (a grant when granting, a deny when denying). Exact-token
310
- // on purpose — see the doc above.
311
- if (existing.some((e) => entryMatches(e.entry, principal, 'exact') && e.entry.deny === deny)) {
312
- return { text, changed: false };
313
- }
314
- const itemValue = renderEntry(principal, deny);
315
- if (block.keyLine === -1) {
316
- // Verb key absent — append a new block at the end of the region.
317
- const lines = opts.allowScalar ? [`${verb}: ${itemValue}`] : [`${verb}:`, ` - ${itemValue}`];
318
- region.lines.push(...lines);
319
- return { text: region.join(), changed: true };
320
- }
321
- if (block.inlineScalarValue !== null) {
322
- // Scalar form — promote to a block list with both the old and new entry.
323
- const old = block.inlineScalarValue;
324
- region.lines.splice(block.keyLine, 1, `${verb}:`, ` - ${old}`, ` - ${itemValue}`);
325
- return { text: region.join(), changed: true };
326
- }
327
- // Block-list (possibly inline `[]`). Insert after the last existing item, or
328
- // right after the key line when the list is currently empty.
329
- const indent = ' '.repeat(block.itemIndent);
330
- const newLine = `${indent}- ${itemValue}`;
331
- if (block.inlineEmpty) {
332
- // `verb: []` -> `verb:` + one item.
333
- region.lines[block.keyLine] = `${' '.repeat(block.keyIndent)}${verb}:`;
334
- region.lines.splice(block.keyLine + 1, 0, newLine);
335
- return { text: region.join(), changed: true };
336
- }
337
- const insertAt = block.itemLines.length > 0 ? block.itemLines[block.itemLines.length - 1] + 1 : block.keyLine + 1;
338
- region.lines.splice(insertAt, 0, newLine);
339
- return { text: region.join(), changed: true };
340
- }
341
- /**
342
- * Remove every entry naming `principal` under `verb` (grant or deny). Idempotent:
343
- * returns `changed: false` when the principal isn't present. If removing the
344
- * last item empties the verb, the `verb:` key is collapsed to `verb: []` so the
345
- * file still parses cleanly (the resolver reads `[]` as an empty list).
346
- * Preserves everything else.
347
- *
348
- * Token matching for role-shaped principals is `tokenMatch` (default `'name'`:
349
- * BOTH spellings — revoking the role removes legacy bare spellings too).
350
- * Callers pass `'exact'` when a GROUP shadows the principal's bare name: the
351
- * bare token then belongs to the group and the `role/` token to the role, so a
352
- * revoke of one must never strip the other (see AccessMutationService, which
353
- * derives the flag from the merged principal index).
354
- */
355
- export function spliceRevoke(text, verb, rawPrincipal, opts = {}) {
356
- const principal = validatePrincipal(rawPrincipal);
357
- const tokenMatch = opts.tokenMatch ?? 'name';
358
- const f = splitFrontmatter(text);
359
- if (!f.hasFrontmatter)
360
- return { text, changed: false };
361
- const region = pickRegion(text, f, opts.target ?? 'node');
362
- const block = findVerbBlock(region.lines, verb);
363
- if (block.keyLine === -1)
364
- return { text, changed: false };
365
- if (block.inlineScalarValue !== null) {
366
- const r = parseAccessEntry(block.inlineScalarValue);
367
- if (r.ok && entryMatches(r.entry, principal, tokenMatch)) {
368
- // Lone scalar entry removed -> collapse to empty list.
369
- region.lines[block.keyLine] = `${' '.repeat(block.keyIndent)}${verb}: []`;
370
- return { text: region.join(), changed: true };
371
- }
372
- return { text, changed: false };
373
- }
374
- const entries = blockEntries(region.lines, block);
375
- const victims = entries.filter((e) => entryMatches(e.entry, principal, tokenMatch)).map((e) => e.line);
376
- if (victims.length === 0)
377
- return { text, changed: false };
378
- // Delete from the bottom up so earlier indices stay valid.
379
- const remaining = block.itemLines.filter((l) => !victims.includes(l));
380
- for (const line of [...victims].sort((a, b) => b - a))
381
- region.lines.splice(line, 1);
382
- if (remaining.length === 0) {
383
- // Verb block is now empty — collapse `verb:` to `verb: []`. The key line
384
- // index is unchanged because we only removed lines that came after it.
385
- region.lines[block.keyLine] = `${' '.repeat(block.keyIndent)}${verb}: []`;
386
- }
387
- return { text: region.join(), changed: true };
388
- }
389
- //# sourceMappingURL=access-splice.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"access-splice.js","sourceRoot":"","sources":["../../../src/modules/access/access-splice.ts"],"names":[],"mappings":"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"}
@@ -1,83 +0,0 @@
1
- /**
2
- * Group files — the "who you are" half of the roles/groups split.
3
- *
4
- * Groups are people-sets referenced by access grants ("GTM Team can read the
5
- * battlecards"). A deployment has exactly ONE group source at a time — a mode
6
- * derived from which file exists, so it holds at any git ref:
7
- *
8
- * - `synced-groups.yaml` EXISTS → IdP mode. The file is MACHINE-OWNED,
9
- * regenerated wholesale from the SCIM directory mirror; membership is
10
- * managed in the IdP. `groups.yaml` is ignored entirely.
11
- * - otherwise → manual mode. `groups.yaml` (UI-edited)
12
- * is the source.
13
- *
14
- * Both files share one format, the group twin of roles.yaml:
15
- *
16
- * groups:
17
- * Engineering:
18
- * - ada@x.io
19
- * - bo@x.io
20
- * Empty Group: []
21
- *
22
- * Parsing is FORGIVING at the entry level by design — a malformed email or a
23
- * reserved/duplicate name skips that entry with a warning, it never takes the
24
- * whole file down. Only structural failures (unparsable YAML, `groups:` not a
25
- * mapping) reject the file, and even then the RESOLVER degrades to "this file
26
- * contributes nothing" rather than throwing: unlike roles.yaml there is no
27
- * admin-lockout risk here, so groups must never be able to brick access
28
- * resolution. Strict acceptance for WRITES is the job of the write-side
29
- * validator (`validateGroupsFile` below), which treats warnings as refusals.
30
- */
31
- export declare const GROUPS_YAML = "groups.yaml";
32
- export declare const SYNCED_GROUPS_YAML = "synced-groups.yaml";
33
- /**
34
- * THE name-safety predicate for principal display names — the single source
35
- * of truth every surface derives from (group creation/rename, IdP sync
36
- * materialization; thin assert wrappers sit on top). Returns a human-readable
37
- * reason the name is unsafe, or null when it is fine.
38
- *
39
- * The character rules keep the emitted YAML parseable and the entry grammar
40
- * unambiguous:
41
- * `:` mis-tokenises as a nested mapping key
42
- * `#` truncated as a comment by the subset parser's stripComment
43
- * `<` / `>` collide with the `Name <email>` user-reference shape
44
- * leading - tokenises as a list item
45
- * \x00-\x1f control chars / newlines break line structure outright
46
- * `role/…` reserved: that spelling is the EXPLICIT role token in access
47
- * entries — a group carrying it could never be referenced.
48
- */
49
- export declare function unsafeNameReason(displayName: string): string | null;
50
- /** One parsed group: display name + canonicalised member emails. */
51
- export interface GroupDefinition {
52
- displayName: string;
53
- emails: Set<string>;
54
- }
55
- /** Canonical group name → definition, in file order. */
56
- export type GroupsIndex = Map<string, GroupDefinition>;
57
- export interface ParsedGroupsFile {
58
- ok: true;
59
- groups: GroupsIndex;
60
- /** Entry-level problems that were skipped over (bad email, dup name, …). */
61
- warnings: string[];
62
- }
63
- export interface GroupsFileError {
64
- ok: false;
65
- errors: string[];
66
- }
67
- /**
68
- * Parse a group file (either of the two — same grammar). See the module note
69
- * for the forgiving-vs-structural split.
70
- */
71
- export declare function parseGroupsFile(text: string, filename: string): ParsedGroupsFile | GroupsFileError;
72
- /**
73
- * Write-gate twin of {@link parseGroupsFile}: strict — structural errors AND
74
- * entry-level warnings both refuse the candidate, so a UI write can never land
75
- * a group the resolver would silently skip.
76
- */
77
- export declare function validateGroupsFile(text: string, filename: string): {
78
- ok: true;
79
- } | {
80
- ok: false;
81
- errors: string[];
82
- };
83
- //# sourceMappingURL=group-files.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"group-files.d.ts","sourceRoot":"","sources":["../../../src/modules/access/group-files.ts"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,eAAO,MAAM,WAAW,gBAAgB,CAAC;AACzC,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAEvD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWnE;AAED,oEAAoE;AACpE,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACrB;AAED,wDAAwD;AACxD,MAAM,MAAM,WAAW,GAAG,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;AAEvD,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,IAAI,CAAC;IACT,MAAM,EAAE,WAAW,CAAC;IACpB,4EAA4E;IAC5E,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf,gBAAgB,GAAG,eAAe,CAyFpC;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAWhD"}
@@ -1,167 +0,0 @@
1
- import { EMAIL_REGEX, GROUP_REF_PREFIX, RESERVED_ROLE_NAMES, ROLE_TOKEN_PREFIX, canonicalEmail, canonicalRoleName, parseYamlSubset, } from './access-control.service.js';
2
- /**
3
- * Group files — the "who you are" half of the roles/groups split.
4
- *
5
- * Groups are people-sets referenced by access grants ("GTM Team can read the
6
- * battlecards"). A deployment has exactly ONE group source at a time — a mode
7
- * derived from which file exists, so it holds at any git ref:
8
- *
9
- * - `synced-groups.yaml` EXISTS → IdP mode. The file is MACHINE-OWNED,
10
- * regenerated wholesale from the SCIM directory mirror; membership is
11
- * managed in the IdP. `groups.yaml` is ignored entirely.
12
- * - otherwise → manual mode. `groups.yaml` (UI-edited)
13
- * is the source.
14
- *
15
- * Both files share one format, the group twin of roles.yaml:
16
- *
17
- * groups:
18
- * Engineering:
19
- * - ada@x.io
20
- * - bo@x.io
21
- * Empty Group: []
22
- *
23
- * Parsing is FORGIVING at the entry level by design — a malformed email or a
24
- * reserved/duplicate name skips that entry with a warning, it never takes the
25
- * whole file down. Only structural failures (unparsable YAML, `groups:` not a
26
- * mapping) reject the file, and even then the RESOLVER degrades to "this file
27
- * contributes nothing" rather than throwing: unlike roles.yaml there is no
28
- * admin-lockout risk here, so groups must never be able to brick access
29
- * resolution. Strict acceptance for WRITES is the job of the write-side
30
- * validator (`validateGroupsFile` below), which treats warnings as refusals.
31
- */
32
- export const GROUPS_YAML = 'groups.yaml';
33
- export const SYNCED_GROUPS_YAML = 'synced-groups.yaml';
34
- /**
35
- * THE name-safety predicate for principal display names — the single source
36
- * of truth every surface derives from (group creation/rename, IdP sync
37
- * materialization; thin assert wrappers sit on top). Returns a human-readable
38
- * reason the name is unsafe, or null when it is fine.
39
- *
40
- * The character rules keep the emitted YAML parseable and the entry grammar
41
- * unambiguous:
42
- * `:` mis-tokenises as a nested mapping key
43
- * `#` truncated as a comment by the subset parser's stripComment
44
- * `<` / `>` collide with the `Name <email>` user-reference shape
45
- * leading - tokenises as a list item
46
- * \x00-\x1f control chars / newlines break line structure outright
47
- * `role/…` reserved: that spelling is the EXPLICIT role token in access
48
- * entries — a group carrying it could never be referenced.
49
- */
50
- export function unsafeNameReason(displayName) {
51
- const trimmed = displayName.trim();
52
- if (!trimmed)
53
- return 'empty name';
54
- if (/[:#<>]/.test(trimmed))
55
- return "contains ':', '#', '<', or '>'";
56
- // eslint-disable-next-line no-control-regex
57
- if (/[\x00-\x1f]/.test(trimmed))
58
- return 'contains control characters';
59
- if (trimmed.startsWith('-'))
60
- return "starts with '-'";
61
- if (canonicalRoleName(trimmed).startsWith(ROLE_TOKEN_PREFIX)) {
62
- return `starts with the reserved '${ROLE_TOKEN_PREFIX}' prefix (the explicit role token in access entries)`;
63
- }
64
- return null;
65
- }
66
- /**
67
- * Parse a group file (either of the two — same grammar). See the module note
68
- * for the forgiving-vs-structural split.
69
- */
70
- export function parseGroupsFile(text, filename) {
71
- // Tolerate empty keys at parse: this file's contract is ENTRY-level
72
- // forgiveness (a blank group name is skipped with a warning below), and a
73
- // hard tokenizer error here would retire every OTHER group fail-closed.
74
- const parsed = parseYamlSubset(text, { tolerateEmptyKeys: true });
75
- if (!parsed.ok)
76
- return { ok: false, errors: [`${filename}: ${parsed.error}`] };
77
- const root = parsed.value;
78
- if (root == null || typeof root !== 'object' || Array.isArray(root)) {
79
- return { ok: false, errors: [`${filename}: must be a top-level mapping`] };
80
- }
81
- const groupsNode = root.groups;
82
- // An empty file (or one with no `groups:` key yet) is a valid empty set —
83
- // a fresh deployment's groups.yaml starts this way.
84
- if (groupsNode == null)
85
- return { ok: true, groups: new Map(), warnings: [] };
86
- if (typeof groupsNode !== 'object' || Array.isArray(groupsNode)) {
87
- return { ok: false, errors: [`${filename}: 'groups' must be a mapping`] };
88
- }
89
- const groups = new Map();
90
- const warnings = [];
91
- for (const [displayName, value] of Object.entries(groupsNode)) {
92
- const canonical = canonicalRoleName(displayName);
93
- if (!canonical) {
94
- warnings.push(`${filename}: empty group name — skipped`);
95
- continue;
96
- }
97
- // THE shared name-safety predicate (see `unsafeNameReason` above) — the
98
- // same rules every write surface asserts. The parser must apply it too:
99
- // a name the writers refuse (control chars, `<`/`>`, a leading `-`, the
100
- // reserved `role/` prefix) can still reach a group file by hand edit or
101
- // through a skewed writer, and accepting it here would let the resolver
102
- // honor — and `validateGroupsFile` pass — a name no editor can ever
103
- // produce or reference safely.
104
- const unsafe = unsafeNameReason(displayName);
105
- if (unsafe) {
106
- warnings.push(`${filename}: group ${JSON.stringify(displayName)} ${unsafe} — skipped`);
107
- continue;
108
- }
109
- if (RESERVED_ROLE_NAMES.has(canonical)) {
110
- warnings.push(`${filename}: group '${displayName}' uses reserved name '${canonical}' — skipped`);
111
- continue;
112
- }
113
- if (groups.has(canonical)) {
114
- warnings.push(`${filename}: group '${displayName}' canonicalises to '${canonical}', already declared as '${groups.get(canonical).displayName}' — skipped`);
115
- continue;
116
- }
117
- if (value !== null && !Array.isArray(value)) {
118
- warnings.push(`${filename}: group '${displayName}' must be a list of emails — skipped`);
119
- continue;
120
- }
121
- const emails = new Set();
122
- for (const raw of value ?? []) {
123
- if (typeof raw !== 'string') {
124
- warnings.push(`${filename}: group '${displayName}' has a non-string entry — entry skipped`);
125
- continue;
126
- }
127
- const email = canonicalEmail(raw);
128
- // The reserved `group:` prefix would PASS the email regex
129
- // (`group:lee@x.io` shapes like an email) but it is the roles.yaml
130
- // group-reference token, not an address — groups contain emails, never
131
- // other groups. Skip it with its own warning so the write gate refuses
132
- // it and the resolver never grants a colon-bearing "email".
133
- if (email.startsWith(GROUP_REF_PREFIX)) {
134
- warnings.push(`${filename}: group '${displayName}' has a '${GROUP_REF_PREFIX}'-prefixed entry ${JSON.stringify(raw)} — groups contain emails, not group references; entry skipped`);
135
- continue;
136
- }
137
- if (!EMAIL_REGEX.test(email)) {
138
- warnings.push(`${filename}: group '${displayName}' has malformed email '${raw}' — entry skipped`);
139
- continue;
140
- }
141
- emails.add(email);
142
- }
143
- groups.set(canonical, { displayName: displayName.trim(), emails });
144
- }
145
- return { ok: true, groups, warnings };
146
- }
147
- /**
148
- * Write-gate twin of {@link parseGroupsFile}: strict — structural errors AND
149
- * entry-level warnings both refuse the candidate, so a UI write can never land
150
- * a group the resolver would silently skip.
151
- */
152
- export function validateGroupsFile(text, filename) {
153
- // Strict PARSE first (no empty-key tolerance): the read side forgives a
154
- // blank key entry-level so other groups keep resolving, but a WRITE that
155
- // contains one — anywhere, including outside the `groups:` mapping — must
156
- // refuse rather than land bytes the tolerant reader silently drops.
157
- const strict = parseYamlSubset(text.trim() ? text : 'groups:');
158
- if (!strict.ok)
159
- return { ok: false, errors: [`${filename}: ${strict.error}`] };
160
- const parsed = parseGroupsFile(text, filename);
161
- if (!parsed.ok)
162
- return { ok: false, errors: parsed.errors };
163
- if (parsed.warnings.length > 0)
164
- return { ok: false, errors: parsed.warnings };
165
- return { ok: true };
166
- }
167
- //# sourceMappingURL=group-files.js.map