@bevel-software/platform-core-backend 0.10.0 → 0.11.1
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.
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +12 -1
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +22 -0
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +34 -1
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +54 -28
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +129 -14
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +435 -66
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-declarations.d.ts.map +1 -1
- package/dist/modules/access/access-declarations.js +5 -3
- package/dist/modules/access/access-declarations.js.map +1 -1
- package/dist/modules/access/access-mutation.service.d.ts +39 -6
- package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
- package/dist/modules/access/access-mutation.service.js +78 -18
- package/dist/modules/access/access-mutation.service.js.map +1 -1
- package/dist/modules/access/access-splice.d.ts +31 -4
- package/dist/modules/access/access-splice.d.ts.map +1 -1
- package/dist/modules/access/access-splice.js +40 -16
- package/dist/modules/access/access-splice.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +204 -82
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/admin-locked-commit.d.ts +134 -0
- package/dist/modules/access/admin-locked-commit.d.ts.map +1 -0
- package/dist/modules/access/admin-locked-commit.js +277 -0
- package/dist/modules/access/admin-locked-commit.js.map +1 -0
- package/dist/modules/access/admin-route-helpers.d.ts +32 -0
- package/dist/modules/access/admin-route-helpers.d.ts.map +1 -0
- package/dist/modules/access/admin-route-helpers.js +44 -0
- package/dist/modules/access/admin-route-helpers.js.map +1 -0
- package/dist/modules/access/capability-registry.d.ts +41 -0
- package/dist/modules/access/capability-registry.d.ts.map +1 -0
- package/dist/modules/access/capability-registry.js +46 -0
- package/dist/modules/access/capability-registry.js.map +1 -0
- package/dist/modules/access/directory-sync-bot.d.ts +13 -0
- package/dist/modules/access/directory-sync-bot.d.ts.map +1 -0
- package/dist/modules/access/directory-sync-bot.js +64 -0
- package/dist/modules/access/directory-sync-bot.js.map +1 -0
- package/dist/modules/access/group-files.d.ts +83 -0
- package/dist/modules/access/group-files.d.ts.map +1 -0
- package/dist/modules/access/group-files.js +167 -0
- package/dist/modules/access/group-files.js.map +1 -0
- package/dist/modules/access/groups-admin.routes.d.ts +19 -0
- package/dist/modules/access/groups-admin.routes.d.ts.map +1 -0
- package/dist/modules/access/groups-admin.routes.js +98 -0
- package/dist/modules/access/groups-admin.routes.js.map +1 -0
- package/dist/modules/access/groups-admin.service.d.ts +166 -0
- package/dist/modules/access/groups-admin.service.d.ts.map +1 -0
- package/dist/modules/access/groups-admin.service.js +442 -0
- package/dist/modules/access/groups-admin.service.js.map +1 -0
- package/dist/modules/access/groups-edit.d.ts +58 -0
- package/dist/modules/access/groups-edit.d.ts.map +1 -0
- package/dist/modules/access/groups-edit.js +162 -0
- package/dist/modules/access/groups-edit.js.map +1 -0
- package/dist/modules/access/reference-scan.d.ts +141 -0
- package/dist/modules/access/reference-scan.d.ts.map +1 -0
- package/dist/modules/access/reference-scan.js +440 -0
- package/dist/modules/access/reference-scan.js.map +1 -0
- package/dist/modules/access/roles-admin.service.d.ts +88 -119
- package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
- package/dist/modules/access/roles-admin.service.js +230 -384
- package/dist/modules/access/roles-admin.service.js.map +1 -1
- package/dist/modules/access/roles-edit.d.ts +51 -25
- package/dist/modules/access/roles-edit.d.ts.map +1 -1
- package/dist/modules/access/roles-edit.js +133 -59
- package/dist/modules/access/roles-edit.js.map +1 -1
- package/dist/modules/access/synced-groups-committer.d.ts +28 -0
- package/dist/modules/access/synced-groups-committer.d.ts.map +1 -0
- package/dist/modules/access/synced-groups-committer.js +139 -0
- package/dist/modules/access/synced-groups-committer.js.map +1 -0
- package/dist/modules/access/synced-groups-writer.d.ts +78 -0
- package/dist/modules/access/synced-groups-writer.d.ts.map +1 -0
- package/dist/modules/access/synced-groups-writer.js +219 -0
- package/dist/modules/access/synced-groups-writer.js.map +1 -0
- package/dist/modules/database/core-schema.d.ts +17 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +9 -0
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.d.ts +13 -2
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +61 -2
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/mcp.routes.d.ts +9 -3
- package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.routes.js +126 -2
- package/dist/modules/mcp/mcp.routes.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +14 -0
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +6 -1
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/workflow/file-lock.service.d.ts +11 -1
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +15 -1
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +34 -11
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +165 -37
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/locking-filesystem.d.ts +4 -0
- package/dist/modules/workflow/locking-filesystem.d.ts.map +1 -1
- package/dist/modules/workflow/locking-filesystem.js +181 -28
- package/dist/modules/workflow/locking-filesystem.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +10 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +19 -1
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +17 -2
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +113 -19
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/kb-template/AGENTS.md +4 -1
- package/migrations/0004_file_lock_mode.sql +2 -0
- package/migrations/meta/0004_snapshot.json +1564 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +3 -3
- package/src/core/create-core-server.ts +13 -0
- package/src/core/create-core-services.ts +68 -0
- package/src/index.ts +10 -0
- package/src/modules/access/__tests__/access-control.service.test.ts +9 -3
- package/src/modules/access/__tests__/access-groups.test.ts +427 -0
- package/src/modules/access/__tests__/access-mutation.service.test.ts +171 -5
- package/src/modules/access/__tests__/access-splice.test.ts +65 -0
- package/src/modules/access/__tests__/access.routes.group-grant.test.ts +337 -0
- package/src/modules/access/__tests__/access.routes.revoke.test.ts +48 -0
- package/src/modules/access/__tests__/admin-locked-commit.test.ts +221 -0
- package/src/modules/access/__tests__/admin-route-helpers.test.ts +61 -0
- package/src/modules/access/__tests__/directory-sync-bot.test.ts +106 -0
- package/src/modules/access/__tests__/grant-sources.test.ts +67 -0
- package/src/modules/access/__tests__/groups-admin.service.test.ts +440 -0
- package/src/modules/access/__tests__/reference-scan.test.ts +288 -0
- package/src/modules/access/__tests__/roles-admin.service.test.ts +161 -83
- package/src/modules/access/__tests__/roles-capabilities.test.ts +325 -0
- package/src/modules/access/__tests__/roles-edit.test.ts +104 -28
- package/src/modules/access/__tests__/roles.routes.test.ts +63 -40
- package/src/modules/access/__tests__/synced-groups-committer.test.ts +238 -0
- package/src/modules/access/__tests__/synced-groups-writer.test.ts +249 -0
- package/src/modules/access/access-control.interface.ts +66 -32
- package/src/modules/access/access-control.service.ts +536 -73
- package/src/modules/access/access-declarations.ts +5 -2
- package/src/modules/access/access-mutation.service.ts +88 -14
- package/src/modules/access/access-splice.ts +55 -17
- package/src/modules/access/access.routes.ts +227 -93
- package/src/modules/access/admin-locked-commit.ts +331 -0
- package/src/modules/access/admin-route-helpers.ts +55 -0
- package/src/modules/access/capability-registry.ts +74 -0
- package/src/modules/access/directory-sync-bot.ts +76 -0
- package/src/modules/access/group-files.ts +212 -0
- package/src/modules/access/groups-admin.routes.ts +113 -0
- package/src/modules/access/groups-admin.service.ts +551 -0
- package/src/modules/access/groups-edit.ts +187 -0
- package/src/modules/access/reference-scan.ts +513 -0
- package/src/modules/access/roles-admin.service.ts +290 -418
- package/src/modules/access/roles-edit.ts +134 -61
- package/src/modules/access/synced-groups-committer.ts +177 -0
- package/src/modules/access/synced-groups-writer.ts +303 -0
- package/src/modules/database/core-schema.ts +9 -0
- package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +116 -0
- package/src/modules/mcp/__tests__/mcp.e2e.test.ts +3 -1
- package/src/modules/mcp/__tests__/mcp.routes.delete.test.ts +6 -1
- package/src/modules/mcp/__tests__/mcp.routes.local-token.test.ts +301 -0
- package/src/modules/mcp/mcp-auth.middleware.ts +61 -1
- package/src/modules/mcp/mcp.routes.ts +137 -2
- package/src/modules/mcp/mcp.service.ts +6 -1
- package/src/modules/workflow/__tests__/locking-filesystem.test.ts +336 -0
- package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
- package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +32 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +13 -2
- package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +139 -1
- package/src/modules/workflow/file-lock.service.ts +15 -0
- package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +0 -1
- package/src/modules/workflow/git/__tests__/git.service.commitChanges.test.ts +132 -0
- package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +0 -1
- package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +0 -1
- package/src/modules/workflow/git/git.service.ts +174 -35
- package/src/modules/workflow/locking-filesystem.ts +188 -26
- package/src/modules/workflow/pending-commits.service.ts +27 -1
- package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +0 -1
- package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +0 -1
- package/src/modules/workflow/workflow.service.ts +140 -20
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHARED transactional lock/commit helper for the admin roster services
|
|
3
|
+
* (roles + groups). The two services used to carry twin copies of this
|
|
4
|
+
* machinery, which diverged once into a real bug — this is the single
|
|
5
|
+
* implementation both delegate to, parameterized only by the domain-error
|
|
6
|
+
* constructor, log tag, and contention wording.
|
|
7
|
+
*
|
|
8
|
+
* The shape of every roster write:
|
|
9
|
+
*
|
|
10
|
+
* withFileLocks(paths, fn) — acquire every path's lock (sorted,
|
|
11
|
+
* all-or-nothing; strict even for the same user), run `fn`, release.
|
|
12
|
+
* HOW each lock is released depends on the outcome (see the finally
|
|
13
|
+
* block for the full reasoning):
|
|
14
|
+
* - `fn` succeeded → releaseLockNoCommit for every path the batch
|
|
15
|
+
* commit LANDED on (tracked by writeAndCommitLocked): the commit
|
|
16
|
+
* landed AND pushed, so the tree there is clean (the discard
|
|
17
|
+
* no-ops); releaseLock would instead ENQUEUE a commit before
|
|
18
|
+
* dropping the row, and an enqueue failure then strands the lock
|
|
19
|
+
* until TTL. A held path the batch never committed keeps
|
|
20
|
+
* commit-on-release — it may carry a PRIOR holder's still-queued
|
|
21
|
+
* work, which a discard would destroy.
|
|
22
|
+
* - `fn` threw PushNeedsAgentResolutionError → releaseLock. The commit
|
|
23
|
+
* landed but the push didn't; the enqueued release commit IS the
|
|
24
|
+
* retry (the worker sees the clean tree, notices unpushed commits,
|
|
25
|
+
* and re-runs the cooperative push ladder).
|
|
26
|
+
* - `fn` threw anything else → releaseLock (commit-on-release): `fn`
|
|
27
|
+
* restored its own bytes, so anything dirty at release time is
|
|
28
|
+
* either clean (queued commit no-ops) or someone else's still-queued
|
|
29
|
+
* work that must be preserved. The ONE exception: a path whose
|
|
30
|
+
* restore itself failed (`unrestoredPaths` on the thrown error)
|
|
31
|
+
* holds known-partial bytes → releaseLockNoCommit discards to HEAD.
|
|
32
|
+
* Every release is per-path best-effort: one failed release logs and
|
|
33
|
+
* moves on so it can never strand the REST of the held locks.
|
|
34
|
+
*
|
|
35
|
+
* writeAndCommitLocked(files) — the write half of a withFileLocks
|
|
36
|
+
* flow: plain-write each file (the lock is already ours — strict
|
|
37
|
+
* same-user acquire forbids LockingFilesystem here) and commit them as
|
|
38
|
+
* ONE path-scoped change. On a PRE-commit failure, best-effort restore of
|
|
39
|
+
* each file's original bytes (`original: null` = the file did not exist,
|
|
40
|
+
* so restoration DELETES it). A POST-commit failure — the push after a
|
|
41
|
+
* landed commit (`PushNeedsAgentResolutionError`, thrown with the commit
|
|
42
|
+
* intact) — must NOT restore: the edit is committed locally and restoring
|
|
43
|
+
* would publish a compensating revert of a landed change. It propagates
|
|
44
|
+
* as-is ("saved locally … will be resolved"), locks release normally, and
|
|
45
|
+
* the pending-commit ladder retries the share.
|
|
46
|
+
*/
|
|
47
|
+
import { LockingFilesystem } from '../workflow/locking-filesystem.js';
|
|
48
|
+
import { WorkflowDomainError } from '../workflow/workflow.errors.js';
|
|
49
|
+
import type { AuthUser, IWorkspaceService, IWorkflowService } from '@bevel-software/platform-shared';
|
|
50
|
+
import type { FileContent } from '@mastra/core/workspace';
|
|
51
|
+
export interface LockedCommitDeps {
|
|
52
|
+
workspaceService: IWorkspaceService;
|
|
53
|
+
workflowService: IWorkflowService;
|
|
54
|
+
kbDirName: string;
|
|
55
|
+
/** Live-binding thunk — DEFAULT_BRANCH stays empty until setup. */
|
|
56
|
+
defaultBranchOf: () => string;
|
|
57
|
+
/** Domain-error constructor (RolesAdminError / GroupsAdminError). */
|
|
58
|
+
makeError: (message: string, status: number, payload?: Record<string, unknown>) => WorkflowDomainError;
|
|
59
|
+
/** Log prefix, e.g. 'roles-admin' / 'groups-admin'. */
|
|
60
|
+
logTag: string;
|
|
61
|
+
/** Contention wording: what is "being edited by <holder>". */
|
|
62
|
+
contendedSubject: string;
|
|
63
|
+
/** Pre-disk write validator handed to LockingFilesystem writes. */
|
|
64
|
+
validateWrite?: (path: string, content: FileContent) => void;
|
|
65
|
+
}
|
|
66
|
+
export interface LockedWrite {
|
|
67
|
+
repoRel: string;
|
|
68
|
+
/** The new content; null = DELETE the file (restore writes `original` back). */
|
|
69
|
+
content: string | null;
|
|
70
|
+
/** null = the file did not exist before (restore DELETES it). */
|
|
71
|
+
original: string | null;
|
|
72
|
+
}
|
|
73
|
+
export declare class AdminLockedCommits {
|
|
74
|
+
private readonly deps;
|
|
75
|
+
/**
|
|
76
|
+
* Workspace-scoped ws-relative paths whose bytes a `writeAndCommitLocked`
|
|
77
|
+
* batch has committed (and pushed) while their locks are held — i.e. paths
|
|
78
|
+
* whose working tree is KNOWN clean. `withFileLocks` releases exactly these
|
|
79
|
+
* with `releaseLockNoCommit` on success (see the module doc) and clears
|
|
80
|
+
* each entry as it releases. Keyed `<workspaceId>\0<wsRelPath>` so two
|
|
81
|
+
* workspaces can't cross-talk.
|
|
82
|
+
*/
|
|
83
|
+
private readonly committedCleanPaths;
|
|
84
|
+
constructor(deps: LockedCommitDeps);
|
|
85
|
+
private cleanKey;
|
|
86
|
+
private get defaultBranch();
|
|
87
|
+
wsRel(repoRel: string): string;
|
|
88
|
+
repoDir(workspaceId: string): Promise<string>;
|
|
89
|
+
/** Read a KB-root file; null when genuinely absent. */
|
|
90
|
+
readKbFile(workspaceId: string, repoRel: string): Promise<string | null>;
|
|
91
|
+
/**
|
|
92
|
+
* A lock-aware filesystem scoped to `actor` — writes auto-commit + push as
|
|
93
|
+
* the actor on release, through the same pipeline as the human editor and
|
|
94
|
+
* agent. Built per-op because the lock context captures the acting user.
|
|
95
|
+
* Only for flows that do NOT already hold the file locks (acquire is strict
|
|
96
|
+
* even same-user); a withFileLocks flow writes via writeAndCommitLocked.
|
|
97
|
+
*/
|
|
98
|
+
lockingFsForActor(workspaceId: string, actor: AuthUser): Promise<LockingFilesystem>;
|
|
99
|
+
/**
|
|
100
|
+
* Map LockingFilesystem's status-less contention error ("Skipped editing …
|
|
101
|
+
* — locked by …") to the friendly 409. Callers pre-check the common case,
|
|
102
|
+
* but that is a TOCTOU check — this catches the race so contention always
|
|
103
|
+
* surfaces as a 409, never an unhandled 500.
|
|
104
|
+
*/
|
|
105
|
+
mapLockContention<T>(write: () => Promise<T>): Promise<T>;
|
|
106
|
+
/**
|
|
107
|
+
* Run `fn` while HOLDING the named KB-file locks (sorted, all-or-nothing):
|
|
108
|
+
* read-modify-write flows must keep the lock across the READ too, or two
|
|
109
|
+
* concurrent edits (another tab, another admin, a conversion) both snapshot
|
|
110
|
+
* the same base and the second write silently discards the first. See the
|
|
111
|
+
* module doc for the release semantics.
|
|
112
|
+
*
|
|
113
|
+
* `opts.coordinationFiles` names files locked ONLY to serialize with their
|
|
114
|
+
* writer — `fn` never writes them. They're acquired with the workflow
|
|
115
|
+
* service's coordination flag (a pure mutex that skips the per-path write
|
|
116
|
+
* gate — required for machine-owned files like `synced-groups.yaml`, which
|
|
117
|
+
* a human actor may not, and need not, write) and always released with
|
|
118
|
+
* `releaseLockNoCommit`: there is never anything of ours to commit there,
|
|
119
|
+
* and enqueueing a release commit as an actor the path's write rule
|
|
120
|
+
* refuses would only feed the worker a doomed row. The discard inside that
|
|
121
|
+
* release no-ops: the machine writer commits synchronously under its own
|
|
122
|
+
* lock, so the path is clean whenever we could acquire it.
|
|
123
|
+
*/
|
|
124
|
+
withFileLocks<T>(workspaceId: string, actor: AuthUser, repoRelFiles: string[], fn: () => Promise<T>, opts?: {
|
|
125
|
+
coordinationFiles?: string[];
|
|
126
|
+
}): Promise<T>;
|
|
127
|
+
/**
|
|
128
|
+
* Plain-write `files` and commit them as ONE path-scoped change — the write
|
|
129
|
+
* half of a `withFileLocks` flow. See the module doc for the pre- vs
|
|
130
|
+
* post-commit failure split; only PRE-commit failures restore.
|
|
131
|
+
*/
|
|
132
|
+
writeAndCommitLocked(workspaceId: string, actor: AuthUser, files: LockedWrite[], summary: string): Promise<void>;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=admin-locked-commit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-locked-commit.d.ts","sourceRoot":"","sources":["../../../src/modules/access/admin-locked-commit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAIH,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,EAAiC,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AACpG,OAAO,KAAK,EAAE,QAAQ,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACrG,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAE1D,MAAM,WAAW,gBAAgB;IAC/B,gBAAgB,EAAE,iBAAiB,CAAC;IACpC,eAAe,EAAE,gBAAgB,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,eAAe,EAAE,MAAM,MAAM,CAAC;IAC9B,qEAAqE;IACrE,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,mBAAmB,CAAC;IACvG,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,gBAAgB,EAAE,MAAM,CAAC;IACzB,mEAAmE;IACnE,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;CAC9D;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,gFAAgF;IAChF,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,iEAAiE;IACjE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,qBAAa,kBAAkB;IAWjB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAVjC;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;gBAE5B,IAAI,EAAE,gBAAgB;IAEnD,OAAO,CAAC,QAAQ;IAIhB,OAAO,KAAK,aAAa,GAExB;IAED,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAIxB,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAKnD,uDAAuD;IACjD,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAa9E;;;;;;OAMG;IACG,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAczF;;;;;OAKG;IACG,iBAAiB,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAc/D;;;;;;;;;;;;;;;;;OAiBG;IACG,aAAa,CAAC,CAAC,EACnB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,QAAQ,EACf,YAAY,EAAE,MAAM,EAAE,EACtB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,CAAC,EAAE;QAAE,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,GACtC,OAAO,CAAC,CAAC,CAAC;IAgFb;;;;OAIG;IACG,oBAAoB,CACxB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,QAAQ,EACf,KAAK,EAAE,WAAW,EAAE,EACpB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC;CAqDjB"}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHARED transactional lock/commit helper for the admin roster services
|
|
3
|
+
* (roles + groups). The two services used to carry twin copies of this
|
|
4
|
+
* machinery, which diverged once into a real bug — this is the single
|
|
5
|
+
* implementation both delegate to, parameterized only by the domain-error
|
|
6
|
+
* constructor, log tag, and contention wording.
|
|
7
|
+
*
|
|
8
|
+
* The shape of every roster write:
|
|
9
|
+
*
|
|
10
|
+
* withFileLocks(paths, fn) — acquire every path's lock (sorted,
|
|
11
|
+
* all-or-nothing; strict even for the same user), run `fn`, release.
|
|
12
|
+
* HOW each lock is released depends on the outcome (see the finally
|
|
13
|
+
* block for the full reasoning):
|
|
14
|
+
* - `fn` succeeded → releaseLockNoCommit for every path the batch
|
|
15
|
+
* commit LANDED on (tracked by writeAndCommitLocked): the commit
|
|
16
|
+
* landed AND pushed, so the tree there is clean (the discard
|
|
17
|
+
* no-ops); releaseLock would instead ENQUEUE a commit before
|
|
18
|
+
* dropping the row, and an enqueue failure then strands the lock
|
|
19
|
+
* until TTL. A held path the batch never committed keeps
|
|
20
|
+
* commit-on-release — it may carry a PRIOR holder's still-queued
|
|
21
|
+
* work, which a discard would destroy.
|
|
22
|
+
* - `fn` threw PushNeedsAgentResolutionError → releaseLock. The commit
|
|
23
|
+
* landed but the push didn't; the enqueued release commit IS the
|
|
24
|
+
* retry (the worker sees the clean tree, notices unpushed commits,
|
|
25
|
+
* and re-runs the cooperative push ladder).
|
|
26
|
+
* - `fn` threw anything else → releaseLock (commit-on-release): `fn`
|
|
27
|
+
* restored its own bytes, so anything dirty at release time is
|
|
28
|
+
* either clean (queued commit no-ops) or someone else's still-queued
|
|
29
|
+
* work that must be preserved. The ONE exception: a path whose
|
|
30
|
+
* restore itself failed (`unrestoredPaths` on the thrown error)
|
|
31
|
+
* holds known-partial bytes → releaseLockNoCommit discards to HEAD.
|
|
32
|
+
* Every release is per-path best-effort: one failed release logs and
|
|
33
|
+
* moves on so it can never strand the REST of the held locks.
|
|
34
|
+
*
|
|
35
|
+
* writeAndCommitLocked(files) — the write half of a withFileLocks
|
|
36
|
+
* flow: plain-write each file (the lock is already ours — strict
|
|
37
|
+
* same-user acquire forbids LockingFilesystem here) and commit them as
|
|
38
|
+
* ONE path-scoped change. On a PRE-commit failure, best-effort restore of
|
|
39
|
+
* each file's original bytes (`original: null` = the file did not exist,
|
|
40
|
+
* so restoration DELETES it). A POST-commit failure — the push after a
|
|
41
|
+
* landed commit (`PushNeedsAgentResolutionError`, thrown with the commit
|
|
42
|
+
* intact) — must NOT restore: the edit is committed locally and restoring
|
|
43
|
+
* would publish a compensating revert of a landed change. It propagates
|
|
44
|
+
* as-is ("saved locally … will be resolved"), locks release normally, and
|
|
45
|
+
* the pending-commit ladder retries the share.
|
|
46
|
+
*/
|
|
47
|
+
import path from 'node:path';
|
|
48
|
+
import { LockingFilesystem } from '../workflow/locking-filesystem.js';
|
|
49
|
+
import { PushNeedsAgentResolutionError } from '../workflow/workflow.errors.js';
|
|
50
|
+
export class AdminLockedCommits {
|
|
51
|
+
deps;
|
|
52
|
+
/**
|
|
53
|
+
* Workspace-scoped ws-relative paths whose bytes a `writeAndCommitLocked`
|
|
54
|
+
* batch has committed (and pushed) while their locks are held — i.e. paths
|
|
55
|
+
* whose working tree is KNOWN clean. `withFileLocks` releases exactly these
|
|
56
|
+
* with `releaseLockNoCommit` on success (see the module doc) and clears
|
|
57
|
+
* each entry as it releases. Keyed `<workspaceId>\0<wsRelPath>` so two
|
|
58
|
+
* workspaces can't cross-talk.
|
|
59
|
+
*/
|
|
60
|
+
committedCleanPaths = new Set();
|
|
61
|
+
constructor(deps) {
|
|
62
|
+
this.deps = deps;
|
|
63
|
+
}
|
|
64
|
+
cleanKey(workspaceId, wsRelPath) {
|
|
65
|
+
return `${workspaceId}\0${wsRelPath}`;
|
|
66
|
+
}
|
|
67
|
+
get defaultBranch() {
|
|
68
|
+
return this.deps.defaultBranchOf();
|
|
69
|
+
}
|
|
70
|
+
wsRel(repoRel) {
|
|
71
|
+
return `${this.deps.kbDirName}/${repoRel}`;
|
|
72
|
+
}
|
|
73
|
+
async repoDir(workspaceId) {
|
|
74
|
+
const wsDir = await this.deps.workspaceService.getWorkspacePath(workspaceId);
|
|
75
|
+
return path.join(wsDir, this.deps.kbDirName);
|
|
76
|
+
}
|
|
77
|
+
/** Read a KB-root file; null when genuinely absent. */
|
|
78
|
+
async readKbFile(workspaceId, repoRel) {
|
|
79
|
+
try {
|
|
80
|
+
return await this.deps.workspaceService.readFile(workspaceId, path.posix.join(this.deps.kbDirName, repoRel));
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
const code = err?.code;
|
|
84
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
85
|
+
return null;
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A lock-aware filesystem scoped to `actor` — writes auto-commit + push as
|
|
91
|
+
* the actor on release, through the same pipeline as the human editor and
|
|
92
|
+
* agent. Built per-op because the lock context captures the acting user.
|
|
93
|
+
* Only for flows that do NOT already hold the file locks (acquire is strict
|
|
94
|
+
* even same-user); a withFileLocks flow writes via writeAndCommitLocked.
|
|
95
|
+
*/
|
|
96
|
+
async lockingFsForActor(workspaceId, actor) {
|
|
97
|
+
const basePath = await this.deps.workspaceService.getWorkspacePath(workspaceId);
|
|
98
|
+
return new LockingFilesystem({ basePath, contained: true }, {
|
|
99
|
+
workflow: this.deps.workflowService,
|
|
100
|
+
workspaceId,
|
|
101
|
+
branch: this.defaultBranch,
|
|
102
|
+
user: actor,
|
|
103
|
+
validateWrite: this.deps.validateWrite,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Map LockingFilesystem's status-less contention error ("Skipped editing …
|
|
108
|
+
* — locked by …") to the friendly 409. Callers pre-check the common case,
|
|
109
|
+
* but that is a TOCTOU check — this catches the race so contention always
|
|
110
|
+
* surfaces as a 409, never an unhandled 500.
|
|
111
|
+
*/
|
|
112
|
+
async mapLockContention(write) {
|
|
113
|
+
try {
|
|
114
|
+
return await write();
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
if (err instanceof Error && /locked by /.test(err.message)) {
|
|
118
|
+
throw this.deps.makeError(`${this.deps.contendedSubject} are being edited by another admin. Try again in a moment.`, 409);
|
|
119
|
+
}
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Run `fn` while HOLDING the named KB-file locks (sorted, all-or-nothing):
|
|
125
|
+
* read-modify-write flows must keep the lock across the READ too, or two
|
|
126
|
+
* concurrent edits (another tab, another admin, a conversion) both snapshot
|
|
127
|
+
* the same base and the second write silently discards the first. See the
|
|
128
|
+
* module doc for the release semantics.
|
|
129
|
+
*
|
|
130
|
+
* `opts.coordinationFiles` names files locked ONLY to serialize with their
|
|
131
|
+
* writer — `fn` never writes them. They're acquired with the workflow
|
|
132
|
+
* service's coordination flag (a pure mutex that skips the per-path write
|
|
133
|
+
* gate — required for machine-owned files like `synced-groups.yaml`, which
|
|
134
|
+
* a human actor may not, and need not, write) and always released with
|
|
135
|
+
* `releaseLockNoCommit`: there is never anything of ours to commit there,
|
|
136
|
+
* and enqueueing a release commit as an actor the path's write rule
|
|
137
|
+
* refuses would only feed the worker a doomed row. The discard inside that
|
|
138
|
+
* release no-ops: the machine writer commits synchronously under its own
|
|
139
|
+
* lock, so the path is clean whenever we could acquire it.
|
|
140
|
+
*/
|
|
141
|
+
async withFileLocks(workspaceId, actor, repoRelFiles, fn, opts) {
|
|
142
|
+
const coordination = new Set((opts?.coordinationFiles ?? []).map((f) => this.wsRel(f)));
|
|
143
|
+
// One globally-sorted acquire order across BOTH kinds of path — a split
|
|
144
|
+
// order would reintroduce the deadlock the sort exists to prevent.
|
|
145
|
+
const paths = [...new Set([...repoRelFiles.map((f) => this.wsRel(f)), ...coordination])].sort();
|
|
146
|
+
const held = [];
|
|
147
|
+
let failure = null;
|
|
148
|
+
let unrestored = null;
|
|
149
|
+
try {
|
|
150
|
+
for (const p of paths) {
|
|
151
|
+
const res = await this.deps.workflowService.acquireLock(workspaceId, this.defaultBranch, p, actor, coordination.has(p) ? { coordination: true } : undefined);
|
|
152
|
+
if (!res.acquired) {
|
|
153
|
+
throw this.deps.makeError(`${this.deps.contendedSubject} are being edited by ${res.lock.holderName || 'another admin'}. Try again in a moment.`, 409);
|
|
154
|
+
}
|
|
155
|
+
held.push(p);
|
|
156
|
+
}
|
|
157
|
+
return await fn();
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
failure = err;
|
|
161
|
+
unrestored = err?.unrestoredPaths ?? null;
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
// Outcome-dependent release — see the module doc. The key hazard this
|
|
166
|
+
// guards: `releaseLock` ENQUEUES the release commit BEFORE dropping the
|
|
167
|
+
// lock row (deliberately, so a crash can't orphan bytes), which means an
|
|
168
|
+
// enqueue failure leaves the row held until TTL. After a SUCCESSFUL
|
|
169
|
+
// batch commit there is nothing left to commit — writeAndCommitLocked
|
|
170
|
+
// committed and pushed these exact paths, and we hold their locks, so
|
|
171
|
+
// the tree there is clean — so releaseLockNoCommit (whose discard
|
|
172
|
+
// no-ops on a clean path) releases without ever touching the queue.
|
|
173
|
+
//
|
|
174
|
+
// EXCEPT on the push-retry path: PushNeedsAgentResolutionError means
|
|
175
|
+
// the commit landed but the push didn't, and the enqueued release
|
|
176
|
+
// commit is what retries the push (runPendingCommit: clean tree +
|
|
177
|
+
// unpushed commits → pushWithRecovery). That path MUST releaseLock.
|
|
178
|
+
// Failures there log + continue so one bad enqueue strands at most its
|
|
179
|
+
// own lock (TTL backstop), never the rest.
|
|
180
|
+
const pushRetry = failure instanceof PushNeedsAgentResolutionError;
|
|
181
|
+
for (const h of held) {
|
|
182
|
+
const committedClean = this.committedCleanPaths.delete(this.cleanKey(workspaceId, h)) && !pushRetry;
|
|
183
|
+
try {
|
|
184
|
+
if (coordination.has(h) || committedClean || unrestored?.has(h)) {
|
|
185
|
+
// Coordination-only hold (see the method doc): `fn` never wrote
|
|
186
|
+
// it, so no outcome — push-retry included — has anything to
|
|
187
|
+
// commit there; the no-commit release just drops the row. Or:
|
|
188
|
+
// Batch-committed path (clean tree — discard no-ops) — even when
|
|
189
|
+
// `fn` threw AFTER the commit+push landed: the failure says
|
|
190
|
+
// nothing about the tree, and releaseLock would enqueue a
|
|
191
|
+
// pointless commit whose enqueue failure strands a known-clean
|
|
192
|
+
// lock until TTL. Or known-partial bytes (restore failed —
|
|
193
|
+
// discarding to HEAD beats committing them).
|
|
194
|
+
await this.deps.workflowService.releaseLockNoCommit(workspaceId, this.defaultBranch, h, actor);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
// Push-retry (the enqueued commit retries the share), the
|
|
198
|
+
// restored-failure path, and any held path the batch never
|
|
199
|
+
// committed (commit-on-release: a prior holder's still-queued
|
|
200
|
+
// work must be preserved, never discarded).
|
|
201
|
+
await this.deps.workflowService.releaseLock(workspaceId, this.defaultBranch, h, actor);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (releaseErr) {
|
|
205
|
+
console.warn(`[${this.deps.logTag}] could not release lock on ${h}${pushRetry ? ' (push-retry release)' : ''}; ` +
|
|
206
|
+
'it frees on TTL — continuing with the remaining locks:', releaseErr instanceof Error ? releaseErr.message : releaseErr);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Plain-write `files` and commit them as ONE path-scoped change — the write
|
|
213
|
+
* half of a `withFileLocks` flow. See the module doc for the pre- vs
|
|
214
|
+
* post-commit failure split; only PRE-commit failures restore.
|
|
215
|
+
*/
|
|
216
|
+
async writeAndCommitLocked(workspaceId, actor, files, summary) {
|
|
217
|
+
const { workspaceService, workflowService, logTag } = this.deps;
|
|
218
|
+
try {
|
|
219
|
+
for (const f of files) {
|
|
220
|
+
if (f.content === null) {
|
|
221
|
+
// A DELETE entry. Tolerate an already-absent file — that IS the
|
|
222
|
+
// desired state (and the caller checked existence under the lock).
|
|
223
|
+
try {
|
|
224
|
+
await workspaceService.deleteFile(workspaceId, this.wsRel(f.repoRel));
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
if (err?.code !== 'ENOENT')
|
|
228
|
+
throw err;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
await workspaceService.writeFile(workspaceId, this.wsRel(f.repoRel), f.content);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
await workflowService.commitChanges(workspaceId, actor, summary, files.map((f) => this.wsRel(f.repoRel)));
|
|
236
|
+
// Commit landed AND pushed — the tree at these paths is clean. Mark
|
|
237
|
+
// them so withFileLocks releases them WITHOUT enqueueing a release
|
|
238
|
+
// commit (see the module doc — an enqueue failure would strand the
|
|
239
|
+
// lock until TTL for a commit there is nothing left to make).
|
|
240
|
+
for (const f of files)
|
|
241
|
+
this.committedCleanPaths.add(this.cleanKey(workspaceId, this.wsRel(f.repoRel)));
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
// POST-commit failure: the commit landed and only the PUSH needs help
|
|
245
|
+
// (thrown "with the commit intact"). The bytes on disk match the landed
|
|
246
|
+
// commit — restoring pre-edit bytes here would publish a compensating
|
|
247
|
+
// revert of a change that exists. No restore; release proceeds
|
|
248
|
+
// normally; the error itself tells the caller "saved locally, sharing
|
|
249
|
+
// will be resolved" and the pending-commit ladder retries the push.
|
|
250
|
+
if (err instanceof PushNeedsAgentResolutionError) {
|
|
251
|
+
console.warn(`[${logTag}] commit landed but the push needs resolution — edit saved; publishing will be retried`);
|
|
252
|
+
throw err;
|
|
253
|
+
}
|
|
254
|
+
const unrestored = new Set();
|
|
255
|
+
for (const f of files) {
|
|
256
|
+
try {
|
|
257
|
+
if (f.original === null)
|
|
258
|
+
await workspaceService.deleteFile(workspaceId, this.wsRel(f.repoRel));
|
|
259
|
+
else
|
|
260
|
+
await workspaceService.writeFile(workspaceId, this.wsRel(f.repoRel), f.original);
|
|
261
|
+
}
|
|
262
|
+
catch (restoreErr) {
|
|
263
|
+
// Deleting an already-absent file IS the original state.
|
|
264
|
+
if (f.original === null && restoreErr?.code === 'ENOENT')
|
|
265
|
+
continue;
|
|
266
|
+
unrestored.add(this.wsRel(f.repoRel));
|
|
267
|
+
console.warn(`[${logTag}] could not restore ${f.repoRel} after a failed commit`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (unrestored.size > 0 && typeof err === 'object' && err !== null) {
|
|
271
|
+
err.unrestoredPaths = unrestored;
|
|
272
|
+
}
|
|
273
|
+
throw err;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
//# sourceMappingURL=admin-locked-commit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-locked-commit.js","sourceRoot":"","sources":["../../../src/modules/access/admin-locked-commit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHARED route plumbing for the access-family routers (access.routes,
|
|
3
|
+
* groups-admin.routes) — one error shape, one input-coercion rule, so the two
|
|
4
|
+
* surfaces can't drift apart again.
|
|
5
|
+
*
|
|
6
|
+
* Error shape: a sub-500 `WorkflowDomainError` renders as its own status +
|
|
7
|
+
* message + payload; everything else — a raw error OR a 500-status domain
|
|
8
|
+
* error (e.g. AccessMutationService wraps unexpected raw errors in a 500
|
|
9
|
+
* AccessMutationError, message included) — is an INTERNAL error: logged
|
|
10
|
+
* server-side in full, answered with a generic message. A raw `err.message`
|
|
11
|
+
* must never leak through a 500 (it can carry fs/db paths and internals the
|
|
12
|
+
* caller has no business seeing).
|
|
13
|
+
*/
|
|
14
|
+
import type express from 'express';
|
|
15
|
+
/** One error shape for every access-family route. */
|
|
16
|
+
export declare function toHttpError(err: unknown, logTag: string): {
|
|
17
|
+
status: number;
|
|
18
|
+
body: Record<string, unknown>;
|
|
19
|
+
};
|
|
20
|
+
/** Answer `res` with the shared error shape. */
|
|
21
|
+
export declare function sendError(res: express.Response, err: unknown, logTag: string): void;
|
|
22
|
+
/**
|
|
23
|
+
* Coerce a request-body field that must be a non-empty string. Rejects arrays,
|
|
24
|
+
* objects, numbers, null — anything `String()` would silently stringify into a
|
|
25
|
+
* bogus principal name / email (`[object Object]`, `"a,b"`) — with a 400
|
|
26
|
+
* instead of persisting garbage. Returns the raw string (untrimmed by default
|
|
27
|
+
* — services trim; pass `trim: true` where the route owns normalization).
|
|
28
|
+
*/
|
|
29
|
+
export declare function requireNonEmptyString(value: unknown, field: string, opts?: {
|
|
30
|
+
trim?: boolean;
|
|
31
|
+
}): string;
|
|
32
|
+
//# sourceMappingURL=admin-route-helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-route-helpers.d.ts","sourceRoot":"","sources":["../../../src/modules/access/admin-route-helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AAGnC,qDAAqD;AACrD,wBAAgB,WAAW,CACzB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,GACb;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CASnD;AAED,gDAAgD;AAChD,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAGnF;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GACxB,MAAM,CAKR"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHARED route plumbing for the access-family routers (access.routes,
|
|
3
|
+
* groups-admin.routes) — one error shape, one input-coercion rule, so the two
|
|
4
|
+
* surfaces can't drift apart again.
|
|
5
|
+
*
|
|
6
|
+
* Error shape: a sub-500 `WorkflowDomainError` renders as its own status +
|
|
7
|
+
* message + payload; everything else — a raw error OR a 500-status domain
|
|
8
|
+
* error (e.g. AccessMutationService wraps unexpected raw errors in a 500
|
|
9
|
+
* AccessMutationError, message included) — is an INTERNAL error: logged
|
|
10
|
+
* server-side in full, answered with a generic message. A raw `err.message`
|
|
11
|
+
* must never leak through a 500 (it can carry fs/db paths and internals the
|
|
12
|
+
* caller has no business seeing).
|
|
13
|
+
*/
|
|
14
|
+
import { WorkflowDomainError } from '../workflow/workflow.errors.js';
|
|
15
|
+
/** One error shape for every access-family route. */
|
|
16
|
+
export function toHttpError(err, logTag) {
|
|
17
|
+
if (err instanceof WorkflowDomainError && err.status < 500) {
|
|
18
|
+
// Spread the payload FIRST and assign `error` LAST — a payload key named
|
|
19
|
+
// `error` must never overwrite the message the client renders.
|
|
20
|
+
return { status: err.status, body: { ...(err.payload ?? {}), error: err.message } };
|
|
21
|
+
}
|
|
22
|
+
console.error(`[${logTag}] route failure:`, err instanceof Error ? err.stack ?? err.message : err);
|
|
23
|
+
const status = err instanceof WorkflowDomainError ? err.status : 500;
|
|
24
|
+
return { status, body: { error: 'Internal error.' } };
|
|
25
|
+
}
|
|
26
|
+
/** Answer `res` with the shared error shape. */
|
|
27
|
+
export function sendError(res, err, logTag) {
|
|
28
|
+
const { status, body } = toHttpError(err, logTag);
|
|
29
|
+
res.status(status).json(body);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Coerce a request-body field that must be a non-empty string. Rejects arrays,
|
|
33
|
+
* objects, numbers, null — anything `String()` would silently stringify into a
|
|
34
|
+
* bogus principal name / email (`[object Object]`, `"a,b"`) — with a 400
|
|
35
|
+
* instead of persisting garbage. Returns the raw string (untrimmed by default
|
|
36
|
+
* — services trim; pass `trim: true` where the route owns normalization).
|
|
37
|
+
*/
|
|
38
|
+
export function requireNonEmptyString(value, field, opts) {
|
|
39
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
40
|
+
throw new WorkflowDomainError(`${field} must be a non-empty string`, 400);
|
|
41
|
+
}
|
|
42
|
+
return opts?.trim ? value.trim() : value;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=admin-route-helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-route-helpers.js","sourceRoot":"","sources":["../../../src/modules/access/admin-route-helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAErE,qDAAqD;AACrD,MAAM,UAAU,WAAW,CACzB,GAAY,EACZ,MAAc;IAEd,IAAI,GAAG,YAAY,mBAAmB,IAAI,GAAG,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;QAC3D,yEAAyE;QACzE,+DAA+D;QAC/D,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,GAAG,CAAC,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC;IACtF,CAAC;IACD,OAAO,CAAC,KAAK,CAAC,IAAI,MAAM,kBAAkB,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACnG,MAAM,MAAM,GAAG,GAAG,YAAY,mBAAmB,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC;IACrE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE,EAAE,CAAC;AACxD,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,SAAS,CAAC,GAAqB,EAAE,GAAY,EAAE,MAAc;IAC3E,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAClD,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAChC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CACnC,KAAc,EACd,KAAa,EACb,IAAyB;IAEzB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACrD,MAAM,IAAI,mBAAmB,CAAC,GAAG,KAAK,6BAA6B,EAAE,GAAG,CAAC,CAAC;IAC5E,CAAC;IACD,OAAO,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;AAC3C,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability registry — the "what you can do in the app" half of the
|
|
3
|
+
* roles/groups split.
|
|
4
|
+
*
|
|
5
|
+
* Roles are CODE-DEFINED capability bundles: the product decides which roles
|
|
6
|
+
* exist and what each unlocks; admins assign them (to individuals, and to
|
|
7
|
+
* groups via `group:` members in roles.yaml — every role, Admin included,
|
|
8
|
+
* kept safe by the parse-time at-least-one-direct-email invariant on Admin).
|
|
9
|
+
* Something like "Developer" is not a role; that's a group.
|
|
10
|
+
*
|
|
11
|
+
* Adding a future role (the planned Plugin Creator) is a registry entry plus
|
|
12
|
+
* capability gates at its feature surfaces — never a parser change: the
|
|
13
|
+
* roles.yaml grammar, expansion, and admin surfaces are already
|
|
14
|
+
* role-name-agnostic.
|
|
15
|
+
*
|
|
16
|
+
* A roles.yaml role NOT in this registry is a LEGACY people-set role from
|
|
17
|
+
* before the split (e.g. "Product") — still valid as a grant principal, but
|
|
18
|
+
* really a group; the roles admin surface flags it with a convert action.
|
|
19
|
+
*/
|
|
20
|
+
/** A capability an app surface gates on. Grow this union with the gates. */
|
|
21
|
+
export type Capability = 'manage-deployment' | 'manage-accounts' | 'manage-roles' | 'manage-groups' | 'manage-directory';
|
|
22
|
+
export interface CapabilityRole {
|
|
23
|
+
canonical: string;
|
|
24
|
+
displayName: string;
|
|
25
|
+
/** Admin-facing one-liner for the Roles page. */
|
|
26
|
+
description: string;
|
|
27
|
+
capabilities: readonly Capability[];
|
|
28
|
+
/**
|
|
29
|
+
* Whether the role may be assigned to groups. True for every role, Admin
|
|
30
|
+
* included — with the parse-time invariant that Admin ALWAYS keeps at least
|
|
31
|
+
* one direct email member (see `parseRolesYaml`), so a misconfigured or
|
|
32
|
+
* unreachable directory can never leave the deployment without a
|
|
33
|
+
* directory-independent admin.
|
|
34
|
+
*/
|
|
35
|
+
groupAssignable: boolean;
|
|
36
|
+
}
|
|
37
|
+
export declare const CAPABILITY_ROLES: readonly CapabilityRole[];
|
|
38
|
+
export declare function capabilityRoleFor(roleName: string): CapabilityRole | null;
|
|
39
|
+
/** A roles.yaml role that is NOT a capability role — a pre-split people-set. */
|
|
40
|
+
export declare function isLegacyPeopleSetRole(roleName: string): boolean;
|
|
41
|
+
//# sourceMappingURL=capability-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability-registry.d.ts","sourceRoot":"","sources":["../../../src/modules/access/capability-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,4EAA4E;AAC5E,MAAM,MAAM,UAAU,GAClB,mBAAmB,GACnB,iBAAiB,GACjB,cAAc,GACd,eAAe,GACf,kBAAkB,CAAC;AAEvB,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,SAAS,UAAU,EAAE,CAAC;IACpC;;;;;;OAMG;IACH,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,eAAO,MAAM,gBAAgB,EAAE,SAAS,cAAc,EAiBrD,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAGzE;AAED,gFAAgF;AAChF,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE/D"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability registry — the "what you can do in the app" half of the
|
|
3
|
+
* roles/groups split.
|
|
4
|
+
*
|
|
5
|
+
* Roles are CODE-DEFINED capability bundles: the product decides which roles
|
|
6
|
+
* exist and what each unlocks; admins assign them (to individuals, and to
|
|
7
|
+
* groups via `group:` members in roles.yaml — every role, Admin included,
|
|
8
|
+
* kept safe by the parse-time at-least-one-direct-email invariant on Admin).
|
|
9
|
+
* Something like "Developer" is not a role; that's a group.
|
|
10
|
+
*
|
|
11
|
+
* Adding a future role (the planned Plugin Creator) is a registry entry plus
|
|
12
|
+
* capability gates at its feature surfaces — never a parser change: the
|
|
13
|
+
* roles.yaml grammar, expansion, and admin surfaces are already
|
|
14
|
+
* role-name-agnostic.
|
|
15
|
+
*
|
|
16
|
+
* A roles.yaml role NOT in this registry is a LEGACY people-set role from
|
|
17
|
+
* before the split (e.g. "Product") — still valid as a grant principal, but
|
|
18
|
+
* really a group; the roles admin surface flags it with a convert action.
|
|
19
|
+
*/
|
|
20
|
+
import { ADMIN_CANONICAL, canonicalRoleName } from './access-control.service.js';
|
|
21
|
+
export const CAPABILITY_ROLES = [
|
|
22
|
+
{
|
|
23
|
+
canonical: ADMIN_CANONICAL,
|
|
24
|
+
displayName: 'Admin',
|
|
25
|
+
description: 'Detailed configuration: deployment settings, user accounts, roles & groups, and the directory connection.',
|
|
26
|
+
capabilities: [
|
|
27
|
+
'manage-deployment',
|
|
28
|
+
'manage-accounts',
|
|
29
|
+
'manage-roles',
|
|
30
|
+
'manage-groups',
|
|
31
|
+
'manage-directory',
|
|
32
|
+
],
|
|
33
|
+
groupAssignable: true,
|
|
34
|
+
},
|
|
35
|
+
// Planned next (lands with its feature gates, not before):
|
|
36
|
+
// { canonical: 'plugin creator', displayName: 'Plugin Creator', … }
|
|
37
|
+
];
|
|
38
|
+
export function capabilityRoleFor(roleName) {
|
|
39
|
+
const canonical = canonicalRoleName(roleName);
|
|
40
|
+
return CAPABILITY_ROLES.find((r) => r.canonical === canonical) ?? null;
|
|
41
|
+
}
|
|
42
|
+
/** A roles.yaml role that is NOT a capability role — a pre-split people-set. */
|
|
43
|
+
export function isLegacyPeopleSetRole(roleName) {
|
|
44
|
+
return capabilityRoleFor(roleName) === null;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=capability-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability-registry.js","sourceRoot":"","sources":["../../../src/modules/access/capability-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AA0BjF,MAAM,CAAC,MAAM,gBAAgB,GAA8B;IACzD;QACE,SAAS,EAAE,eAAe;QAC1B,WAAW,EAAE,OAAO;QACpB,WAAW,EACT,2GAA2G;QAC7G,YAAY,EAAE;YACZ,mBAAmB;YACnB,iBAAiB;YACjB,cAAc;YACd,eAAe;YACf,kBAAkB;SACnB;QACD,eAAe,EAAE,IAAI;KACtB;IACD,2DAA2D;IAC3D,oEAAoE;CACrE,CAAC;AAEF,MAAM,UAAU,iBAAiB,CAAC,QAAgB;IAChD,MAAM,SAAS,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IAC9C,OAAO,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,SAAS,CAAC,IAAI,IAAI,CAAC;AACzE,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,qBAAqB,CAAC,QAAgB;IACpD,OAAO,iBAAiB,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC;AAC9C,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AuthUser } from '@bevel-software/platform-shared';
|
|
2
|
+
import type { Database } from '../database/connection.js';
|
|
3
|
+
/**
|
|
4
|
+
* Synthetic identity for the synced-groups materializer's commits: every
|
|
5
|
+
* `synced-groups.yaml` regeneration lands attributed to this bot, so the git
|
|
6
|
+
* history reads as "the system mirrored the IdP", never as any human.
|
|
7
|
+
* Same `<role>-bot@<host>` convention (and race-safe ensure shape) as the
|
|
8
|
+
* recovery bot.
|
|
9
|
+
*/
|
|
10
|
+
export declare const DIRECTORY_SYNC_BOT_EMAIL: string;
|
|
11
|
+
export declare const DIRECTORY_SYNC_BOT_NAME = "Directory Sync Bot";
|
|
12
|
+
export declare function ensureDirectorySyncBot(db: Database): Promise<AuthUser>;
|
|
13
|
+
//# sourceMappingURL=directory-sync-bot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"directory-sync-bot.d.ts","sourceRoot":"","sources":["../../../src/modules/access/directory-sync-bot.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAG1D;;;;;;GAMG;AAKH,eAAO,MAAM,wBAAwB,QAEtB,CAAC;AAEhB,eAAO,MAAM,uBAAuB,uBAAuB,CAAC;AAE5D,wBAAsB,sBAAsB,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAqD5E"}
|