@oscharko-dev/keiko-server 0.2.8 → 0.2.9
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/.tsbuildinfo +1 -1
- package/dist/chat-handlers.d.ts +18 -2
- package/dist/chat-handlers.d.ts.map +1 -1
- package/dist/chat-handlers.js +185 -3
- package/dist/command-runner-errors.d.ts +17 -0
- package/dist/command-runner-errors.d.ts.map +1 -0
- package/dist/command-runner-errors.js +37 -0
- package/dist/command-runner-evidence.d.ts +23 -0
- package/dist/command-runner-evidence.d.ts.map +1 -0
- package/dist/command-runner-evidence.js +69 -0
- package/dist/command-runner-routes.d.ts +7 -0
- package/dist/command-runner-routes.d.ts.map +1 -0
- package/dist/command-runner-routes.js +175 -0
- package/dist/command-runner.d.ts +29 -0
- package/dist/command-runner.d.ts.map +1 -0
- package/dist/command-runner.js +348 -0
- package/dist/conversation-prompt.d.ts +2 -2
- package/dist/conversation-prompt.d.ts.map +1 -1
- package/dist/conversation-prompt.js +17 -1
- package/dist/csp.d.ts.map +1 -1
- package/dist/csp.js +3 -0
- package/dist/deps.d.ts +27 -1
- package/dist/deps.d.ts.map +1 -1
- package/dist/deps.js +288 -13
- package/dist/discussion-prompt.d.ts +4 -0
- package/dist/discussion-prompt.d.ts.map +1 -0
- package/dist/discussion-prompt.js +19 -0
- package/dist/editor/agentActionAudit.d.ts +18 -0
- package/dist/editor/agentActionAudit.d.ts.map +1 -0
- package/dist/editor/agentActionAudit.js +80 -0
- package/dist/editor/agentRoutes.d.ts +1 -0
- package/dist/editor/agentRoutes.d.ts.map +1 -1
- package/dist/editor/agentRoutes.js +292 -55
- package/dist/editor/agentSessionRegistry.d.ts +35 -0
- package/dist/editor/agentSessionRegistry.d.ts.map +1 -0
- package/dist/editor/agentSessionRegistry.js +243 -0
- package/dist/editor/completionRoutes.d.ts.map +1 -1
- package/dist/editor/completionRoutes.js +5 -10
- package/dist/editor/languageRoutes.d.ts +12 -1
- package/dist/editor/languageRoutes.d.ts.map +1 -1
- package/dist/editor/languageRoutes.js +71 -8
- package/dist/editor/languageService.d.ts +3 -2
- package/dist/editor/languageService.d.ts.map +1 -1
- package/dist/editor/languageService.js +41 -3
- package/dist/editor/languageServiceHost.d.ts.map +1 -1
- package/dist/editor/languageServiceHost.js +2 -2
- package/dist/editor/lsp/hostLanguageOperation.d.ts +17 -0
- package/dist/editor/lsp/hostLanguageOperation.d.ts.map +1 -0
- package/dist/editor/lsp/hostLanguageOperation.js +436 -0
- package/dist/editor/lsp/hostLanguageProviders.d.ts +26 -0
- package/dist/editor/lsp/hostLanguageProviders.d.ts.map +1 -0
- package/dist/editor/lsp/hostLanguageProviders.js +161 -0
- package/dist/editor/lsp/lspFrameCodec.d.ts +13 -0
- package/dist/editor/lsp/lspFrameCodec.d.ts.map +1 -0
- package/dist/editor/lsp/lspFrameCodec.js +164 -0
- package/dist/editor/lsp/lspJsonRpcClient.d.ts +34 -0
- package/dist/editor/lsp/lspJsonRpcClient.d.ts.map +1 -0
- package/dist/editor/lsp/lspJsonRpcClient.js +173 -0
- package/dist/editor/lsp/lspLanguageProvider.d.ts +7 -0
- package/dist/editor/lsp/lspLanguageProvider.d.ts.map +1 -0
- package/dist/editor/lsp/lspLanguageProvider.js +29 -0
- package/dist/editor/lsp/lspLifecycleLedger.d.ts +5 -0
- package/dist/editor/lsp/lspLifecycleLedger.d.ts.map +1 -0
- package/dist/editor/lsp/lspLifecycleLedger.js +37 -0
- package/dist/editor/lsp/lspNodeAdapter.d.ts +31 -0
- package/dist/editor/lsp/lspNodeAdapter.d.ts.map +1 -0
- package/dist/editor/lsp/lspNodeAdapter.js +230 -0
- package/dist/editor/lsp/lspProcessManager.d.ts +24 -0
- package/dist/editor/lsp/lspProcessManager.d.ts.map +1 -0
- package/dist/editor/lsp/lspProcessManager.js +255 -0
- package/dist/editor/lsp/lspRestartThrottle.d.ts +6 -0
- package/dist/editor/lsp/lspRestartThrottle.d.ts.map +1 -0
- package/dist/editor/lsp/lspRestartThrottle.js +24 -0
- package/dist/editor/lsp/lspStatusRoute.d.ts +8 -0
- package/dist/editor/lsp/lspStatusRoute.d.ts.map +1 -0
- package/dist/editor/lsp/lspStatusRoute.js +22 -0
- package/dist/editor/lsp/lspTransport.d.ts +19 -0
- package/dist/editor/lsp/lspTransport.d.ts.map +1 -0
- package/dist/editor/lsp/lspTransport.js +55 -0
- package/dist/editor/lsp/testing/fakeLspProcess.d.ts +23 -0
- package/dist/editor/lsp/testing/fakeLspProcess.d.ts.map +1 -0
- package/dist/editor/lsp/testing/fakeLspProcess.js +132 -0
- package/dist/files.d.ts +45 -0
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +631 -7
- package/dist/gateway-readiness.js +3 -3
- package/dist/gateway-setup.d.ts +2 -0
- package/dist/gateway-setup.d.ts.map +1 -1
- package/dist/gateway-setup.js +275 -11
- package/dist/gitDelivery/actionSheetProjection.d.ts +30 -0
- package/dist/gitDelivery/actionSheetProjection.d.ts.map +1 -0
- package/dist/gitDelivery/actionSheetProjection.js +206 -0
- package/dist/gitDelivery/actionSheetRoutes.d.ts +29 -0
- package/dist/gitDelivery/actionSheetRoutes.d.ts.map +1 -0
- package/dist/gitDelivery/actionSheetRoutes.js +293 -0
- package/dist/gitDelivery/agentOperationsRoutes.d.ts +33 -0
- package/dist/gitDelivery/agentOperationsRoutes.d.ts.map +1 -0
- package/dist/gitDelivery/agentOperationsRoutes.js +405 -0
- package/dist/gitDelivery/commitRoutes.d.ts +23 -0
- package/dist/gitDelivery/commitRoutes.d.ts.map +1 -0
- package/dist/gitDelivery/commitRoutes.js +204 -0
- package/dist/gitDelivery/evidenceRoutes.d.ts +9 -0
- package/dist/gitDelivery/evidenceRoutes.d.ts.map +1 -0
- package/dist/gitDelivery/evidenceRoutes.js +101 -0
- package/dist/gitDelivery/execution.d.ts +38 -0
- package/dist/gitDelivery/execution.d.ts.map +1 -0
- package/dist/gitDelivery/execution.js +117 -0
- package/dist/gitDelivery/localMutationRoutes.d.ts +30 -0
- package/dist/gitDelivery/localMutationRoutes.d.ts.map +1 -0
- package/dist/gitDelivery/localMutationRoutes.js +165 -0
- package/dist/gitDelivery/mergeExecution.d.ts +63 -0
- package/dist/gitDelivery/mergeExecution.d.ts.map +1 -0
- package/dist/gitDelivery/mergeExecution.js +168 -0
- package/dist/gitDelivery/mergeRoutes.d.ts +12 -0
- package/dist/gitDelivery/mergeRoutes.d.ts.map +1 -0
- package/dist/gitDelivery/mergeRoutes.js +218 -0
- package/dist/gitDelivery/mutationEvidenceLedger.d.ts +23 -0
- package/dist/gitDelivery/mutationEvidenceLedger.d.ts.map +1 -0
- package/dist/gitDelivery/mutationEvidenceLedger.js +87 -0
- package/dist/gitDelivery/prExecution.d.ts +54 -0
- package/dist/gitDelivery/prExecution.d.ts.map +1 -0
- package/dist/gitDelivery/prExecution.js +192 -0
- package/dist/gitDelivery/prRoutes.d.ts +12 -0
- package/dist/gitDelivery/prRoutes.d.ts.map +1 -0
- package/dist/gitDelivery/prRoutes.js +256 -0
- package/dist/gitDelivery/pushExecution.d.ts +43 -0
- package/dist/gitDelivery/pushExecution.d.ts.map +1 -0
- package/dist/gitDelivery/pushExecution.js +124 -0
- package/dist/gitDelivery/pushRoutes.d.ts +12 -0
- package/dist/gitDelivery/pushRoutes.d.ts.map +1 -0
- package/dist/gitDelivery/pushRoutes.js +200 -0
- package/dist/gitDelivery/requestGuards.d.ts +15 -0
- package/dist/gitDelivery/requestGuards.d.ts.map +1 -0
- package/dist/gitDelivery/requestGuards.js +97 -0
- package/dist/gitDelivery/syncEvidence.d.ts +37 -0
- package/dist/gitDelivery/syncEvidence.d.ts.map +1 -0
- package/dist/gitDelivery/syncEvidence.js +85 -0
- package/dist/gitDelivery/syncExecution.d.ts +30 -0
- package/dist/gitDelivery/syncExecution.d.ts.map +1 -0
- package/dist/gitDelivery/syncExecution.js +266 -0
- package/dist/gitDelivery/syncRoutes.d.ts +13 -0
- package/dist/gitDelivery/syncRoutes.d.ts.map +1 -0
- package/dist/gitDelivery/syncRoutes.js +200 -0
- package/dist/gitPorcelainStatus.d.ts +15 -0
- package/dist/gitPorcelainStatus.d.ts.map +1 -0
- package/dist/gitPorcelainStatus.js +104 -0
- package/dist/gitRepositoryReads.d.ts +10 -0
- package/dist/gitRepositoryReads.d.ts.map +1 -0
- package/dist/gitRepositoryReads.js +314 -0
- package/dist/gitRepositoryRoutes.d.ts +7 -0
- package/dist/gitRepositoryRoutes.d.ts.map +1 -0
- package/dist/gitRepositoryRoutes.js +221 -0
- package/dist/gitRoutes.d.ts +66 -0
- package/dist/gitRoutes.d.ts.map +1 -0
- package/dist/gitRoutes.js +543 -0
- package/dist/governed-workflow.d.ts +2 -0
- package/dist/governed-workflow.d.ts.map +1 -1
- package/dist/governed-workflow.js +4 -0
- package/dist/grounded-qa.d.ts +11 -0
- package/dist/grounded-qa.d.ts.map +1 -1
- package/dist/grounded-qa.js +13 -4
- package/dist/headers.d.ts +4 -1
- package/dist/headers.d.ts.map +1 -1
- package/dist/headers.js +11 -4
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/qualityIntelligence/figmaSnapshotRoutes.d.ts +1 -1
- package/dist/qualityIntelligence/figmaSnapshotRoutes.d.ts.map +1 -1
- package/dist/qualityIntelligence/figmaSnapshotRoutes.js +1 -1
- package/dist/read-handlers.d.ts +5 -0
- package/dist/read-handlers.d.ts.map +1 -1
- package/dist/read-handlers.js +57 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +259 -6
- package/dist/run-engine.d.ts.map +1 -1
- package/dist/run-engine.js +3 -0
- package/dist/run-handlers.d.ts.map +1 -1
- package/dist/run-handlers.js +74 -4
- package/dist/run-request.d.ts +11 -0
- package/dist/run-request.d.ts.map +1 -1
- package/dist/run-request.js +158 -10
- package/dist/runtime/capabilityDetector.d.ts +38 -0
- package/dist/runtime/capabilityDetector.d.ts.map +1 -0
- package/dist/runtime/capabilityDetector.js +443 -0
- package/dist/runtime/capabilityRoutes.d.ts +9 -0
- package/dist/runtime/capabilityRoutes.d.ts.map +1 -0
- package/dist/runtime/capabilityRoutes.js +45 -0
- package/dist/runtime/containerEngineDetector.d.ts +17 -0
- package/dist/runtime/containerEngineDetector.d.ts.map +1 -0
- package/dist/runtime/containerEngineDetector.js +222 -0
- package/dist/runtime/containerRoutes.d.ts +8 -0
- package/dist/runtime/containerRoutes.d.ts.map +1 -0
- package/dist/runtime/containerRoutes.js +207 -0
- package/dist/runtime/containerRunner-errors.d.ts +18 -0
- package/dist/runtime/containerRunner-errors.d.ts.map +1 -0
- package/dist/runtime/containerRunner-errors.js +42 -0
- package/dist/runtime/containerRunner-evidence.d.ts +24 -0
- package/dist/runtime/containerRunner-evidence.d.ts.map +1 -0
- package/dist/runtime/containerRunner-evidence.js +74 -0
- package/dist/runtime/containerRunner.d.ts +37 -0
- package/dist/runtime/containerRunner.d.ts.map +1 -0
- package/dist/runtime/containerRunner.js +443 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +24 -4
- package/dist/store/schema.d.ts +1 -1
- package/dist/store/schema.d.ts.map +1 -1
- package/dist/store/schema.js +62 -1
- package/dist/task-workspace/active-store.d.ts +21 -0
- package/dist/task-workspace/active-store.d.ts.map +1 -0
- package/dist/task-workspace/active-store.js +55 -0
- package/dist/task-workspace/authorization.d.ts +7 -0
- package/dist/task-workspace/authorization.d.ts.map +1 -0
- package/dist/task-workspace/authorization.js +54 -0
- package/dist/task-workspace/binding.d.ts +3 -0
- package/dist/task-workspace/binding.d.ts.map +1 -0
- package/dist/task-workspace/binding.js +22 -0
- package/dist/task-workspace/cleanup.d.ts +4 -0
- package/dist/task-workspace/cleanup.d.ts.map +1 -0
- package/dist/task-workspace/cleanup.js +428 -0
- package/dist/task-workspace/errors.d.ts +14 -0
- package/dist/task-workspace/errors.d.ts.map +1 -0
- package/dist/task-workspace/errors.js +81 -0
- package/dist/task-workspace/evidence.d.ts +32 -0
- package/dist/task-workspace/evidence.d.ts.map +1 -0
- package/dist/task-workspace/evidence.js +52 -0
- package/dist/task-workspace/field-safety.d.ts +3 -0
- package/dist/task-workspace/field-safety.d.ts.map +1 -0
- package/dist/task-workspace/field-safety.js +42 -0
- package/dist/task-workspace/health.d.ts +4 -0
- package/dist/task-workspace/health.d.ts.map +1 -0
- package/dist/task-workspace/health.js +163 -0
- package/dist/task-workspace/lifecycle.d.ts +3 -0
- package/dist/task-workspace/lifecycle.d.ts.map +1 -0
- package/dist/task-workspace/lifecycle.js +248 -0
- package/dist/task-workspace/locks.d.ts +13 -0
- package/dist/task-workspace/locks.d.ts.map +1 -0
- package/dist/task-workspace/locks.js +44 -0
- package/dist/task-workspace/managed-root.d.ts +7 -0
- package/dist/task-workspace/managed-root.d.ts.map +1 -0
- package/dist/task-workspace/managed-root.js +98 -0
- package/dist/task-workspace/mutex.d.ts +8 -0
- package/dist/task-workspace/mutex.d.ts.map +1 -0
- package/dist/task-workspace/mutex.js +82 -0
- package/dist/task-workspace/naming.d.ts +15 -0
- package/dist/task-workspace/naming.d.ts.map +1 -0
- package/dist/task-workspace/naming.js +0 -0
- package/dist/task-workspace/provisioning.d.ts +3 -0
- package/dist/task-workspace/provisioning.d.ts.map +1 -0
- package/dist/task-workspace/provisioning.js +528 -0
- package/dist/task-workspace/reconciliation.d.ts +15 -0
- package/dist/task-workspace/reconciliation.d.ts.map +1 -0
- package/dist/task-workspace/reconciliation.js +274 -0
- package/dist/task-workspace/repair.d.ts +3 -0
- package/dist/task-workspace/repair.d.ts.map +1 -0
- package/dist/task-workspace/repair.js +286 -0
- package/dist/task-workspace/routes.d.ts +19 -0
- package/dist/task-workspace/routes.d.ts.map +1 -0
- package/dist/task-workspace/routes.js +481 -0
- package/dist/task-workspace/store.d.ts +12 -0
- package/dist/task-workspace/store.d.ts.map +1 -0
- package/dist/task-workspace/store.js +128 -0
- package/dist/task-workspace/types.d.ts +170 -0
- package/dist/task-workspace/types.d.ts.map +1 -0
- package/dist/task-workspace/types.js +5 -0
- package/dist/voice-action-governance.d.ts +23 -0
- package/dist/voice-action-governance.d.ts.map +1 -0
- package/dist/voice-action-governance.js +126 -0
- package/dist/voice-handlers.d.ts +6 -0
- package/dist/voice-handlers.d.ts.map +1 -0
- package/dist/voice-handlers.js +570 -0
- package/dist/voice-realtime-grounded-tool.d.ts +31 -0
- package/dist/voice-realtime-grounded-tool.d.ts.map +1 -0
- package/dist/voice-realtime-grounded-tool.js +322 -0
- package/dist/voice-realtime.d.ts +69 -0
- package/dist/voice-realtime.d.ts.map +1 -0
- package/dist/voice-realtime.js +787 -0
- package/dist/workspace-state-handlers.d.ts +5 -0
- package/dist/workspace-state-handlers.d.ts.map +1 -0
- package/dist/workspace-state-handlers.js +106 -0
- package/package.json +20 -19
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// Governed remote publish routes: read-only preview + governed execute (Issue #476, Epic #470).
|
|
2
|
+
//
|
|
3
|
+
// * POST /api/git-delivery/push/preview — READ-ONLY. Builds the pre-publish risk context: the remote
|
|
4
|
+
// target, the risk class, would-create-remote-branch / force-blocked flags, the preflight findings
|
|
5
|
+
// (incl. non-fast-forward and missing-upstream), and the policy decision. Never mutates, never
|
|
6
|
+
// records evidence.
|
|
7
|
+
// * POST /api/git-delivery/push/execute — Governed. Drives the #476 publish gateway end-to-end through
|
|
8
|
+
// executeGovernedPublish (preflight + policy + approval + the dedicated push-only adapter) and
|
|
9
|
+
// appends content-free evidence for the allowed AND blocked outcome alike. Returns the typed
|
|
10
|
+
// publish-rejection reason + reused recovery hint so a rejected push can be recovered without
|
|
11
|
+
// guessing.
|
|
12
|
+
//
|
|
13
|
+
// Content-free throughout: counts, flags, typed codes, branch/remote NAMES only — never command output,
|
|
14
|
+
// diff content, secrets, or credentials. CSRF + JSON content type are enforced centrally by server.ts.
|
|
15
|
+
import { isGitDeliveryApprovalRequirement, } from "@oscharko-dev/keiko-contracts";
|
|
16
|
+
import { readWorktreeSnapshotFor, resolveProjectWorkspace } from "./execution.js";
|
|
17
|
+
import { buildGitDeliveryPushPreview, executeGovernedPublish, gitDeliveryPublishExecuteResponse, KEIKO_DEFAULT_PUBLISH_POLICY_PACK, } from "./pushExecution.js";
|
|
18
|
+
import { GitDeliveryBodyTooLargeError, hasOnlyAllowedKeys, isNonEmptyString, isPlainObject, readGitDeliveryBody, scanForbiddenStrings, scanUnsafeFormatChars, } from "./requestGuards.js";
|
|
19
|
+
const SAFE_MESSAGES = {
|
|
20
|
+
GIT_DELIVERY_PUSH_BAD_REQUEST: "The request body is not a valid governed publish request.",
|
|
21
|
+
GIT_DELIVERY_PUSH_PAYLOAD_TOO_LARGE: "The governed publish request exceeds the maximum size.",
|
|
22
|
+
GIT_DELIVERY_PUSH_FORBIDDEN_PAYLOAD: "The request contained a forbidden field. Requests may not carry credentials, headers, or URLs.",
|
|
23
|
+
GIT_DELIVERY_PUSH_UNKNOWN_PROJECT: "The requested project is not a known workspace.",
|
|
24
|
+
GIT_DELIVERY_PUSH_WORKTREE_UNAVAILABLE: "The repository worktree could not be inspected. Confirm the project is a Git repository.",
|
|
25
|
+
};
|
|
26
|
+
const errResult = (status, code) => ({
|
|
27
|
+
status,
|
|
28
|
+
body: { error: { code, message: SAFE_MESSAGES[code] } },
|
|
29
|
+
});
|
|
30
|
+
async function readParsed(req) {
|
|
31
|
+
let raw;
|
|
32
|
+
try {
|
|
33
|
+
raw = await readGitDeliveryBody(req);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
const result = error instanceof GitDeliveryBodyTooLargeError
|
|
37
|
+
? errResult(413, "GIT_DELIVERY_PUSH_PAYLOAD_TOO_LARGE")
|
|
38
|
+
: errResult(400, "GIT_DELIVERY_PUSH_BAD_REQUEST");
|
|
39
|
+
return { ok: false, result };
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
return { ok: true, value: JSON.parse(raw) };
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return { ok: false, result: errResult(400, "GIT_DELIVERY_PUSH_BAD_REQUEST") };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// A git ref / remote alias operand: non-empty, no whitespace, no leading "-" (flag-injection guard), no
|
|
49
|
+
// ":" (refspec-injection guard), no NUL. Defence-in-depth above the gateway's own argv assertions, so a
|
|
50
|
+
// malformed ref is a clean 400 rather than an internal execution error.
|
|
51
|
+
// eslint-disable-next-line no-control-regex -- intentionally matches control chars to REJECT them
|
|
52
|
+
const REF_CONTROL_CHAR = new RegExp("[\u0000-\u001f\u007f]");
|
|
53
|
+
function isSafeGitRef(value) {
|
|
54
|
+
if (typeof value !== "string" || value.length === 0)
|
|
55
|
+
return false;
|
|
56
|
+
if (/\s/.test(value))
|
|
57
|
+
return false;
|
|
58
|
+
if (value.startsWith("-"))
|
|
59
|
+
return false;
|
|
60
|
+
if (REF_CONTROL_CHAR.test(value))
|
|
61
|
+
return false;
|
|
62
|
+
if (value.includes(":"))
|
|
63
|
+
return false;
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
const ALLOWED_KEYS = new Set([
|
|
67
|
+
"schemaVersion",
|
|
68
|
+
"projectId",
|
|
69
|
+
"remoteAlias",
|
|
70
|
+
"remoteBranchName",
|
|
71
|
+
"sourceBranchName",
|
|
72
|
+
"forcePush",
|
|
73
|
+
"setUpstreamTracking",
|
|
74
|
+
"approval",
|
|
75
|
+
]);
|
|
76
|
+
const NO_APPROVAL = { required: false };
|
|
77
|
+
function parseApproval(value) {
|
|
78
|
+
if (value === undefined)
|
|
79
|
+
return NO_APPROVAL;
|
|
80
|
+
return isGitDeliveryApprovalRequirement(value) ? value : undefined;
|
|
81
|
+
}
|
|
82
|
+
function optionalBool(value) {
|
|
83
|
+
if (value === undefined)
|
|
84
|
+
return false;
|
|
85
|
+
return typeof value === "boolean" ? value : undefined;
|
|
86
|
+
}
|
|
87
|
+
// The credential-shape + unsafe-format-char boundary scans. Returns the typed error RouteResult or
|
|
88
|
+
// undefined when the payload is clean.
|
|
89
|
+
function scanError(parsed) {
|
|
90
|
+
if (scanForbiddenStrings(parsed)) {
|
|
91
|
+
return errResult(400, "GIT_DELIVERY_PUSH_FORBIDDEN_PAYLOAD");
|
|
92
|
+
}
|
|
93
|
+
if (scanUnsafeFormatChars(parsed)) {
|
|
94
|
+
return errResult(400, "GIT_DELIVERY_PUSH_BAD_REQUEST");
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
// Builds the typed push command from validated ref + boolean operands, or undefined when any operand is
|
|
99
|
+
// malformed.
|
|
100
|
+
function buildPushCommand(parsed) {
|
|
101
|
+
if (!isSafeGitRef(parsed.remoteAlias) ||
|
|
102
|
+
!isSafeGitRef(parsed.remoteBranchName) ||
|
|
103
|
+
!isSafeGitRef(parsed.sourceBranchName)) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const forcePush = optionalBool(parsed.forcePush);
|
|
107
|
+
const setUpstreamTracking = optionalBool(parsed.setUpstreamTracking);
|
|
108
|
+
if (forcePush === undefined || setUpstreamTracking === undefined)
|
|
109
|
+
return undefined;
|
|
110
|
+
return {
|
|
111
|
+
kind: "push",
|
|
112
|
+
sourceBranchName: parsed.sourceBranchName,
|
|
113
|
+
remoteAlias: parsed.remoteAlias,
|
|
114
|
+
remoteBranchName: parsed.remoteBranchName,
|
|
115
|
+
forcePush,
|
|
116
|
+
setUpstreamTracking,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function validate(parsed) {
|
|
120
|
+
const bad = { kind: "err", result: errResult(400, "GIT_DELIVERY_PUSH_BAD_REQUEST") };
|
|
121
|
+
if (!isPlainObject(parsed) || !hasOnlyAllowedKeys(parsed, ALLOWED_KEYS))
|
|
122
|
+
return bad;
|
|
123
|
+
if (parsed.schemaVersion !== "1" || !isNonEmptyString(parsed.projectId))
|
|
124
|
+
return bad;
|
|
125
|
+
const scanErr = scanError(parsed);
|
|
126
|
+
if (scanErr !== undefined)
|
|
127
|
+
return { kind: "err", result: scanErr };
|
|
128
|
+
const command = buildPushCommand(parsed);
|
|
129
|
+
const approval = parseApproval(parsed.approval);
|
|
130
|
+
if (command === undefined || approval === undefined)
|
|
131
|
+
return bad;
|
|
132
|
+
return { kind: "ok", value: { projectId: parsed.projectId, command, approval } };
|
|
133
|
+
}
|
|
134
|
+
// ─── Preview handler (read-only) ────────────────────────────────────────────────────────────────
|
|
135
|
+
export const createHandlePushPreview = (options = {}) => {
|
|
136
|
+
const seams = options.execution ?? {};
|
|
137
|
+
const now = () => (seams.now ?? Date.now)();
|
|
138
|
+
return async (ctx, deps) => {
|
|
139
|
+
const read = await readParsed(ctx.req);
|
|
140
|
+
if (!read.ok)
|
|
141
|
+
return read.result;
|
|
142
|
+
const validation = validate(read.value);
|
|
143
|
+
if (validation.kind === "err")
|
|
144
|
+
return validation.result;
|
|
145
|
+
const { projectId, command } = validation.value;
|
|
146
|
+
const workspace = resolveProjectWorkspace(deps, projectId);
|
|
147
|
+
if (workspace === undefined)
|
|
148
|
+
return errResult(404, "GIT_DELIVERY_PUSH_UNKNOWN_PROJECT");
|
|
149
|
+
const packs = seams.policyPacks ?? { repoPack: KEIKO_DEFAULT_PUBLISH_POLICY_PACK };
|
|
150
|
+
try {
|
|
151
|
+
const snapshot = await readWorktreeSnapshotFor(workspace, seams, now);
|
|
152
|
+
return {
|
|
153
|
+
status: 200,
|
|
154
|
+
body: deps.redactor(buildGitDeliveryPushPreview(command, snapshot, packs)),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return errResult(409, "GIT_DELIVERY_PUSH_WORKTREE_UNAVAILABLE");
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
// ─── Execute handler (governed) ───────────────────────────────────────────────────────────────
|
|
163
|
+
export const createHandlePushExecute = (options = {}) => {
|
|
164
|
+
const seams = options.execution ?? {};
|
|
165
|
+
return async (ctx, deps) => {
|
|
166
|
+
const read = await readParsed(ctx.req);
|
|
167
|
+
if (!read.ok)
|
|
168
|
+
return read.result;
|
|
169
|
+
const validation = validate(read.value);
|
|
170
|
+
if (validation.kind === "err")
|
|
171
|
+
return validation.result;
|
|
172
|
+
const { projectId, command, approval } = validation.value;
|
|
173
|
+
const workspace = resolveProjectWorkspace(deps, projectId);
|
|
174
|
+
if (workspace === undefined)
|
|
175
|
+
return errResult(404, "GIT_DELIVERY_PUSH_UNKNOWN_PROJECT");
|
|
176
|
+
let result;
|
|
177
|
+
try {
|
|
178
|
+
result = await executeGovernedPublish(command, approval, workspace, deps, seams);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// Only the read-only snapshot step can throw (not a git repository); the gateway never throws.
|
|
182
|
+
return errResult(409, "GIT_DELIVERY_PUSH_WORKTREE_UNAVAILABLE");
|
|
183
|
+
}
|
|
184
|
+
return { status: 200, body: deps.redactor(gitDeliveryPublishExecuteResponse(result)) };
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
// ─── Route group ───────────────────────────────────────────────────────────────────────────────
|
|
188
|
+
export const createGitDeliveryPushRouteGroup = (options = {}) => [
|
|
189
|
+
{
|
|
190
|
+
method: "POST",
|
|
191
|
+
pattern: "/api/git-delivery/push/preview",
|
|
192
|
+
handler: createHandlePushPreview(options),
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
method: "POST",
|
|
196
|
+
pattern: "/api/git-delivery/push/execute",
|
|
197
|
+
handler: createHandlePushExecute(options),
|
|
198
|
+
},
|
|
199
|
+
];
|
|
200
|
+
export const GIT_DELIVERY_PUSH_ROUTE_GROUP = createGitDeliveryPushRouteGroup();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { IncomingMessage } from "node:http";
|
|
2
|
+
export declare const GIT_DELIVERY_MAX_BODY_BYTES: number;
|
|
3
|
+
export declare class GitDeliveryBodyTooLargeError extends Error {
|
|
4
|
+
constructor();
|
|
5
|
+
}
|
|
6
|
+
export declare const readGitDeliveryBody: (req: IncomingMessage) => Promise<string>;
|
|
7
|
+
export declare const isPlainObject: (value: unknown) => value is Record<string, unknown>;
|
|
8
|
+
export declare const hasOnlyAllowedKeys: (obj: Readonly<Record<string, unknown>>, allowed: ReadonlySet<string>) => boolean;
|
|
9
|
+
export declare const scanForbiddenStrings: (value: unknown) => boolean;
|
|
10
|
+
export declare const GIT_DELIVERY_UNSAFE_FORMAT_CHAR: RegExp;
|
|
11
|
+
export declare const scanUnsafeFormatChars: (value: unknown) => boolean;
|
|
12
|
+
export declare const isNonEmptyString: (value: unknown) => value is string;
|
|
13
|
+
export declare const GIT_DELIVERY_PATHSPEC_CONTROL_CHAR: RegExp;
|
|
14
|
+
export declare const isContainedPathspec: (value: unknown) => value is string;
|
|
15
|
+
//# sourceMappingURL=requestGuards.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"requestGuards.d.ts","sourceRoot":"","sources":["../../src/gitDelivery/requestGuards.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAGjD,eAAO,MAAM,2BAA2B,QAAY,CAAC;AAErD,qBAAa,4BAA6B,SAAQ,KAAK;;CAKtD;AAED,eAAO,MAAM,mBAAmB,GAAI,KAAK,eAAe,KAAG,OAAO,CAAC,MAAM,CAsBrE,CAAC;AAEL,eAAO,MAAM,aAAa,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CACR,CAAC;AAEvE,eAAO,MAAM,kBAAkB,GAC7B,KAAK,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACtC,SAAS,WAAW,CAAC,MAAM,CAAC,KAC3B,OAKF,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,OAAO,OAAO,KAAG,OAUrD,CAAC;AAKF,eAAO,MAAM,+BAA+B,QAG3C,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAAI,OAAO,OAAO,KAAG,OAKtD,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,MACZ,CAAC;AAOhD,eAAO,MAAM,kCAAkC,QAAyC,CAAC;AAKzF,eAAO,MAAM,mBAAmB,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,MAO7D,CAAC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Shared request guards for the governed local Git delivery execution routes (Issue #475, Epic #470).
|
|
2
|
+
//
|
|
3
|
+
// The #473 action-sheet route established the BFF request-hardening shape (bounded body read, allowed-
|
|
4
|
+
// key whitelist, credential-shape scan, unsafe-format-char scan, content-free error envelope). The
|
|
5
|
+
// #475 execution routes (branch / staging / commit) share that exact discipline; this module is the
|
|
6
|
+
// single home for those guards so the new routes never duplicate the boundary logic.
|
|
7
|
+
//
|
|
8
|
+
// CSRF + JSON content-type are enforced CENTRALLY by server.ts for POST, so they are NOT re-checked
|
|
9
|
+
// here — these guards run AFTER that central gate.
|
|
10
|
+
import { containsForbiddenSecretShape } from "../qualityIntelligence/connectorErrors.js";
|
|
11
|
+
export const GIT_DELIVERY_MAX_BODY_BYTES = 64 * 1024;
|
|
12
|
+
export class GitDeliveryBodyTooLargeError extends Error {
|
|
13
|
+
constructor() {
|
|
14
|
+
super("Request body exceeds the git-delivery route cap");
|
|
15
|
+
this.name = "GitDeliveryBodyTooLargeError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export const readGitDeliveryBody = (req) => new Promise((resolve, reject) => {
|
|
19
|
+
const chunks = [];
|
|
20
|
+
let total = 0;
|
|
21
|
+
let capped = false;
|
|
22
|
+
req.on("data", (chunk) => {
|
|
23
|
+
total += chunk.length;
|
|
24
|
+
if (total > GIT_DELIVERY_MAX_BODY_BYTES) {
|
|
25
|
+
if (!capped) {
|
|
26
|
+
capped = true;
|
|
27
|
+
chunks.length = 0;
|
|
28
|
+
reject(new GitDeliveryBodyTooLargeError());
|
|
29
|
+
req.resume();
|
|
30
|
+
}
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
chunks.push(chunk);
|
|
34
|
+
});
|
|
35
|
+
req.on("end", () => {
|
|
36
|
+
if (!capped)
|
|
37
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
38
|
+
});
|
|
39
|
+
req.on("error", reject);
|
|
40
|
+
});
|
|
41
|
+
export const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
42
|
+
export const hasOnlyAllowedKeys = (obj, allowed) => {
|
|
43
|
+
for (const key of Object.keys(obj)) {
|
|
44
|
+
if (!allowed.has(key))
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
};
|
|
49
|
+
export const scanForbiddenStrings = (value) => {
|
|
50
|
+
if (typeof value === "string")
|
|
51
|
+
return containsForbiddenSecretShape(value);
|
|
52
|
+
if (Array.isArray(value))
|
|
53
|
+
return value.some(scanForbiddenStrings);
|
|
54
|
+
if (isPlainObject(value)) {
|
|
55
|
+
for (const v of Object.values(value)) {
|
|
56
|
+
if (scanForbiddenStrings(v))
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
};
|
|
63
|
+
// Zero-width, bidi-control, and BOM format characters. Branch names / refs / pathspecs in these
|
|
64
|
+
// content-free requests never legitimately contain them; a U+202E override or zero-width char would
|
|
65
|
+
// spoof the displayed action target (Trojan-Source). Reject fail-closed at the boundary.
|
|
66
|
+
export const GIT_DELIVERY_UNSAFE_FORMAT_CHAR = new RegExp("[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF]", "u");
|
|
67
|
+
export const scanUnsafeFormatChars = (value) => {
|
|
68
|
+
if (typeof value === "string")
|
|
69
|
+
return GIT_DELIVERY_UNSAFE_FORMAT_CHAR.test(value);
|
|
70
|
+
if (Array.isArray(value))
|
|
71
|
+
return value.some(scanUnsafeFormatChars);
|
|
72
|
+
if (isPlainObject(value))
|
|
73
|
+
return Object.values(value).some(scanUnsafeFormatChars);
|
|
74
|
+
return false;
|
|
75
|
+
};
|
|
76
|
+
export const isNonEmptyString = (value) => typeof value === "string" && value.length > 0;
|
|
77
|
+
// C0 control characters (incl. TAB/LF/CR/NUL) and DEL. A pathspec in these content-free requests never
|
|
78
|
+
// legitimately contains them; rejecting fail-closed keeps the pathspec guard symmetric with the
|
|
79
|
+
// network-ref REF_CONTROL_CHAR guard (syncRoutes.ts). Pathspecs are already literalized as
|
|
80
|
+
// :(literal)<value> after a "--" sentinel at the adapter, so this is defence-in-depth, not a live fix.
|
|
81
|
+
// eslint-disable-next-line no-control-regex -- intentionally matches control chars to REJECT them
|
|
82
|
+
export const GIT_DELIVERY_PATHSPEC_CONTROL_CHAR = new RegExp("[\\u0000-\\u001f\\u007f]");
|
|
83
|
+
// A staged/unstaged pathspec must be a relative path that cannot escape the repository working tree:
|
|
84
|
+
// no absolute path, no leading "-" (flag injection — also guarded at the adapter), no ".." traversal
|
|
85
|
+
// segment, and no NUL / C0 control / DEL char. Defence-in-depth above git's own working-tree containment.
|
|
86
|
+
export const isContainedPathspec = (value) => {
|
|
87
|
+
if (typeof value !== "string" || value.length === 0)
|
|
88
|
+
return false;
|
|
89
|
+
if (GIT_DELIVERY_PATHSPEC_CONTROL_CHAR.test(value))
|
|
90
|
+
return false;
|
|
91
|
+
if (value.startsWith("-") || value.startsWith("/"))
|
|
92
|
+
return false;
|
|
93
|
+
if (/^[A-Za-z]:[\\/]/.test(value))
|
|
94
|
+
return false; // Windows absolute
|
|
95
|
+
const segments = value.split(/[\\/]+/);
|
|
96
|
+
return !segments.includes("..");
|
|
97
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { GitSyncOperation, GitSyncOutcome } from "@oscharko-dev/keiko-contracts";
|
|
2
|
+
import type { EvidenceStore } from "@oscharko-dev/keiko-evidence";
|
|
3
|
+
export declare const GIT_SYNC_EVIDENCE_RUNID_PREFIX: "git-sync-evidence-";
|
|
4
|
+
export declare const GIT_SYNC_EVIDENCE_DEFAULT_BUCKET_CAP = 500;
|
|
5
|
+
export declare const GIT_SYNC_EVIDENCE_SCHEMA_VERSION: "1";
|
|
6
|
+
export interface GitSyncEvidenceRecord {
|
|
7
|
+
readonly schemaVersion: typeof GIT_SYNC_EVIDENCE_SCHEMA_VERSION;
|
|
8
|
+
readonly operation: GitSyncOperation;
|
|
9
|
+
readonly outcome: GitSyncOutcome;
|
|
10
|
+
readonly repoIdHash: string;
|
|
11
|
+
readonly branch?: string | undefined;
|
|
12
|
+
readonly remote?: string | undefined;
|
|
13
|
+
readonly aheadBefore?: number | undefined;
|
|
14
|
+
readonly behindBefore?: number | undefined;
|
|
15
|
+
readonly aheadAfter?: number | undefined;
|
|
16
|
+
readonly behindAfter?: number | undefined;
|
|
17
|
+
readonly recordedAtMs: number;
|
|
18
|
+
}
|
|
19
|
+
export interface GitSyncEvidenceLedgerDoc {
|
|
20
|
+
readonly schemaVersion: typeof GIT_SYNC_EVIDENCE_SCHEMA_VERSION;
|
|
21
|
+
readonly records: readonly GitSyncEvidenceRecord[];
|
|
22
|
+
}
|
|
23
|
+
export declare function gitSyncRepoIdHash(workspaceRoot: string): string;
|
|
24
|
+
export declare function gitSyncEvidenceRunIdFor(nowMs: number): string;
|
|
25
|
+
export interface RecordGitSyncEvidenceOptions {
|
|
26
|
+
readonly evidenceStore: EvidenceStore;
|
|
27
|
+
readonly redactString: (input: string) => string;
|
|
28
|
+
readonly maxRecordsPerBucket?: number | undefined;
|
|
29
|
+
readonly onPersistError?: ((error: unknown) => void) | undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Appends one fetch/pull sync evidence record to its date-bucketed ledger. Redacts every string leaf,
|
|
33
|
+
* bounds the bucket, and never throws into the caller's path. Returns nothing — audit recording is
|
|
34
|
+
* best-effort and is reported (not propagated) on failure.
|
|
35
|
+
*/
|
|
36
|
+
export declare function recordGitSyncEvidence(options: RecordGitSyncEvidenceOptions, record: GitSyncEvidenceRecord): void;
|
|
37
|
+
//# sourceMappingURL=syncEvidence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"syncEvidence.d.ts","sourceRoot":"","sources":["../../src/gitDelivery/syncEvidence.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAEtF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAGlE,eAAO,MAAM,8BAA8B,EAAG,oBAA6B,CAAC;AAG5E,eAAO,MAAM,oCAAoC,MAAM,CAAC;AAExD,eAAO,MAAM,gCAAgC,EAAG,GAAY,CAAC;AAE7D,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,aAAa,EAAE,OAAO,gCAAgC,CAAC;IAChE,QAAQ,CAAC,SAAS,EAAE,gBAAgB,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IAEjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,aAAa,EAAE,OAAO,gCAAgC,CAAC;IAChE,QAAQ,CAAC,OAAO,EAAE,SAAS,qBAAqB,EAAE,CAAC;CACpD;AAGD,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAE/D;AAID,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAG7D;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IAGtC,QAAQ,CAAC,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACjD,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAElD,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CAClE;AA6DD;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,4BAA4B,EACrC,MAAM,EAAE,qBAAqB,GAC5B,IAAI,CAUN"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Content-free fetch/pull sync EVIDENCE ledger (Issue #1573, Epic #1572).
|
|
2
|
+
//
|
|
3
|
+
// fetch/pull do NOT enter the #472 kernel / #474 mutation ledger (that taxonomy is frozen), so this is
|
|
4
|
+
// a dedicated SIBLING of mutationEvidenceLedger.ts that records the terminal outcome of every executed
|
|
5
|
+
// sync. Persistence shape mirrors the mutation ledger exactly: ONE document per UTC date bucket (runId
|
|
6
|
+
// `git-sync-evidence-YYYY-MM-DD`), serialized through `EvidenceStore.update ?? get+put`, redacted leaf
|
|
7
|
+
// by leaf with `deepRedactStrings`, bounded to the most recent N records, and best-effort — an audit
|
|
8
|
+
// write must NEVER break the user's sync. Corrupt buckets fail closed and are never overwritten.
|
|
9
|
+
//
|
|
10
|
+
// The repository is identified only by `sha256Hex(workspace.root).slice(0,24)` — a content-free hash,
|
|
11
|
+
// never the path itself. Records carry counts, the typed outcome, and branch/remote NAMES only; no
|
|
12
|
+
// URLs, secrets, or command output.
|
|
13
|
+
import { deepRedactStrings } from "@oscharko-dev/keiko-evidence";
|
|
14
|
+
import { sha256Hex } from "@oscharko-dev/keiko-security";
|
|
15
|
+
export const GIT_SYNC_EVIDENCE_RUNID_PREFIX = "git-sync-evidence-";
|
|
16
|
+
// Default bound on records retained per UTC date bucket. The most recent records are kept.
|
|
17
|
+
export const GIT_SYNC_EVIDENCE_DEFAULT_BUCKET_CAP = 500;
|
|
18
|
+
export const GIT_SYNC_EVIDENCE_SCHEMA_VERSION = "1";
|
|
19
|
+
// Content-free repository identifier from the workspace root path. Never the path itself.
|
|
20
|
+
export function gitSyncRepoIdHash(workspaceRoot) {
|
|
21
|
+
return sha256Hex(workspaceRoot).slice(0, 24);
|
|
22
|
+
}
|
|
23
|
+
// UTC date-bucket runId. `git-sync-evidence-` (17) + `YYYY-MM-DD` (10) = 27, well under the
|
|
24
|
+
// evidence-store run-id length cap.
|
|
25
|
+
export function gitSyncEvidenceRunIdFor(nowMs) {
|
|
26
|
+
const iso = new Date(nowMs).toISOString();
|
|
27
|
+
return `${GIT_SYNC_EVIDENCE_RUNID_PREFIX}${iso.slice(0, 10)}`;
|
|
28
|
+
}
|
|
29
|
+
function defaultOnPersistError(error) {
|
|
30
|
+
// eslint-disable-next-line no-console
|
|
31
|
+
console.error("git-sync evidence ledger: persistence failed", error);
|
|
32
|
+
}
|
|
33
|
+
function isPlainObject(value) {
|
|
34
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
// Parses an existing bucket document. Returns the records faithfully (they were written valid).
|
|
37
|
+
// Throws on gross corruption so the caller fails closed and preserves the artifact.
|
|
38
|
+
function parseExistingRecords(json) {
|
|
39
|
+
if (json === undefined) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
let parsed;
|
|
43
|
+
try {
|
|
44
|
+
parsed = JSON.parse(json);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
throw new Error("git-sync evidence ledger is corrupt; refusing to overwrite existing audit evidence", { cause: error });
|
|
48
|
+
}
|
|
49
|
+
if (!isPlainObject(parsed) || !Array.isArray(parsed.records)) {
|
|
50
|
+
throw new Error("git-sync evidence ledger has an unexpected shape; refusing to overwrite existing audit evidence");
|
|
51
|
+
}
|
|
52
|
+
return parsed.records;
|
|
53
|
+
}
|
|
54
|
+
function boundedDoc(existing, record, cap) {
|
|
55
|
+
const records = [...existing, record];
|
|
56
|
+
return {
|
|
57
|
+
schemaVersion: GIT_SYNC_EVIDENCE_SCHEMA_VERSION,
|
|
58
|
+
records: cap > 0 && records.length > cap ? records.slice(records.length - cap) : records,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function appendRecord(store, runId, record, cap) {
|
|
62
|
+
const append = (existingJson) => JSON.stringify(boundedDoc(parseExistingRecords(existingJson), record, cap));
|
|
63
|
+
if (store.update !== undefined) {
|
|
64
|
+
store.update(runId, append);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
store.put(runId, append(store.get(runId)));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Appends one fetch/pull sync evidence record to its date-bucketed ledger. Redacts every string leaf,
|
|
71
|
+
* bounds the bucket, and never throws into the caller's path. Returns nothing — audit recording is
|
|
72
|
+
* best-effort and is reported (not propagated) on failure.
|
|
73
|
+
*/
|
|
74
|
+
export function recordGitSyncEvidence(options, record) {
|
|
75
|
+
const onPersistError = options.onPersistError ?? defaultOnPersistError;
|
|
76
|
+
const cap = options.maxRecordsPerBucket ?? GIT_SYNC_EVIDENCE_DEFAULT_BUCKET_CAP;
|
|
77
|
+
const safe = deepRedactStrings(record, options.redactString);
|
|
78
|
+
const runId = gitSyncEvidenceRunIdFor(record.recordedAtMs);
|
|
79
|
+
try {
|
|
80
|
+
appendRecord(options.evidenceStore, runId, safe, cap);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
onPersistError(error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { GitSyncOperation, GitSyncOutcome, GitUpstreamSummary } from "@oscharko-dev/keiko-contracts";
|
|
2
|
+
import type { GitSyncPreview } from "@oscharko-dev/keiko-contracts";
|
|
3
|
+
import { type GitProcessRunner } from "../gitRoutes.js";
|
|
4
|
+
export interface GitDeliverySyncSeams {
|
|
5
|
+
readonly runner?: GitProcessRunner | undefined;
|
|
6
|
+
readonly now?: (() => number) | undefined;
|
|
7
|
+
readonly maxBytes?: number | undefined;
|
|
8
|
+
readonly timeoutMs?: number | undefined;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Read-only fetch/pull readiness. Runs `status --porcelain=v2 --branch -z` + `git remote` (names only)
|
|
12
|
+
* and projects a content-free preview with an executable gate. Throws only when the status read fails
|
|
13
|
+
* (not a repository / unsafe owner); the caller maps that to a 409.
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildSyncPreview(operation: GitSyncOperation, repoRoot: string, remote: string | undefined, seams?: GitDeliverySyncSeams): Promise<GitSyncPreview>;
|
|
16
|
+
export interface SyncExecuteResult {
|
|
17
|
+
readonly outcome: GitSyncOutcome;
|
|
18
|
+
readonly branch?: string | undefined;
|
|
19
|
+
readonly upstream?: GitUpstreamSummary | undefined;
|
|
20
|
+
readonly ahead?: number | undefined;
|
|
21
|
+
readonly behind?: number | undefined;
|
|
22
|
+
readonly truncated: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Runs ONE bounded fetch/pull and classifies the outcome. Never governs (no kernel, no policy) — the
|
|
26
|
+
* control surface is the fixed argv + hardened env of the reused runner. After a settled op the
|
|
27
|
+
* branch/upstream/ahead/behind are re-read best-effort for the response.
|
|
28
|
+
*/
|
|
29
|
+
export declare function runSyncExecute(operation: GitSyncOperation, repoRoot: string, remote: string | undefined, seams?: GitDeliverySyncSeams, preflight?: GitSyncPreview): Promise<SyncExecuteResult>;
|
|
30
|
+
//# sourceMappingURL=syncExecution.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"syncExecution.d.ts","sourceRoot":"","sources":["../../src/gitDelivery/syncExecution.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAEV,gBAAgB,EAChB,cAAc,EACd,kBAAkB,EACnB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAIL,KAAK,gBAAgB,EACtB,MAAM,iBAAiB,CAAC;AAMzB,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;IAC/C,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACzC;AA0GD;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,SAAS,EAAE,gBAAgB,EAC3B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,KAAK,GAAE,oBAAyB,GAC/B,OAAO,CAAC,cAAc,CAAC,CAgBzB;AAID,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,QAAQ,CAAC,EAAE,kBAAkB,GAAG,SAAS,CAAC;IACnD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAkJD;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,gBAAgB,EAC3B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,KAAK,GAAE,oBAAyB,EAChC,SAAS,CAAC,EAAE,cAAc,GACzB,OAAO,CAAC,iBAAiB,CAAC,CAa5B"}
|