@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, isAbsolute } from 'node:path';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stravica-ai/digital-operator-pa",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "description": "Digital Operator PA \u2014 single-bundled public package surface (bins + library) for the platform.",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",