@yiaany/ghostapi 0.1.9 → 0.2.0

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 (292) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/CONTRIBUTING.md +132 -119
  3. package/LICENSE +21 -21
  4. package/PROVENANCE.md +13 -0
  5. package/README.md +78 -84
  6. package/SECURITY.md +56 -53
  7. package/dist/actions/gateway.js +320 -72
  8. package/dist/actions/gateway.js.map +1 -1
  9. package/dist/actions/index.d.ts +2 -2
  10. package/dist/actions/index.js +1 -1
  11. package/dist/actions/index.js.map +1 -1
  12. package/dist/agents/agentPrompt.js +7 -5
  13. package/dist/agents/agentPrompt.js.map +1 -1
  14. package/dist/ai/aiClient.js +5 -5
  15. package/dist/ai/aiClient.js.map +1 -1
  16. package/dist/ai/aiGenerator.js +66 -26
  17. package/dist/ai/aiGenerator.js.map +1 -1
  18. package/dist/ai/genericInference.js +151 -36
  19. package/dist/ai/genericInference.js.map +1 -1
  20. package/dist/ai/prompts.js +55 -49
  21. package/dist/ai/prompts.js.map +1 -1
  22. package/dist/approvals/approvalInbox.js +649 -133
  23. package/dist/approvals/approvalInbox.js.map +1 -1
  24. package/dist/approvals/index.d.ts +2 -2
  25. package/dist/approvals/index.js +1 -1
  26. package/dist/approvals/index.js.map +1 -1
  27. package/dist/behavior/behaviorStore.js +34 -14
  28. package/dist/behavior/behaviorStore.js.map +1 -1
  29. package/dist/cache/index.js +14 -5
  30. package/dist/cache/index.js.map +1 -1
  31. package/dist/cli/index.js +226 -93
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/cli/openUrl.js +17 -3
  34. package/dist/cli/openUrl.js.map +1 -1
  35. package/dist/cli/parser.js +79 -18
  36. package/dist/cli/parser.js.map +1 -1
  37. package/dist/config/dataPaths.js +4 -2
  38. package/dist/config/dataPaths.js.map +1 -1
  39. package/dist/config/localConfig.js +5 -2
  40. package/dist/config/localConfig.js.map +1 -1
  41. package/dist/config/serverConfig.js +11 -5
  42. package/dist/config/serverConfig.js.map +1 -1
  43. package/dist/contracts/contract.js +311 -83
  44. package/dist/contracts/contract.js.map +1 -1
  45. package/dist/contracts/index.d.ts +2 -2
  46. package/dist/contracts/index.js +1 -1
  47. package/dist/contracts/index.js.map +1 -1
  48. package/dist/credentials/credentialBroker.js +391 -79
  49. package/dist/credentials/credentialBroker.js.map +1 -1
  50. package/dist/credentials/index.d.ts +2 -2
  51. package/dist/credentials/index.js +1 -1
  52. package/dist/credentials/index.js.map +1 -1
  53. package/dist/dashboard/app.js +823 -685
  54. package/dist/dashboard/dashboard.js.map +1 -1
  55. package/dist/dashboard/index.html +704 -267
  56. package/dist/dashboard/styles.css +1040 -349
  57. package/dist/egress/capabilities.js +55 -25
  58. package/dist/egress/capabilities.js.map +1 -1
  59. package/dist/egress/linuxBootstrap.js +47 -11
  60. package/dist/egress/linuxBootstrap.js.map +1 -1
  61. package/dist/egress/run.js +170 -31
  62. package/dist/egress/run.js.map +1 -1
  63. package/dist/errors/errorEngine.js +15 -3
  64. package/dist/errors/errorEngine.js.map +1 -1
  65. package/dist/errors/providerErrors.js.map +1 -1
  66. package/dist/evals/evals.js +304 -78
  67. package/dist/evals/evals.js.map +1 -1
  68. package/dist/evals/index.d.ts +2 -2
  69. package/dist/evals/index.js +1 -1
  70. package/dist/evals/index.js.map +1 -1
  71. package/dist/evidence/index.d.ts +2 -2
  72. package/dist/evidence/index.js +1 -1
  73. package/dist/evidence/index.js.map +1 -1
  74. package/dist/evidence/report.d.ts +1 -1
  75. package/dist/evidence/report.js +263 -79
  76. package/dist/evidence/report.js.map +1 -1
  77. package/dist/fault/faultLab.js +24 -9
  78. package/dist/fault/faultLab.js.map +1 -1
  79. package/dist/index.d.ts +42 -42
  80. package/dist/index.js +21 -21
  81. package/dist/index.js.map +1 -1
  82. package/dist/inventory/analysis.d.ts +1 -1
  83. package/dist/inventory/analysis.js +174 -50
  84. package/dist/inventory/analysis.js.map +1 -1
  85. package/dist/inventory/index.d.ts +4 -4
  86. package/dist/inventory/index.js +3 -3
  87. package/dist/inventory/index.js.map +1 -1
  88. package/dist/inventory/inventoryController.js +362 -115
  89. package/dist/inventory/inventoryController.js.map +1 -1
  90. package/dist/inventory/types.js +680 -140
  91. package/dist/inventory/types.js.map +1 -1
  92. package/dist/ledger/actionLedger.js +355 -78
  93. package/dist/ledger/actionLedger.js.map +1 -1
  94. package/dist/ledger/index.d.ts +2 -2
  95. package/dist/ledger/index.js +1 -1
  96. package/dist/ledger/index.js.map +1 -1
  97. package/dist/mcp/server.js +26 -11
  98. package/dist/mcp/server.js.map +1 -1
  99. package/dist/policy/index.d.ts +2 -2
  100. package/dist/policy/index.js +1 -1
  101. package/dist/policy/index.js.map +1 -1
  102. package/dist/policy/policy.js +112 -32
  103. package/dist/policy/policy.js.map +1 -1
  104. package/dist/productTelemetry/index.js +71 -20
  105. package/dist/productTelemetry/index.js.map +1 -1
  106. package/dist/providers/conformance.js +10 -5
  107. package/dist/providers/conformance.js.map +1 -1
  108. package/dist/providers/discord.js +2 -2
  109. package/dist/providers/generic.js +3 -3
  110. package/dist/providers/github.js +2 -2
  111. package/dist/providers/index.d.ts +3 -3
  112. package/dist/providers/index.js +2 -2
  113. package/dist/providers/index.js.map +1 -1
  114. package/dist/providers/openai.js +3 -3
  115. package/dist/providers/packs/resendPack.js +51 -25
  116. package/dist/providers/packs/resendPack.js.map +1 -1
  117. package/dist/providers/packs/stripePack.js +590 -121
  118. package/dist/providers/packs/stripePack.js.map +1 -1
  119. package/dist/providers/registry.js +4 -4
  120. package/dist/providers/registry.js.map +1 -1
  121. package/dist/providers/runtime.js +7 -6
  122. package/dist/providers/runtime.js.map +1 -1
  123. package/dist/providers/stripeWebhook.js +39 -10
  124. package/dist/providers/stripeWebhook.js.map +1 -1
  125. package/dist/providers/twilio.js +2 -2
  126. package/dist/proxy/cacheKey.js +9 -2
  127. package/dist/proxy/cacheKey.js.map +1 -1
  128. package/dist/proxy/providerDetector.js +22 -9
  129. package/dist/proxy/providerDetector.js.map +1 -1
  130. package/dist/proxy/proxyHandler.js +57 -22
  131. package/dist/proxy/proxyHandler.js.map +1 -1
  132. package/dist/proxy/requestNormalizer.js +4 -2
  133. package/dist/proxy/requestNormalizer.js.map +1 -1
  134. package/dist/reliability/costGovernance.js +211 -49
  135. package/dist/reliability/costGovernance.js.map +1 -1
  136. package/dist/reliability/index.d.ts +8 -8
  137. package/dist/reliability/index.js +4 -4
  138. package/dist/reliability/index.js.map +1 -1
  139. package/dist/reliability/reconciliation.js +274 -65
  140. package/dist/reliability/reconciliation.js.map +1 -1
  141. package/dist/reliability/runtimeHealth.js +127 -35
  142. package/dist/reliability/runtimeHealth.js.map +1 -1
  143. package/dist/reliability/slo.js +257 -50
  144. package/dist/reliability/slo.js.map +1 -1
  145. package/dist/report/safetyReport.d.ts +6 -0
  146. package/dist/report/safetyReport.js +56 -12
  147. package/dist/report/safetyReport.js.map +1 -1
  148. package/dist/rules/aiRules.js +5 -5
  149. package/dist/rules/aiRules.js.map +1 -1
  150. package/dist/safety/index.d.ts +2 -2
  151. package/dist/safety/index.js +1 -1
  152. package/dist/safety/index.js.map +1 -1
  153. package/dist/safety/safetyController.js +675 -165
  154. package/dist/safety/safetyController.js.map +1 -1
  155. package/dist/scenarios/scenarioBundle.js +204 -71
  156. package/dist/scenarios/scenarioBundle.js.map +1 -1
  157. package/dist/scenarios/scenarioStore.js +88 -29
  158. package/dist/scenarios/scenarioStore.js.map +1 -1
  159. package/dist/security/headerSanitizer.js +52 -10
  160. package/dist/security/headerSanitizer.js.map +1 -1
  161. package/dist/security/secrets.js +10 -5
  162. package/dist/security/secrets.js.map +1 -1
  163. package/dist/server/accessControl.js +37 -10
  164. package/dist/server/accessControl.js.map +1 -1
  165. package/dist/server/createServer.js +1 -1
  166. package/dist/server/createServer.js.map +1 -1
  167. package/dist/server/eventsStore.js +15 -5
  168. package/dist/server/eventsStore.js.map +1 -1
  169. package/dist/server/routes.js +61 -19
  170. package/dist/server/routes.js.map +1 -1
  171. package/dist/server/sse.js +1 -1
  172. package/dist/server/sse.js.map +1 -1
  173. package/dist/setup/sdkDetector.js +7 -2
  174. package/dist/setup/sdkDetector.js.map +1 -1
  175. package/dist/setup/setupGenerator.js +136 -48
  176. package/dist/setup/setupGenerator.js.map +1 -1
  177. package/dist/state/stateExtractor.js +3 -1
  178. package/dist/state/stateExtractor.js.map +1 -1
  179. package/dist/state/stateResolver.js +10 -7
  180. package/dist/state/stateResolver.js.map +1 -1
  181. package/dist/state/stateStore.js +13 -4
  182. package/dist/state/stateStore.js.map +1 -1
  183. package/dist/storage/fileStore.js +12 -4
  184. package/dist/storage/fileStore.js.map +1 -1
  185. package/dist/teamControl/controlPlane.js +846 -174
  186. package/dist/teamControl/controlPlane.js.map +1 -1
  187. package/dist/teamControl/deployment.js +19 -5
  188. package/dist/teamControl/deployment.js.map +1 -1
  189. package/dist/teamControl/index.d.ts +4 -4
  190. package/dist/teamControl/index.js +2 -2
  191. package/dist/teamControl/index.js.map +1 -1
  192. package/dist/tests/testGenerator.js +15 -7
  193. package/dist/tests/testGenerator.js.map +1 -1
  194. package/dist/trust/index.d.ts +2 -2
  195. package/dist/trust/index.js +1 -1
  196. package/dist/trust/index.js.map +1 -1
  197. package/dist/trust/trustLadder.js +309 -63
  198. package/dist/trust/trustLadder.js.map +1 -1
  199. package/dist/worlds/index.d.ts +2 -2
  200. package/dist/worlds/index.js +1 -1
  201. package/dist/worlds/index.js.map +1 -1
  202. package/dist/worlds/worlds.js +364 -65
  203. package/dist/worlds/worlds.js.map +1 -1
  204. package/docs/assets/dashboard.png +0 -0
  205. package/docs/ci.md +49 -49
  206. package/docs/github-actions.md +84 -84
  207. package/docs/mcp.md +58 -52
  208. package/docs/policy.md +63 -61
  209. package/docs/providers/authoring-packs.md +180 -175
  210. package/docs/providers/stripe-core-pack.md +84 -82
  211. package/docs/releases/README.md +13 -0
  212. package/docs/security/action-gateway-threat-model.md +34 -34
  213. package/docs/security/action-ledger-incident-replay-threat-model.md +42 -42
  214. package/docs/security/approval-inbox-threat-model.md +35 -35
  215. package/docs/security/credential-broker-threat-model.md +52 -52
  216. package/docs/security/egress-threat-model.md +126 -126
  217. package/docs/security/inventory-threat-model.md +104 -104
  218. package/docs/security/kill-switch-budgets-threat-model.md +37 -37
  219. package/docs/security/reliability-threat-model.md +111 -111
  220. package/docs/security/trust-ladder-threat-model.md +33 -33
  221. package/docs/telemetry.md +14 -0
  222. package/docs/usage.md +680 -571
  223. package/examples/README.md +10 -10
  224. package/examples/agent-instructions/README.md +39 -39
  225. package/examples/ci-smoke/README.md +21 -21
  226. package/examples/ci-smoke/ghostapi.policy.yaml +22 -22
  227. package/examples/ci-smoke/package.json +9 -9
  228. package/examples/ci-smoke/production-egress.mjs +4 -2
  229. package/examples/ci-smoke/safe.mjs +21 -17
  230. package/examples/evals/README.md +24 -24
  231. package/examples/evals/retry-after.eval.json +71 -52
  232. package/examples/generic-rest/README.md +27 -27
  233. package/examples/github-fetch/README.md +38 -35
  234. package/examples/openai-streaming/README.md +16 -16
  235. package/examples/openai-streaming/streaming-tool-call.mjs +44 -31
  236. package/examples/policy/ghostapi.policy.yaml +27 -27
  237. package/examples/record-replay/README.md +19 -19
  238. package/examples/record-replay/replay-requests.json +22 -22
  239. package/examples/record-replay/stripe-checkout.har.json +30 -30
  240. package/examples/resend-node/README.md +22 -22
  241. package/examples/stripe-node/README.md +35 -35
  242. package/examples/stripe-node/checkout-flow.mjs +87 -61
  243. package/examples/twilio-node/README.md +21 -21
  244. package/examples/worlds/README.md +19 -19
  245. package/examples/worlds/subscription-recovery.mjs +21 -12
  246. package/package.json +28 -6
  247. package/dist/landing/assets/index-BXR41fAD.css +0 -1
  248. package/dist/landing/assets/index-CxWsopzP.css +0 -1
  249. package/dist/landing/assets/index-D8_ggMRE.js +0 -55
  250. package/dist/landing/assets/index-DZlpyhqd.js +0 -55
  251. package/dist/landing/assets/index-Dk-Xezs3.js +0 -55
  252. package/dist/landing/index.html +0 -15
  253. package/dist/landing/landing.d.ts +0 -3
  254. package/dist/landing/landing.js +0 -12
  255. package/dist/landing/landing.js.map +0 -1
  256. package/docs/commercial/README.md +0 -29
  257. package/docs/commercial/data-inventory.md +0 -36
  258. package/docs/commercial/manual-invoicing.md +0 -44
  259. package/docs/commercial/metrics.md +0 -51
  260. package/docs/commercial/pricing.md +0 -103
  261. package/docs/commercial/privacy-policy-draft.md +0 -41
  262. package/docs/commercial/terms-topics-for-counsel.md +0 -51
  263. package/docs/design-partners/README.md +0 -52
  264. package/docs/design-partners/demo-narratives.md +0 -33
  265. package/docs/design-partners/design-partner-offer.md +0 -39
  266. package/docs/design-partners/discovery-questions.md +0 -28
  267. package/docs/design-partners/feedback-capture-template.md +0 -48
  268. package/docs/design-partners/icp-one-pager.md +0 -43
  269. package/docs/design-partners/onboarding-checklist.md +0 -33
  270. package/docs/design-partners/pilot-success-criteria.md +0 -27
  271. package/docs/design-partners/pricing-interview-script.md +0 -18
  272. package/docs/design-partners/telemetry-plan.md +0 -48
  273. package/docs/development/baseline.md +0 -114
  274. package/docs/development/onboarding-smoke.md +0 -37
  275. package/docs/development/verification-0.1.8.md +0 -25
  276. package/docs/enterprise-product-roadmap-ru.md +0 -1879
  277. package/docs/fundraising/README.md +0 -87
  278. package/docs/fundraising/data-room-checklist.md +0 -72
  279. package/docs/fundraising/demo-script.md +0 -52
  280. package/docs/fundraising/design-partners-50.md +0 -74
  281. package/docs/fundraising/launch-posts.md +0 -37
  282. package/docs/fundraising/metrics-and-evidence.md +0 -30
  283. package/docs/fundraising/roadmap-12-month.md +0 -25
  284. package/docs/fundraising/technical-due-diligence-index.md +0 -37
  285. package/docs/fundraising/yc-application.md +0 -25
  286. package/docs/hosted-pilot.md +0 -111
  287. package/docs/operations/disaster-recovery-runbook.md +0 -81
  288. package/docs/operations/kill-switch-runbook.md +0 -32
  289. package/docs/release-checklist.md +0 -83
  290. package/docs/release-migration-and-rollback.md +0 -63
  291. package/docs/release-readiness.md +0 -39
  292. package/docs/team-control-plane.md +0 -72
@@ -1,8 +1,8 @@
1
1
  import { lstat, mkdir, readFile, realpath } from "node:fs/promises";
2
- import { basename, dirname, isAbsolute, relative, resolve, sep, join } from "node:path";
2
+ import { basename, dirname, isAbsolute, relative, resolve, sep, join, } from "node:path";
3
3
  import { getDataPaths } from "../config/dataPaths.js";
4
4
  import { atomicWriteJson } from "../storage/fileStore.js";
5
- import { isSecretFieldName, sanitizeSecretString, sanitizeSecrets } from "../security/secrets.js";
5
+ import { isSecretFieldName, sanitizeSecretString, sanitizeSecrets, } from "../security/secrets.js";
6
6
  import { isSafeRelativeLocation } from "../security/headerSanitizer.js";
7
7
  const MAX_CAPTURE_BYTES = 1024 * 1024;
8
8
  const MAX_BUNDLE_BYTES = 512 * 1024;
@@ -11,10 +11,34 @@ const MAX_VARIABLES = 200;
11
11
  const MAX_VALUE_DEPTH = 30;
12
12
  const MAX_VALUE_KEYS = 200;
13
13
  const VARIABLE_PATTERN = /\{\{ghostapi\.var\.([a-z][a-z0-9_]*)\}\}/g;
14
- const SENSITIVE_QUERY_KEYS = new Set(["api_key", "apikey", "access_token", "refresh_token", "token", "secret", "key", "client_secret", "password", "authorization", "cookie"]);
14
+ const SENSITIVE_QUERY_KEYS = new Set([
15
+ "api_key",
16
+ "apikey",
17
+ "access_token",
18
+ "refresh_token",
19
+ "token",
20
+ "secret",
21
+ "key",
22
+ "client_secret",
23
+ "password",
24
+ "authorization",
25
+ "cookie",
26
+ ]);
15
27
  const SAFE_REQUEST_HEADERS = new Set(["accept", "content-type"]);
16
- const SAFE_RESPONSE_HEADERS = new Set(["content-type", "retry-after", "location"]);
17
- const METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
28
+ const SAFE_RESPONSE_HEADERS = new Set([
29
+ "content-type",
30
+ "retry-after",
31
+ "location",
32
+ ]);
33
+ const METHODS = new Set([
34
+ "GET",
35
+ "POST",
36
+ "PUT",
37
+ "PATCH",
38
+ "DELETE",
39
+ "HEAD",
40
+ "OPTIONS",
41
+ ]);
18
42
  export class ScenarioBundleError extends Error {
19
43
  constructor(message) {
20
44
  super(message);
@@ -47,7 +71,7 @@ export class ScenarioReplayer {
47
71
  index: this.cursor,
48
72
  status: interaction.response.status,
49
73
  headers: resolveHeaders(interaction.response.headers, this.bundle.variables, this.bindings),
50
- body: resolveVariables(interaction.response.body, this.bundle.variables, this.bindings)
74
+ body: resolveVariables(interaction.response.body, this.bundle.variables, this.bindings),
51
75
  };
52
76
  }
53
77
  get remaining() {
@@ -67,7 +91,7 @@ export function prepareScenarioRecording(input, options) {
67
91
  pii: { emails: true, phones: true, addresses: true, ...options.pii },
68
92
  categories: new Set(),
69
93
  replacements: 0,
70
- variables: new Map()
94
+ variables: new Map(),
71
95
  };
72
96
  const hosts = new Set();
73
97
  const interactions = rawInteractions.map((raw) => {
@@ -78,28 +102,32 @@ export function prepareScenarioRecording(input, options) {
78
102
  method: raw.request.method,
79
103
  path: sanitizePath(url, state),
80
104
  headers: sanitizeHeaders(raw.request.headers, SAFE_REQUEST_HEADERS, state, false),
81
- body: sanitizeBody(raw.request.body, raw.request.binary || raw.request.multipart, state)
105
+ body: sanitizeBody(raw.request.body, raw.request.binary || raw.request.multipart, state),
82
106
  },
83
107
  response: {
84
108
  status: raw.response.status,
85
109
  headers: sanitizeHeaders(raw.response.headers, SAFE_RESPONSE_HEADERS, state, true),
86
- body: sanitizeBody(raw.response.body, raw.response.binary || raw.response.multipart, state)
87
- }
110
+ body: sanitizeBody(raw.response.body, raw.response.binary || raw.response.multipart, state),
111
+ },
88
112
  };
89
113
  });
90
114
  const title = normalizeTitle(options.title ?? "Sandbox recording");
91
115
  const summary = {
92
116
  categories: [...state.categories].sort(),
93
117
  replacements: state.replacements,
94
- requiresApproval: state.categories.size > 0
118
+ requiresApproval: state.categories.size > 0,
95
119
  };
96
120
  const bundle = {
97
121
  schemaVersion: 1,
98
122
  kind: "ghostapi.scenario-bundle",
99
- metadata: { title, recordedAt: options.recordedAt ?? new Date().toISOString(), sandboxHosts: [...hosts].sort() },
123
+ metadata: {
124
+ title,
125
+ recordedAt: options.recordedAt ?? new Date().toISOString(),
126
+ sandboxHosts: [...hosts].sort(),
127
+ },
100
128
  sanitization: summary,
101
129
  variables: [...state.variables.values()],
102
- interactions
130
+ interactions,
103
131
  };
104
132
  return validateScenarioBundle(bundle);
105
133
  }
@@ -130,12 +158,19 @@ export function formatScenarioSanitizationSummary(summary) {
130
158
  "GhostAPI recording sanitization summary",
131
159
  `Categories: ${categories}`,
132
160
  `Replacements: ${summary.replacements}`,
133
- `Approval required: ${summary.requiresApproval ? "yes" : "no"}`
161
+ `Approval required: ${summary.requiresApproval ? "yes" : "no"}`,
134
162
  ].join("\n");
135
163
  }
136
164
  export function validateScenarioBundle(input) {
137
165
  const root = readObject(migrateScenarioBundle(input), "Scenario bundle must be an object.");
138
- assertExactKeys(root, ["schemaVersion", "kind", "metadata", "sanitization", "variables", "interactions"], "Scenario bundle");
166
+ assertExactKeys(root, [
167
+ "schemaVersion",
168
+ "kind",
169
+ "metadata",
170
+ "sanitization",
171
+ "variables",
172
+ "interactions",
173
+ ], "Scenario bundle");
139
174
  if (root.schemaVersion !== 1)
140
175
  throw new ScenarioBundleError("Unsupported scenario bundle schema version.");
141
176
  if (root.kind !== "ghostapi.scenario-bundle")
@@ -149,7 +184,9 @@ export function validateScenarioBundle(input) {
149
184
  const sanitization = readObject(root.sanitization, "Scenario bundle sanitization summary is invalid.");
150
185
  assertExactKeys(sanitization, ["categories", "replacements", "requiresApproval"], "Scenario bundle sanitization summary");
151
186
  const categories = readStringArray(sanitization.categories, "Scenario bundle sanitization categories are invalid.");
152
- if (!Number.isInteger(sanitization.replacements) || sanitization.replacements < 0 || typeof sanitization.requiresApproval !== "boolean") {
187
+ if (!Number.isInteger(sanitization.replacements) ||
188
+ sanitization.replacements < 0 ||
189
+ typeof sanitization.requiresApproval !== "boolean") {
153
190
  throw new ScenarioBundleError("Scenario bundle sanitization summary is invalid.");
154
191
  }
155
192
  if (!Array.isArray(root.variables) || root.variables.length > MAX_VARIABLES)
@@ -158,7 +195,11 @@ export function validateScenarioBundle(input) {
158
195
  const variables = root.variables.map((entry) => {
159
196
  const variable = readObject(entry, "Scenario bundle variable is invalid.");
160
197
  assertExactKeys(variable, ["name", "value"], "Scenario bundle variable");
161
- if (typeof variable.name !== "string" || !/^[a-z][a-z0-9_]{0,63}$/.test(variable.name) || typeof variable.value !== "string" || variable.value.length > 2_000 || sanitizeSecretString(variable.value) !== variable.value) {
198
+ if (typeof variable.name !== "string" ||
199
+ !/^[a-z][a-z0-9_]{0,63}$/.test(variable.name) ||
200
+ typeof variable.value !== "string" ||
201
+ variable.value.length > 2_000 ||
202
+ sanitizeSecretString(variable.value) !== variable.value) {
162
203
  throw new ScenarioBundleError("Scenario bundle variable is invalid.");
163
204
  }
164
205
  if (names.has(variable.name))
@@ -166,17 +207,27 @@ export function validateScenarioBundle(input) {
166
207
  names.add(variable.name);
167
208
  return { name: variable.name, value: variable.value };
168
209
  });
169
- if (!Array.isArray(root.interactions) || root.interactions.length === 0 || root.interactions.length > MAX_INTERACTIONS) {
210
+ if (!Array.isArray(root.interactions) ||
211
+ root.interactions.length === 0 ||
212
+ root.interactions.length > MAX_INTERACTIONS) {
170
213
  throw new ScenarioBundleError(`Scenario bundle interactions must contain 1-${MAX_INTERACTIONS} entries.`);
171
214
  }
172
215
  const interactions = root.interactions.map((entry) => normalizeBundleInteraction(entry, names));
173
216
  return {
174
217
  schemaVersion: 1,
175
218
  kind: "ghostapi.scenario-bundle",
176
- metadata: { title, recordedAt: metadata.recordedAt, sandboxHosts },
177
- sanitization: { categories: [...new Set(categories)].sort(), replacements: sanitization.replacements, requiresApproval: sanitization.requiresApproval },
219
+ metadata: {
220
+ title,
221
+ recordedAt: metadata.recordedAt,
222
+ sandboxHosts,
223
+ },
224
+ sanitization: {
225
+ categories: [...new Set(categories)].sort(),
226
+ replacements: sanitization.replacements,
227
+ requiresApproval: sanitization.requiresApproval,
228
+ },
178
229
  variables,
179
- interactions
230
+ interactions,
180
231
  };
181
232
  }
182
233
  export function migrateScenarioBundle(input) {
@@ -185,15 +236,31 @@ export function migrateScenarioBundle(input) {
185
236
  return source;
186
237
  if (source.schemaVersion !== 0)
187
238
  throw new ScenarioBundleError("Unsupported scenario bundle schema version; no migration is registered.");
188
- assertExactKeys(source, ["schemaVersion", "kind", "title", "recordedAt", "sandboxHosts", "variables", "interactions"], "Scenario bundle v0");
239
+ assertExactKeys(source, [
240
+ "schemaVersion",
241
+ "kind",
242
+ "title",
243
+ "recordedAt",
244
+ "sandboxHosts",
245
+ "variables",
246
+ "interactions",
247
+ ], "Scenario bundle v0");
189
248
  return {
190
249
  schemaVersion: 1,
191
250
  kind: source.kind,
192
- metadata: { title: source.title, recordedAt: source.recordedAt, sandboxHosts: source.sandboxHosts },
251
+ metadata: {
252
+ title: source.title,
253
+ recordedAt: source.recordedAt,
254
+ sandboxHosts: source.sandboxHosts,
255
+ },
193
256
  // A migrated legacy bundle has no verifiable sanitizer receipt, so force operator review.
194
- sanitization: { categories: ["legacy-bundle"], replacements: 0, requiresApproval: true },
257
+ sanitization: {
258
+ categories: ["legacy-bundle"],
259
+ replacements: 0,
260
+ requiresApproval: true,
261
+ },
195
262
  variables: source.variables,
196
- interactions: source.interactions
263
+ interactions: source.interactions,
197
264
  };
198
265
  }
199
266
  function normalizeBundleInteraction(input, variableNames) {
@@ -215,11 +282,17 @@ function normalizeBundleRequest(input, variableNames) {
215
282
  function normalizeBundleResponse(input, variableNames) {
216
283
  const response = readObject(input, "Scenario bundle response is invalid.");
217
284
  assertExactKeys(response, ["status", "headers", "body"], "Scenario bundle response");
218
- if (!Number.isInteger(response.status) || response.status < 100 || response.status > 599)
285
+ if (!Number.isInteger(response.status) ||
286
+ response.status < 100 ||
287
+ response.status > 599)
219
288
  throw new ScenarioBundleError("Scenario bundle response status must be 100-599.");
220
289
  const headers = validateHeaders(response.headers, SAFE_RESPONSE_HEADERS, variableNames, true);
221
290
  validateValue(response.body, variableNames);
222
- return { status: response.status, headers, body: structuredClone(response.body) };
291
+ return {
292
+ status: response.status,
293
+ headers,
294
+ body: structuredClone(response.body),
295
+ };
223
296
  }
224
297
  function parseCapture(input) {
225
298
  const capture = readObject(input, "Recording input must be an object.");
@@ -243,15 +316,15 @@ function parseDirectInteraction(input) {
243
316
  headers: requestHeaders,
244
317
  body: request.body ?? null,
245
318
  binary: request.bodyEncoding === "base64",
246
- multipart: isMultipart(requestHeaders)
319
+ multipart: isMultipart(requestHeaders),
247
320
  },
248
321
  response: {
249
322
  status: readStatus(response.status),
250
323
  headers: responseHeaders,
251
324
  body: response.body ?? null,
252
325
  binary: response.bodyEncoding === "base64",
253
- multipart: isMultipart(responseHeaders)
254
- }
326
+ multipart: isMultipart(responseHeaders),
327
+ },
255
328
  };
256
329
  }
257
330
  function parseHarInteraction(input) {
@@ -260,8 +333,12 @@ function parseHarInteraction(input) {
260
333
  const response = readObject(entry.response, "HAR response is invalid.");
261
334
  const requestHeaders = readHeaderInput(request.headers);
262
335
  const responseHeaders = readHeaderInput(response.headers);
263
- const postData = request.postData === undefined ? null : readObject(request.postData, "HAR request postData is invalid.");
264
- const content = response.content === undefined ? null : readObject(response.content, "HAR response content is invalid.");
336
+ const postData = request.postData === undefined
337
+ ? null
338
+ : readObject(request.postData, "HAR request postData is invalid.");
339
+ const content = response.content === undefined
340
+ ? null
341
+ : readObject(response.content, "HAR response content is invalid.");
265
342
  return {
266
343
  request: {
267
344
  method: normalizeMethod(request.method),
@@ -269,15 +346,15 @@ function parseHarInteraction(input) {
269
346
  headers: requestHeaders,
270
347
  body: parseCaptureText(postData?.text, postData?.mimeType),
271
348
  binary: postData?.encoding === "base64",
272
- multipart: isMultipart(requestHeaders) || isMultipartMime(postData?.mimeType)
349
+ multipart: isMultipart(requestHeaders) || isMultipartMime(postData?.mimeType),
273
350
  },
274
351
  response: {
275
352
  status: readStatus(response.status),
276
353
  headers: responseHeaders,
277
354
  body: parseCaptureText(content?.text, content?.mimeType),
278
355
  binary: content?.encoding === "base64",
279
- multipart: isMultipart(responseHeaders) || isMultipartMime(content?.mimeType)
280
- }
356
+ multipart: isMultipart(responseHeaders) || isMultipartMime(content?.mimeType),
357
+ },
281
358
  };
282
359
  }
283
360
  function parseCaptureText(value, mimeType) {
@@ -317,7 +394,11 @@ function sanitizeHeaders(headers, allowed, state, response) {
317
394
  for (const [name, value] of Object.entries(headers)) {
318
395
  const lower = name.toLowerCase();
319
396
  if (isSecretFieldName(lower)) {
320
- mark(state, lower.includes("cookie") ? "cookie" : lower.includes("authorization") ? "authorization" : "secret-header");
397
+ mark(state, lower.includes("cookie")
398
+ ? "cookie"
399
+ : lower.includes("authorization")
400
+ ? "authorization"
401
+ : "secret-header");
321
402
  continue;
322
403
  }
323
404
  if (!allowed.has(lower))
@@ -355,7 +436,9 @@ function sanitizeValue(value, state, key, depth = 0) {
355
436
  mark(state, "known-api-key");
356
437
  return [];
357
438
  }
358
- return [[childKey, sanitizeValue(childValue, state, childKey, depth + 1)]];
439
+ return [
440
+ [childKey, sanitizeValue(childValue, state, childKey, depth + 1)],
441
+ ];
359
442
  }));
360
443
  }
361
444
  if (typeof value !== "string")
@@ -364,18 +447,22 @@ function sanitizeValue(value, state, key, depth = 0) {
364
447
  mark(state, "known-api-key");
365
448
  return "***";
366
449
  }
367
- if (state.pii.addresses && key !== undefined && /(?:address|line1|line2|city|state|postal|zip)/i.test(key)) {
450
+ if (state.pii.addresses &&
451
+ key !== undefined &&
452
+ /(?:address|line1|line2|city|state|postal|zip)/i.test(key)) {
368
453
  mark(state, "address");
369
454
  return "[redacted:address]";
370
455
  }
371
456
  let result = sanitizeSecretString(value);
372
457
  if (result !== value)
373
458
  mark(state, "known-api-key");
374
- if (state.pii.emails && /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i.test(result)) {
459
+ if (state.pii.emails &&
460
+ /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i.test(result)) {
375
461
  result = result.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "[redacted:email]");
376
462
  mark(state, "email");
377
463
  }
378
- if (state.pii.phones && /(?:\+\d[\d(). -]{7,}\d|\b\d{3}[ .-]\d{3}[ .-]\d{4}\b)/.test(result)) {
464
+ if (state.pii.phones &&
465
+ /(?:\+\d[\d(). -]{7,}\d|\b\d{3}[ .-]\d{3}[ .-]\d{4}\b)/.test(result)) {
379
466
  result = result.replace(/(?:\+\d[\d(). -]{7,}\d|\b\d{3}[ .-]\d{3}[ .-]\d{4}\b)/g, "[redacted:phone]");
380
467
  mark(state, "phone");
381
468
  }
@@ -385,8 +472,12 @@ function sanitizeValue(value, state, key, depth = 0) {
385
472
  }
386
473
  function replaceUnstableIdentifiers(value, state, key) {
387
474
  return value.replace(/\b(?:cus|pi|in|sub|evt|re|ch|pm|price|prod|cs|ord|user|msg|email)_[A-Za-z0-9]+\b|\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/gi, (match) => {
388
- const prefix = match.includes("_") ? match.split("_", 1)[0].toLowerCase() : "id";
389
- const nameHint = key !== undefined && /id$/i.test(key) ? key.replace(/[^a-z0-9]+/gi, "_").toLowerCase() : `${prefix}_id`;
475
+ const prefix = match.includes("_")
476
+ ? match.split("_", 1)[0].toLowerCase()
477
+ : "id";
478
+ const nameHint = key !== undefined && /id$/i.test(key)
479
+ ? key.replace(/[^a-z0-9]+/gi, "_").toLowerCase()
480
+ : `${prefix}_id`;
390
481
  return variableReference(getVariable(state, match, nameHint, `${prefix}_recorded_${state.variables.size + 1}`));
391
482
  });
392
483
  }
@@ -396,7 +487,10 @@ function getVariable(state, raw, hint, value) {
396
487
  return existing;
397
488
  if (state.variables.size >= MAX_VARIABLES)
398
489
  throw new ScenarioBundleError(`Recording exceeds ${MAX_VARIABLES} stateful variables.`);
399
- const base = hint.replace(/[^a-z0-9]+/gi, "_").replace(/^_+|_+$/g, "").toLowerCase() || "value";
490
+ const base = hint
491
+ .replace(/[^a-z0-9]+/gi, "_")
492
+ .replace(/^_+|_+$/g, "")
493
+ .toLowerCase() || "value";
400
494
  let name = base;
401
495
  let suffix = 1;
402
496
  const names = new Set([...state.variables.values()].map((entry) => entry.name));
@@ -433,13 +527,21 @@ function parseSandboxUrl(value, allowedHosts, headers) {
433
527
  function looksLikeSandboxHost(host, headers) {
434
528
  if (host === "api.stripe.com")
435
529
  return /\b(?:sk|rk)_test_[A-Za-z0-9_-]+/i.test(headers.authorization ?? "");
436
- return host === "localhost" || host.endsWith(".localhost") || /(?:sandbox|test)/i.test(host);
530
+ return (host === "localhost" ||
531
+ host.endsWith(".localhost") ||
532
+ /(?:sandbox|test)/i.test(host));
437
533
  }
438
534
  function normalizeAllowedSandboxHosts(hosts) {
439
535
  if (hosts.length === 0)
440
536
  throw new ScenarioBundleError("Recording requires at least one explicit --allow-sandbox-host.");
441
- const normalized = hosts.map((host) => host.trim().toLowerCase()).filter(Boolean);
442
- if (normalized.length !== hosts.length || normalized.some((host) => !/^[a-z0-9.-]+$/.test(host) || host.includes("..") || host.startsWith(".") || host.endsWith("."))) {
537
+ const normalized = hosts
538
+ .map((host) => host.trim().toLowerCase())
539
+ .filter(Boolean);
540
+ if (normalized.length !== hosts.length ||
541
+ normalized.some((host) => !/^[a-z0-9.-]+$/.test(host) ||
542
+ host.includes("..") ||
543
+ host.startsWith(".") ||
544
+ host.endsWith("."))) {
443
545
  throw new ScenarioBundleError("Allowed sandbox hosts must be plain host names without paths, ports, or wildcards.");
444
546
  }
445
547
  return new Set(normalized);
@@ -449,7 +551,7 @@ function normalizeReplayRequest(input) {
449
551
  method: normalizeMethod(input.method),
450
552
  path: normalizeRelativePath(input.path),
451
553
  headers: normalizeReplayHeaders(input.headers),
452
- body: sanitizeSecrets(input.body ?? null)
554
+ body: sanitizeSecrets(input.body ?? null),
453
555
  };
454
556
  }
455
557
  function normalizeReplayHeaders(headers) {
@@ -476,14 +578,22 @@ function compareRequest(expected, actual, bindings, variables) {
476
578
  return mismatches;
477
579
  }
478
580
  function resolveHeaders(headers, variables, bindings) {
479
- return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, resolveTemplate(value, variables, bindings)]));
581
+ return Object.fromEntries(Object.entries(headers).map(([key, value]) => [
582
+ key,
583
+ resolveTemplate(value, variables, bindings),
584
+ ]));
480
585
  }
481
586
  function resolveVariables(value, variables, bindings) {
482
587
  if (Array.isArray(value))
483
588
  return value.map((entry) => resolveVariables(entry, variables, bindings));
484
589
  if (value !== null && typeof value === "object")
485
- return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, resolveVariables(entry, variables, bindings)]));
486
- return typeof value === "string" ? resolveTemplate(value, variables, bindings) : value;
590
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
591
+ key,
592
+ resolveVariables(entry, variables, bindings),
593
+ ]));
594
+ return typeof value === "string"
595
+ ? resolveTemplate(value, variables, bindings)
596
+ : value;
487
597
  }
488
598
  function resolveTemplate(value, variables, bindings) {
489
599
  const dictionary = new Map(variables.map((entry) => [entry.name, entry.value]));
@@ -496,14 +606,22 @@ function resolveTemplate(value, variables, bindings) {
496
606
  function matchValue(expected, actual, bindings, variables) {
497
607
  if (typeof expected === "string" && typeof actual === "string")
498
608
  return matchTemplate(expected, actual, bindings, variables);
499
- if (expected === null || actual === null || typeof expected !== "object" || typeof actual !== "object")
609
+ if (expected === null ||
610
+ actual === null ||
611
+ typeof expected !== "object" ||
612
+ typeof actual !== "object")
500
613
  return expected === actual;
501
614
  if (Array.isArray(expected) || Array.isArray(actual)) {
502
- return Array.isArray(expected) && Array.isArray(actual) && expected.length === actual.length && expected.every((entry, index) => matchValue(entry, actual[index], bindings, variables));
615
+ return (Array.isArray(expected) &&
616
+ Array.isArray(actual) &&
617
+ expected.length === actual.length &&
618
+ expected.every((entry, index) => matchValue(entry, actual[index], bindings, variables)));
503
619
  }
504
620
  const expectedEntries = Object.entries(expected);
505
621
  const actualRecord = actual;
506
- return expectedEntries.length === Object.keys(actualRecord).length && expectedEntries.every(([key, entry]) => Object.hasOwn(actualRecord, key) && matchValue(entry, actualRecord[key], bindings, variables));
622
+ return (expectedEntries.length === Object.keys(actualRecord).length &&
623
+ expectedEntries.every(([key, entry]) => Object.hasOwn(actualRecord, key) &&
624
+ matchValue(entry, actualRecord[key], bindings, variables)));
507
625
  }
508
626
  function matchTemplate(expected, actual, bindings, variables) {
509
627
  const known = new Set(variables.map((entry) => entry.name));
@@ -546,7 +664,10 @@ function validateHeaders(input, allowed, variableNames, response) {
546
664
  const normalized = {};
547
665
  for (const [name, value] of Object.entries(headers)) {
548
666
  const lower = name.toLowerCase();
549
- if (!allowed.has(lower) || typeof value !== "string" || value.length > 2_000 || isSecretFieldName(lower))
667
+ if (!allowed.has(lower) ||
668
+ typeof value !== "string" ||
669
+ value.length > 2_000 ||
670
+ isSecretFieldName(lower))
550
671
  throw new ScenarioBundleError(`Scenario bundle header is not allowed: ${name}`);
551
672
  if (response && lower === "location" && !isSafeRelativeLocation(value))
552
673
  throw new ScenarioBundleError("Scenario bundle redirect location must be relative.");
@@ -615,7 +736,7 @@ async function resolveExistingPath(path, projectRoot, label) {
615
736
  const info = await lstat(target);
616
737
  if (!info.isFile() || info.isSymbolicLink())
617
738
  throw new ScenarioBundleError(`${label} must be a regular non-symlink file.`);
618
- if (!await isRealPathInsideAllowedRoots(target, projectRoot))
739
+ if (!(await isRealPathInsideAllowedRoots(target, projectRoot)))
619
740
  throw new ScenarioBundleError(`${label} resolves outside the project root or GHOSTAPI_DATA_DIR through a symlink.`);
620
741
  return target;
621
742
  }
@@ -626,7 +747,7 @@ async function resolveOutputPath(path, projectRoot) {
626
747
  const parent = await lstat(dirname(target));
627
748
  if (!parent.isDirectory() || parent.isSymbolicLink())
628
749
  throw new ScenarioBundleError("Scenario bundle output parent must be a real directory, not a symlink.");
629
- if (!await isRealPathInsideAllowedRoots(dirname(target), projectRoot))
750
+ if (!(await isRealPathInsideAllowedRoots(dirname(target), projectRoot)))
630
751
  throw new ScenarioBundleError("Scenario bundle output parent resolves outside the project root or GHOSTAPI_DATA_DIR through a symlink.");
631
752
  const existing = await lstat(target).catch((error) => isErrorCode(error, "ENOENT") ? null : Promise.reject(error));
632
753
  if (existing?.isSymbolicLink())
@@ -641,13 +762,16 @@ function assertPathInsideAllowedRoots(projectRoot, target, label) {
641
762
  }
642
763
  function isInside(root, target) {
643
764
  const relativePath = relative(resolve(root), target);
644
- return relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
765
+ return (relativePath !== ".." &&
766
+ !relativePath.startsWith(`..${sep}`) &&
767
+ !isAbsolute(relativePath));
645
768
  }
646
769
  async function isRealPathInsideAllowedRoots(target, projectRoot) {
647
770
  const realTarget = await realpath(target);
648
771
  const realProjectRoot = await realpath(projectRoot);
649
772
  const dataRoot = await realpath(getDataPaths().root).catch(() => null);
650
- return isInside(realProjectRoot, realTarget) || (dataRoot !== null && isInside(dataRoot, realTarget));
773
+ return (isInside(realProjectRoot, realTarget) ||
774
+ (dataRoot !== null && isInside(dataRoot, realTarget)));
651
775
  }
652
776
  function readObject(value, message) {
653
777
  if (value === null || typeof value !== "object" || Array.isArray(value))
@@ -688,12 +812,19 @@ function readUrl(value) {
688
812
  return value;
689
813
  }
690
814
  function readStatus(value) {
691
- if (!Number.isInteger(value) || value < 100 || value > 599)
815
+ if (!Number.isInteger(value) ||
816
+ value < 100 ||
817
+ value > 599)
692
818
  throw new ScenarioBundleError("Capture response status must be 100-599.");
693
819
  return value;
694
820
  }
695
821
  function normalizeRelativePath(value) {
696
- if (typeof value !== "string" || value.length === 0 || value.length > 8_000 || !value.startsWith("/") || value.startsWith("//") || /[\r\n]/.test(value)) {
822
+ if (typeof value !== "string" ||
823
+ value.length === 0 ||
824
+ value.length > 8_000 ||
825
+ !value.startsWith("/") ||
826
+ value.startsWith("//") ||
827
+ /[\r\n]/.test(value)) {
697
828
  throw new ScenarioBundleError("Scenario request path must be a relative path.");
698
829
  }
699
830
  return value;
@@ -705,7 +836,8 @@ function readHostArray(value) {
705
836
  return [...new Set(hosts)].sort();
706
837
  }
707
838
  function readStringArray(value, message) {
708
- if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.length > 256))
839
+ if (!Array.isArray(value) ||
840
+ value.some((entry) => typeof entry !== "string" || entry.length > 256))
709
841
  throw new ScenarioBundleError(message);
710
842
  return value;
711
843
  }
@@ -716,25 +848,26 @@ function isMultipartMime(value) {
716
848
  return typeof value === "string" && /^multipart\//i.test(value);
717
849
  }
718
850
  function isTimestamp(value) {
719
- return typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value) && Number.isFinite(Date.parse(value));
851
+ return (typeof value === "string" &&
852
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value) &&
853
+ Number.isFinite(Date.parse(value)));
720
854
  }
721
855
  function deterministicTimestamp(index) {
722
856
  return `2020-01-01T00:00:${String(index % 60).padStart(2, "0")}.000Z`;
723
857
  }
724
858
  function normalizeTitle(value) {
725
- if (typeof value !== "string" || value.trim() === "" || value.trim().length > 120)
859
+ if (typeof value !== "string" ||
860
+ value.trim() === "" ||
861
+ value.trim().length > 120)
726
862
  throw new ScenarioBundleError("Scenario bundle title must be 1-120 characters.");
727
863
  return value.trim();
728
864
  }
729
865
  function slugify(value) {
730
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80) || "sandbox-recording";
731
- }
732
- function stableStringify(value) {
733
- if (value === null || typeof value !== "object")
734
- return JSON.stringify(value);
735
- if (Array.isArray(value))
736
- return `[${value.map(stableStringify).join(",")}]`;
737
- return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`).join(",")}}`;
866
+ return (value
867
+ .toLowerCase()
868
+ .replace(/[^a-z0-9]+/g, "-")
869
+ .replace(/^-|-$/g, "")
870
+ .slice(0, 80) || "sandbox-recording");
738
871
  }
739
872
  function serializedBytes(value, label) {
740
873
  try {