@ricsam/r5d-worker 0.0.137 → 0.0.138
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/README.md +4 -94
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/main.mjs +46 -7543
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/personal/action-scheduler.mjs +31 -0
- package/dist/mjs/personal/client.mjs +215 -0
- package/dist/mjs/personal/runtime.mjs +328 -0
- package/dist/mjs/personal/tcp.mjs +217 -0
- package/dist/mjs/runtime/adapter.mjs +42 -0
- package/dist/mjs/runtime/client.mjs +95 -0
- package/dist/mjs/runtime/daemon.mjs +131 -0
- package/dist/mjs/runtime/executor.mjs +436 -0
- package/dist/mjs/runtime/index.mjs +15 -0
- package/dist/mjs/runtime/protocol.mjs +139 -0
- package/dist/mjs/runtime/pty-host.mjs +156 -0
- package/dist/mjs/runtime/pty.mjs +127 -0
- package/dist/mjs/runtime/releases/artifacts.mjs +201 -0
- package/dist/mjs/runtime/releases/grants.mjs +93 -0
- package/dist/mjs/runtime/releases/main.mjs +101 -0
- package/dist/mjs/runtime/releases/manager.mjs +541 -0
- package/dist/mjs/runtime/releases/process.mjs +88 -0
- package/dist/mjs/runtime/releases/retirement.mjs +18 -0
- package/dist/mjs/runtime/releases/rpc-client.mjs +184 -0
- package/dist/mjs/runtime/releases/rpc-main.mjs +213 -0
- package/dist/mjs/runtime/releases/rpc-protocol.mjs +103 -0
- package/dist/mjs/runtime/storage.mjs +156 -0
- package/dist/mjs/runtime/workspace/artifacts.mjs +121 -0
- package/dist/mjs/runtime/workspace/authority.mjs +1355 -0
- package/dist/mjs/runtime/workspace/contracts.mjs +60 -0
- package/dist/mjs/runtime/workspace/file-write.mjs +23 -0
- package/dist/mjs/runtime/workspace/files.mjs +308 -0
- package/dist/mjs/runtime/workspace/http-body.mjs +45 -0
- package/dist/mjs/runtime/workspace/storage-client.mjs +59 -0
- package/dist/mjs/runtime/workspace/storage-wire.mjs +53 -0
- package/dist/types/main.d.ts +0 -1167
- package/dist/types/personal/action-scheduler.d.ts +23 -0
- package/dist/types/personal/client.d.ts +14 -0
- package/dist/types/personal/runtime.d.ts +121 -0
- package/dist/types/personal/tcp.d.ts +14 -0
- package/dist/types/runtime/client.d.ts +3 -1
- package/dist/types/runtime/executor.d.ts +6 -1
- package/dist/types/runtime/protocol.d.ts +176 -4
- package/dist/types/runtime/pty.d.ts +1 -0
- package/dist/types/runtime/releases/artifacts.d.ts +51 -0
- package/dist/types/runtime/releases/grants.d.ts +16 -0
- package/dist/types/runtime/releases/manager.d.ts +145 -0
- package/dist/types/runtime/releases/process.d.ts +14 -0
- package/dist/types/runtime/releases/retirement.d.ts +15 -0
- package/dist/types/runtime/releases/rpc-client.d.ts +52 -0
- package/dist/types/runtime/releases/rpc-main.d.ts +10 -0
- package/dist/types/runtime/releases/rpc-protocol.d.ts +1139 -0
- package/dist/types/runtime/workspace/artifacts.d.ts +30 -0
- package/dist/types/runtime/workspace/authority.d.ts +338 -0
- package/dist/types/runtime/workspace/contracts.d.ts +86 -0
- package/dist/types/runtime/workspace/file-write.d.ts +14 -0
- package/dist/types/runtime/workspace/files.d.ts +21 -0
- package/dist/types/runtime/workspace/http-body.d.ts +9 -0
- package/dist/types/runtime/workspace/storage-client.d.ts +23 -0
- package/dist/types/runtime/workspace/storage-wire.d.ts +138 -0
- package/package.json +8 -6
- package/dist/cjs/atomic-rename.cjs +0 -303
- package/dist/cjs/browser-upload.cjs +0 -73
- package/dist/cjs/cli-update.cjs +0 -108
- package/dist/cjs/command-launcher.cjs +0 -423
- package/dist/cjs/control-command-policy.cjs +0 -177
- package/dist/cjs/git-blob-hash.cjs +0 -41
- package/dist/cjs/git-identity.cjs +0 -39
- package/dist/cjs/git-process-environment.cjs +0 -133
- package/dist/cjs/inline-output.cjs +0 -62
- package/dist/cjs/main.cjs +0 -7517
- package/dist/cjs/managed-paths.cjs +0 -215
- package/dist/cjs/plan-lint.cjs +0 -165
- package/dist/cjs/plan-parser.cjs +0 -127
- package/dist/cjs/plan-paths.cjs +0 -53
- package/dist/cjs/port-forward-client.cjs +0 -206
- package/dist/cjs/process-tree.cjs +0 -117
- package/dist/cjs/project-checkout-garbage.cjs +0 -219
- package/dist/cjs/project-mirror-fetch-policy.cjs +0 -82
- package/dist/cjs/project-mirror-refs-token.cjs +0 -85
- package/dist/cjs/project-workspace-state.cjs +0 -972
- package/dist/cjs/project-worktrees.cjs +0 -1713
- package/dist/cjs/pty-output-coalescer.cjs +0 -87
- package/dist/cjs/recovery-journal-protocol.cjs +0 -56
- package/dist/cjs/recovery-journal-runtime.cjs +0 -133
- package/dist/cjs/recovery-journal-thread.cjs +0 -9
- package/dist/cjs/recovery-journal.cjs +0 -742
- package/dist/cjs/recovery-output-window.cjs +0 -77
- package/dist/cjs/recovery-protocol.cjs +0 -31
- package/dist/cjs/recovery-store.cjs +0 -252
- package/dist/cjs/registry-auth.cjs +0 -310
- package/dist/cjs/repository-transition-policy.cjs +0 -49
- package/dist/cjs/runtime-version.cjs +0 -88
- package/dist/cjs/session-file-mutations.cjs +0 -61
- package/dist/cjs/supervisor.cjs +0 -147
- package/dist/cjs/three-way-merge.cjs +0 -346
- package/dist/cjs/working-tree-mirror.cjs +0 -1872
- package/dist/cjs/workspace-automatic-sync-policy.cjs +0 -79
- package/dist/cjs/workspace-branch-incarnation-policy.cjs +0 -37
- package/dist/cjs/workspace-command-sync-policy.cjs +0 -112
- package/dist/cjs/workspace-command-targets.cjs +0 -63
- package/dist/cjs/workspace-filesystem-executor-thread.cjs +0 -36
- package/dist/cjs/workspace-filesystem-executor.cjs +0 -327
- package/dist/cjs/workspace-filesystem-job-types.cjs +0 -144
- package/dist/cjs/workspace-filesystem-jobs.cjs +0 -59
- package/dist/cjs/workspace-git-sync.cjs +0 -3595
- package/dist/cjs/workspace-hydration-ledger.cjs +0 -83
- package/dist/cjs/workspace-hydration-merge.cjs +0 -448
- package/dist/cjs/workspace-hydration-recovery-state.cjs +0 -53
- package/dist/cjs/workspace-incident-state.cjs +0 -37
- package/dist/cjs/workspace-merge-projection.cjs +0 -628
- package/dist/cjs/workspace-mount-boundary.cjs +0 -71
- package/dist/cjs/workspace-mount-hold-fence.cjs +0 -120
- package/dist/cjs/workspace-mutation-gate.cjs +0 -127
- package/dist/cjs/workspace-path-move.cjs +0 -195
- package/dist/cjs/workspace-preserve-only-policy.cjs +0 -36
- package/dist/cjs/workspace-project-config-policy.cjs +0 -53
- package/dist/cjs/workspace-projection-ledger.cjs +0 -81
- package/dist/cjs/workspace-publication-evidence.cjs +0 -46
- package/dist/cjs/workspace-sync-coalescer.cjs +0 -58
- package/dist/mjs/atomic-rename.mjs +0 -261
- package/dist/mjs/browser-upload.mjs +0 -42
- package/dist/mjs/command-launcher.mjs +0 -393
- package/dist/mjs/control-command-policy.mjs +0 -146
- package/dist/mjs/git-blob-hash.mjs +0 -16
- package/dist/mjs/git-identity.mjs +0 -15
- package/dist/mjs/git-process-environment.mjs +0 -105
- package/dist/mjs/inline-output.mjs +0 -37
- package/dist/mjs/managed-paths.mjs +0 -174
- package/dist/mjs/plan-lint.mjs +0 -129
- package/dist/mjs/plan-parser.mjs +0 -94
- package/dist/mjs/plan-paths.mjs +0 -25
- package/dist/mjs/port-forward-client.mjs +0 -172
- package/dist/mjs/project-checkout-garbage.mjs +0 -178
- package/dist/mjs/project-mirror-fetch-policy.mjs +0 -56
- package/dist/mjs/project-mirror-refs-token.mjs +0 -53
- package/dist/mjs/project-workspace-state.mjs +0 -937
- package/dist/mjs/project-worktrees.mjs +0 -1657
- package/dist/mjs/pty-output-coalescer.mjs +0 -61
- package/dist/mjs/recovery-journal-protocol.mjs +0 -30
- package/dist/mjs/recovery-journal-runtime.mjs +0 -112
- package/dist/mjs/recovery-journal-thread.mjs +0 -8
- package/dist/mjs/recovery-journal.mjs +0 -694
- package/dist/mjs/recovery-output-window.mjs +0 -53
- package/dist/mjs/recovery-protocol.mjs +0 -6
- package/dist/mjs/recovery-store.mjs +0 -217
- package/dist/mjs/registry-auth.mjs +0 -269
- package/dist/mjs/repository-transition-policy.mjs +0 -22
- package/dist/mjs/runtime-version.mjs +0 -54
- package/dist/mjs/session-file-mutations.mjs +0 -37
- package/dist/mjs/supervisor.mjs +0 -114
- package/dist/mjs/three-way-merge.mjs +0 -318
- package/dist/mjs/working-tree-mirror.mjs +0 -1817
- package/dist/mjs/workspace-automatic-sync-policy.mjs +0 -48
- package/dist/mjs/workspace-branch-incarnation-policy.mjs +0 -13
- package/dist/mjs/workspace-command-sync-policy.mjs +0 -84
- package/dist/mjs/workspace-command-targets.mjs +0 -37
- package/dist/mjs/workspace-filesystem-executor-thread.mjs +0 -38
- package/dist/mjs/workspace-filesystem-executor.mjs +0 -287
- package/dist/mjs/workspace-filesystem-job-types.mjs +0 -116
- package/dist/mjs/workspace-filesystem-jobs.mjs +0 -55
- package/dist/mjs/workspace-git-sync.mjs +0 -3553
- package/dist/mjs/workspace-hydration-ledger.mjs +0 -58
- package/dist/mjs/workspace-hydration-merge.mjs +0 -414
- package/dist/mjs/workspace-hydration-recovery-state.mjs +0 -29
- package/dist/mjs/workspace-incident-state.mjs +0 -12
- package/dist/mjs/workspace-merge-projection.mjs +0 -594
- package/dist/mjs/workspace-mount-boundary.mjs +0 -37
- package/dist/mjs/workspace-mount-hold-fence.mjs +0 -95
- package/dist/mjs/workspace-mutation-gate.mjs +0 -103
- package/dist/mjs/workspace-path-move.mjs +0 -160
- package/dist/mjs/workspace-preserve-only-policy.mjs +0 -11
- package/dist/mjs/workspace-project-config-policy.mjs +0 -27
- package/dist/mjs/workspace-projection-ledger.mjs +0 -57
- package/dist/mjs/workspace-publication-evidence.mjs +0 -21
- package/dist/mjs/workspace-sync-coalescer.mjs +0 -34
- package/dist/types/atomic-rename.d.ts +0 -78
- package/dist/types/browser-upload.d.ts +0 -7
- package/dist/types/command-launcher.d.ts +0 -104
- package/dist/types/control-command-policy.d.ts +0 -53
- package/dist/types/git-blob-hash.d.ts +0 -10
- package/dist/types/git-identity.d.ts +0 -9
- package/dist/types/git-process-environment.d.ts +0 -9
- package/dist/types/inline-output.d.ts +0 -3
- package/dist/types/managed-paths.d.ts +0 -19
- package/dist/types/plan-lint.d.ts +0 -16
- package/dist/types/plan-parser.d.ts +0 -38
- package/dist/types/plan-paths.d.ts +0 -36
- package/dist/types/port-forward-client.d.ts +0 -8
- package/dist/types/project-checkout-garbage.d.ts +0 -86
- package/dist/types/project-mirror-fetch-policy.d.ts +0 -84
- package/dist/types/project-mirror-refs-token.d.ts +0 -53
- package/dist/types/project-workspace-state.d.ts +0 -188
- package/dist/types/project-worktrees.d.ts +0 -397
- package/dist/types/pty-output-coalescer.d.ts +0 -20
- package/dist/types/recovery-journal-protocol.d.ts +0 -35
- package/dist/types/recovery-journal-runtime.d.ts +0 -12
- package/dist/types/recovery-journal-stall-fixture.d.ts +0 -1
- package/dist/types/recovery-journal-thread.d.ts +0 -1
- package/dist/types/recovery-journal.d.ts +0 -248
- package/dist/types/recovery-output-window.d.ts +0 -19
- package/dist/types/recovery-protocol.d.ts +0 -59
- package/dist/types/recovery-store.d.ts +0 -52
- package/dist/types/registry-auth.d.ts +0 -47
- package/dist/types/repository-transition-policy.d.ts +0 -26
- package/dist/types/runtime-version.d.ts +0 -1
- package/dist/types/session-file-mutations.d.ts +0 -22
- package/dist/types/supervisor-daemonized-fixture.d.ts +0 -1
- package/dist/types/supervisor-launcher-cleanup-fixture.d.ts +0 -1
- package/dist/types/supervisor-signal-fixture.d.ts +0 -1
- package/dist/types/supervisor.d.ts +0 -23
- package/dist/types/three-way-merge.d.ts +0 -77
- package/dist/types/working-tree-mirror.d.ts +0 -424
- package/dist/types/workspace-automatic-sync-policy.d.ts +0 -57
- package/dist/types/workspace-branch-incarnation-policy.d.ts +0 -14
- package/dist/types/workspace-command-sync-policy.d.ts +0 -57
- package/dist/types/workspace-command-targets.d.ts +0 -37
- package/dist/types/workspace-filesystem-executor-thread.d.ts +0 -1
- package/dist/types/workspace-filesystem-executor.d.ts +0 -123
- package/dist/types/workspace-filesystem-job-types.d.ts +0 -302
- package/dist/types/workspace-filesystem-jobs.d.ts +0 -7
- package/dist/types/workspace-git-sync.d.ts +0 -514
- package/dist/types/workspace-hydration-ledger.d.ts +0 -76
- package/dist/types/workspace-hydration-merge.d.ts +0 -103
- package/dist/types/workspace-hydration-recovery-state.d.ts +0 -10
- package/dist/types/workspace-incident-state.d.ts +0 -19
- package/dist/types/workspace-merge-projection.d.ts +0 -69
- package/dist/types/workspace-mount-boundary.d.ts +0 -10
- package/dist/types/workspace-mount-hold-fence.d.ts +0 -42
- package/dist/types/workspace-mutation-gate.d.ts +0 -42
- package/dist/types/workspace-path-move.d.ts +0 -21
- package/dist/types/workspace-preserve-only-policy.d.ts +0 -15
- package/dist/types/workspace-project-config-policy.d.ts +0 -48
- package/dist/types/workspace-projection-ledger.d.ts +0 -59
- package/dist/types/workspace-publication-evidence.d.ts +0 -17
- package/dist/types/workspace-sync-coalescer.d.ts +0 -28
- /package/dist/types/{credential-authority-lock-fixture.d.ts → runtime/releases/main.d.ts} +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { RuntimeId } from "@ricsam/r5d-api/runtime-protocol";
|
|
4
|
+
import { BranchName, StorageId } from "./storage-wire.mjs";
|
|
5
|
+
const ApprovedWorkbench = z.object({
|
|
6
|
+
id: StorageId,
|
|
7
|
+
userId: RuntimeId,
|
|
8
|
+
sessionId: RuntimeId,
|
|
9
|
+
sharedSessionId: RuntimeId.optional(),
|
|
10
|
+
repositoryId: StorageId,
|
|
11
|
+
branch: BranchName,
|
|
12
|
+
cwd: z.string().refine(path.isAbsolute),
|
|
13
|
+
// Supervisor-owned exact argv approvals, NOT an agent-supplied read-only flag.
|
|
14
|
+
// Approved servers must keep generated/cache files ignored and never write source.
|
|
15
|
+
nonmutatingArgv: z.array(z.array(z.string()).min(1)).max(20).default([])
|
|
16
|
+
}).strict();
|
|
17
|
+
const WorkspaceConfig = z.object({
|
|
18
|
+
installationId: RuntimeId,
|
|
19
|
+
root: z.string().refine(path.isAbsolute),
|
|
20
|
+
workbenches: z.array(ApprovedWorkbench).max(100),
|
|
21
|
+
dynamicWorkbenches: z.boolean().optional()
|
|
22
|
+
}).strict();
|
|
23
|
+
class WorkspaceError extends Error {
|
|
24
|
+
constructor(code, message, rejectedBeforeAdmission = false) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.code = code;
|
|
27
|
+
this.rejectedBeforeAdmission = rejectedBeforeAdmission;
|
|
28
|
+
this.name = "WorkspaceError";
|
|
29
|
+
}
|
|
30
|
+
code;
|
|
31
|
+
rejectedBeforeAdmission;
|
|
32
|
+
}
|
|
33
|
+
class WorkspacePublicationNotAdmitted extends WorkspaceError {
|
|
34
|
+
}
|
|
35
|
+
const DEFAULT_WORKSPACE_IGNORE = `# Required destination workbench exclusions; keep these rules effective.
|
|
36
|
+
.env
|
|
37
|
+
.env.*
|
|
38
|
+
!.env.example
|
|
39
|
+
node_modules/
|
|
40
|
+
.r5d/
|
|
41
|
+
.r5d-next/
|
|
42
|
+
.ssh/
|
|
43
|
+
.kube/
|
|
44
|
+
secrets/
|
|
45
|
+
credentials/
|
|
46
|
+
.next/
|
|
47
|
+
dist/
|
|
48
|
+
build/
|
|
49
|
+
coverage/
|
|
50
|
+
.cache/
|
|
51
|
+
*.pem
|
|
52
|
+
*.key
|
|
53
|
+
`;
|
|
54
|
+
export {
|
|
55
|
+
ApprovedWorkbench,
|
|
56
|
+
DEFAULT_WORKSPACE_IGNORE,
|
|
57
|
+
WorkspaceConfig,
|
|
58
|
+
WorkspaceError,
|
|
59
|
+
WorkspacePublicationNotAdmitted
|
|
60
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { RuntimeId } from "@ricsam/r5d-api/runtime-protocol";
|
|
3
|
+
import { safeTreePath } from "./storage-wire.mjs";
|
|
4
|
+
const WorkspaceFileWrite = z.object({
|
|
5
|
+
method: z.literal("fileWrite"),
|
|
6
|
+
id: RuntimeId,
|
|
7
|
+
path: z.string().min(1).max(4096).refine((value) => {
|
|
8
|
+
try {
|
|
9
|
+
safeTreePath(value);
|
|
10
|
+
return true;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}),
|
|
15
|
+
expectedSha256: z.string().regex(/^[a-f0-9]{64}$/).nullable(),
|
|
16
|
+
intentHash: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
17
|
+
base64: z.string().max(1048576)
|
|
18
|
+
}).strict();
|
|
19
|
+
const WorkspaceFileWriteLookup = z.object({ method: z.literal("fileWriteResult"), id: RuntimeId }).strict();
|
|
20
|
+
export {
|
|
21
|
+
WorkspaceFileWrite,
|
|
22
|
+
WorkspaceFileWriteLookup
|
|
23
|
+
};
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { safeTreePath, STORAGE_LIMITS } from "./storage-wire.mjs";
|
|
7
|
+
import { WorkspaceError } from "./contracts.mjs";
|
|
8
|
+
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
9
|
+
async function privateRoot(root, installationId) {
|
|
10
|
+
if (!path.isAbsolute(root) || path.normalize(root) !== root || path.basename(root) !== installationId || root.split(path.sep).some((p) => [".r5d", "r5d-dev", "legacy", "app-data"].includes(p.toLowerCase())))
|
|
11
|
+
throw new WorkspaceError("unsafe_root", "Use an independently allocated private new-installation root, never a legacy worktree");
|
|
12
|
+
await noSymlinkAncestors(root);
|
|
13
|
+
const st = await fs.lstat(root);
|
|
14
|
+
if (!st.isDirectory() || st.uid !== process.getuid?.() || st.mode & 63)
|
|
15
|
+
throw new WorkspaceError("unsafe_root", "Root must already exist, be owned by current UID and mode 0700");
|
|
16
|
+
}
|
|
17
|
+
async function noSymlinkAncestors(file) {
|
|
18
|
+
let current = path.parse(file).root;
|
|
19
|
+
for (const part of file.slice(current.length).split(path.sep).filter(Boolean)) {
|
|
20
|
+
current = path.join(current, part);
|
|
21
|
+
if ((await fs.lstat(current)).isSymbolicLink()) throw new WorkspaceError("unsafe_path", "Symlink path component rejected");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function readRegular(file, limit = 32 * 1024 * 1024) {
|
|
25
|
+
await noSymlinkAncestors(file);
|
|
26
|
+
const handle = await fs.open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
27
|
+
try {
|
|
28
|
+
const st = await handle.stat();
|
|
29
|
+
if (!st.isFile() || st.nlink !== 1 || st.size > limit)
|
|
30
|
+
throw new WorkspaceError("unsafe_file", "Only bounded single-link regular files accepted");
|
|
31
|
+
const bytes = await handle.readFile();
|
|
32
|
+
if (bytes.length > limit) throw new WorkspaceError("unsafe_file", "File exceeded budget during read");
|
|
33
|
+
return bytes;
|
|
34
|
+
} finally {
|
|
35
|
+
await handle.close();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function durableJson(file, value) {
|
|
39
|
+
const temporary = `${file}.next`;
|
|
40
|
+
const handle = await fs.open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
|
|
41
|
+
try {
|
|
42
|
+
await handle.writeFile(JSON.stringify(value));
|
|
43
|
+
await handle.sync();
|
|
44
|
+
} finally {
|
|
45
|
+
await handle.close();
|
|
46
|
+
}
|
|
47
|
+
await fs.rename(temporary, file);
|
|
48
|
+
const dir = await fs.open(path.dirname(file), constants.O_RDONLY);
|
|
49
|
+
try {
|
|
50
|
+
await dir.sync();
|
|
51
|
+
} finally {
|
|
52
|
+
await dir.close();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function git(cwd, args, input, indexFile, transport) {
|
|
56
|
+
if (indexFile && path.dirname(indexFile) !== cwd) throw new WorkspaceError("unsafe_path", "Index override must be authority-owned");
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const child = spawn(
|
|
59
|
+
"/usr/bin/git",
|
|
60
|
+
[
|
|
61
|
+
"-c",
|
|
62
|
+
"core.hooksPath=/dev/null",
|
|
63
|
+
"-c",
|
|
64
|
+
"core.attributesFile=/dev/null",
|
|
65
|
+
"-c",
|
|
66
|
+
"core.fsmonitor=false",
|
|
67
|
+
"-c",
|
|
68
|
+
"protocol.allow=never",
|
|
69
|
+
"-c",
|
|
70
|
+
"gc.auto=0",
|
|
71
|
+
"-c",
|
|
72
|
+
"maintenance.auto=false",
|
|
73
|
+
"-c",
|
|
74
|
+
"pack.threads=1",
|
|
75
|
+
"-c",
|
|
76
|
+
"pack.windowMemory=16m",
|
|
77
|
+
...args
|
|
78
|
+
],
|
|
79
|
+
{
|
|
80
|
+
cwd,
|
|
81
|
+
env: {
|
|
82
|
+
PATH: "/usr/bin:/bin",
|
|
83
|
+
HOME: cwd,
|
|
84
|
+
LANG: "C",
|
|
85
|
+
LC_ALL: "C",
|
|
86
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
87
|
+
GIT_CONFIG_SYSTEM: "/dev/null",
|
|
88
|
+
GIT_CONFIG_GLOBAL: "/dev/null",
|
|
89
|
+
GIT_ATTR_NOSYSTEM: "1",
|
|
90
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
91
|
+
GIT_AUTHOR_NAME: "Destination Workspace",
|
|
92
|
+
GIT_AUTHOR_EMAIL: "workspace@invalid",
|
|
93
|
+
GIT_COMMITTER_NAME: "Destination Workspace",
|
|
94
|
+
GIT_COMMITTER_EMAIL: "workspace@invalid",
|
|
95
|
+
// Only the reviewed GitHub import/push boundary supplies this option.
|
|
96
|
+
// Tokens are process-private environment values, never argv, URLs or diagnostics.
|
|
97
|
+
...transport ? { GIT_CONFIG_COUNT: "3", GIT_CONFIG_KEY_0: "http.https://github.com/.extraheader", GIT_CONFIG_VALUE_0: `AUTHORIZATION: basic ${Buffer.from(`x-access-token:${transport.token}`).toString("base64")}`, GIT_CONFIG_KEY_1: "http.followRedirects", GIT_CONFIG_VALUE_1: "false", GIT_CONFIG_KEY_2: "credential.helper", GIT_CONFIG_VALUE_2: "" } : {},
|
|
98
|
+
...indexFile ? { GIT_INDEX_FILE: indexFile } : {}
|
|
99
|
+
},
|
|
100
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
let bytes = 0, failed = false;
|
|
104
|
+
const output = [];
|
|
105
|
+
const timer = setTimeout(() => {
|
|
106
|
+
failed = true;
|
|
107
|
+
child.kill("SIGKILL");
|
|
108
|
+
}, 3e4);
|
|
109
|
+
child.stdout.on("data", (b) => {
|
|
110
|
+
bytes += b.length;
|
|
111
|
+
if (bytes > 32 * 1024 * 1024) {
|
|
112
|
+
failed = true;
|
|
113
|
+
child.kill("SIGKILL");
|
|
114
|
+
} else output.push(b);
|
|
115
|
+
});
|
|
116
|
+
child.stderr.on("data", (b) => {
|
|
117
|
+
bytes += b.length;
|
|
118
|
+
if (bytes > 32 * 1024 * 1024) {
|
|
119
|
+
failed = true;
|
|
120
|
+
child.kill("SIGKILL");
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
child.on("error", (error) => {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
reject(error);
|
|
126
|
+
});
|
|
127
|
+
child.on("close", (code) => {
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
if (failed || code !== 0)
|
|
130
|
+
reject(new WorkspaceError("git_failed", "Bounded private Git operation failed; preserve state for inspection"));
|
|
131
|
+
else resolve(Buffer.concat(output));
|
|
132
|
+
});
|
|
133
|
+
child.stdin.on("error", () => {
|
|
134
|
+
});
|
|
135
|
+
child.stdin.end(input);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const denied = /* @__PURE__ */ new Set([
|
|
139
|
+
".r5d",
|
|
140
|
+
".r5d-next",
|
|
141
|
+
".ssh",
|
|
142
|
+
".kube",
|
|
143
|
+
".aws",
|
|
144
|
+
".config",
|
|
145
|
+
"secrets",
|
|
146
|
+
"credentials",
|
|
147
|
+
"node_modules",
|
|
148
|
+
".next",
|
|
149
|
+
"dist",
|
|
150
|
+
"coverage",
|
|
151
|
+
".cache",
|
|
152
|
+
".pypirc",
|
|
153
|
+
".netrc",
|
|
154
|
+
".git-credentials"
|
|
155
|
+
]);
|
|
156
|
+
function sourcePath(file) {
|
|
157
|
+
safeTreePath(file);
|
|
158
|
+
if (file.split("/").some(
|
|
159
|
+
(p) => denied.has(p.toLowerCase()) || /^\.env(?:\.|$)/i.test(p) && p !== ".env.example" || /\.(pem|key|p12|pfx)$/i.test(p) || /^(id_rsa|id_ed25519|kubeconfig)$/i.test(p)
|
|
160
|
+
))
|
|
161
|
+
throw new WorkspaceError("secret_or_runtime_path", `Refusing secret/runtime source path: ${file}`);
|
|
162
|
+
}
|
|
163
|
+
function sourceBytes(bytes, file) {
|
|
164
|
+
const name = file?.split("/").at(-1)?.toLowerCase();
|
|
165
|
+
if (name && /\.(sqlite|sqlite3|db)$/.test(name) && bytes.length !== 0)
|
|
166
|
+
throw new WorkspaceError("secret_content", "Only empty database placeholders may be published as source");
|
|
167
|
+
if (name === "credentials.json") {
|
|
168
|
+
const empty = (value) => typeof value === "string" ? value === "" : Array.isArray(value) ? value.every(empty) : !!value && typeof value === "object" && Object.values(value).every(empty);
|
|
169
|
+
let valid = false;
|
|
170
|
+
try {
|
|
171
|
+
valid = empty(JSON.parse(bytes.toString("utf8")));
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
if (!valid) throw new WorkspaceError("secret_content", "Credential source templates must contain only empty placeholders");
|
|
175
|
+
}
|
|
176
|
+
if (name === ".npmrc") {
|
|
177
|
+
for (const raw of bytes.toString("utf8").split(/\r?\n/)) {
|
|
178
|
+
const line = raw.trim();
|
|
179
|
+
if (!line || /^[#;]/.test(line)) continue;
|
|
180
|
+
const equal = line.indexOf("=");
|
|
181
|
+
const key = (equal < 0 ? line : line.slice(0, equal)).trim();
|
|
182
|
+
if (!/auth|password|token|secret/i.test(key) || /^always-auth$/i.test(key)) continue;
|
|
183
|
+
const value = (equal < 0 ? "" : line.slice(equal + 1)).trim().replace(/^(?:"(.*)"|'(.*)')$/, (_match, a, b) => a ?? b);
|
|
184
|
+
if (equal < 0 || !(value === "" || /^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(value))) throw new WorkspaceError("secret_content", "npm authentication source must reference an environment variable");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const syntheticFixture = JSON.stringify("-----BEGIN OPENSSH PRIVATE KEY-----\nsynthetic\n");
|
|
188
|
+
const keyContent = bytes.toString("utf8").replaceAll(syntheticFixture, "");
|
|
189
|
+
if (/-----BEGIN (?:[A-Z ]*PRIVATE KEY|OPENSSH PRIVATE KEY)-----/.test(keyContent))
|
|
190
|
+
throw new WorkspaceError("secret_content", "Private key material cannot be published");
|
|
191
|
+
}
|
|
192
|
+
async function selectedTree(repo, commit) {
|
|
193
|
+
if (!/^[0-9a-f]{40}$/.test(commit) || (await git(repo, ["cat-file", "-t", commit])).toString().trim() !== "commit")
|
|
194
|
+
throw new WorkspaceError("invalid_tip", "Selected tip must be a SHA-1 commit");
|
|
195
|
+
const raw = await git(repo, ["ls-tree", "-rz", "--full-tree", commit]);
|
|
196
|
+
if (!Buffer.from(raw.toString("utf8")).equals(raw)) throw new WorkspaceError("unsafe_path", "Non-UTF8 tree names unsupported");
|
|
197
|
+
const entries = raw.toString("utf8").split("\0").filter(Boolean).map((record) => {
|
|
198
|
+
const match = /^(100644|100755) blob ([0-9a-f]{40})\t(.+)$/.exec(record);
|
|
199
|
+
if (!match) throw new WorkspaceError("unsafe_tree", "Symlinks, gitlinks and special modes unsupported");
|
|
200
|
+
sourcePath(match[3]);
|
|
201
|
+
return { file: match[3], mode: match[1], oid: match[2] };
|
|
202
|
+
});
|
|
203
|
+
if (entries.length > STORAGE_LIMITS.treeEntries) throw new WorkspaceError("too_large", "Too many source entries");
|
|
204
|
+
return entries;
|
|
205
|
+
}
|
|
206
|
+
async function verifyIgnore(repo, cwd) {
|
|
207
|
+
await readRegular(path.join(cwd, ".gitignore"), 64 * 1024);
|
|
208
|
+
const probes = [
|
|
209
|
+
".env",
|
|
210
|
+
".env.local",
|
|
211
|
+
"node_modules/probe",
|
|
212
|
+
".r5d/probe",
|
|
213
|
+
".r5d-next/probe",
|
|
214
|
+
".ssh/probe",
|
|
215
|
+
".kube/probe",
|
|
216
|
+
"secrets/probe",
|
|
217
|
+
"credentials/probe",
|
|
218
|
+
".next/probe",
|
|
219
|
+
"dist/probe",
|
|
220
|
+
"coverage/probe",
|
|
221
|
+
".cache/probe",
|
|
222
|
+
"probe.pem",
|
|
223
|
+
"probe.key"
|
|
224
|
+
];
|
|
225
|
+
const ignored = (await git(repo, [`--work-tree=${cwd}`, "check-ignore", "--no-index", "-z", "--stdin"], probes.join("\0") + "\0")).toString().split("\0").filter(Boolean);
|
|
226
|
+
if (ignored.length !== probes.length || probes.some((p) => !ignored.includes(p)))
|
|
227
|
+
throw new WorkspaceError("ignore_policy", "Restore effective .gitignore rules for secrets, dependency, cache and runtime roots");
|
|
228
|
+
}
|
|
229
|
+
async function snapshotTree(repo, cwd, canonicalHead) {
|
|
230
|
+
const metadata = path.join(cwd, ".git");
|
|
231
|
+
if (await fs.lstat(metadata).then(
|
|
232
|
+
() => true,
|
|
233
|
+
(e) => {
|
|
234
|
+
if (e.code === "ENOENT") return false;
|
|
235
|
+
throw e;
|
|
236
|
+
}
|
|
237
|
+
)) {
|
|
238
|
+
await noSymlinkAncestors(metadata);
|
|
239
|
+
if (!(await fs.lstat(metadata)).isDirectory()) throw new WorkspaceError("unsafe_git", "External worktree metadata unsupported");
|
|
240
|
+
for (const name of ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD", "REBASE_HEAD", "rebase-merge", "rebase-apply", "index.lock"]) {
|
|
241
|
+
if (await fs.lstat(path.join(metadata, name)).then(
|
|
242
|
+
() => true,
|
|
243
|
+
(e) => {
|
|
244
|
+
if (e.code === "ENOENT") return false;
|
|
245
|
+
throw e;
|
|
246
|
+
}
|
|
247
|
+
))
|
|
248
|
+
throw new WorkspaceError(
|
|
249
|
+
"workbench_conflict",
|
|
250
|
+
"Git merge/rebase/index operation remains in progress; preserve and resolve it before publication"
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
const index = path.join(metadata, "index");
|
|
254
|
+
if (await fs.lstat(index).then(
|
|
255
|
+
() => true,
|
|
256
|
+
(e) => {
|
|
257
|
+
if (e.code === "ENOENT") return false;
|
|
258
|
+
throw e;
|
|
259
|
+
}
|
|
260
|
+
)) {
|
|
261
|
+
const copy = path.join(repo, "inspection-index");
|
|
262
|
+
await fs.writeFile(copy, await readRegular(index, 8 * 1024 * 1024), { mode: 384 });
|
|
263
|
+
if ((await git(repo, ["ls-files", "--unmerged", "-z"], void 0, copy)).length)
|
|
264
|
+
throw new WorkspaceError("workbench_conflict", "Unmerged index entries retained; resolve explicitly before publication");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
await verifyIgnore(repo, cwd);
|
|
268
|
+
await git(repo, ["read-tree", "--empty"]);
|
|
269
|
+
const listing = await git(repo, [`--work-tree=${cwd}`, "ls-files", "--others", "--exclude-standard", "-z"]);
|
|
270
|
+
if (!Buffer.from(listing.toString()).equals(listing)) throw new WorkspaceError("unsafe_path", "Non-UTF8 names unsupported");
|
|
271
|
+
const tracked = [];
|
|
272
|
+
if (canonicalHead) for (const entry of await selectedTree(repo, canonicalHead)) {
|
|
273
|
+
const exists = await fs.lstat(path.join(cwd, entry.file)).then(() => true, (error) => {
|
|
274
|
+
if (error.code === "ENOENT") return false;
|
|
275
|
+
throw error;
|
|
276
|
+
});
|
|
277
|
+
if (exists) tracked.push(entry.file);
|
|
278
|
+
}
|
|
279
|
+
const files = [.../* @__PURE__ */ new Set([...listing.toString().split("\0").filter(Boolean), ...tracked])].sort();
|
|
280
|
+
if (files.length > STORAGE_LIMITS.treeEntries) throw new WorkspaceError("too_large", "Too many source files");
|
|
281
|
+
let size = 0;
|
|
282
|
+
const records = [];
|
|
283
|
+
for (const file of files) {
|
|
284
|
+
sourcePath(file);
|
|
285
|
+
const bytes = await readRegular(path.join(cwd, file));
|
|
286
|
+
sourceBytes(bytes, file);
|
|
287
|
+
if ((size += bytes.length) > 128 * 1024 * 1024) throw new WorkspaceError("too_large", "Source snapshot exceeds 128 MiB");
|
|
288
|
+
const st = await fs.lstat(path.join(cwd, file));
|
|
289
|
+
const oid = (await git(repo, ["hash-object", "-w", "--stdin", "--no-filters"], bytes)).toString().trim();
|
|
290
|
+
records.push(`${st.mode & 73 ? "100755" : "100644"} ${oid} ${file}\0`);
|
|
291
|
+
}
|
|
292
|
+
if (!files.includes(".gitignore")) throw new WorkspaceError("ignore_policy", "The .gitignore policy itself must be published");
|
|
293
|
+
await git(repo, ["update-index", "-z", "--index-info"], records.join(""));
|
|
294
|
+
return (await git(repo, ["write-tree"])).toString().trim();
|
|
295
|
+
}
|
|
296
|
+
export {
|
|
297
|
+
durableJson,
|
|
298
|
+
git,
|
|
299
|
+
noSymlinkAncestors,
|
|
300
|
+
privateRoot,
|
|
301
|
+
readRegular,
|
|
302
|
+
selectedTree,
|
|
303
|
+
sha256,
|
|
304
|
+
snapshotTree,
|
|
305
|
+
sourceBytes,
|
|
306
|
+
sourcePath,
|
|
307
|
+
verifyIgnore
|
|
308
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
class BodyReadError extends Error {
|
|
2
|
+
constructor(code) {
|
|
3
|
+
super(code);
|
|
4
|
+
this.code = code;
|
|
5
|
+
}
|
|
6
|
+
code;
|
|
7
|
+
}
|
|
8
|
+
async function readBoundedBody(body, limit, timeoutMs) {
|
|
9
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1)
|
|
10
|
+
throw new Error("Invalid body budget");
|
|
11
|
+
if (!body) throw new BodyReadError("body_missing");
|
|
12
|
+
const reader = body.getReader();
|
|
13
|
+
const deadline = performance.now() + timeoutMs;
|
|
14
|
+
let timer;
|
|
15
|
+
const timeout = new Promise((_, reject) => {
|
|
16
|
+
timer = setTimeout(() => reject(new BodyReadError("body_timeout")), timeoutMs);
|
|
17
|
+
});
|
|
18
|
+
const bytes = Buffer.allocUnsafe(limit);
|
|
19
|
+
let size = 0, complete = false;
|
|
20
|
+
const reading = (async () => {
|
|
21
|
+
while (true) {
|
|
22
|
+
if (performance.now() >= deadline) throw new BodyReadError("body_timeout");
|
|
23
|
+
const next = await reader.read();
|
|
24
|
+
if (next.done) {
|
|
25
|
+
complete = true;
|
|
26
|
+
return bytes.subarray(0, size);
|
|
27
|
+
}
|
|
28
|
+
if (next.value.byteLength > limit - size) throw new BodyReadError("body_too_large");
|
|
29
|
+
bytes.set(next.value, size);
|
|
30
|
+
size += next.value.byteLength;
|
|
31
|
+
}
|
|
32
|
+
})();
|
|
33
|
+
try {
|
|
34
|
+
return await Promise.race([reading, timeout]);
|
|
35
|
+
} finally {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
if (!complete) void reader.cancel().catch(() => {
|
|
38
|
+
});
|
|
39
|
+
reader.releaseLock();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
BodyReadError,
|
|
44
|
+
readBoundedBody
|
|
45
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { OwnershipFence } from "@ricsam/r5d-api/runtime-protocol";
|
|
3
|
+
import { STORAGE_LIMITS, StorageRequest } from "./storage-wire.mjs";
|
|
4
|
+
import { WorkspaceError } from "./contracts.mjs";
|
|
5
|
+
import { readBoundedBody } from "./http-body.mjs";
|
|
6
|
+
class WorkspaceStorageClient {
|
|
7
|
+
constructor(installationId, transport, caller) {
|
|
8
|
+
this.installationId = installationId;
|
|
9
|
+
this.transport = transport;
|
|
10
|
+
this.caller = caller;
|
|
11
|
+
}
|
|
12
|
+
installationId;
|
|
13
|
+
transport;
|
|
14
|
+
caller;
|
|
15
|
+
static http(options) {
|
|
16
|
+
const url = new URL(options.url);
|
|
17
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== "/internal/storage/v1/rpc" || url.protocol !== "https:" && !(url.protocol === "http:" && ["127.0.0.1", "[::1]"].includes(url.hostname)))
|
|
18
|
+
throw new WorkspaceError("invalid_storage_url", "Use private verified HTTPS or explicit loopback storage RPC");
|
|
19
|
+
return new WorkspaceStorageClient(
|
|
20
|
+
options.installationId,
|
|
21
|
+
async (request) => {
|
|
22
|
+
const response = await (options.fetch ?? fetch)(url, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
redirect: "error",
|
|
25
|
+
signal: AbortSignal.timeout(3e4),
|
|
26
|
+
headers: { authorization: `Bearer ${await options.token(request)}`, "content-type": "application/json" },
|
|
27
|
+
body: JSON.stringify({ protocol: 1, installationId: options.installationId, request })
|
|
28
|
+
});
|
|
29
|
+
const bytes = await readBoundedBody(response.body, STORAGE_LIMITS.requestBytes, 3e4).catch(() => {
|
|
30
|
+
throw new WorkspaceError("invalid_response", "Storage response exceeded byte/time budget; inspect original receipt, no retry");
|
|
31
|
+
});
|
|
32
|
+
const body = JSON.parse(new TextDecoder().decode(bytes));
|
|
33
|
+
if (!response.ok)
|
|
34
|
+
throw new WorkspaceError(body.error ?? "storage_unavailable", "Storage rejected request; inspect original operation receipt", body.rejectedBeforeAdmission === true);
|
|
35
|
+
if (body.protocol !== 1 || !("result" in body)) throw new WorkspaceError("invalid_response", "Invalid storage envelope");
|
|
36
|
+
return body.result;
|
|
37
|
+
},
|
|
38
|
+
options.caller
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
read(request) {
|
|
42
|
+
return this.transport(StorageRequest.parse(request));
|
|
43
|
+
}
|
|
44
|
+
async mutate(input) {
|
|
45
|
+
const hello = z.object({
|
|
46
|
+
protocol: z.literal(1),
|
|
47
|
+
installationId: z.literal(this.installationId),
|
|
48
|
+
fence: OwnershipFence,
|
|
49
|
+
capabilities: z.array(z.string())
|
|
50
|
+
}).passthrough().parse(await this.read({ method: "hello" }));
|
|
51
|
+
const callerFence = OwnershipFence.parse(await this.caller());
|
|
52
|
+
if (hello.fence.installationId !== this.installationId || callerFence.installationId !== this.installationId || callerFence.resourceType === "storage-authority")
|
|
53
|
+
throw new WorkspaceError("wrong_installation", "Separate installation-bound caller ownership required");
|
|
54
|
+
return this.transport(StorageRequest.parse({ ...input, fence: hello.fence, callerFence }));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
WorkspaceStorageClient
|
|
59
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { OwnershipFence, RuntimeId } from "@ricsam/r5d-api/runtime-protocol";
|
|
3
|
+
const STORAGE_LIMITS = { chunkBytes: 64 * 1024, blobBytes: 128 * 1024 * 1024, requestBytes: 96 * 1024, treeEntries: 1e4 };
|
|
4
|
+
const StorageId = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,99}$/);
|
|
5
|
+
const GitOid = z.string().regex(/^[0-9a-f]{40}$/);
|
|
6
|
+
const BranchName = z.string().min(1).max(150).refine((value) => value.split("/").every((part) => /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(part)));
|
|
7
|
+
const mutation = { operationId: RuntimeId, fence: OwnershipFence, callerFence: OwnershipFence };
|
|
8
|
+
const Offset = z.number().int().min(0).max(STORAGE_LIMITS.blobBytes);
|
|
9
|
+
const StorageRequest = z.discriminatedUnion("method", [
|
|
10
|
+
z.object({ method: z.literal("hello") }).strict(),
|
|
11
|
+
z.object({ method: z.literal("repository.get"), repositoryId: StorageId, branch: BranchName }).strict(),
|
|
12
|
+
z.object({
|
|
13
|
+
method: z.literal("repository.bundle"),
|
|
14
|
+
repositoryId: StorageId,
|
|
15
|
+
snapshot: z.string().uuid().nullable(),
|
|
16
|
+
offset: Offset,
|
|
17
|
+
limit: z.number().int().min(1).max(STORAGE_LIMITS.chunkBytes)
|
|
18
|
+
}).strict(),
|
|
19
|
+
z.object({ method: z.literal("operation.get"), lookupId: RuntimeId }).strict(),
|
|
20
|
+
z.object({ method: z.literal("repository.provision"), ...mutation, repositoryId: StorageId }).strict(),
|
|
21
|
+
z.object({ method: z.literal("repository.initializeAccount"), ...mutation, repositoryId: StorageId }).strict(),
|
|
22
|
+
z.object({ method: z.literal("repository.archive"), ...mutation, repositoryId: StorageId, branch: BranchName, expectedHead: GitOid.nullable() }).strict(),
|
|
23
|
+
z.object({
|
|
24
|
+
method: z.literal("repository.publish"),
|
|
25
|
+
...mutation,
|
|
26
|
+
repositoryId: StorageId,
|
|
27
|
+
branch: BranchName,
|
|
28
|
+
expectedHead: GitOid.nullable(),
|
|
29
|
+
commit: GitOid,
|
|
30
|
+
bundleId: StorageId
|
|
31
|
+
}).strict(),
|
|
32
|
+
z.object({
|
|
33
|
+
method: z.literal("blob.append"),
|
|
34
|
+
...mutation,
|
|
35
|
+
blobId: StorageId,
|
|
36
|
+
kind: z.enum(["blob", "artifact", "log"]),
|
|
37
|
+
offset: Offset,
|
|
38
|
+
data: z.string().max(Math.ceil(STORAGE_LIMITS.chunkBytes / 3) * 4),
|
|
39
|
+
seal: z.boolean()
|
|
40
|
+
}).strict()
|
|
41
|
+
]);
|
|
42
|
+
function safeTreePath(value) {
|
|
43
|
+
if (!value || value.length > 4096 || value.includes("\\") || /[\x00-\x1f\x7f]/.test(value) || value.split("/").some((part) => !part || part === "." || part === ".." || part.toLowerCase() === ".git"))
|
|
44
|
+
throw Object.assign(new Error("Unsafe Git tree path"), { code: "unsafe_path" });
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
BranchName,
|
|
48
|
+
GitOid,
|
|
49
|
+
STORAGE_LIMITS,
|
|
50
|
+
StorageId,
|
|
51
|
+
StorageRequest,
|
|
52
|
+
safeTreePath
|
|
53
|
+
};
|