@skyramp/mcp 0.4.4 → 0.5.0-rc.10

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 (144) hide show
  1. package/build/entitlements/entitlementGate.d.ts +8 -0
  2. package/build/entitlements/entitlementGate.js +57 -0
  3. package/build/entitlements/runCodes.d.ts +18 -0
  4. package/build/entitlements/runCodes.js +16 -0
  5. package/build/index.js +32 -6
  6. package/build/playwright/PlaywrightTraceService.d.ts +17 -0
  7. package/build/playwright/PlaywrightTraceService.js +29 -1
  8. package/build/playwright/dataSafetyConfig.d.ts +74 -0
  9. package/build/playwright/dataSafetyConfig.js +109 -0
  10. package/build/playwright/essentialTools.d.ts +1 -0
  11. package/build/playwright/essentialTools.js +44 -0
  12. package/build/playwright/registerPlaywrightTools.d.ts +36 -1
  13. package/build/playwright/registerPlaywrightTools.js +87 -43
  14. package/build/playwright/safeResolveDataSafety.d.ts +24 -0
  15. package/build/playwright/safeResolveDataSafety.js +33 -0
  16. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.d.ts +17 -0
  17. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +46 -177
  18. package/build/prompts/modularization/ui-test-modularization.d.ts +1 -0
  19. package/build/prompts/modularization/ui-test-modularization.js +9 -2
  20. package/build/prompts/test-maintenance/driftAnalysisShared.js +1 -1
  21. package/build/services/FleetExecutionService.d.ts +87 -16
  22. package/build/services/FleetExecutionService.js +487 -48
  23. package/build/services/FleetRunRegistry.d.ts +49 -0
  24. package/build/services/FleetRunRegistry.js +178 -0
  25. package/build/services/containerEnv.js +8 -2
  26. package/build/services/fleetLimitConfirmation.d.ts +19 -0
  27. package/build/services/fleetLimitConfirmation.js +126 -0
  28. package/build/tool-phases.d.ts +14 -8
  29. package/build/tool-phases.js +88 -43
  30. package/build/tools/code-refactor/assertion-state.js +3 -1
  31. package/build/tools/code-refactor/codeReuseTool.js +2 -1
  32. package/build/tools/code-refactor/utils-verify-gates.js +1 -1
  33. package/build/tools/executeFleetTool.d.ts +6 -4
  34. package/build/tools/executeFleetTool.js +67 -23
  35. package/build/tools/fixErrorTool.js +2 -1
  36. package/build/tools/fleetLifecycleTools.d.ts +20 -0
  37. package/build/tools/fleetLifecycleTools.js +124 -0
  38. package/build/tools/generate-tests/generateIntegrationRestTool.d.ts +38 -0
  39. package/build/tools/generate-tests/generateIntegrationRestTool.js +7 -1
  40. package/build/tools/generate-tests/generateUILoadRestTool.d.ts +19 -1
  41. package/build/tools/generate-tests/generateUILoadRestTool.js +316 -50
  42. package/build/tools/submitReportTool.js +4 -122
  43. package/build/tools/test-management/registerTestPlanTool.js +22 -6
  44. package/build/tools/workspace/initScanWorkspaceTool.d.ts +11 -1
  45. package/build/tools/workspace/initScanWorkspaceTool.js +111 -9
  46. package/build/tools/workspace/initializeWorkspaceTool.js +40 -4
  47. package/build/types/AssertionOutcome.d.ts +3 -0
  48. package/build/types/TestbotReport.d.ts +0 -37
  49. package/build/types/TestbotReport.js +0 -22
  50. package/build/types/index.d.ts +1 -1
  51. package/build/types/index.js +1 -1
  52. package/build/utils/assertion-verify/created-add-guard.d.ts +12 -0
  53. package/build/utils/assertion-verify/created-add-guard.js +173 -0
  54. package/build/utils/assertion-verify/index.d.ts +1 -0
  55. package/build/utils/assertion-verify/index.js +1 -0
  56. package/build/utils/assertion-verify/verify.d.ts +7 -3
  57. package/build/utils/assertion-verify/verify.js +9 -3
  58. package/build/utils/featureFlags.js +6 -3
  59. package/build/utils/repositorySlug.d.ts +14 -0
  60. package/build/utils/repositorySlug.js +46 -1
  61. package/build/utils/telemetry.d.ts +2 -0
  62. package/build/utils/telemetry.js +12 -0
  63. package/build/utils/utils-verify/action-key.d.ts +59 -2
  64. package/build/utils/utils-verify/action-key.js +162 -6
  65. package/build/utils/utils-verify/action-sites.d.ts +7 -0
  66. package/build/utils/utils-verify/action-sites.js +277 -35
  67. package/build/utils/utils-verify/call-sites.d.ts +8 -2
  68. package/build/utils/utils-verify/language-spec.js +1 -1
  69. package/build/utils/utils-verify/parse.d.ts +4 -0
  70. package/build/utils/utils-verify/parse.js +2 -1
  71. package/build/utils/versions.d.ts +3 -3
  72. package/build/utils/versions.js +1 -1
  73. package/build/workspace/workspace.d.ts +63 -121
  74. package/build/workspace/workspace.js +45 -48
  75. package/build/workspace/workspaceVerifier.d.ts +22 -0
  76. package/build/workspace/workspaceVerifier.js +65 -0
  77. package/node_modules/playwright/lib/dom-analyzer/blueprint.js +7 -6
  78. package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +5 -4
  79. package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +12 -0
  80. package/node_modules/playwright/lib/mcp/skyramp/dataSafetyGuard.js +822 -0
  81. package/node_modules/playwright/lib/mcp/skyramp/dataSafetySession.js +372 -0
  82. package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +101 -8
  83. package/node_modules/playwright/lib/mcp/skyramp/finishTestSetupTool.js +45 -0
  84. package/node_modules/playwright/lib/mcp/skyramp/index.js +16 -0
  85. package/node_modules/playwright/lib/mcp/skyramp/recordingPhase.js +65 -0
  86. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +1 -0
  87. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +598 -127
  88. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +85 -31
  89. package/node_modules/playwright/lib/mcp/test/skyRampExport.test.js +132 -5
  90. package/node_modules/playwright/node_modules/playwright-core/lib/cli/program.js +2 -0
  91. package/node_modules/playwright/node_modules/playwright-core/lib/client/browserContext.js +4 -2
  92. package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
  93. package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
  94. package/node_modules/playwright/node_modules/playwright-core/lib/protocol/validator.js +2 -1
  95. package/node_modules/playwright/node_modules/playwright-core/lib/server/browserContext.js +17 -1
  96. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
  97. package/node_modules/playwright/node_modules/playwright-core/lib/server/har/harRecorder.js +1 -0
  98. package/node_modules/playwright/node_modules/playwright-core/lib/server/har/harTracer.js +1 -1
  99. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderRunner.js +2 -1
  100. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js +2 -1
  101. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/clickExpandedState.js +75 -0
  102. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder.js +4 -2
  103. package/node_modules/playwright/node_modules/playwright-core/lib/utils/isomorphic/volatileDate.js +85 -0
  104. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-Bou8kKzE.js → codeMirrorModule-BuhAdckO.js} +1 -1
  105. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-COAHN430.js +809 -0
  106. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.0MJW0uG7.js → index.7AU8h0PZ.js} +1 -1
  107. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  108. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.GLqsx5cI.js → uiMode.Dl7smBJn.js} +1 -1
  109. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  110. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  111. package/node_modules/playwright/node_modules/playwright-core/src/cli/program.ts +5 -0
  112. package/node_modules/playwright/node_modules/playwright-core/src/client/browserContext.ts +3 -1
  113. package/node_modules/playwright/node_modules/playwright-core/src/client/types.ts +2 -0
  114. package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
  115. package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
  116. package/node_modules/playwright/node_modules/playwright-core/src/protocol/validator.ts +1 -0
  117. package/node_modules/playwright/node_modules/playwright-core/src/server/browserContext.ts +25 -1
  118. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +2 -0
  119. package/node_modules/playwright/node_modules/playwright-core/src/server/har/harRecorder.ts +1 -0
  120. package/node_modules/playwright/node_modules/playwright-core/src/server/har/harTracer.ts +3 -1
  121. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderRunner.ts +3 -1
  122. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderSignalProcessor.ts +1 -0
  123. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/clickExpandedState.ts +82 -0
  124. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder.ts +2 -0
  125. package/node_modules/playwright/node_modules/playwright-core/src/utils/isomorphic/volatileDate.ts +137 -1
  126. package/node_modules/playwright/package.json +1 -1
  127. package/package.json +4 -4
  128. package/plugin/prompts/plan-tests.md +2 -2
  129. package/plugin/prompts/testbot-task1.md +1 -1
  130. package/plugin/skills/enhance-assertions/reference/integration.md +1 -0
  131. package/plugin/skills/setup-testbot-target-sut/SKILL.md +60 -0
  132. package/plugin/skills/setup-testbot-target-sut/reference/ci-workflow.md +26 -0
  133. package/plugin/skills/setup-testbot-target-sut/reference/deployed-environment.md +105 -0
  134. package/plugin/skills/setup-testbot-target-sut/reference/existing-tests.md +14 -0
  135. package/plugin/skills/setup-testbot-target-sut/reference/troubleshooting.md +20 -0
  136. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.d.ts +0 -2
  137. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +0 -178
  138. package/build/prompts/sut-setup/modes/dockerComposePrompt.d.ts +0 -2
  139. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +0 -311
  140. package/build/prompts/sut-setup/shared.d.ts +0 -62
  141. package/build/prompts/sut-setup/shared.js +0 -196
  142. package/build/resources/sutSetupResource.d.ts +0 -2
  143. package/build/resources/sutSetupResource.js +0 -63
  144. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-DTp2-8Si.js +0 -809
@@ -0,0 +1,822 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var dataSafetyGuard_exports = {};
30
+ __export(dataSafetyGuard_exports, {
31
+ DataSafetyGuard: () => DataSafetyGuard,
32
+ TEARDOWN_DELETE_TIMEOUT_MS: () => TEARDOWN_DELETE_TIMEOUT_MS,
33
+ blockReason: () => blockReason,
34
+ compileOffLimits: () => compileOffLimits,
35
+ matchesOffLimits: () => matchesOffLimits,
36
+ parseDataSafetyConfig: () => parseDataSafetyConfig,
37
+ renderCappedList: () => renderCappedList,
38
+ teardownCredentialHeaders: () => teardownCredentialHeaders,
39
+ writeLedgerFile: () => writeLedgerFile
40
+ });
41
+ module.exports = __toCommonJS(dataSafetyGuard_exports);
42
+ var import_fs = __toESM(require("fs"));
43
+ const MODES = ["strict", "readonly", "off"];
44
+ const TEARDOWN_DELETE_TIMEOUT_MS = 1e4;
45
+ function writeLedgerFile(file, json) {
46
+ const temp = `${file}.${process.pid}.tmp`;
47
+ try {
48
+ import_fs.default.writeFileSync(temp, json);
49
+ import_fs.default.renameSync(temp, file);
50
+ } catch (e) {
51
+ import_fs.default.rmSync(temp, { force: true });
52
+ throw e;
53
+ }
54
+ }
55
+ const SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
56
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
57
+ const NUMERIC_RE = /^\d+$/;
58
+ const OBJECTID_RE = /^[0-9a-f]{24}$/i;
59
+ const AUTH_PATH_RE = /\/(login|logout|signin|sign-in|signup|sign-up|auth|oauth|token|sessions?)(\/|$)/i;
60
+ const AUTH_TOKEN_SEGMENT_RE = /^(login|logout|signin|sign-in|signup|sign-up|auth|oauth|token|sessions?)$/i;
61
+ const GRAPHQL_PATH_RE = /(^|\/)(graphql(\.json)?|gql)(\/|$)/i;
62
+ function isAuthExemptPath(pathName) {
63
+ const segs = pathName.split("/").filter(Boolean);
64
+ if (!segs.length)
65
+ return false;
66
+ const last = segs[segs.length - 1];
67
+ if (AUTH_TOKEN_SEGMENT_RE.test(last))
68
+ return true;
69
+ return segs.length === 2 && AUTH_TOKEN_SEGMENT_RE.test(segs[0]);
70
+ }
71
+ const READONLY_LOGIN_STEP_RE = /^(login|logout|signin|sign-in|sign_in|signout|sign-out|sign_out|authenticate)$/i;
72
+ const READONLY_AUTH_WORD_RE = /^(token|refresh|session|sessions|callback|authorize|auth|mfa|otp|2fa|totp)$/i;
73
+ const READONLY_AUTH_PREFIX_RE = /^(api|v\d+(\.\d+)?|auth|oauth2?|connect|openid-connect|login-actions|sso|identity|idp|user|users|account|accounts|login|session|sessions|token)$/i;
74
+ function isReadonlyAuthPath(pathName) {
75
+ const segs = pathName.split("/").filter(Boolean);
76
+ if (!segs.length || segs.some(isIdShaped))
77
+ return false;
78
+ const last = segs[segs.length - 1];
79
+ if (READONLY_LOGIN_STEP_RE.test(last))
80
+ return true;
81
+ if (!READONLY_AUTH_WORD_RE.test(last))
82
+ return false;
83
+ return segs.length === 1 || READONLY_AUTH_PREFIX_RE.test(segs[segs.length - 2]);
84
+ }
85
+ const ID_FIELDS = ["id", "uuid", "_id", "pk"];
86
+ const UNSAFE_ID_RE = /[/\\?#\x00-\x20\x7f]/;
87
+ const UNSAFE_ID_ENCODED_RE = /%2f|%5c|%2e/i;
88
+ const MAX_ID_LENGTH = 512;
89
+ function isValidId(id) {
90
+ if (id === "" || id === "." || id === "..")
91
+ return false;
92
+ if (id.length > MAX_ID_LENGTH)
93
+ return false;
94
+ if (UNSAFE_ID_RE.test(id) || UNSAFE_ID_ENCODED_RE.test(id))
95
+ return false;
96
+ return true;
97
+ }
98
+ function isIdShaped(seg) {
99
+ return NUMERIC_RE.test(seg) || UUID_RE.test(seg) || OBJECTID_RE.test(seg);
100
+ }
101
+ const MAX_EXTRACT_DEPTH = 20;
102
+ const UNSAFE_NUMERIC_ID = Symbol("unsafe-numeric-id");
103
+ function extractCreatedId(parsed, depth = 0, derivedIdFields = []) {
104
+ if (depth > MAX_EXTRACT_DEPTH)
105
+ return null;
106
+ if (Array.isArray(parsed))
107
+ return parsed.length === 1 ? extractCreatedId(parsed[0], depth + 1, derivedIdFields) : null;
108
+ if (parsed && typeof parsed === "object") {
109
+ const obj = parsed;
110
+ for (const f of [...ID_FIELDS, ...derivedIdFields]) {
111
+ const v = obj[f];
112
+ if (v === void 0 || v === null)
113
+ continue;
114
+ if (typeof v === "string")
115
+ return v;
116
+ if (typeof v === "number")
117
+ return Number.isSafeInteger(v) ? String(v) : UNSAFE_NUMERIC_ID;
118
+ if (typeof v === "object" && !Array.isArray(v)) {
119
+ const found = extractCreatedId(v, depth + 1, derivedIdFields);
120
+ if (found !== null)
121
+ return found;
122
+ }
123
+ }
124
+ if (depth < 2) {
125
+ for (const key of ["data", "result"]) {
126
+ const nested = obj[key];
127
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
128
+ const found = extractCreatedId(nested, depth + 1, derivedIdFields);
129
+ if (found !== null)
130
+ return found;
131
+ }
132
+ }
133
+ }
134
+ return null;
135
+ }
136
+ return null;
137
+ }
138
+ function collectionDerivedIdFields(collectionPathname) {
139
+ const segs = collectionPathname.split("/").filter(Boolean);
140
+ const collection = segs[segs.length - 1] ?? "";
141
+ if (collection.length < 3 || !/s$/i.test(collection) || !/^[a-z][a-z0-9_-]*$/i.test(collection))
142
+ return [];
143
+ const singular = collection.slice(0, -1).toLowerCase();
144
+ return [`${singular}_id`, `${singular}Id`];
145
+ }
146
+ function renderCappedList(items, tailWord, cap = 10) {
147
+ const shown = items.slice(0, cap);
148
+ const remaining = items.length - shown.length;
149
+ const lines = [...shown];
150
+ if (remaining > 0)
151
+ lines.push(`\u2026and ${remaining} more ${tailWord}`);
152
+ return lines.join("\n");
153
+ }
154
+ function globToRegExp(glob) {
155
+ const DOUBLE_STAR = "\0";
156
+ const escaped = glob.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, DOUBLE_STAR).replace(/\*/g, "[^/]*").replace(/\u0000/g, ".*");
157
+ return new RegExp(`^${escaped}$`, "i");
158
+ }
159
+ function compileOffLimits(patterns) {
160
+ const compiled = patterns.map(compileOffLimitsPattern);
161
+ return (url) => compiled.some((re) => re.test(url));
162
+ }
163
+ function matchesOffLimits(url, patterns) {
164
+ return compileOffLimits(patterns)(url);
165
+ }
166
+ function isUnmatchablePattern(pattern) {
167
+ return !pattern.includes("://") && !pattern.startsWith("*");
168
+ }
169
+ function compileOffLimitsPattern(pattern) {
170
+ if (isUnmatchablePattern(pattern))
171
+ process.stderr.write(`[skyramp] Data safety: offLimits pattern "${pattern}" has no scheme/host and will match nothing \u2014 patterns match the full URL (e.g. "**/admin/**")
172
+ `);
173
+ if (pattern.endsWith("/**")) {
174
+ const root = pattern.slice(0, -3);
175
+ return new RegExp(`(?:${globToRegExp(pattern).source})|(?:${globToRegExp(root).source})`, "i");
176
+ }
177
+ return globToRegExp(pattern);
178
+ }
179
+ function warnInvalidConfig(reason, outcome) {
180
+ process.stderr.write(`[skyramp] Data safety: invalid config (${reason}) \u2014 ${outcome}
181
+ `);
182
+ }
183
+ function validateConfig(parsed) {
184
+ if (!parsed || typeof parsed !== "object") {
185
+ warnInvalidConfig("config is not an object", "failing closed to mode=strict");
186
+ return freezeConfig({ mode: "strict", offLimits: [] });
187
+ }
188
+ const p = parsed;
189
+ const errors = [];
190
+ let mode;
191
+ if (MODES.includes(p.mode)) {
192
+ mode = p.mode;
193
+ } else {
194
+ mode = "strict";
195
+ warnInvalidConfig(`mode must be "strict", "readonly" or "off", got ${JSON.stringify(p.mode)}`, "failing closed to mode=strict");
196
+ }
197
+ let offLimits = [];
198
+ if (typeof p.offLimits === "string") {
199
+ offLimits = [p.offLimits];
200
+ warnInvalidConfig("offLimits must be an array of strings, got one string", "using it as one pattern");
201
+ } else if (Array.isArray(p.offLimits)) {
202
+ offLimits = p.offLimits.filter((x) => typeof x === "string");
203
+ if (offLimits.length !== p.offLimits.length)
204
+ errors.push(`offLimits has ${p.offLimits.length - offLimits.length} entries that are not strings`);
205
+ } else if (p.offLimits !== void 0 && p.offLimits !== null) {
206
+ errors.push("offLimits must be an array of strings");
207
+ }
208
+ const unmatchable = offLimits.filter(isUnmatchablePattern);
209
+ if (unmatchable.length)
210
+ errors.push(`offLimits patterns match the full URL; start each with a scheme (https://) or *: ${unmatchable.map((x) => JSON.stringify(x)).join(", ")}`);
211
+ let ledgerDir;
212
+ if (typeof p.ledgerDir === "string" && p.ledgerDir.length > 0)
213
+ ledgerDir = p.ledgerDir;
214
+ else if (p.ledgerDir !== void 0 && p.ledgerDir !== null)
215
+ errors.push("ledgerDir must be a non-empty string");
216
+ const startupError = errors.length ? `Data safety config is invalid: ${errors.join("; ")}.` : void 0;
217
+ if (startupError)
218
+ warnInvalidConfig(errors.join("; "), "every tool call will fail until the config is fixed");
219
+ return freezeConfig({ mode, offLimits, ...ledgerDir ? { ledgerDir } : {}, ...startupError ? { startupError } : {} });
220
+ }
221
+ function freezeConfig(config) {
222
+ return Object.freeze({ ...config, offLimits: Object.freeze([...config.offLimits]) });
223
+ }
224
+ function parseDataSafetyConfig(options, envValue) {
225
+ if (options !== void 0)
226
+ return validateConfig(options);
227
+ if (!envValue)
228
+ return freezeConfig({ mode: "off", offLimits: [] });
229
+ const bareMode = envValue.trim().toLowerCase();
230
+ if (MODES.includes(bareMode))
231
+ return freezeConfig({ mode: bareMode, offLimits: [] });
232
+ let parsed;
233
+ try {
234
+ parsed = JSON.parse(envValue);
235
+ } catch (e) {
236
+ warnInvalidConfig(`malformed SKYRAMP_DATA_SAFETY JSON: ${String(e)}`, "failing closed to mode=strict");
237
+ return freezeConfig({ mode: "strict", offLimits: [] });
238
+ }
239
+ return validateConfig(parsed);
240
+ }
241
+ class DataSafetyGuard {
242
+ /**
243
+ * nextLedgerPath, when given, names a fresh ledger file for each new
244
+ * session after resetSession() — so a ledger in a caller-read directory
245
+ * keeps the finished session's record instead of being overwritten.
246
+ */
247
+ constructor(config, ledgerPath, _setupCompleted, _nextLedgerPath) {
248
+ this._setupCompleted = _setupCompleted;
249
+ this._nextLedgerPath = _nextLedgerPath;
250
+ // Ownership is URL-scoped: (origin, entity pathname) pairs derived from the
251
+ // ledgered entityUrls. A bare id is never owned globally — order id 7 must
252
+ // not grant DELETE /api/users/7, a different origin, or an id a third-party
253
+ // beacon happened to return (ledger poisoning is contained to the beacon's
254
+ // own URL).
255
+ this._ownedEntities = [];
256
+ this._blocks = [];
257
+ this._pendingNotices = [];
258
+ this._entries = [];
259
+ // 2xx/3xx POST responses where no entity id could be identified — teardown
260
+ // cannot delete what the ledger never recorded, so this is the audit trail
261
+ // for entities that may have been created but are otherwise untracked.
262
+ this._unparsedCreates = [];
263
+ // Only warn once — a broken ledger path must not spam stderr on every
264
+ // single request for the rest of the session.
265
+ this._persistWarned = false;
266
+ this._persistFailed = false;
267
+ this._teardownStarted = false;
268
+ // The credential headers each create was sent with, so teardown can
269
+ // authenticate the way the app did (a JS-set Authorization header never
270
+ // reaches the context's cookie jar). Memory only, by design: the ledger
271
+ // is written to disk, zipped and uploaded, and must never carry a secret.
272
+ this._teardownHeaders = /* @__PURE__ */ new Map();
273
+ // Entity URLs (origin + normalized path) that answered 2xx during the
274
+ // recording. A 404 on teardown means "already gone" only for a URL known
275
+ // to exist; otherwise it can mean the guard built the wrong delete URL.
276
+ this._confirmedEntities = /* @__PURE__ */ new Set();
277
+ // Entries whose teardown DELETE timed out: the server may have deleted
278
+ // them, which a later 404 must say.
279
+ this._timedOutDeletes = /* @__PURE__ */ new Set();
280
+ this.config = Object.isFrozen(config) ? config : freezeConfig(config);
281
+ this._ledgerPath = ledgerPath;
282
+ this._offLimits = this.config.offLimits.map(compileOffLimitsPattern);
283
+ }
284
+ /** Where the crash-safe ledger is persisted right now. */
285
+ get ledgerPath() {
286
+ return this._ledgerPath;
287
+ }
288
+ /** Grant session ownership of one entity URL (origin + path scoped). Only a ledger entry grants it. */
289
+ _addOwnedEntity(entityUrl) {
290
+ const parsed = tryParseUrl(entityUrl);
291
+ if (parsed)
292
+ this._ownedEntities.push({ origin: parsed.origin, pathname: normalizePathname(parsed.pathname) });
293
+ }
294
+ get ledger() {
295
+ return this._entries;
296
+ }
297
+ /** Move a ledger entry to another source action, or drop its source action (undefined). */
298
+ setSourceAction(entry, actionId) {
299
+ if (!this._entries.includes(entry))
300
+ return;
301
+ if (actionId)
302
+ entry.sourceActionId = actionId;
303
+ else
304
+ delete entry.sourceActionId;
305
+ this._persist();
306
+ }
307
+ get unparsedCreates() {
308
+ return this._unparsedCreates;
309
+ }
310
+ /**
311
+ * requestHeaders: the create request's headers. Only the credential ones
312
+ * (teardownCredentialHeaders) are kept, in memory, for that entity's
313
+ * teardown DELETE.
314
+ */
315
+ recordResponse(method, url, status, locationHeader, body, contentType, context, requestHeaders) {
316
+ if (this.config.mode !== "strict")
317
+ return;
318
+ if (method.toUpperCase() !== "POST")
319
+ return;
320
+ const is2xx = status >= 200 && status < 300;
321
+ const is3xx = status >= 300 && status < 400;
322
+ if (!is2xx && !is3xx)
323
+ return;
324
+ const pathName = safePathname(url);
325
+ if (isAuthExemptPath(pathName)) {
326
+ this.recordUnparsedCreate(url, status, contentType, "auth-exempt");
327
+ return;
328
+ }
329
+ if (GRAPHQL_PATH_RE.test(pathName))
330
+ return;
331
+ let entityUrl = null;
332
+ let id = null;
333
+ let idSource = null;
334
+ let unparsedRule;
335
+ if (locationHeader) {
336
+ try {
337
+ const resolved = new URL(locationHeader, url);
338
+ const requestUrl = new URL(url);
339
+ if (resolved.origin === requestUrl.origin) {
340
+ const resolvedPath = normalizePathname(resolved.pathname);
341
+ const requestPath = normalizePathname(requestUrl.pathname);
342
+ if (resolvedPath.length > requestPath.length && resolvedPath.startsWith(requestPath + "/")) {
343
+ const segs = resolvedPath.split("/").filter(Boolean);
344
+ const lastSeg = segs[segs.length - 1] ?? null;
345
+ if (lastSeg && isValidId(lastSeg) && isIdShaped(lastSeg)) {
346
+ resolved.search = "";
347
+ resolved.hash = "";
348
+ entityUrl = resolved.toString();
349
+ id = lastSeg;
350
+ idSource = "locationHeader";
351
+ }
352
+ }
353
+ }
354
+ } catch {
355
+ }
356
+ }
357
+ if (is2xx && (!entityUrl || !id) && body) {
358
+ let parsedBody;
359
+ let bodyParsed = true;
360
+ try {
361
+ parsedBody = JSON.parse(body);
362
+ } catch {
363
+ bodyParsed = false;
364
+ }
365
+ if (bodyParsed) {
366
+ let rawId = null;
367
+ try {
368
+ const extracted = extractCreatedId(parsedBody, 0, collectionDerivedIdFields(pathName));
369
+ if (extracted === UNSAFE_NUMERIC_ID)
370
+ unparsedRule = "unsafe-numeric-id";
371
+ else
372
+ rawId = extracted;
373
+ } catch {
374
+ }
375
+ if (rawId !== null && isValidId(rawId)) {
376
+ try {
377
+ const collection = new URL(url);
378
+ collection.search = "";
379
+ collection.hash = "";
380
+ const candidate = `${collection.toString().replace(/\/+$/, "")}/${rawId}`;
381
+ if (isSingleChildPath(new URL(candidate), collection)) {
382
+ entityUrl = candidate;
383
+ id = rawId;
384
+ idSource = "responseBody";
385
+ }
386
+ } catch {
387
+ }
388
+ }
389
+ }
390
+ }
391
+ if (entityUrl && id && idSource) {
392
+ this._addOwnedEntity(entityUrl);
393
+ const entry = {
394
+ entityUrl,
395
+ id,
396
+ method: "POST",
397
+ createdAt: Date.now(),
398
+ deleteEndpoint: entityUrl,
399
+ phase: context.phase,
400
+ ...context.actionId ? { sourceActionId: context.actionId } : {},
401
+ idSource,
402
+ resourceType: context.resourceType,
403
+ responseStatus: status,
404
+ // The raw header, only for the branch that used it — a Location the
405
+ // guard IGNORED (collection-shaped, cross-origin, non-id last
406
+ // segment) must not leak onto a body-sourced entry.
407
+ ...idSource === "locationHeader" && locationHeader ? { locationHeader } : {}
408
+ };
409
+ this._entries.push(entry);
410
+ const credentials = teardownCredentialHeaders(requestHeaders);
411
+ if (credentials)
412
+ this._teardownHeaders.set(entry, credentials);
413
+ this._persist();
414
+ return;
415
+ }
416
+ this.recordUnparsedCreate(url, status, contentType, unparsedRule);
417
+ }
418
+ /**
419
+ * Audit a 2xx POST whose created entity could not be identified (or, with
420
+ * status 0, a create attempt whose route-time fetch never completed — the
421
+ * request may or may not have reached the server). Deduped on url+status.
422
+ */
423
+ recordUnparsedCreate(url, status, contentType, rule) {
424
+ const existing = this._unparsedCreates.find((u) => u.url === url && u.status === status);
425
+ if (existing)
426
+ existing.count = (existing.count ?? 1) + 1;
427
+ else
428
+ this._unparsedCreates.push({ url, status, contentType: contentType ?? void 0, count: 1, rule });
429
+ this._persist();
430
+ }
431
+ /**
432
+ * A non-POST response the recording saw. A 2xx at exactly a ledgered
433
+ * delete URL, query string included, confirms that URL is the entity's
434
+ * real address (the page read, changed or deleted it there), so teardown
435
+ * can trust a later 404. A response with another query
436
+ * (`/api/items/7?workspace=3`) confirms nothing: on a query-scoped API the
437
+ * query-less delete URL can name no entity at all.
438
+ */
439
+ noteEntityResponse(method, url, status) {
440
+ if (this.config.mode !== "strict" || method.toUpperCase() === "POST" || status < 200 || status >= 300 || !this._entries.length)
441
+ return;
442
+ const key = entityKey(url);
443
+ if (key && this._entries.some((e) => entityKey(e.deleteEndpoint) === key))
444
+ this._confirmedEntities.add(key);
445
+ }
446
+ /**
447
+ * True when a request is a strict-mode ledgerable create — the routing
448
+ * layer captures these bodies synchronously (route.fetch → fulfill) so the
449
+ * created id can never be lost to a renderer-side navigation racing the
450
+ * deferred response.text() read.
451
+ */
452
+ shouldCaptureCreateBody(method, url) {
453
+ if (this.config.mode !== "strict" || method.toUpperCase() !== "POST")
454
+ return false;
455
+ const pathName = safePathname(url);
456
+ return !isAuthExemptPath(pathName) && !GRAPHQL_PATH_RE.test(pathName);
457
+ }
458
+ classify(method, url, postData) {
459
+ const m = method.toUpperCase();
460
+ if (this._offLimits.some((re) => re.test(url)))
461
+ return "BLOCK_DENYLISTED";
462
+ if (this.config.mode === "off")
463
+ return "ALLOW";
464
+ if (SAFE_METHODS.has(m))
465
+ return "ALLOW";
466
+ const target = tryParseUrl(url);
467
+ if (!target || target.origin === "null")
468
+ return "BLOCK_FOREIGN_MUTATION";
469
+ const pathName = target.pathname;
470
+ if (this.config.mode === "readonly")
471
+ return m === "POST" && isReadonlyAuthPath(pathName) ? "ALLOW" : "BLOCK_FOREIGN_MUTATION";
472
+ if (m === "POST") {
473
+ const foreignSegment = this._foreignEntitySegment(target, pathName);
474
+ if (isAuthExemptPath(pathName) && !foreignSegment)
475
+ return "ALLOW";
476
+ if (AUTH_PATH_RE.test(pathName))
477
+ return "BLOCK_FOREIGN_MUTATION";
478
+ if (GRAPHQL_PATH_RE.test(pathName))
479
+ return isGraphQLMutation(postData) ? "BLOCK_FOREIGN_MUTATION" : "ALLOW";
480
+ return foreignSegment ? "BLOCK_FOREIGN_MUTATION" : "ALLOW";
481
+ }
482
+ const owned = this._isOwnedPath(target.origin, target.pathname);
483
+ return owned ? "ALLOW" : "BLOCK_FOREIGN_MUTATION";
484
+ }
485
+ /**
486
+ * Detect 3xx responses whose redirect target would have been blocked.
487
+ * Pre-flight interception of redirect hops is NOT supported by the browser
488
+ * stack — the network layer auto-follows redirects without re-entering
489
+ * route handlers (see crNetworkManager.ts:339 "We do not support
490
+ * intercepting redirects") — so this is detection-only mitigation: the hop
491
+ * already happened, but the incident lands in notices + ledger blocks
492
+ * instead of vanishing. 307/308 preserve the request method; 301/302/303
493
+ * downgrade to GET, so for those only an offLimits target can flag.
494
+ */
495
+ noteRedirect(method, url, status, locationHeader) {
496
+ if (status < 300 || status >= 400 || !locationHeader)
497
+ return;
498
+ let target;
499
+ try {
500
+ target = new URL(locationHeader, url).toString();
501
+ } catch {
502
+ this.recordBlock({
503
+ url: locationHeader,
504
+ method: method.toUpperCase(),
505
+ verdict: "BLOCK_FOREIGN_MUTATION",
506
+ rule: "post-hoc: unresolvable redirect Location \u2014 hop could not be audited"
507
+ });
508
+ return;
509
+ }
510
+ const redirectedMethod = status === 307 || status === 308 ? method.toUpperCase() : "GET";
511
+ const verdict = this.classify(redirectedMethod, target);
512
+ if (verdict === "ALLOW")
513
+ return;
514
+ const why = blockReason(verdict, this.config.mode);
515
+ this.recordBlock({
516
+ url: target,
517
+ method: redirectedMethod,
518
+ verdict,
519
+ rule: `post-hoc: redirect (${status} from ${url}) \u2014 ${why}; redirect hops cannot be blocked pre-flight`
520
+ });
521
+ }
522
+ /** URL-scoped ownership: pathname equals a ledgered entity path or is nested under it, same origin. */
523
+ _isOwnedPath(origin, pathname) {
524
+ const target = normalizePathname(pathname);
525
+ return this._ownedEntities.some((o) => o.origin === origin && (target === o.pathname || target.startsWith(o.pathname + "/")));
526
+ }
527
+ /**
528
+ * First path segment that looks like an entity id but is not session-owned.
529
+ * An id segment counts as owned only in the context of its own entity URL:
530
+ * the URL prefix up to (and including) the segment must match a ledgered
531
+ * entityUrl on the same origin — never a bare-id match on another
532
+ * collection or origin.
533
+ *
534
+ * Id-like is numeric | UUID | ObjectId ONLY. An opaque slug is not
535
+ * id-like even after a plural-looking collection segment: that would
536
+ * over-block ordinary collection-level actions on slug-addressed
537
+ * collections (`POST
538
+ * /api/orders/bulk/cancel`, `/api/teams/members/add`, `/api/settings/
539
+ * notifications/mark-read` — none of these touch any specific entity), and
540
+ * a PUT/PATCH/DELETE on a genuinely slug-addressed entity already fails
541
+ * closed via positive ownership below regardless. Residual accepted gap:
542
+ * an opaque/slug id in a POST path (`/api/posts/my-first-post/comments`,
543
+ * `/api/u/aB3xQ/disable`) is not detectable by shape alone — PUT/PATCH/
544
+ * DELETE on that entity still blocks unless the session owns it.
545
+ */
546
+ _foreignEntitySegment(target, pathName) {
547
+ let prefix = "";
548
+ const segs = pathName.split("/").filter(Boolean);
549
+ for (const seg of segs) {
550
+ prefix += "/" + seg;
551
+ if (!isIdShaped(seg))
552
+ continue;
553
+ if (!this._isOwnedPath(target.origin, prefix))
554
+ return seg;
555
+ }
556
+ return null;
557
+ }
558
+ /**
559
+ * Identical blocks (same method+url+verdict+rule — e.g. an agent
560
+ * retrying the same blocked request, or a page firing it in a loop) dedupe
561
+ * into one BlockRecord with an incrementing count instead of spamming the
562
+ * ledger and the notice text with N copies of the same line. A repeat of
563
+ * an already-notified block is re-queued (same object, already-bumped
564
+ * count) so it's still called out again on the next notice read.
565
+ */
566
+ recordBlock(rec) {
567
+ const existing = this._blocks.find((b) => b.method === rec.method && b.url === rec.url && b.verdict === rec.verdict && b.rule === rec.rule);
568
+ if (existing) {
569
+ existing.count = (existing.count ?? 1) + 1;
570
+ if (!this._pendingNotices.includes(existing))
571
+ this._pendingNotices.push(existing);
572
+ } else {
573
+ const withCount = { ...rec, count: 1 };
574
+ this._blocks.push(withCount);
575
+ this._pendingNotices.push(withCount);
576
+ }
577
+ this._persist();
578
+ }
579
+ get blocks() {
580
+ return this._blocks;
581
+ }
582
+ /**
583
+ * Render at most 10 distinct blocked-request lines (each carrying a
584
+ * "(×N)" suffix once count > 1), plus a non-counted tail line naming how
585
+ * many more distinct blocks are pending — the tail deliberately does not
586
+ * start with the "Blocked by data safety: " prefix testbot counts report
587
+ * lines by (renderCappedList's contract: the tail is never one of the
588
+ * counted lines).
589
+ */
590
+ consumeBlockNotices() {
591
+ if (!this._pendingNotices.length)
592
+ return "";
593
+ const advice = this.config.mode === "readonly" ? "Do not retry; this environment is read-only." : "Do not retry; use scratch entities you create in this session.";
594
+ const lines = this._pendingNotices.map((b) => {
595
+ const countSuffix = b.count && b.count > 1 ? ` (\xD7${b.count})` : "";
596
+ return `Blocked by data safety: ${b.method} ${b.url} \u2014 ${b.rule}${countSuffix}. ${advice}`;
597
+ });
598
+ this._pendingNotices = [];
599
+ return `
600
+
601
+ ### Data safety
602
+ ${renderCappedList(lines, "distinct blocked requests \u2014 see dataSafetyLedger.json.")}`;
603
+ }
604
+ ledgerJson(setupCompleted) {
605
+ return JSON.stringify({ ledgerVersion: 1, mode: this.config.mode, setupCompleted, entries: this._entries, blocks: this._blocks, unparsedCreates: this._unparsedCreates }, null, 2);
606
+ }
607
+ /** Write the ledger now, before any event — proof the guard is live for a caller that reads ledgerDir. */
608
+ flush() {
609
+ this._persist();
610
+ }
611
+ /**
612
+ * A successful export ends the data-safety session: the next recording in
613
+ * the same process must not inherit this one's entries, blocks, or owned
614
+ * entities (cross-recording ledger contamination), and the persisted ledger
615
+ * file is rewritten to match.
616
+ */
617
+ resetSession() {
618
+ this._entries = [];
619
+ this._blocks = [];
620
+ this._pendingNotices = [];
621
+ this._ownedEntities = [];
622
+ this._unparsedCreates = [];
623
+ this._teardownHeaders.clear();
624
+ this._confirmedEntities.clear();
625
+ this._timedOutDeletes.clear();
626
+ this._teardownStarted = false;
627
+ if (this._nextLedgerPath) {
628
+ this._ledgerPath = this._nextLedgerPath();
629
+ return;
630
+ }
631
+ this._persist();
632
+ }
633
+ /**
634
+ * Teardown ran for this session (it had entries). Its records are gone, so
635
+ * the recording can no longer be relabelled, for example by finishing
636
+ * setup after the fact.
637
+ */
638
+ get teardownStarted() {
639
+ return this._teardownStarted;
640
+ }
641
+ /** The last ledger write failed; ensureLedgerWritable() rewrites it in full. */
642
+ get persistFailed() {
643
+ return this._persistFailed;
644
+ }
645
+ /** Entries not yet confirmed torn down — what a retry would still attempt. */
646
+ get pendingTeardownCount() {
647
+ return this._entries.filter((e) => !e.tornDown).length;
648
+ }
649
+ async teardown(request) {
650
+ const result = { deleted: [], leftovers: [] };
651
+ if (this.config.mode !== "strict")
652
+ return result;
653
+ if (this._entries.length)
654
+ this._teardownStarted = true;
655
+ for (const entry of [...this._entries].reverse()) {
656
+ if (entry.tornDown === true)
657
+ continue;
658
+ try {
659
+ const headers = this._teardownHeaders.get(entry);
660
+ const response = await request.delete(entry.deleteEndpoint, { maxRedirects: 0, timeout: TEARDOWN_DELETE_TIMEOUT_MS, ...headers ? { headers } : {} });
661
+ const status = response.status();
662
+ if (status >= 200 && status < 300) {
663
+ entry.tornDown = true;
664
+ result.deleted.push(entry);
665
+ } else if (status === 404 && this._deleteUrlKnownRight(entry)) {
666
+ entry.tornDown = true;
667
+ result.deleted.push(entry);
668
+ } else if (status === 404) {
669
+ result.leftovers.push({ entry, status: "not-found", ...this._timedOutDeletes.has(entry) ? { error: "an earlier DELETE timed out, so the server may already have deleted it" } : {} });
670
+ } else if (status >= 300 && status < 400) {
671
+ const location = response.headers?.()["location"];
672
+ result.leftovers.push({ entry, status, error: `redirected to ${location ?? "(no Location header)"} \u2014 not followed (maxRedirects: 0)` });
673
+ } else {
674
+ result.leftovers.push({ entry, status });
675
+ }
676
+ } catch (e) {
677
+ if (e instanceof Error && e.name === "TimeoutError") {
678
+ this._timedOutDeletes.add(entry);
679
+ result.leftovers.push({ entry, status: "error", timedOut: true, error: `${this._safeErrorText(entry, e)} \u2014 the server may have processed it` });
680
+ } else {
681
+ result.leftovers.push({ entry, status: "error", error: this._safeErrorText(entry, e) });
682
+ }
683
+ }
684
+ if (entry.tornDown)
685
+ this._persist();
686
+ }
687
+ return result;
688
+ }
689
+ /**
690
+ * A delete URL is known to be right when exactly that URL answered 2xx
691
+ * during the recording, whatever the id's source: a Location header names
692
+ * the right URL, but an expired session or credentials that were not
693
+ * replayed can still answer 404 while the record stays live. The id must
694
+ * be id-shaped as well: a 404 on a slug-like id is the wrong-URL signal.
695
+ */
696
+ _deleteUrlKnownRight(entry) {
697
+ return isIdShaped(entry.id) && this._confirmedEntities.has(entityKey(entry.deleteEndpoint) ?? "");
698
+ }
699
+ /**
700
+ * The error text of a failed teardown DELETE, safe to show the agent and
701
+ * to put in a manifest. Playwright appends a call log that prints every
702
+ * request header, so cut it off, then redact any replayed credential
703
+ * value that is still in the text.
704
+ */
705
+ _safeErrorText(entry, e) {
706
+ let text = String(e);
707
+ const callLog = text.indexOf("\nCall log:");
708
+ if (callLog !== -1)
709
+ text = text.slice(0, callLog);
710
+ for (const value of Object.values(this._teardownHeaders.get(entry) ?? {})) {
711
+ if (value)
712
+ text = text.split(value).join("<redacted>");
713
+ }
714
+ return text.trim();
715
+ }
716
+ /**
717
+ * Write the ledger now and fail if it cannot be written. A caller that
718
+ * reads ledgerDir has no other record (the staged engine never exports),
719
+ * so recording must not start without the file.
720
+ */
721
+ ensureLedgerWritable() {
722
+ if (this.config.startupError)
723
+ throw new Error(this.config.startupError);
724
+ try {
725
+ if (this.config.ledgerDir)
726
+ import_fs.default.mkdirSync(this.config.ledgerDir, { recursive: true });
727
+ writeLedgerFile(this._ledgerPath, this.ledgerJson(this._setupCompleted()));
728
+ this._persistFailed = false;
729
+ this._persistWarned = false;
730
+ } catch (e) {
731
+ throw new Error(`Data safety cannot write its ledger to ledgerDir ${this.config.ledgerDir ?? this._ledgerPath}: ${String(e)}`);
732
+ }
733
+ }
734
+ _persist() {
735
+ try {
736
+ writeLedgerFile(this._ledgerPath, this.ledgerJson(this._setupCompleted()));
737
+ this._persistFailed = false;
738
+ this._persistWarned = false;
739
+ } catch (e) {
740
+ this._persistFailed = true;
741
+ if (!this._persistWarned) {
742
+ this._persistWarned = true;
743
+ process.stderr.write(`[skyramp] Data safety: could not persist ledger to ${this._ledgerPath}: ${String(e)}
744
+ `);
745
+ }
746
+ }
747
+ }
748
+ }
749
+ function blockReason(verdict, mode) {
750
+ if (verdict === "BLOCK_DENYLISTED")
751
+ return "matches an off-limits pattern";
752
+ return mode === "readonly" ? "mutation (mode=readonly)" : `foreign entity mutation (mode=${mode})`;
753
+ }
754
+ const CREDENTIAL_HEADER_RE = /^(authorization|proxy-authorization)$|auth|token|session|api[-_]?key|[-_]key$|csrf|xsrf/i;
755
+ const NOT_CREDENTIAL_HEADER_RE = /idempotency/i;
756
+ function teardownCredentialHeaders(headers) {
757
+ if (!headers)
758
+ return void 0;
759
+ const kept = Object.entries(headers).filter(([name]) => name.toLowerCase() !== "cookie" && CREDENTIAL_HEADER_RE.test(name) && !NOT_CREDENTIAL_HEADER_RE.test(name));
760
+ return kept.length ? Object.fromEntries(kept.map(([name, value]) => [name.toLowerCase(), value])) : void 0;
761
+ }
762
+ function isSingleChildPath(entity, collection) {
763
+ if (entity.origin !== collection.origin)
764
+ return false;
765
+ const parent = normalizePathname(collection.pathname);
766
+ const child = normalizePathname(entity.pathname);
767
+ const prefix = parent === "/" ? "/" : parent + "/";
768
+ return child.startsWith(prefix) && child.length > prefix.length && !child.slice(prefix.length).includes("/");
769
+ }
770
+ function tryParseUrl(url) {
771
+ try {
772
+ return new URL(url);
773
+ } catch {
774
+ return null;
775
+ }
776
+ }
777
+ function entityKey(url) {
778
+ const parsed = tryParseUrl(url);
779
+ return parsed ? parsed.origin + normalizePathname(parsed.pathname) + parsed.search : void 0;
780
+ }
781
+ function normalizePathname(pathname) {
782
+ const stripped = pathname.replace(/\/+$/, "");
783
+ return stripped || "/";
784
+ }
785
+ function safePathname(url) {
786
+ try {
787
+ return new URL(url).pathname;
788
+ } catch {
789
+ return url;
790
+ }
791
+ }
792
+ function isGraphQLMutation(postData) {
793
+ if (!postData)
794
+ return true;
795
+ try {
796
+ const body = JSON.parse(postData);
797
+ const elements = Array.isArray(body) ? body : [body];
798
+ if (!elements.length)
799
+ return true;
800
+ for (const el of elements) {
801
+ if (!el || typeof el !== "object" || typeof el.query !== "string")
802
+ return true;
803
+ if (/\bmutation\b/.test(el.query))
804
+ return true;
805
+ }
806
+ return false;
807
+ } catch {
808
+ return true;
809
+ }
810
+ }
811
+ // Annotate the CommonJS export names for ESM import in node:
812
+ 0 && (module.exports = {
813
+ DataSafetyGuard,
814
+ TEARDOWN_DELETE_TIMEOUT_MS,
815
+ blockReason,
816
+ compileOffLimits,
817
+ matchesOffLimits,
818
+ parseDataSafetyConfig,
819
+ renderCappedList,
820
+ teardownCredentialHeaders,
821
+ writeLedgerFile
822
+ });