@bevel-software/platform-core-backend 0.10.0 → 0.11.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 (188) hide show
  1. package/dist/core/create-core-server.d.ts.map +1 -1
  2. package/dist/core/create-core-server.js +12 -1
  3. package/dist/core/create-core-server.js.map +1 -1
  4. package/dist/core/create-core-services.d.ts +22 -0
  5. package/dist/core/create-core-services.d.ts.map +1 -1
  6. package/dist/core/create-core-services.js +34 -1
  7. package/dist/core/create-core-services.js.map +1 -1
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +4 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/modules/access/access-control.interface.d.ts +54 -28
  13. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  14. package/dist/modules/access/access-control.service.d.ts +129 -14
  15. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  16. package/dist/modules/access/access-control.service.js +435 -66
  17. package/dist/modules/access/access-control.service.js.map +1 -1
  18. package/dist/modules/access/access-declarations.d.ts.map +1 -1
  19. package/dist/modules/access/access-declarations.js +5 -3
  20. package/dist/modules/access/access-declarations.js.map +1 -1
  21. package/dist/modules/access/access-mutation.service.d.ts +39 -6
  22. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  23. package/dist/modules/access/access-mutation.service.js +78 -18
  24. package/dist/modules/access/access-mutation.service.js.map +1 -1
  25. package/dist/modules/access/access-splice.d.ts +31 -4
  26. package/dist/modules/access/access-splice.d.ts.map +1 -1
  27. package/dist/modules/access/access-splice.js +40 -16
  28. package/dist/modules/access/access-splice.js.map +1 -1
  29. package/dist/modules/access/access.routes.d.ts.map +1 -1
  30. package/dist/modules/access/access.routes.js +204 -82
  31. package/dist/modules/access/access.routes.js.map +1 -1
  32. package/dist/modules/access/admin-locked-commit.d.ts +134 -0
  33. package/dist/modules/access/admin-locked-commit.d.ts.map +1 -0
  34. package/dist/modules/access/admin-locked-commit.js +277 -0
  35. package/dist/modules/access/admin-locked-commit.js.map +1 -0
  36. package/dist/modules/access/admin-route-helpers.d.ts +32 -0
  37. package/dist/modules/access/admin-route-helpers.d.ts.map +1 -0
  38. package/dist/modules/access/admin-route-helpers.js +44 -0
  39. package/dist/modules/access/admin-route-helpers.js.map +1 -0
  40. package/dist/modules/access/capability-registry.d.ts +41 -0
  41. package/dist/modules/access/capability-registry.d.ts.map +1 -0
  42. package/dist/modules/access/capability-registry.js +46 -0
  43. package/dist/modules/access/capability-registry.js.map +1 -0
  44. package/dist/modules/access/directory-sync-bot.d.ts +13 -0
  45. package/dist/modules/access/directory-sync-bot.d.ts.map +1 -0
  46. package/dist/modules/access/directory-sync-bot.js +64 -0
  47. package/dist/modules/access/directory-sync-bot.js.map +1 -0
  48. package/dist/modules/access/group-files.d.ts +83 -0
  49. package/dist/modules/access/group-files.d.ts.map +1 -0
  50. package/dist/modules/access/group-files.js +167 -0
  51. package/dist/modules/access/group-files.js.map +1 -0
  52. package/dist/modules/access/groups-admin.routes.d.ts +19 -0
  53. package/dist/modules/access/groups-admin.routes.d.ts.map +1 -0
  54. package/dist/modules/access/groups-admin.routes.js +98 -0
  55. package/dist/modules/access/groups-admin.routes.js.map +1 -0
  56. package/dist/modules/access/groups-admin.service.d.ts +166 -0
  57. package/dist/modules/access/groups-admin.service.d.ts.map +1 -0
  58. package/dist/modules/access/groups-admin.service.js +442 -0
  59. package/dist/modules/access/groups-admin.service.js.map +1 -0
  60. package/dist/modules/access/groups-edit.d.ts +58 -0
  61. package/dist/modules/access/groups-edit.d.ts.map +1 -0
  62. package/dist/modules/access/groups-edit.js +162 -0
  63. package/dist/modules/access/groups-edit.js.map +1 -0
  64. package/dist/modules/access/reference-scan.d.ts +141 -0
  65. package/dist/modules/access/reference-scan.d.ts.map +1 -0
  66. package/dist/modules/access/reference-scan.js +440 -0
  67. package/dist/modules/access/reference-scan.js.map +1 -0
  68. package/dist/modules/access/roles-admin.service.d.ts +88 -119
  69. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  70. package/dist/modules/access/roles-admin.service.js +230 -384
  71. package/dist/modules/access/roles-admin.service.js.map +1 -1
  72. package/dist/modules/access/roles-edit.d.ts +51 -25
  73. package/dist/modules/access/roles-edit.d.ts.map +1 -1
  74. package/dist/modules/access/roles-edit.js +133 -59
  75. package/dist/modules/access/roles-edit.js.map +1 -1
  76. package/dist/modules/access/synced-groups-committer.d.ts +28 -0
  77. package/dist/modules/access/synced-groups-committer.d.ts.map +1 -0
  78. package/dist/modules/access/synced-groups-committer.js +139 -0
  79. package/dist/modules/access/synced-groups-committer.js.map +1 -0
  80. package/dist/modules/access/synced-groups-writer.d.ts +78 -0
  81. package/dist/modules/access/synced-groups-writer.d.ts.map +1 -0
  82. package/dist/modules/access/synced-groups-writer.js +219 -0
  83. package/dist/modules/access/synced-groups-writer.js.map +1 -0
  84. package/dist/modules/database/core-schema.d.ts +17 -0
  85. package/dist/modules/database/core-schema.d.ts.map +1 -1
  86. package/dist/modules/database/core-schema.js +9 -0
  87. package/dist/modules/database/core-schema.js.map +1 -1
  88. package/dist/modules/mcp/mcp-auth.middleware.d.ts +13 -2
  89. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  90. package/dist/modules/mcp/mcp-auth.middleware.js +61 -2
  91. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  92. package/dist/modules/mcp/mcp.routes.d.ts +9 -3
  93. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  94. package/dist/modules/mcp/mcp.routes.js +126 -2
  95. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  96. package/dist/modules/mcp/mcp.service.d.ts +14 -0
  97. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  98. package/dist/modules/mcp/mcp.service.js +6 -1
  99. package/dist/modules/mcp/mcp.service.js.map +1 -1
  100. package/dist/modules/workflow/file-lock.service.d.ts +11 -1
  101. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  102. package/dist/modules/workflow/file-lock.service.js +15 -1
  103. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  104. package/dist/modules/workflow/git/git.service.d.ts +34 -11
  105. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  106. package/dist/modules/workflow/git/git.service.js +165 -37
  107. package/dist/modules/workflow/git/git.service.js.map +1 -1
  108. package/dist/modules/workflow/locking-filesystem.d.ts +4 -0
  109. package/dist/modules/workflow/locking-filesystem.d.ts.map +1 -1
  110. package/dist/modules/workflow/locking-filesystem.js +181 -28
  111. package/dist/modules/workflow/locking-filesystem.js.map +1 -1
  112. package/dist/modules/workflow/pending-commits.service.d.ts +10 -0
  113. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  114. package/dist/modules/workflow/pending-commits.service.js +19 -1
  115. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  116. package/dist/modules/workflow/workflow.service.d.ts +17 -2
  117. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  118. package/dist/modules/workflow/workflow.service.js +113 -19
  119. package/dist/modules/workflow/workflow.service.js.map +1 -1
  120. package/kb-template/AGENTS.md +4 -1
  121. package/migrations/0004_file_lock_mode.sql +2 -0
  122. package/migrations/meta/0004_snapshot.json +1564 -0
  123. package/migrations/meta/_journal.json +7 -0
  124. package/package.json +3 -3
  125. package/src/core/create-core-server.ts +13 -0
  126. package/src/core/create-core-services.ts +68 -0
  127. package/src/index.ts +10 -0
  128. package/src/modules/access/__tests__/access-control.service.test.ts +9 -3
  129. package/src/modules/access/__tests__/access-groups.test.ts +427 -0
  130. package/src/modules/access/__tests__/access-mutation.service.test.ts +171 -5
  131. package/src/modules/access/__tests__/access-splice.test.ts +65 -0
  132. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +337 -0
  133. package/src/modules/access/__tests__/access.routes.revoke.test.ts +48 -0
  134. package/src/modules/access/__tests__/admin-locked-commit.test.ts +221 -0
  135. package/src/modules/access/__tests__/admin-route-helpers.test.ts +61 -0
  136. package/src/modules/access/__tests__/directory-sync-bot.test.ts +106 -0
  137. package/src/modules/access/__tests__/grant-sources.test.ts +67 -0
  138. package/src/modules/access/__tests__/groups-admin.service.test.ts +440 -0
  139. package/src/modules/access/__tests__/reference-scan.test.ts +288 -0
  140. package/src/modules/access/__tests__/roles-admin.service.test.ts +161 -83
  141. package/src/modules/access/__tests__/roles-capabilities.test.ts +325 -0
  142. package/src/modules/access/__tests__/roles-edit.test.ts +104 -28
  143. package/src/modules/access/__tests__/roles.routes.test.ts +63 -40
  144. package/src/modules/access/__tests__/synced-groups-committer.test.ts +238 -0
  145. package/src/modules/access/__tests__/synced-groups-writer.test.ts +249 -0
  146. package/src/modules/access/access-control.interface.ts +66 -32
  147. package/src/modules/access/access-control.service.ts +536 -73
  148. package/src/modules/access/access-declarations.ts +5 -2
  149. package/src/modules/access/access-mutation.service.ts +88 -14
  150. package/src/modules/access/access-splice.ts +55 -17
  151. package/src/modules/access/access.routes.ts +227 -93
  152. package/src/modules/access/admin-locked-commit.ts +331 -0
  153. package/src/modules/access/admin-route-helpers.ts +55 -0
  154. package/src/modules/access/capability-registry.ts +74 -0
  155. package/src/modules/access/directory-sync-bot.ts +76 -0
  156. package/src/modules/access/group-files.ts +212 -0
  157. package/src/modules/access/groups-admin.routes.ts +113 -0
  158. package/src/modules/access/groups-admin.service.ts +551 -0
  159. package/src/modules/access/groups-edit.ts +187 -0
  160. package/src/modules/access/reference-scan.ts +513 -0
  161. package/src/modules/access/roles-admin.service.ts +290 -418
  162. package/src/modules/access/roles-edit.ts +134 -61
  163. package/src/modules/access/synced-groups-committer.ts +177 -0
  164. package/src/modules/access/synced-groups-writer.ts +303 -0
  165. package/src/modules/database/core-schema.ts +9 -0
  166. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +116 -0
  167. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +3 -1
  168. package/src/modules/mcp/__tests__/mcp.routes.delete.test.ts +6 -1
  169. package/src/modules/mcp/__tests__/mcp.routes.local-token.test.ts +301 -0
  170. package/src/modules/mcp/mcp-auth.middleware.ts +61 -1
  171. package/src/modules/mcp/mcp.routes.ts +137 -2
  172. package/src/modules/mcp/mcp.service.ts +6 -1
  173. package/src/modules/workflow/__tests__/locking-filesystem.test.ts +336 -0
  174. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
  175. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +32 -0
  176. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +13 -2
  177. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +139 -1
  178. package/src/modules/workflow/file-lock.service.ts +15 -0
  179. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +0 -1
  180. package/src/modules/workflow/git/__tests__/git.service.commitChanges.test.ts +132 -0
  181. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +0 -1
  182. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +0 -1
  183. package/src/modules/workflow/git/git.service.ts +174 -35
  184. package/src/modules/workflow/locking-filesystem.ts +188 -26
  185. package/src/modules/workflow/pending-commits.service.ts +27 -1
  186. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +0 -1
  187. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +0 -1
  188. package/src/modules/workflow/workflow.service.ts +140 -20
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Pure parse → edit → re-emit editor for `groups.yaml` — the manual-mode
3
+ * group file. Mirrors `roles-edit.ts` (same re-emit rationale: no body, no
4
+ * comments to preserve, deterministic canonical output), with two deliberate
5
+ * differences:
6
+ *
7
+ * - No Admin invariants — groups carry no capabilities, so there is no
8
+ * lockout class of mistake here.
9
+ * - Members are EMAILS ONLY. `group:` references are a roles.yaml grammar
10
+ * (a role assigned to a group); a group containing a group is nesting,
11
+ * which is a deliberate non-goal.
12
+ *
13
+ * Policy that needs more than this file's text — IdP-mode gating, collision
14
+ * with role names — lives in `groups-admin.service.ts`.
15
+ */
16
+ import { canonicalRoleName, canonicalEmail, EMAIL_REGEX, GROUP_REF_PREFIX, RESERVED_ROLE_NAMES, } from './access-control.service.js';
17
+ import { GROUPS_YAML, parseGroupsFile, unsafeNameReason } from './group-files.js';
18
+ /** Bad-input failure when editing groups.yaml. */
19
+ export class GroupsEditError extends Error {
20
+ status;
21
+ constructor(message, status = 422) {
22
+ super(message);
23
+ this.name = 'GroupsEditError';
24
+ this.status = status;
25
+ }
26
+ }
27
+ /** Thin assert wrapper over the SHARED name-safety predicate (`group-files.ts`
28
+ * `unsafeNameReason`) — one namespace, one set of rules, including the
29
+ * reserved `role/` prefix. */
30
+ export function assertSafeGroupDisplayName(displayName) {
31
+ const reason = unsafeNameReason(displayName);
32
+ if (reason) {
33
+ throw new GroupsEditError(`group name ${reason}: ${JSON.stringify(displayName)}`);
34
+ }
35
+ }
36
+ /**
37
+ * Parse groups.yaml into the ordered model; `[]` for an empty/missing file.
38
+ *
39
+ * DERIVED from the resolver's own `parseGroupsFile` — one parser for the
40
+ * grammar, so the editor's skip rules (empty/reserved/duplicate names,
41
+ * malformed member emails) can never drift from the resolver's: the admin
42
+ * roster and the edit surface must never show an entry the resolver ignores,
43
+ * or an admin "manages" a group that grants nothing. Consequence, deliberate:
44
+ * the next edit's re-emit garbage-collects those dead entries from the file.
45
+ * Structural failures throw GroupsEditError (422) — the caller reads under
46
+ * the lock, so a broken on-disk file is an operator problem surfaced loudly,
47
+ * not a silent reset.
48
+ */
49
+ export function parseGroupsModel(text) {
50
+ if (!text.trim())
51
+ return [];
52
+ const parsed = parseGroupsFile(text, GROUPS_YAML);
53
+ if (!parsed.ok)
54
+ throw new GroupsEditError(parsed.errors.join('; '));
55
+ return [...parsed.groups.values()].map((def) => ({
56
+ displayName: def.displayName,
57
+ members: [...def.emails],
58
+ }));
59
+ }
60
+ /** Deterministic canonical emit; empty groups as `Name: []`. */
61
+ export function emitGroupsModel(model) {
62
+ const lines = ['groups:'];
63
+ for (const group of model) {
64
+ if (group.members.length === 0) {
65
+ lines.push(` ${group.displayName}: []`);
66
+ }
67
+ else {
68
+ lines.push(` ${group.displayName}:`);
69
+ for (const email of group.members)
70
+ lines.push(` - ${email}`);
71
+ }
72
+ }
73
+ return lines.join('\n') + '\n';
74
+ }
75
+ function findGroup(model, canonical) {
76
+ return model.find((g) => canonicalRoleName(g.displayName) === canonical);
77
+ }
78
+ function reemit(original, model) {
79
+ const text = emitGroupsModel(model);
80
+ let originalCanonical;
81
+ try {
82
+ originalCanonical = emitGroupsModel(parseGroupsModel(original));
83
+ }
84
+ catch {
85
+ originalCanonical = original;
86
+ }
87
+ return { text, changed: text !== originalCanonical };
88
+ }
89
+ export function createGroup(text, displayName) {
90
+ assertSafeGroupDisplayName(displayName);
91
+ const canonical = canonicalRoleName(displayName);
92
+ if (RESERVED_ROLE_NAMES.has(canonical)) {
93
+ throw new GroupsEditError(`'${displayName.trim()}' is a reserved name and cannot be a group`);
94
+ }
95
+ const model = parseGroupsModel(text);
96
+ if (findGroup(model, canonical)) {
97
+ throw new GroupsEditError(`a group named '${displayName.trim()}' already exists`);
98
+ }
99
+ model.push({ displayName: displayName.trim(), members: [] });
100
+ return reemit(text, model);
101
+ }
102
+ export function deleteGroup(text, canonical) {
103
+ const model = parseGroupsModel(text);
104
+ const idx = model.findIndex((g) => canonicalRoleName(g.displayName) === canonical);
105
+ if (idx < 0)
106
+ throw new GroupsEditError(`group not found: ${canonical}`, 404);
107
+ model.splice(idx, 1);
108
+ return reemit(text, model);
109
+ }
110
+ export function addGroupMember(text, canonical, rawEmail) {
111
+ const email = canonicalEmail(rawEmail);
112
+ // A `group:`-prefixed value is roles.yaml grammar (a role assigned to a
113
+ // group) — and `group:lee@x.io` would pass the email regex, landing a dead
114
+ // entry the resolver reads as a plain (never-matching) address. Mirrors
115
+ // `roles-edit.ts` addMember; groups do not nest, so there is no "right
116
+ // editor" to route to — a group member is always an email.
117
+ if (email.startsWith(GROUP_REF_PREFIX)) {
118
+ throw new GroupsEditError(`member ${JSON.stringify(rawEmail)} starts with '${GROUP_REF_PREFIX}' — a group member is always a person's email (groups cannot contain other groups). Drop the '${GROUP_REF_PREFIX}' prefix and add the email itself`);
119
+ }
120
+ if (!EMAIL_REGEX.test(email)) {
121
+ throw new GroupsEditError(`malformed email: ${JSON.stringify(rawEmail)}`);
122
+ }
123
+ const model = parseGroupsModel(text);
124
+ const group = findGroup(model, canonical);
125
+ if (!group)
126
+ throw new GroupsEditError(`group not found: ${canonical}`, 404);
127
+ if (group.members.includes(email))
128
+ return { text: emitGroupsModel(model), changed: false };
129
+ group.members.push(email);
130
+ return reemit(text, model);
131
+ }
132
+ export function removeGroupMember(text, canonical, rawEmail) {
133
+ const email = canonicalEmail(rawEmail);
134
+ const model = parseGroupsModel(text);
135
+ const group = findGroup(model, canonical);
136
+ if (!group)
137
+ throw new GroupsEditError(`group not found: ${canonical}`, 404);
138
+ const idx = group.members.indexOf(email);
139
+ if (idx < 0)
140
+ return { text: emitGroupsModel(model), changed: false };
141
+ group.members.splice(idx, 1);
142
+ return reemit(text, model);
143
+ }
144
+ /** Rename a group's display name (canonical may change — service layer owns
145
+ * the reference rewrite when it does). */
146
+ export function renameGroupDisplay(text, canonical, newDisplayName) {
147
+ assertSafeGroupDisplayName(newDisplayName);
148
+ const newCanonical = canonicalRoleName(newDisplayName);
149
+ if (RESERVED_ROLE_NAMES.has(newCanonical)) {
150
+ throw new GroupsEditError(`'${newDisplayName.trim()}' is a reserved name and cannot be a group`);
151
+ }
152
+ const model = parseGroupsModel(text);
153
+ const group = findGroup(model, canonical);
154
+ if (!group)
155
+ throw new GroupsEditError(`group not found: ${canonical}`, 404);
156
+ if (newCanonical !== canonical && findGroup(model, newCanonical)) {
157
+ throw new GroupsEditError(`a group named '${newDisplayName.trim()}' already exists`);
158
+ }
159
+ group.displayName = newDisplayName.trim();
160
+ return reemit(text, model);
161
+ }
162
+ //# sourceMappingURL=groups-edit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"groups-edit.js","sourceRoot":"","sources":["../../../src/modules/access/groups-edit.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,141 @@
1
+ /**
2
+ * SHARED grant-reference machinery — the single implementation both admin
3
+ * services (roles + groups) drive for "which access rules name this
4
+ * principal" and "rewrite every reference atomically":
5
+ *
6
+ * - `findRoleRefsInText` / `rewriteRoleTokensInText`: the config-region
7
+ * parse that decides what IS a principal reference. One source of truth,
8
+ * so the delete/rename warning and the rename rewrite can never disagree.
9
+ * - `KbReferenceScanner`: candidate collection (every KB file with an
10
+ * access-frontmatter extension — `.md` and `.tool`, the resolver's own
11
+ * set), the advisory reference scan (cached — roster mutations must not
12
+ * rerun a full-KB read sweep, invalidated by file-changed events and a
13
+ * TTL backstop), and the fail-closed reference rewrite (returns each
14
+ * file's ORIGINAL text too, so rollback snapshots reuse the read the
15
+ * rewrite already did instead of a second full-KB pass).
16
+ *
17
+ * Tokens here are the ENTRY-GRAMMAR canonical tokens: a bare name (group
18
+ * first, then role) or an explicit `role/<name>`. Callers translate: a ROLE's
19
+ * references are its bare token plus its `role/` token; a GROUP's are its
20
+ * bare token only.
21
+ */
22
+ import type { IWorkspaceService } from '@bevel-software/platform-shared';
23
+ /**
24
+ * Every genuine role/group reference in `text`'s rule regions, as
25
+ * {role, verb} where `role` is the canonical ENTRY TOKEN (bare name or
26
+ * `role/<name>`). `isAccessMd` marks an `access.md` file, whose BODY is a
27
+ * rule source in the body-governed format — see {@link ruleLineRanges}.
28
+ */
29
+ export declare function findRoleRefsInText(text: string, isMarkdown?: boolean, isAccessMd?: boolean): {
30
+ role: string;
31
+ verb: string;
32
+ }[];
33
+ /**
34
+ * Rewrite every RULE-REGION line that PARSES as a role reference whose
35
+ * canonical token == `oldToken`, replacing the token with `newDisplayName`
36
+ * (preserving any leading `deny ` and indentation). A prose body is never
37
+ * touched — a line like `- Sales` in a node file or a legacy `access.md`
38
+ * stays byte-for-byte intact; the body is eligible ONLY when the file is a
39
+ * body-governed `access.md` (`isAccessMd` + the body parses as rules), where
40
+ * the body IS what the resolver enforces — see {@link ruleLineRanges}. Lines
41
+ * that don't parse as a matching role entry (user entries, other keys,
42
+ * substrings) are also untouched. Exported for test.
43
+ *
44
+ * `isMarkdown` (default true) marks files that carry a markdown body below the
45
+ * frontmatter; pass false only for a pure-config file with no body.
46
+ */
47
+ export declare function rewriteRoleTokensInText(text: string, oldToken: string, newDisplayName: string, isMarkdown?: boolean, isAccessMd?: boolean): string;
48
+ export interface ReferenceHit {
49
+ path: string;
50
+ verb: string;
51
+ }
52
+ /** One rewritten file: the new content plus the ORIGINAL text the rewrite
53
+ * read (rollback snapshots reuse it — no second full-KB read). */
54
+ export interface ReferenceRewrite {
55
+ repoRelativePath: string;
56
+ content: string;
57
+ original: string;
58
+ }
59
+ /**
60
+ * The slice of the workflow event bus the scanner taps for cache
61
+ * invalidation. Structural (not the concrete class) so test doubles and the
62
+ * services' optional bus stay compatible.
63
+ */
64
+ export interface ReferenceScanInvalidationBus {
65
+ /** Optional so record-only test doubles (emit-only stubs) stay valid. */
66
+ onEmit?(listener: (event: {
67
+ kind: string;
68
+ workspaceId?: string;
69
+ path?: string;
70
+ }) => void): () => void;
71
+ }
72
+ /**
73
+ * Candidate collection + cached scan + fail-closed rewrite over one KB.
74
+ * Constructed per admin service; the cache means roster MUTATIONS never rerun
75
+ * the full-KB reference sweep (only the roster GET pays it, at most once per
76
+ * TTL), and `invalidate` drops it whenever a write could have moved
77
+ * references (rename/delete rewrites, or any external change signal).
78
+ *
79
+ * FRESHNESS: an `access.md` grant/revoke (or any other write to a scanned
80
+ * file) lands OUTSIDE the admin services, so their explicit invalidations
81
+ * can't see it — the roster's `referencedBy` would serve up to `SCAN_TTL_MS`
82
+ * of staleness. When an `eventBus` is provided, the scanner taps its emits
83
+ * and drops the workspace's cache on every path-carrying event
84
+ * (`file-changed` from the commit pipeline, and `lock-released` — which the
85
+ * share routes emit synchronously at write time, before the async commit's
86
+ * `file-changed` lands) whose path has a scanned extension. The TTL stays as
87
+ * the backstop for deployments/tests without a bus.
88
+ */
89
+ export declare class KbReferenceScanner {
90
+ private readonly workspaceService;
91
+ private readonly kbDirName;
92
+ private readonly cache;
93
+ /**
94
+ * Per-workspace in-flight scan state: a scan may only CACHE its result if
95
+ * no invalidation landed after it started — otherwise the pre-change
96
+ * snapshot would repopulate the cache and stick for a whole TTL. Tracked
97
+ * ONLY while scans run (a future scan reads post-write state by nature, so
98
+ * an invalidation with nothing in flight needs only the cache drop): the
99
+ * entry is created by the first concurrent scan and removed by the last,
100
+ * so the map never accumulates workspaces.
101
+ */
102
+ private readonly inFlight;
103
+ constructor(workspaceService: IWorkspaceService, kbDirName: string, eventBus?: ReferenceScanInvalidationBus);
104
+ invalidate(workspaceId: string): void;
105
+ /**
106
+ * Repo-relative paths of every file that could carry a principal reference:
107
+ * all `access.md` files plus every node file whose own frontmatter the
108
+ * resolver reads access verbs from — the shared
109
+ * `ACCESS_FRONTMATTER_EXTENSIONS` set (`.md` AND `.tool`; a rename that
110
+ * skipped `.tool` would strand a live frontmatter grant there). Sourced
111
+ * from the workspace file tree (which already skips `.git` and honours
112
+ * `.bevelignore`), filtered under the KB dir and returned bare
113
+ * repo-relative. (`roles.yaml` matches no scanned extension, so it's
114
+ * excluded; callers commit it separately.) Under save=share the working
115
+ * tree matches the committed set, so this is the same candidate list
116
+ * git-tracking would give.
117
+ */
118
+ collectCandidateFiles(workspaceId: string): Promise<string[]>;
119
+ /**
120
+ * Sound scan of EVERY candidate file (folder `access.md` + `.md`/`.tool`
121
+ * node frontmatter) for genuine principal references, indexed by canonical
122
+ * entry TOKEN. Shares the
123
+ * candidate set and the config-region parse with the rename rewrite, so the
124
+ * delete warning and the rewrite see the SAME references. A file we cannot
125
+ * read is skipped (this is an advisory read, not the atomic write path).
126
+ * Cached per workspace (see class doc); reads are batched with Promise.all
127
+ * rather than serial per-file awaits.
128
+ */
129
+ scan(workspaceId: string): Promise<Map<string, ReferenceHit[]>>;
130
+ /**
131
+ * Find every genuine reference to `oldToken` and rewrite it to
132
+ * `newDisplayName`. FAIL-CLOSED: a candidate we cannot read might reference
133
+ * the old token, and skipping it would commit a partial rewrite (a
134
+ * half-renamed principal still pointed at by stragglers = silent access
135
+ * drop) — so any read failure aborts via `makeError` with nothing written.
136
+ * Returns the writes WITH each file's original text (rollback snapshots).
137
+ */
138
+ rewriteReferences(workspaceId: string, oldToken: string, newDisplayName: string, makeError: (message: string, cause?: string) => Error): Promise<ReferenceRewrite[]>;
139
+ private repoDir;
140
+ }
141
+ //# sourceMappingURL=reference-scan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reference-scan.d.ts","sourceRoot":"","sources":["../../../src/modules/access/reference-scan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAKH,OAAO,KAAK,EAAiB,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AAoNxF;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,UAAU,UAAO,EACjB,UAAU,UAAQ,GACjB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,CAQlC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,EACtB,UAAU,UAAO,EACjB,UAAU,UAAQ,GACjB,MAAM,CAcR;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED;mEACmE;AACnE,MAAM,WAAW,gBAAgB;IAC/B,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAOD;;;;GAIG;AACH,MAAM,WAAW,4BAA4B;IAC3C,yEAAyE;IACzE,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACvG;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,kBAAkB;IAiB3B,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS;IAjB5B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAGlB;IACJ;;;;;;;;OAQG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqD;gBAG3D,gBAAgB,EAAE,iBAAiB,EACnC,SAAS,EAAE,MAAM,EAClC,QAAQ,CAAC,EAAE,4BAA4B;IAczC,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAMrC;;;;;;;;;;;;OAYG;IACG,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAiBnE;;;;;;;;;OASG;IACG,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;IA8CrE;;;;;;;OAOG;IACG,iBAAiB,CACrB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,EACtB,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,KAAK,GACpD,OAAO,CAAC,gBAAgB,EAAE,CAAC;YAgChB,OAAO;CAItB"}