@stravica-ai/digital-operator-pa 0.1.29 → 0.1.30
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.
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { createHash, randomUUID } from 'node:crypto';
|
|
15
15
|
import { hostname } from 'node:os';
|
|
16
|
-
import { relative,
|
|
16
|
+
import { isAbsolute, normalize, relative, sep } from 'node:path';
|
|
17
17
|
|
|
18
18
|
import { EVENT_KINDS, canonicalJson } from '@stravica-ai/remote-library';
|
|
19
19
|
|
|
@@ -31,6 +31,47 @@ export function contentShaOf(payload) {
|
|
|
31
31
|
return `sha256:${createHash('sha256').update(canonical).digest('hex')}`;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Refuse a source path that would encode as an escape from the repo root.
|
|
36
|
+
* Root-cause cure for the poison-event wedge Stream 5 re-verify Scope C
|
|
37
|
+
* surfaced (w-2026-08-24-dave-025, SA14). Called by buildEnvelope after
|
|
38
|
+
* relPath is computed. Semantics match the server's assertPathSafe
|
|
39
|
+
* (spec §4 apply gate): reject empty, absolute, NUL, drive-letter,
|
|
40
|
+
* root-backslash, `..`-segments, and any string that normalises to a
|
|
41
|
+
* path starting with `..`. The error names the input sourcePath and
|
|
42
|
+
* repoRoot so an operator can see immediately why the encoding refused.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} relPath candidate repo-relative path
|
|
45
|
+
* @param {string} sourcePath original input (for the error message)
|
|
46
|
+
* @param {string} repoRoot resolved repo root (for the error message)
|
|
47
|
+
* @throws {Error} with exitCode=1 if the path is unsafe
|
|
48
|
+
*/
|
|
49
|
+
export function assertRepoRelativeSafe(relPath, sourcePath, repoRoot) {
|
|
50
|
+
const fail = (reason) => {
|
|
51
|
+
const err = new Error(
|
|
52
|
+
`refusing to encode source path outside repo root (${reason}): ` +
|
|
53
|
+
`sourcePath=${JSON.stringify(sourcePath)}, repoRoot=${JSON.stringify(repoRoot)}, ` +
|
|
54
|
+
`resolvedRelative=${JSON.stringify(relPath)}. ` +
|
|
55
|
+
`Move the file into the repo or stage it under a repo-resident path before calling library put.`,
|
|
56
|
+
);
|
|
57
|
+
err.exitCode = 1;
|
|
58
|
+
throw err;
|
|
59
|
+
};
|
|
60
|
+
if (typeof relPath !== 'string') fail('path must be a string');
|
|
61
|
+
if (relPath.length === 0) fail('path resolves empty');
|
|
62
|
+
if (relPath.includes('\0')) fail('path contains NUL byte');
|
|
63
|
+
if (isAbsolute(relPath)) fail('resolved path is absolute');
|
|
64
|
+
if (/^[A-Za-z]:[\\/]/.test(relPath) || relPath.startsWith('\\')) {
|
|
65
|
+
fail('drive-letter or root-backslash path');
|
|
66
|
+
}
|
|
67
|
+
const parts = relPath.split(/[/\\]/);
|
|
68
|
+
if (parts.some((seg) => seg === '..')) fail("'..' segments not permitted");
|
|
69
|
+
const normalised = normalize(relPath);
|
|
70
|
+
if (normalised === '..' || normalised.startsWith('..' + sep) || normalised.startsWith('../')) {
|
|
71
|
+
fail('path normalises outside repo root');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
34
75
|
/**
|
|
35
76
|
* Guess the event kind from a staging file path when the operator did
|
|
36
77
|
* not supply one via `--kind`. Falls back to
|
|
@@ -77,9 +118,22 @@ export function buildEnvelope({
|
|
|
77
118
|
sourceKind = 'manual',
|
|
78
119
|
clock = () => new Date(),
|
|
79
120
|
}) {
|
|
121
|
+
// Repo-relative resolution + refusal for anything that would encode as
|
|
122
|
+
// a `..`-segment or otherwise escape the repo root. This is the root
|
|
123
|
+
// cause cure for the Stream 5 re-verify Scope C poison-event wedge
|
|
124
|
+
// (w-2026-08-24-dave-025). Before this cure `relative(repoRoot, abs)`
|
|
125
|
+
// silently produced `../../etc/...` for files sitting outside the
|
|
126
|
+
// repo and the envelope was posted with a payload path the worker's
|
|
127
|
+
// apply gate would later refuse — halting single-writer propagation
|
|
128
|
+
// for the whole estate. Refusing here (client-side, at encoding time)
|
|
129
|
+
// stops the poison at source; SA14's ingest-side check is the second
|
|
130
|
+
// line; the worker's apply gate remains the third.
|
|
80
131
|
const relPath = sourcePath
|
|
81
132
|
? isAbsolute(sourcePath) ? relative(repoRoot, sourcePath) : sourcePath
|
|
82
133
|
: undefined;
|
|
134
|
+
if (relPath !== undefined) {
|
|
135
|
+
assertRepoRelativeSafe(relPath, sourcePath, repoRoot);
|
|
136
|
+
}
|
|
83
137
|
|
|
84
138
|
const resolvedKind = kind ?? guessKindFromPath(relPath ?? sourcePath ?? '') ?? null;
|
|
85
139
|
if (!resolvedKind) {
|
package/package.json
CHANGED