@vibe-agent-toolkit/utils 0.2.0-rc.3 → 0.2.0-rc.4

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.
Files changed (74) hide show
  1. package/README.md +21 -13
  2. package/dist/crawl-timing.d.ts +43 -66
  3. package/dist/crawl-timing.d.ts.map +1 -1
  4. package/dist/crawl-timing.js +35 -79
  5. package/dist/crawl-timing.js.map +1 -1
  6. package/dist/crawl.d.ts +2 -4
  7. package/dist/crawl.d.ts.map +1 -1
  8. package/dist/crawl.js +2 -4
  9. package/dist/crawl.js.map +1 -1
  10. package/dist/git.d.ts +11 -4
  11. package/dist/git.d.ts.map +1 -1
  12. package/dist/git.js +11 -4
  13. package/dist/git.js.map +1 -1
  14. package/dist/index.d.ts +19 -20
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +19 -41
  17. package/dist/index.js.map +1 -1
  18. package/dist/project.d.ts +7 -5
  19. package/dist/project.d.ts.map +1 -1
  20. package/dist/project.js +7 -5
  21. package/dist/project.js.map +1 -1
  22. package/dist/testing.d.ts +73 -3
  23. package/dist/testing.d.ts.map +1 -1
  24. package/dist/testing.js +93 -3
  25. package/dist/testing.js.map +1 -1
  26. package/dist/timing-dump.d.ts +22 -5
  27. package/dist/timing-dump.d.ts.map +1 -1
  28. package/dist/timing-dump.js +89 -23
  29. package/dist/timing-dump.js.map +1 -1
  30. package/eslint/index.cjs +9 -0
  31. package/eslint/index.d.cts +14 -7
  32. package/eslint/rules/no-process-exit-in-phase.cjs +117 -0
  33. package/package.json +5 -7
  34. package/dist/link-auth/build-headers.d.ts +0 -34
  35. package/dist/link-auth/build-headers.d.ts.map +0 -1
  36. package/dist/link-auth/build-headers.js +0 -58
  37. package/dist/link-auth/build-headers.js.map +0 -1
  38. package/dist/link-auth/expand-macro.d.ts +0 -38
  39. package/dist/link-auth/expand-macro.d.ts.map +0 -1
  40. package/dist/link-auth/expand-macro.js +0 -139
  41. package/dist/link-auth/expand-macro.js.map +0 -1
  42. package/dist/link-auth/macros.yaml +0 -50
  43. package/dist/link-auth/resolve-token.d.ts +0 -83
  44. package/dist/link-auth/resolve-token.d.ts.map +0 -1
  45. package/dist/link-auth/resolve-token.js +0 -115
  46. package/dist/link-auth/resolve-token.js.map +0 -1
  47. package/dist/link-auth/resolve.d.ts +0 -102
  48. package/dist/link-auth/resolve.d.ts.map +0 -1
  49. package/dist/link-auth/resolve.js +0 -66
  50. package/dist/link-auth/resolve.js.map +0 -1
  51. package/dist/link-auth/rewrite.d.ts +0 -52
  52. package/dist/link-auth/rewrite.d.ts.map +0 -1
  53. package/dist/link-auth/rewrite.js +0 -102
  54. package/dist/link-auth/rewrite.js.map +0 -1
  55. package/dist/link-auth/select-provider.d.ts +0 -30
  56. package/dist/link-auth/select-provider.d.ts.map +0 -1
  57. package/dist/link-auth/select-provider.js +0 -55
  58. package/dist/link-auth/select-provider.js.map +0 -1
  59. package/dist/link-auth/template.d.ts +0 -40
  60. package/dist/link-auth/template.d.ts.map +0 -1
  61. package/dist/link-auth/template.js +0 -89
  62. package/dist/link-auth/template.js.map +0 -1
  63. package/dist/link-auth/transforms.d.ts +0 -46
  64. package/dist/link-auth/transforms.d.ts.map +0 -1
  65. package/dist/link-auth/transforms.js +0 -52
  66. package/dist/link-auth/transforms.js.map +0 -1
  67. package/dist/template-entry.d.ts +0 -10
  68. package/dist/template-entry.d.ts.map +0 -1
  69. package/dist/template-entry.js +0 -10
  70. package/dist/template-entry.js.map +0 -1
  71. package/dist/template.d.ts +0 -7
  72. package/dist/template.d.ts.map +0 -1
  73. package/dist/template.js +0 -18
  74. package/dist/template.js.map +0 -1
@@ -18,9 +18,14 @@
18
18
  * 2. A dump failure is written to **stderr and never thrown**. These run from
19
19
  * an `exit` listener, where a throw changes the process's exit behaviour, and
20
20
  * they must never touch stdout, which carries vat's report.
21
- * 3. A pid can file more than one dump. `vat validate` spawns the vat binary
22
- * once per phase and pids are reused, so `<stem>-<pid>.json` genuinely
23
- * collides; the name gains a counter rather than overwriting.
21
+ * 3. A pid can file more than one dump, and the name is CLAIMED rather than
22
+ * checked. `vat validate` spawns the vat binary once per phase and pids are
23
+ * reused, so `<stem>-<pid>.json` genuinely collides; worse, worker threads
24
+ * SHARE their parent's pid, so a thread pool's whole cohort competes for one
25
+ * `<stem>-<pid>` sequence at the same instant. The name gains a counter
26
+ * rather than overwriting, and the counter is settled by
27
+ * {@link EXCLUSIVE_CREATE} so the OS — not a check-then-write gap — decides
28
+ * who won it.
24
29
  * 4. The process's own wall and CPU time is read ONCE, at dump time. It is a
25
30
  * lifetime figure for the process and never a duration of the measured work;
26
31
  * its value is the RATIO, which tells a reader whether the wall-timed
@@ -42,7 +47,7 @@
42
47
  * thing a reader has to be able to trust identically. What each seam keeps for
43
48
  * itself is its accumulator shape, its dump body and the noun it is called by.
44
49
  */
45
- import { existsSync, writeFileSync } from 'node:fs';
50
+ import { writeFileSync } from 'node:fs';
46
51
  import { safePath } from './path-core.js';
47
52
  import { mkdirSyncReal } from './path-utils.js';
48
53
  /** `process.cpuUsage()` reports microseconds; a dump reports milliseconds. */
@@ -51,10 +56,27 @@ const MICROSECONDS_PER_MS = 1000;
51
56
  const MS_PER_SECOND = 1000;
52
57
  /**
53
58
  * Ceiling on the pid-collision search. A directory holding this many dumps for
54
- * one pid is a runaway, not a collision; overwriting the last slot is a better
55
- * outcome than spinning.
59
+ * one pid is a runaway, not a collision, and giving up on a reported line is a
60
+ * better outcome than spinning.
61
+ *
62
+ * Exhausting it does NOT fall back to overwriting the last slot. That was the
63
+ * old behaviour, and it destroyed a dump for the same reason the check-then-
64
+ * write gap did — silently, and precisely when the directory was busiest.
56
65
  */
57
- const MAX_DUMP_COLLISIONS = 1000;
66
+ export const MAX_DUMP_COLLISIONS = 1000;
67
+ /**
68
+ * The `writeFileSync` flag that makes claiming a name atomic.
69
+ *
70
+ * `wx` is create-exclusive: the OS fails the call with `EEXIST` when the path
71
+ * already exists, so exactly one caller wins each name no matter how many are
72
+ * racing for it. Asking `existsSync` first and writing second is two operations
73
+ * with a gap in between, and threads sharing a pid land in that gap routinely —
74
+ * a measured 8-worker parse pool lost 6 of 9 dumps to it, reporting 44 of 172
75
+ * documents as if that were the whole run.
76
+ */
77
+ const EXCLUSIVE_CREATE = 'wx';
78
+ /** How the failure line names the ceiling case, so it reads unlike an OS error. */
79
+ const CEILING_DETAIL = `every name up to the ${String(MAX_DUMP_COLLISIONS)}-collision ceiling was already claimed`;
58
80
  /**
59
81
  * Reduce a raw env value to a directory or `null`.
60
82
  *
@@ -119,46 +141,90 @@ export function readTimingProcess() {
119
141
  };
120
142
  }
121
143
  /**
122
- * Pick a dump path that does not already exist.
144
+ * The nth candidate name in one pid's dump sequence.
145
+ *
146
+ * @param directory - Directory dumps are written to
147
+ * @param stem - Basename and pid, already joined
148
+ * @param collision - 0 for the unsuffixed name, then the collision counter
149
+ * @returns The candidate path
150
+ */
151
+ function timingDumpCandidate(directory, stem, collision) {
152
+ const suffix = collision === 0 ? '' : `-${String(collision)}`;
153
+ return safePath.join(directory, `${stem}${suffix}.json`);
154
+ }
155
+ /**
156
+ * Whether a caught write failure means somebody else already holds the name.
157
+ *
158
+ * @param error - Whatever `writeFileSync` threw
159
+ * @returns `true` only for `EEXIST`, which is a lost race and not a fault
160
+ */
161
+ function isNameAlreadyTaken(error) {
162
+ return error instanceof Error && 'code' in error && error.code === 'EEXIST';
163
+ }
164
+ /**
165
+ * Claim a name by creating it, and fill it in the same operation.
166
+ *
167
+ * Choosing the name and writing it CANNOT be two steps: whatever separates them
168
+ * is a window in which another writer takes the name that was just declared
169
+ * free, and the loser's dump disappears with no error raised anywhere. So each
170
+ * candidate is written create-exclusively ({@link EXCLUSIVE_CREATE}) and an
171
+ * `EEXIST` — the OS saying somebody else got there — advances to the next
172
+ * counter rather than overwriting.
123
173
  *
124
174
  * @param directory - Directory dumps are written to
125
175
  * @param basename - Basename stem; the pid and any collision counter follow
126
- * @returns An unused path, or the last candidate tried
176
+ * @param contents - The already-serialized dump body
177
+ * @returns Which name was claimed, or why none could be
127
178
  */
128
- function nextTimingDumpPath(directory, basename) {
179
+ function claimTimingDump(directory, basename, contents) {
129
180
  const stem = `${basename}-${String(process.pid)}`;
130
- let candidate = safePath.join(directory, `${stem}.json`);
131
- for (let collision = 1;
132
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- operator-supplied diagnostic directory from a VAT_*_TIMING variable
133
- collision <= MAX_DUMP_COLLISIONS && existsSync(candidate); collision += 1) {
134
- candidate = safePath.join(directory, `${stem}-${String(collision)}.json`);
181
+ for (let collision = 0; collision <= MAX_DUMP_COLLISIONS; collision += 1) {
182
+ const candidate = timingDumpCandidate(directory, stem, collision);
183
+ try {
184
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- operator-supplied diagnostic directory from a VAT_*_TIMING variable
185
+ writeFileSync(candidate, contents, { encoding: 'utf-8', flag: EXCLUSIVE_CREATE });
186
+ return { outcome: 'written', path: candidate };
187
+ }
188
+ catch (error) {
189
+ if (!isNameAlreadyTaken(error))
190
+ return { outcome: 'failed', path: candidate, error };
191
+ }
135
192
  }
136
- return candidate;
193
+ return {
194
+ outcome: 'exhausted',
195
+ path: timingDumpCandidate(directory, stem, MAX_DUMP_COLLISIONS),
196
+ };
137
197
  }
138
198
  /**
139
199
  * Write one seam's dump, if the seam is on.
140
200
  *
141
201
  * The body is built lazily, inside this call, so a disabled seam never pays to
142
- * snapshot accumulators nobody will read.
202
+ * snapshot accumulators nobody will read — and a `build` that throws is reported
203
+ * like any other failure, because this runs from an `exit` listener where a
204
+ * throw would change the process's exit behaviour.
143
205
  *
144
206
  * @param noun - What the seam is called, for any failure line
145
207
  * @param directory - Where to write, or `null` when the seam is off
146
208
  * @param basename - Basename stem for the file
147
209
  * @param build - Produces the dump body
148
- * @returns The path written, or `null` when the seam is off or the write failed
210
+ * @returns The path written, or `null` when the seam is off or nothing was written
149
211
  */
150
212
  export function writeTimingDump(noun, directory, basename, build) {
151
213
  if (directory === null)
152
214
  return null;
153
- const target = nextTimingDumpPath(directory, basename);
215
+ let claim;
154
216
  try {
155
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- operator-supplied diagnostic directory from a VAT_*_TIMING variable
156
- writeFileSync(target, `${JSON.stringify(build(), null, 2)}\n`, 'utf-8');
217
+ claim = claimTimingDump(directory, basename, `${JSON.stringify(build(), null, 2)}\n`);
157
218
  }
158
219
  catch (error) {
159
- reportTimingDumpFailure(noun, target, error);
220
+ // Only `build()` and its serialization can reach here; every filesystem
221
+ // failure is already an outcome rather than a throw.
222
+ reportTimingDumpFailure(noun, directory, error);
160
223
  return null;
161
224
  }
162
- return target;
225
+ if (claim.outcome === 'written')
226
+ return claim.path;
227
+ reportTimingDumpFailure(noun, claim.path, claim.outcome === 'exhausted' ? CEILING_DETAIL : claim.error);
228
+ return null;
163
229
  }
164
230
  //# sourceMappingURL=timing-dump.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"timing-dump.js","sourceRoot":"","sources":["../src/timing-dump.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"timing-dump.js","sourceRoot":"","sources":["../src/timing-dump.ts"],"names":[],"mappings":"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"}
package/eslint/index.cjs CHANGED
@@ -54,6 +54,7 @@ const rules = {
54
54
  'no-self-package-import': require('./rules/no-self-package-import.cjs'),
55
55
  'require-justified-skip': require('./rules/require-justified-skip.cjs'),
56
56
  'no-bare-symlink-in-tests': require('./rules/no-bare-symlink-in-tests.cjs'),
57
+ 'no-process-exit-in-phase': require('./rules/no-process-exit-in-phase.cjs'),
57
58
  };
58
59
 
59
60
  /**
@@ -136,6 +137,14 @@ const RECOMMENDED_EXCLUDE = new Set([
136
137
  // ride in `recommended`; that is a public-API change and has not been made.
137
138
  // VAT enables the whole rule explicitly, scoped to its own conventions.
138
139
  'no-bare-symlink-in-tests',
140
+ // Excluded because it keys on a NAMING CONVENTION that is VAT's, not a
141
+ // portable fact — the same reason `no-unsafe-root-join` is excluded above. The
142
+ // hazard it guards is real and general (an in-process orchestrator whose step
143
+ // calls `process.exit()` silently skips every later step), but the marker is
144
+ // the `…Phase` suffix, and an adopter with an unrelated `computeRenderPhase()`
145
+ // that legitimately exits would get a finding they cannot act on. It ships in
146
+ // `rules` and VAT enables it explicitly, scoped to its own orchestrators.
147
+ 'no-process-exit-in-phase',
139
148
  ]);
140
149
 
141
150
  /**
@@ -47,13 +47,20 @@ declare namespace plugin {
47
47
  rules: Record<string, RuleModule>;
48
48
  configs: {
49
49
  /**
50
- * The cross-platform safety core: 18 of the 22 rules, 15 `error` / 3 `warn`.
51
- * Four are excluded, for three reasons: `no-test-scoped-functions` and
52
- * `require-justified-skip` are positions on test style rather than
53
- * portability facts; `no-unsafe-root-join` keys on naming rather than taint;
54
- * and `no-raw-text-decode` names a decoding seam that only exists in the
55
- * consuming repo. All four still ship in `rules` and are enabled by naming
56
- * them.
50
+ * The cross-platform safety core: 18 of the 25 rules, 15 `error` / 3 `warn`.
51
+ *
52
+ * Seven are excluded, for four reasons. `no-test-scoped-functions`,
53
+ * `require-justified-skip` and `no-bare-symlink-in-tests` are positions on
54
+ * TEST STYLE rather than portability facts. `no-unsafe-root-join` and
55
+ * `no-process-exit-in-phase` key on NAMING rather than on the property they
56
+ * care about (taint, and an orchestrated call site). `no-raw-text-decode`
57
+ * names a decoding SEAM that only exists in the consuming repo. And
58
+ * `no-self-package-import` REQUIRES an option this config cannot supply.
59
+ *
60
+ * All seven still ship in `rules` and are enabled by naming them — which is
61
+ * what this repo's own `eslint.config.js` does. The count above is asserted
62
+ * by `packages/utils/test/eslint/rules.test.ts`, so it cannot drift
63
+ * unnoticed the way it did when this comment said "four".
57
64
  */
58
65
  recommended: FlatConfig;
59
66
  };
@@ -0,0 +1,117 @@
1
+ /**
2
+ * ESLint Rule: no-process-exit-in-phase
3
+ *
4
+ * Forbid `process.exit()` inside a phase entry point — a function whose name
5
+ * ends in `Phase`.
6
+ *
7
+ * ## Why this rule exists
8
+ *
9
+ * `vat validate`, `vat verify` and `vat build` used to run each phase as a
10
+ * SEPARATE CHILD PROCESS. A `process.exit()` inside a phase was then perfectly
11
+ * safe: it ended that child, the parent read the exit code, and the run carried
12
+ * on to the next phase. Nothing about the code said so — the safety came
13
+ * entirely from the process boundary.
14
+ *
15
+ * That boundary is gone. Phases now run in the orchestrator's own process, so a
16
+ * surviving `process.exit()` ends THE WHOLE RUN: every later phase is silently
17
+ * skipped, the parent's aggregation never happens, and the process exits 0-or-1
18
+ * having done half the work with nothing in the document to say so. It is the
19
+ * worst shape of failure this codebase has — a confident, well-formed, wrong
20
+ * answer — and it cannot be caught by a type, because `process.exit()`
21
+ * typechecks anywhere.
22
+ *
23
+ * A phase must RETURN `{ document, exitCode }` and let its caller decide whether
24
+ * to print and exit (a command-line run) or to fold the result into the run (an
25
+ * orchestrated one).
26
+ *
27
+ * ## Why the name is the marker
28
+ *
29
+ * The `…Phase` suffix is the convention every phase entry point already follows
30
+ * (`runResourcesValidatePhase`, `runSkillsBuildPhase`, …). Keying the rule to it
31
+ * means a new phase is protected the moment it is named like one, with no list
32
+ * to keep in sync — a list is the thing that goes stale silently, which is the
33
+ * defect class this rule exists to prevent in the first place.
34
+ *
35
+ * The thin Commander wrappers that CALL these functions keep their
36
+ * `process.exit()`: deciding how the process ends is exactly their job, and they
37
+ * are not named `…Phase`.
38
+ */
39
+
40
+ /** Does this call expression read as `process.exit(...)`? */
41
+ function isProcessExitCall(node) {
42
+ const { callee } = node;
43
+ return (
44
+ callee.type === 'MemberExpression' &&
45
+ !callee.computed &&
46
+ callee.object.type === 'Identifier' &&
47
+ callee.object.name === 'process' &&
48
+ callee.property.type === 'Identifier' &&
49
+ callee.property.name === 'exit'
50
+ );
51
+ }
52
+
53
+ /**
54
+ * The declared name of a function-ish node, however it was declared.
55
+ *
56
+ * Covers the three spellings a phase entry point can legitimately take: a
57
+ * function declaration, a `const x = function () {}`, and a `const x = () => {}`.
58
+ * A rule that only understood declarations would be silently inert against an
59
+ * arrow const — the same blind spot `no-test-scoped-functions` documents.
60
+ */
61
+ function functionName(node) {
62
+ if (node.id && node.id.type === 'Identifier') return node.id.name;
63
+
64
+ const { parent } = node;
65
+ if (parent && parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
66
+ return parent.id.name;
67
+ }
68
+ if (parent && parent.type === 'Property' && parent.key.type === 'Identifier') {
69
+ return parent.key.name;
70
+ }
71
+ return undefined;
72
+ }
73
+
74
+ const FUNCTION_TYPES = new Set([
75
+ 'FunctionDeclaration',
76
+ 'FunctionExpression',
77
+ 'ArrowFunctionExpression',
78
+ ]);
79
+
80
+ /** The nearest enclosing function named like a phase entry point, if any. */
81
+ function enclosingPhaseName(node) {
82
+ for (let current = node.parent; current; current = current.parent) {
83
+ if (!FUNCTION_TYPES.has(current.type)) continue;
84
+ const name = functionName(current);
85
+ if (name !== undefined && name.endsWith('Phase')) return name;
86
+ }
87
+ return undefined;
88
+ }
89
+
90
+ module.exports = {
91
+ meta: {
92
+ type: 'problem',
93
+ docs: {
94
+ description:
95
+ 'Forbid process.exit() inside a phase entry point, where it would end the whole orchestrated run',
96
+ category: 'Agentic Code Safety',
97
+ recommended: true,
98
+ },
99
+ fixable: null,
100
+ schema: [],
101
+ messages: {
102
+ exitInPhase:
103
+ "'{{name}}' is a phase entry point, so process.exit() here ends the ENTIRE run — every later phase is skipped and the orchestrator never aggregates. Return { document, exitCode } instead and let the caller decide how the process ends.",
104
+ },
105
+ },
106
+
107
+ create(context) {
108
+ return {
109
+ CallExpression(node) {
110
+ if (!isProcessExitCall(node)) return;
111
+ const name = enclosingPhaseName(node);
112
+ if (name === undefined) return;
113
+ context.report({ node, messageId: 'exitInPhase', data: { name } });
114
+ },
115
+ };
116
+ },
117
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-agent-toolkit/utils",
3
- "version": "0.2.0-rc.3",
3
+ "version": "0.2.0-rc.4",
4
4
  "type": "module",
5
5
  "description": "Core utility functions shared across the vibe-agent-toolkit packages",
6
6
  "sideEffects": [
@@ -54,9 +54,9 @@
54
54
  "types": "./dist/zod.d.ts",
55
55
  "import": "./dist/zod.js"
56
56
  },
57
- "./template": {
58
- "types": "./dist/template-entry.d.ts",
59
- "import": "./dist/template-entry.js"
57
+ "./skill-test": {
58
+ "types": "./dist/skill-test/index.d.ts",
59
+ "import": "./dist/skill-test/index.js"
60
60
  },
61
61
  "./yaml": {
62
62
  "types": "./dist/yaml.d.ts",
@@ -90,17 +90,15 @@
90
90
  "README.md"
91
91
  ],
92
92
  "scripts": {
93
- "build": "rimraf --glob dist \"*.tsbuildinfo\" && tsc && tsx ../dev-tools/src/copy-yaml-assets.ts",
93
+ "build": "rimraf --glob dist \"*.tsbuildinfo\" && tsc",
94
94
  "test:unit": "vitest run",
95
95
  "test:integration": "vitest run --config vitest.integration.config.ts",
96
- "test:system": "vitest run --config vitest.system.config.ts",
97
96
  "test:watch": "vitest",
98
97
  "typecheck": "tsc --noEmit",
99
98
  "clean": "rimraf dist *.tsbuildinfo"
100
99
  },
101
100
  "dependencies": {
102
101
  "@vibe-validate/git": "0.20.1",
103
- "handlebars": "^4.7.8",
104
102
  "ignore": "^7.0.5",
105
103
  "picomatch": "^4.0.3",
106
104
  "which": "^5.0.0",
@@ -1,34 +0,0 @@
1
- /**
2
- * Render auth-header templates with rendered values, and a structural
3
- * redaction helper for serialization.
4
- *
5
- * `buildHeaders` renders each header value template against a context that
6
- * carries `${token}` plus any named captures / vars from the rewrite step.
7
- * `redactHeaders` masks `Authorization` values for any caller that needs to
8
- * serialize headers into logs, errors, or cache entries — the design's §8
9
- * "tokens never leak" claim depends on every such site routing through this.
10
- *
11
- * Per design issue #113 §4 (auth.headers vocabulary) and §8 (redaction is
12
- * structural; Authorization values never appear in serialized output).
13
- */
14
- export declare const REDACTED_VALUE = "<redacted>";
15
- /**
16
- * Render a map of header templates into a map of concrete header values.
17
- *
18
- * @throws {TemplateMissingVarError} if a header template references an
19
- * unknown context key
20
- * @throws {TemplateSyntaxError} from a malformed template expression
21
- * @throws {UnknownTransformError} from a template calling an unknown transform
22
- */
23
- export declare function buildHeaders(templates: Record<string, string>, context: Record<string, string>): Record<string, string>;
24
- /**
25
- * Return a copy of `headers` with sensitive values replaced by `REDACTED_VALUE`.
26
- * Header-name matching is case-insensitive but exact (no prefix matching) — a
27
- * header like `X-Authorization-Foo` is NOT considered sensitive.
28
- *
29
- * **Input must be a plain key-value object.** A `Headers` instance (Web Fetch
30
- * API) or a `Map` yields `[]` from `Object.entries` and would silently no-op
31
- * redaction — converting to a plain object is the caller's responsibility.
32
- */
33
- export declare function redactHeaders(headers: Record<string, string>): Record<string, string>;
34
- //# sourceMappingURL=build-headers.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"build-headers.d.ts","sourceRoot":"","sources":["../../src/link-auth/build-headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,eAAO,MAAM,cAAc,eAAe,CAAC;AAc3C;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMxB;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMrF"}
@@ -1,58 +0,0 @@
1
- /**
2
- * Render auth-header templates with rendered values, and a structural
3
- * redaction helper for serialization.
4
- *
5
- * `buildHeaders` renders each header value template against a context that
6
- * carries `${token}` plus any named captures / vars from the rewrite step.
7
- * `redactHeaders` masks `Authorization` values for any caller that needs to
8
- * serialize headers into logs, errors, or cache entries — the design's §8
9
- * "tokens never leak" claim depends on every such site routing through this.
10
- *
11
- * Per design issue #113 §4 (auth.headers vocabulary) and §8 (redaction is
12
- * structural; Authorization values never appear in serialized output).
13
- */
14
- import { renderTemplate } from './template.js';
15
- export const REDACTED_VALUE = '<redacted>';
16
- /**
17
- * Header names whose values must be masked when serialized.
18
- *
19
- * v1 ships only `authorization` because that is the only secret-bearing
20
- * header the current macros emit. **Omission is the security risk** — any
21
- * future macro that emits a header carrying a secret (`Cookie`,
22
- * `Proxy-Authorization`, `X-API-Key`, custom bearer-style headers) must add
23
- * that name here, or the token will silently leak through serialization.
24
- * Extending the set is not the dangerous edit; forgetting to extend it is.
25
- */
26
- const SENSITIVE_HEADER_NAMES = new Set(['authorization']);
27
- /**
28
- * Render a map of header templates into a map of concrete header values.
29
- *
30
- * @throws {TemplateMissingVarError} if a header template references an
31
- * unknown context key
32
- * @throws {TemplateSyntaxError} from a malformed template expression
33
- * @throws {UnknownTransformError} from a template calling an unknown transform
34
- */
35
- export function buildHeaders(templates, context) {
36
- const headers = Object.create(null);
37
- for (const [name, template] of Object.entries(templates)) {
38
- headers[name] = renderTemplate(template, context);
39
- }
40
- return headers;
41
- }
42
- /**
43
- * Return a copy of `headers` with sensitive values replaced by `REDACTED_VALUE`.
44
- * Header-name matching is case-insensitive but exact (no prefix matching) — a
45
- * header like `X-Authorization-Foo` is NOT considered sensitive.
46
- *
47
- * **Input must be a plain key-value object.** A `Headers` instance (Web Fetch
48
- * API) or a `Map` yields `[]` from `Object.entries` and would silently no-op
49
- * redaction — converting to a plain object is the caller's responsibility.
50
- */
51
- export function redactHeaders(headers) {
52
- const redacted = Object.create(null);
53
- for (const [name, value] of Object.entries(headers)) {
54
- redacted[name] = SENSITIVE_HEADER_NAMES.has(name.toLowerCase()) ? REDACTED_VALUE : value;
55
- }
56
- return redacted;
57
- }
58
- //# sourceMappingURL=build-headers.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"build-headers.js","sourceRoot":"","sources":["../../src/link-auth/build-headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,MAAM,CAAC,MAAM,cAAc,GAAG,YAAY,CAAC;AAE3C;;;;;;;;;GASG;AACH,MAAM,sBAAsB,GAAwB,IAAI,GAAG,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC;AAE/E;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAC1B,SAAiC,EACjC,OAA+B;IAE/B,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAA2B,CAAC;IAC9D,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QACzD,OAAO,CAAC,IAAI,CAAC,GAAG,cAAc,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACpD,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,OAA+B;IAC3D,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAA2B,CAAC;IAC/D,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACpD,QAAQ,CAAC,IAAI,CAAC,GAAG,sBAAsB,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC;IAC3F,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -1,38 +0,0 @@
1
- /**
2
- * Macro loader + expander.
3
- *
4
- * Loads the bundled `macros.yaml` once at module init and exposes
5
- * `expandMacro(name, overrides?)` which deep-merges the named macro with any
6
- * adopter overrides. The merge is "adopter wins": objects merge recursively,
7
- * arrays and primitives are replaced wholesale (no element-wise array merge).
8
- *
9
- * The shipped macro file is at `link-auth/macros.yaml`, copied into the dist
10
- * tree by `packages/dev-tools/src/copy-yaml-assets.ts` during build so the
11
- * runtime `fs.readFileSync(new URL('./macros.yaml', import.meta.url))`
12
- * resolves in both source-mode (vitest) and built-mode (dist).
13
- *
14
- * Per design issue #113 §5 (macros are config, not a privileged code path).
15
- */
16
- /**
17
- * Thrown when a `use: <name>` references a macro not in the shipped set.
18
- * Message lists the available macros so a typo surfaces clearly.
19
- */
20
- export declare class UnknownMacroError extends Error {
21
- constructor(name: string, available: readonly string[]);
22
- }
23
- /**
24
- * Look up a macro by name and deep-merge optional adopter overrides on top.
25
- *
26
- * Merge semantics:
27
- * - Plain objects merge recursively (sibling keys preserved).
28
- * - Arrays are replaced wholesale (override's array wins; no concat).
29
- * - Primitives are replaced.
30
- * - `undefined` in an override is treated as "not provided" (base wins).
31
- *
32
- * The returned object and all nested plain objects use null prototypes so
33
- * `__proto__` / `constructor` keys can never poison consumers.
34
- *
35
- * @throws {UnknownMacroError} if `name` is not in the shipped macro set
36
- */
37
- export declare function expandMacro(name: string, overrides?: Record<string, unknown>): Record<string, unknown>;
38
- //# sourceMappingURL=expand-macro.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"expand-macro.d.ts","sourceRoot":"","sources":["../../src/link-auth/expand-macro.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AA+CH;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,MAAM,EAAE;CAIvD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAYzB"}