@apifuse/provider-sdk 2.2.0-beta.4 → 2.2.0-beta.41

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 (315) hide show
  1. package/AUTHORING.md +501 -13
  2. package/CHANGELOG.md +169 -1
  3. package/README.md +72 -14
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-check.ts +150 -5
  6. package/bin/apifuse-dev.ts +38 -5
  7. package/bin/apifuse-migrate-shape.ts +84 -0
  8. package/bin/apifuse-pack-check.ts +22 -2
  9. package/bin/apifuse-pack-smoke.ts +57 -2
  10. package/bin/apifuse-pack-types.ts +356 -38
  11. package/bin/apifuse-perf.ts +14 -13
  12. package/bin/apifuse-record.ts +691 -68
  13. package/bin/apifuse-submit-check.ts +2301 -282
  14. package/bin/apifuse-sync-assets.ts +117 -0
  15. package/bin/submit-check-delimited-text.ts +50 -0
  16. package/dist/auth-turn/index.d.ts +3 -3
  17. package/dist/auth-turn/index.js +1 -1
  18. package/dist/auth.d.ts +14 -0
  19. package/dist/auth.js +67 -0
  20. package/dist/ceremonies/index.d.ts +16 -0
  21. package/dist/ceremonies/index.js +141 -36
  22. package/dist/cli/commands.d.ts +1 -1
  23. package/dist/cli/commands.js +16 -0
  24. package/dist/cli/create.d.ts +3 -0
  25. package/dist/cli/create.js +40 -35
  26. package/dist/cli/migrate-provider-shape.d.ts +52 -0
  27. package/dist/cli/migrate-provider-shape.js +515 -0
  28. package/dist/cli/prompt-assets.d.ts +80 -0
  29. package/dist/cli/prompt-assets.js +743 -0
  30. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  31. package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
  32. package/dist/cli/templates/provider/README.md.tpl +4 -4
  33. package/dist/cli/templates/provider/index.ts.tpl +6 -3
  34. package/dist/cli/templates/provider/operations/ping.ts.tpl +2 -1
  35. package/dist/cli/templates/provider/provider.json.tpl +6 -0
  36. package/dist/config/loader.d.ts +177 -16
  37. package/dist/config/loader.js +424 -127
  38. package/dist/contract-serialization.js +4 -8
  39. package/dist/contract-types.d.ts +1 -0
  40. package/dist/contract.js +3 -0
  41. package/dist/declaration-validation.d.ts +32 -0
  42. package/dist/declaration-validation.js +207 -0
  43. package/dist/define.d.ts +51 -25
  44. package/dist/define.js +774 -39
  45. package/dist/error-observability.d.ts +7 -0
  46. package/dist/error-observability.js +61 -0
  47. package/dist/error-resolution.d.ts +4 -0
  48. package/dist/error-resolution.js +122 -0
  49. package/dist/errors.d.ts +33 -0
  50. package/dist/errors.js +40 -0
  51. package/dist/fixture-sanitization.d.ts +28 -0
  52. package/dist/fixture-sanitization.js +227 -0
  53. package/dist/health-scenario.d.ts +1842 -0
  54. package/dist/health-scenario.js +624 -0
  55. package/dist/index.d.ts +19 -9
  56. package/dist/index.js +10 -6
  57. package/dist/lint.d.ts +6 -1
  58. package/dist/lint.js +362 -3
  59. package/dist/native-address.d.ts +43 -0
  60. package/dist/native-address.js +281 -0
  61. package/dist/native-egress-policy.d.ts +31 -0
  62. package/dist/native-egress-policy.js +288 -0
  63. package/dist/observability.d.ts +5 -2
  64. package/dist/observability.js +48 -1
  65. package/dist/provider.d.ts +8 -2
  66. package/dist/provider.js +3 -1
  67. package/dist/runtime/auth-flow.d.ts +5 -1
  68. package/dist/runtime/auth-flow.js +6 -0
  69. package/dist/runtime/browser.d.ts +1 -0
  70. package/dist/runtime/browser.js +492 -49
  71. package/dist/runtime/cache.d.ts +1 -0
  72. package/dist/runtime/cache.js +169 -15
  73. package/dist/runtime/choice-wordlist.d.ts +9 -0
  74. package/dist/runtime/choice-wordlist.js +138 -0
  75. package/dist/runtime/choice.d.ts +13 -1
  76. package/dist/runtime/choice.js +490 -102
  77. package/dist/runtime/executor.d.ts +2 -2
  78. package/dist/runtime/executor.js +37 -3
  79. package/dist/runtime/http.d.ts +1 -0
  80. package/dist/runtime/http.js +515 -53
  81. package/dist/runtime/instrumentation.d.ts +2 -2
  82. package/dist/runtime/instrumentation.js +366 -8
  83. package/dist/runtime/native-network-errors.d.ts +33 -0
  84. package/dist/runtime/native-network-errors.js +69 -0
  85. package/dist/runtime/native-network.d.ts +96 -0
  86. package/dist/runtime/native-network.js +1232 -0
  87. package/dist/runtime/ocr.d.ts +29 -0
  88. package/dist/runtime/ocr.js +440 -0
  89. package/dist/runtime/proxy-errors.js +6 -2
  90. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  91. package/dist/runtime/proxy-nodemaven.js +146 -0
  92. package/dist/runtime/proxy-telemetry.d.ts +80 -1
  93. package/dist/runtime/proxy-telemetry.js +154 -47
  94. package/dist/runtime/redirects.d.ts +29 -0
  95. package/dist/runtime/redirects.js +36 -0
  96. package/dist/runtime/redis.d.ts +1 -1
  97. package/dist/runtime/redis.js +4 -2
  98. package/dist/runtime/request-options.d.ts +68 -1
  99. package/dist/runtime/request-options.js +548 -0
  100. package/dist/runtime/resolver-config.d.ts +6 -0
  101. package/dist/runtime/resolver-config.js +6 -0
  102. package/dist/runtime/resolver-public.d.ts +1 -0
  103. package/dist/runtime/resolver-public.js +1 -0
  104. package/dist/runtime/resolver-shared.d.ts +3 -0
  105. package/dist/runtime/resolver-shared.js +12 -0
  106. package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
  107. package/dist/runtime/resolver-vendors/bindings.js +40 -0
  108. package/dist/runtime/resolver-vendors/browser.d.ts +22 -0
  109. package/dist/runtime/resolver-vendors/browser.js +377 -0
  110. package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
  111. package/dist/runtime/resolver-vendors/capsolver.js +526 -0
  112. package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
  113. package/dist/runtime/resolver-vendors/hosts.js +33 -0
  114. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
  115. package/dist/runtime/resolver-vendors/twocaptcha.js +407 -0
  116. package/dist/runtime/resolver-vendors/types.d.ts +94 -0
  117. package/dist/runtime/resolver-vendors/types.js +96 -0
  118. package/dist/runtime/resolver.d.ts +60 -0
  119. package/dist/runtime/resolver.js +737 -0
  120. package/dist/runtime/secrets.d.ts +27 -0
  121. package/dist/runtime/secrets.js +51 -0
  122. package/dist/runtime/state.d.ts +3 -0
  123. package/dist/runtime/state.js +277 -71
  124. package/dist/runtime/stealth-cookies.d.ts +20 -0
  125. package/dist/runtime/stealth-cookies.js +111 -0
  126. package/dist/runtime/stealth.d.ts +28 -3
  127. package/dist/runtime/stealth.js +519 -255
  128. package/dist/runtime/stt.js +1 -12
  129. package/dist/runtime/timeout.d.ts +5 -0
  130. package/dist/runtime/timeout.js +12 -0
  131. package/dist/runtime/trace-config.d.ts +12 -0
  132. package/dist/runtime/trace-config.js +61 -0
  133. package/dist/serve.d.ts +1 -1
  134. package/dist/serve.js +1 -1
  135. package/dist/server/error-observability.d.ts +1 -0
  136. package/dist/server/error-observability.js +1 -0
  137. package/dist/server/index.d.ts +6 -3
  138. package/dist/server/index.js +3 -3
  139. package/dist/server/self-test-input-tokens.d.ts +2 -1
  140. package/dist/server/self-test-input-tokens.js +18 -14
  141. package/dist/server/self-test.d.ts +114 -0
  142. package/dist/server/self-test.js +787 -148
  143. package/dist/server/serve-implementation.d.ts +225 -0
  144. package/dist/server/serve-implementation.js +2242 -0
  145. package/dist/server/serve.d.ts +1 -70
  146. package/dist/server/serve.js +1 -1130
  147. package/dist/server/trace-output.d.ts +4 -0
  148. package/dist/server/trace-output.js +20 -0
  149. package/dist/server/types.d.ts +30 -5
  150. package/dist/server/types.js +13 -1
  151. package/dist/stateful/errors.d.ts +19 -0
  152. package/dist/stateful/errors.js +24 -0
  153. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  154. package/dist/stateful/http-provider-event-emitter.js +237 -0
  155. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  156. package/dist/stateful/http-session-owner-registry.js +210 -0
  157. package/dist/stateful/index.d.ts +18 -0
  158. package/dist/stateful/index.js +18 -0
  159. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  160. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  161. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  162. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  163. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  164. package/dist/stateful/provider-event-pipeline.js +1 -0
  165. package/dist/stateful/provider-events.d.ts +101 -0
  166. package/dist/stateful/provider-events.js +289 -0
  167. package/dist/stateful/session-key.d.ts +15 -0
  168. package/dist/stateful/session-key.js +86 -0
  169. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  170. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  171. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  172. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  173. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  174. package/dist/stateful/stateful-provider-adapter.js +287 -0
  175. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  176. package/dist/stateful/stateful-provider-observability.js +161 -0
  177. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  178. package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
  179. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  180. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  181. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  182. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  183. package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
  184. package/dist/stateful/stateful-provider-session-routing.js +345 -0
  185. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  186. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  187. package/dist/stateful-signing.d.ts +18 -0
  188. package/dist/stateful-signing.js +27 -0
  189. package/dist/stealth/profiles.js +16 -7
  190. package/dist/stream-evidence.d.ts +74 -0
  191. package/dist/stream-evidence.js +785 -0
  192. package/dist/stream.js +7 -1
  193. package/dist/testing/index.d.ts +2 -1
  194. package/dist/testing/index.js +2 -1
  195. package/dist/testing/run.d.ts +32 -2
  196. package/dist/testing/run.js +489 -21
  197. package/dist/trace-sanitization.d.ts +5 -0
  198. package/dist/trace-sanitization.js +45 -0
  199. package/dist/types.d.ts +563 -33
  200. package/dist/types.js +1 -0
  201. package/package.json +44 -5
  202. package/src/auth-turn/index.ts +1 -1
  203. package/src/auth.ts +118 -0
  204. package/src/ceremonies/index.ts +189 -46
  205. package/src/cli/commands.ts +20 -0
  206. package/src/cli/create.ts +48 -35
  207. package/src/cli/migrate-provider-shape.ts +701 -0
  208. package/src/cli/prompt-assets.ts +865 -0
  209. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  210. package/src/cli/templates/provider/Dockerfile.tpl +1 -1
  211. package/src/cli/templates/provider/README.md.tpl +4 -4
  212. package/src/cli/templates/provider/index.ts.tpl +6 -3
  213. package/src/cli/templates/provider/operations/ping.ts.tpl +2 -1
  214. package/src/cli/templates/provider/provider.json.tpl +6 -0
  215. package/src/config/loader.ts +665 -163
  216. package/src/contract-serialization.ts +5 -7
  217. package/src/contract-types.ts +1 -0
  218. package/src/contract.ts +3 -0
  219. package/src/declaration-validation.ts +266 -0
  220. package/src/define.ts +1003 -88
  221. package/src/error-observability.ts +64 -0
  222. package/src/error-resolution.ts +127 -0
  223. package/src/errors.ts +68 -0
  224. package/src/fixture-sanitization.ts +264 -0
  225. package/src/health-scenario.ts +875 -0
  226. package/src/index.ts +205 -8
  227. package/src/lint.ts +408 -4
  228. package/src/native-address.ts +340 -0
  229. package/src/native-egress-policy.ts +358 -0
  230. package/src/observability.ts +51 -1
  231. package/src/provider.ts +134 -0
  232. package/src/runtime/auth-flow.ts +12 -0
  233. package/src/runtime/browser.ts +661 -63
  234. package/src/runtime/cache.ts +189 -14
  235. package/src/runtime/choice-wordlist.ts +145 -0
  236. package/src/runtime/choice.ts +631 -120
  237. package/src/runtime/executor.ts +53 -8
  238. package/src/runtime/http.ts +641 -61
  239. package/src/runtime/instrumentation.ts +520 -15
  240. package/src/runtime/native-network-errors.ts +99 -0
  241. package/src/runtime/native-network.ts +1605 -0
  242. package/src/runtime/ocr.ts +523 -0
  243. package/src/runtime/proxy-errors.ts +12 -4
  244. package/src/runtime/proxy-nodemaven.ts +221 -0
  245. package/src/runtime/proxy-telemetry.ts +244 -75
  246. package/src/runtime/redirects.ts +66 -0
  247. package/src/runtime/redis.ts +7 -2
  248. package/src/runtime/request-options.ts +680 -1
  249. package/src/runtime/resolver-config.ts +6 -0
  250. package/src/runtime/resolver-public.ts +20 -0
  251. package/src/runtime/resolver-shared.ts +17 -0
  252. package/src/runtime/resolver-vendors/bindings.ts +56 -0
  253. package/src/runtime/resolver-vendors/browser.ts +533 -0
  254. package/src/runtime/resolver-vendors/capsolver.ts +700 -0
  255. package/src/runtime/resolver-vendors/hosts.ts +38 -0
  256. package/src/runtime/resolver-vendors/twocaptcha.ts +539 -0
  257. package/src/runtime/resolver-vendors/types.ts +212 -0
  258. package/src/runtime/resolver.ts +1103 -0
  259. package/src/runtime/secrets.ts +64 -0
  260. package/src/runtime/state.ts +394 -77
  261. package/src/runtime/stealth-cookies.ts +132 -0
  262. package/src/runtime/stealth.ts +675 -289
  263. package/src/runtime/stt.ts +1 -19
  264. package/src/runtime/timeout.ts +18 -0
  265. package/src/runtime/trace-config.ts +77 -0
  266. package/src/serve.ts +6 -1
  267. package/src/server/error-observability.ts +1 -0
  268. package/src/server/index.ts +39 -2
  269. package/src/server/self-test-input-tokens.ts +29 -14
  270. package/src/server/self-test.ts +1030 -175
  271. package/src/server/serve-implementation.ts +3338 -0
  272. package/src/server/serve.ts +1 -1626
  273. package/src/server/trace-output.ts +32 -0
  274. package/src/server/types.ts +13 -1
  275. package/src/stateful/README.md +146 -0
  276. package/src/stateful/errors.ts +35 -0
  277. package/src/stateful/http-provider-event-emitter.ts +314 -0
  278. package/src/stateful/http-session-owner-registry.ts +306 -0
  279. package/src/stateful/index.ts +18 -0
  280. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  281. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  282. package/src/stateful/provider-event-pipeline.ts +61 -0
  283. package/src/stateful/provider-events.ts +462 -0
  284. package/src/stateful/session-key.ts +111 -0
  285. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  286. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  287. package/src/stateful/stateful-provider-adapter.ts +562 -0
  288. package/src/stateful/stateful-provider-observability.ts +261 -0
  289. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  290. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  291. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  292. package/src/stateful/stateful-provider-session-routing.ts +546 -0
  293. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  294. package/src/stateful-signing.ts +46 -0
  295. package/src/stealth/profiles.ts +17 -7
  296. package/src/stream-evidence.ts +988 -0
  297. package/src/stream.ts +8 -1
  298. package/src/testing/index.ts +10 -1
  299. package/src/testing/run.ts +658 -15
  300. package/src/trace-sanitization.ts +63 -0
  301. package/src/types.ts +698 -65
  302. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  303. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  304. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  305. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  306. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  307. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  308. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  309. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  310. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  311. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  312. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  313. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  314. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  315. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
@@ -0,0 +1,64 @@
1
+ import { isProviderError, type ProviderErrorObservability } from "./errors.js";
2
+
3
+ const PROVIDER_OBSERVABILITY_TOKEN_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
4
+ const PROVIDER_OBSERVABILITY_FINGERPRINT_PATTERN = /^[A-Fa-f0-9]{12}$/;
5
+ const MAX_PROVIDER_OBSERVABILITY_MESSAGE_LENGTH = 10_000_000;
6
+
7
+ /**
8
+ * Extracts only own data properties from branded provider errors. In
9
+ * particular, descriptor reads reject options/observability accessors without
10
+ * invoking provider-controlled getters.
11
+ */
12
+ export function safeProviderErrorObservability(
13
+ error: unknown,
14
+ ): ProviderErrorObservability | undefined {
15
+ if (!isProviderError(error)) return undefined;
16
+ let candidate: unknown;
17
+ let reason: unknown;
18
+ let fingerprint: unknown;
19
+ let messageLength: unknown;
20
+ try {
21
+ const optionsDescriptor = Object.getOwnPropertyDescriptor(error, "options");
22
+ if (optionsDescriptor === undefined || !Object.hasOwn(optionsDescriptor, "value")) {
23
+ return undefined;
24
+ }
25
+ const options = optionsDescriptor.value;
26
+ if (options === null || typeof options !== "object" || Array.isArray(options)) {
27
+ return undefined;
28
+ }
29
+ const observabilityDescriptor = Object.getOwnPropertyDescriptor(options, "observability");
30
+ if (observabilityDescriptor === undefined || !Object.hasOwn(observabilityDescriptor, "value")) {
31
+ return undefined;
32
+ }
33
+ candidate = observabilityDescriptor.value;
34
+ if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) {
35
+ return undefined;
36
+ }
37
+ const ownValue = (key: keyof ProviderErrorObservability): unknown => {
38
+ const descriptor = Object.getOwnPropertyDescriptor(candidate as object, key);
39
+ return descriptor && Object.hasOwn(descriptor, "value") ? descriptor.value : undefined;
40
+ };
41
+ reason = ownValue("reason");
42
+ fingerprint = ownValue("fingerprint");
43
+ messageLength = ownValue("messageLength");
44
+ } catch {
45
+ return undefined;
46
+ }
47
+ const safe: ProviderErrorObservability = {
48
+ ...(typeof reason === "string" && PROVIDER_OBSERVABILITY_TOKEN_PATTERN.test(reason)
49
+ ? { reason }
50
+ : {}),
51
+ ...(typeof fingerprint === "string" &&
52
+ PROVIDER_OBSERVABILITY_FINGERPRINT_PATTERN.test(fingerprint)
53
+ ? { fingerprint }
54
+ : {}),
55
+ ...(typeof messageLength === "number" &&
56
+ Number.isInteger(messageLength) &&
57
+ messageLength >= 0 &&
58
+ messageLength <= MAX_PROVIDER_OBSERVABILITY_MESSAGE_LENGTH
59
+ ? { messageLength }
60
+ : {}),
61
+ };
62
+
63
+ return Object.keys(safe).length > 0 ? safe : undefined;
64
+ }
@@ -0,0 +1,127 @@
1
+ import type { ProviderErrorStatus } from "./types.js";
2
+
3
+ // This set suppresses the unregistered-provider-error-code signal for codes
4
+ // intentionally emitted by SDK paths. It is not the complete authority for
5
+ // runtime error resolution: branded errors and additional canonical SDK codes
6
+ // must also remain immune to provider-declared status/retryability overrides.
7
+ export const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
8
+ "MISSING_SECRET",
9
+ "AUTH_PROMPT_UNAVAILABLE",
10
+ "BROWSER_CDP_POOL_REQUIRED",
11
+ "BROWSER_RUNTIME_UNSUPPORTED",
12
+ "STEALTH_RUNTIME_UNSUPPORTED",
13
+ "SSE_EVENT_UNDECLARED",
14
+ "STREAM_EVENT_TOO_LARGE",
15
+ "STREAM_CHUNK_TOO_LARGE",
16
+ "SSE_RESULT_UNSUPPORTED",
17
+ "STREAM_RESULT_UNSUPPORTED",
18
+ "AUTH_FLOW_NOT_CONFIGURED",
19
+ "refresh_not_supported",
20
+ "RUNTIME_UNSUPPORTED",
21
+ "PROVIDER_STATE_UNSUPPORTED",
22
+ "CHOICE_TOKEN_MASTER_SECRET_NOT_CONFIGURED",
23
+ "CHOICE_STATE_PAYLOAD_TOO_LARGE",
24
+ "CHOICE_STATE_UNAVAILABLE",
25
+ "CHOICE_CONTEXT_REQUIRED",
26
+ "unsupported_stealth_cookie_store_version",
27
+ "provider_secret_error",
28
+ "credential_key_error",
29
+ "credential_mode_error",
30
+ "flow_expired",
31
+ "turn_validation_error",
32
+ "context_access_error",
33
+ "OCR_UPSTREAM_FAILED",
34
+ "UNSUPPORTED_STT_OPTION",
35
+ "INVALID_STT_AUDIO",
36
+ "STT_AUDIO_TOO_LARGE",
37
+ "STT_UPSTREAM_FAILED",
38
+ "INVALID_STT_VERIFICATION_CODE_OPTIONS",
39
+ "NO_CODE_FOUND",
40
+ "AMBIGUOUS_CODE",
41
+ "retry_invalid_policy",
42
+ "retry_unsafe_method",
43
+ "stealth_cookie_store_serialize_failed",
44
+ "response_too_large",
45
+ "transport_stream_unavailable",
46
+ "transport_invalid_method",
47
+ "http_transport_override_unsupported",
48
+ "http_redirect_policy_invalid",
49
+ "http_redirect_stopped",
50
+ "http_redirect_max_hops",
51
+ "http_redirect_missing_location",
52
+ "http_redirect_loop",
53
+ "transport_invalid_url",
54
+ "retry_exhausted",
55
+ "auth_abort_unsafe_data",
56
+ "credentials_auth_missing_credential_keys",
57
+ "credentials_auth_missing_credential",
58
+ "credentials_auth_invalid_login_result",
59
+ "credentials_auth_unknown_challenge",
60
+ "credentials_auth_unknown_pending_challenge",
61
+ "STATEFUL_FORWARDING_NOT_CONFIGURED",
62
+ "STATEFUL_FORWARDING_SIGNATURE_MISSING",
63
+ "STATEFUL_FORWARDING_NONCE_INVALID",
64
+ "STATEFUL_FORWARDING_TIMESTAMP_INVALID",
65
+ "STATEFUL_FORWARDING_SIGNATURE_INVALID",
66
+ "STATEFUL_FORWARDING_REPLAY_DETECTED",
67
+ "STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
68
+ "STATEFUL_FORWARDING_ENVELOPE_INVALID",
69
+ "STATEFUL_FORWARDING_PROVIDER_MISMATCH",
70
+ "STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
71
+ "STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
72
+ "STATEFUL_FORWARDING_REQUEST_FAILED",
73
+ "STATEFUL_FORWARDING_CONTEXT_MISSING",
74
+ "STATEFUL_FORWARDING_BAD_RESPONSE",
75
+ "STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
76
+ "STATEFUL_FILE_FORWARDING_UNSUPPORTED",
77
+ "STATEFUL_CONTROL_PLANE_OPERATION_AMBIGUOUS",
78
+ "STATEFUL_CONTROL_PLANE_REQUEST_FAILED",
79
+ "STATEFUL_CONTROL_PLANE_HTTP_ERROR",
80
+ "STATEFUL_CONTROL_PLANE_INVALID_RESPONSE",
81
+ ]);
82
+
83
+ // Complete code authority for provider-declared runtime resolution. Keep this
84
+ // separate from signal suppression: declarations may document these codes, but
85
+ // their status and retryability can never override the SDK's canonical result.
86
+ export const SDK_RUNTIME_OWNED_ERROR_CODES = new Set([
87
+ ...SDK_OWNED_PROVIDER_ERROR_CODES,
88
+ "reauth_required",
89
+ "OCR_UNAVAILABLE",
90
+ "UNSUPPORTED_OCR_BACKEND",
91
+ "STT_UNAVAILABLE",
92
+ "UNSUPPORTED_STT_BACKEND",
93
+ "OUTPUT_VALIDATION_FAILED",
94
+ "NOT_FOUND",
95
+ "not_found",
96
+ ]);
97
+
98
+ // Canonical SDK status mapping for recognized provider-thrown error codes.
99
+ // serve.ts toStatusCode consults this map (after operation-declared overrides
100
+ // for non-SDK-owned codes), and the authoring lint treats these codes as
101
+ // SDK-registered. Add new codes here instead of duplicating literals in
102
+ // either consumer.
103
+ export const SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES: ReadonlyMap<string, ProviderErrorStatus> =
104
+ new Map<string, ProviderErrorStatus>([
105
+ ["AUTH_REQUIRED", 401],
106
+ ["reauth_required", 401],
107
+ // Unprovisioned declared secret: a deployment/config defect, never an
108
+ // upstream failure — explicit 400.
109
+ ["MISSING_SECRET", 400],
110
+ ["NOT_FOUND", 404],
111
+ ["not_found", 404],
112
+ ["NO_DATA", 404],
113
+ ["RATE_LIMITED", 429],
114
+ ["UPSTREAM_RATE_LIMIT", 429],
115
+ ["LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR", 429],
116
+ // Deterministic upstream business refusal (honest-provider-error-
117
+ // contract): the upstream evaluated the request and said no under its
118
+ // own rules — a conflict with upstream state, never a 5xx.
119
+ ["UPSTREAM_REJECTED", 409],
120
+ ["UPSTREAM_ERROR", 502],
121
+ ["BLOCKED", 502],
122
+ ["OCR_UNAVAILABLE", 503],
123
+ ["UNSUPPORTED_OCR_BACKEND", 503],
124
+ ["STT_UNAVAILABLE", 503],
125
+ ["UNSUPPORTED_STT_BACKEND", 503],
126
+ ["STATEFUL_FORWARDING_REPLAY_CACHE_FULL", 503],
127
+ ]);
package/src/errors.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ProviderErrorCategory } from "./observability.js";
2
+ import type { HttpRedirectFailureReason } from "./types.js";
2
3
 
3
4
  // Versioned, cross-realm brands. `Symbol.for` resolves to the same symbol in
4
5
  // any copy/entrypoint of this SDK major version, so an error created by a
@@ -10,6 +11,7 @@ const PROVIDER_ERROR_BRAND = Symbol.for("@apifuse/provider-sdk/error-brand@1");
10
11
  const PROVIDER_ERROR_BRAND_VALUE = 1;
11
12
  const SESSION_EXPIRED_BRAND = Symbol.for("@apifuse/provider-sdk/error-kind/session-expired@1");
12
13
  const TRANSPORT_BRAND = Symbol.for("@apifuse/provider-sdk/error-kind/transport@1");
14
+ const VALIDATION_BRAND = Symbol.for("@apifuse/provider-sdk/error-kind/validation@1");
13
15
 
14
16
  // Defines a non-enumerable, non-writable, non-configurable own data property.
15
17
  // Immutable + own means a guard can trust it via a single descriptor read
@@ -44,6 +46,22 @@ export type ProviderErrorOptions = {
44
46
  cause?: Error;
45
47
  category?: ProviderErrorCategory;
46
48
  retryable?: boolean;
49
+ /** Provider-authored, bounded metadata safe for operational logs and error headers. */
50
+ observability?: ProviderErrorObservability;
51
+ };
52
+
53
+ /**
54
+ * Provider-authored error diagnostics whose runtime values are validated before emission.
55
+ * Classification tokens such as `reason` are source literals, not runtime user input or
56
+ * credentials. The gateway removes the observability header from tenant responses.
57
+ */
58
+ export type ProviderErrorObservability = {
59
+ /** A 1-64 character `[A-Za-z0-9_.-]` classification token, for example `LOGIN_COMPLETE_FAILED`. */
60
+ reason?: string;
61
+ /** A provider-computed 12-hex-character fingerprint of private diagnostic input. */
62
+ fingerprint?: string;
63
+ /** The non-negative length of the private diagnostic input, capped at 10,000,000. */
64
+ messageLength?: number;
47
65
  };
48
66
 
49
67
  export class ProviderError extends Error {
@@ -79,6 +97,21 @@ export class SDKError extends ProviderError {
79
97
  }
80
98
  }
81
99
 
100
+ /** Raised when persisted stealth cookies use a store version this SDK cannot read. */
101
+ export class StealthCookieStoreVersionError extends SDKError {
102
+ constructor(public readonly version: unknown) {
103
+ const displayedVersion =
104
+ typeof version === "string" || typeof version === "number"
105
+ ? String(version)
106
+ : "missing or invalid";
107
+ super(`Unsupported stealth cookie store version: ${displayedVersion}`, {
108
+ code: "unsupported_stealth_cookie_store_version",
109
+ details: { receivedVersion: version, supportedVersions: [1] },
110
+ });
111
+ this.name = "StealthCookieStoreVersionError";
112
+ }
113
+ }
114
+
82
115
  export class AuthError extends ProviderError {
83
116
  constructor(message: string, options?: ProviderErrorOptions) {
84
117
  super(message, options);
@@ -110,6 +143,7 @@ export class ValidationError extends ProviderError {
110
143
  super(message, options);
111
144
  this.name = "ValidationError";
112
145
  this.zodError = options?.zodError;
146
+ defineErrorBrand(this, VALIDATION_BRAND, true);
113
147
  }
114
148
  }
115
149
 
@@ -131,6 +165,33 @@ export class TransportError extends ProviderError {
131
165
  }
132
166
  }
133
167
 
168
+ export type HttpRedirectErrorOptions = TransportErrorOptions & {
169
+ reason: HttpRedirectFailureReason;
170
+ /** Redacted redirect target suitable for provider diagnostics. */
171
+ target?: string;
172
+ };
173
+
174
+ /** Raised when an opt-in ctx.http redirect policy refuses or cannot resolve a hop. */
175
+ export class HttpRedirectError extends TransportError {
176
+ readonly reason: HttpRedirectFailureReason;
177
+ readonly target?: string;
178
+
179
+ constructor(message: string, options: HttpRedirectErrorOptions) {
180
+ const { reason, target, ...transportOptions } = options;
181
+ super(message, {
182
+ ...transportOptions,
183
+ code: `http_redirect_${reason}`,
184
+ details: {
185
+ reason,
186
+ ...(target ? { target } : {}),
187
+ },
188
+ });
189
+ this.name = "HttpRedirectError";
190
+ this.reason = reason;
191
+ this.target = target;
192
+ }
193
+ }
194
+
134
195
  // Cross-module type guards. Prefer these over `instanceof` at any boundary that
135
196
  // may receive an error from a different copy/entrypoint of the SDK (see the HTTP
136
197
  // server error boundary). They recognize branded errors regardless of which
@@ -147,6 +208,13 @@ export function isTransportError(value: unknown): value is TransportError {
147
208
  return isProviderError(value) && hasOwnBrand(value, TRANSPORT_BRAND, true);
148
209
  }
149
210
 
211
+ export function isValidationError(value: unknown): value is ValidationError {
212
+ return (
213
+ isProviderError(value) &&
214
+ (hasOwnBrand(value, VALIDATION_BRAND, true) || value.name === "ValidationError")
215
+ );
216
+ }
217
+
150
218
  export class ProviderSecretError extends ProviderError {
151
219
  constructor(message: string, options?: ProviderErrorOptions) {
152
220
  super(message, { code: "provider_secret_error", ...options });
@@ -0,0 +1,264 @@
1
+ import type { JsonValue } from "./contract-json.js";
2
+
3
+ export const REDACTED_FIXTURE_VALUE = "[REDACTED]";
4
+
5
+ const OPAQUE_TOKEN = /^[A-Za-z0-9_+/=.:~-]+$/;
6
+ const OPAQUE_TOKEN_RUN = /[A-Za-z0-9_+/=.:~-]{24,}/g;
7
+ const URL_RUN = /https?:\/\/[^\s"'<>]+/gi;
8
+ const EMAIL_ADDRESS_RUN = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
9
+ const DIAGNOSTIC_URL_SENTINEL_DELIMITER = String.fromCodePoint(0);
10
+ const DIAGNOSTIC_URL_SENTINEL_RUN = new RegExp(
11
+ `${DIAGNOSTIC_URL_SENTINEL_DELIMITER}APIFUSE_URL(\\d+)${DIAGNOSTIC_URL_SENTINEL_DELIMITER}`,
12
+ "g",
13
+ );
14
+ const PEM_PRIVATE_KEY =
15
+ /-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/g;
16
+
17
+ /** Matches credential field names without treating benign prefixes such as `author` as `auth`. */
18
+ export function isSensitiveFixtureKey(key: string): boolean {
19
+ const normalized = key.replace(/[-_\s]/g, "").toLowerCase();
20
+ const candidates = [normalized, normalized.replace(/(?:value|payload|header)$/, "")];
21
+ return candidates.some(
22
+ (candidate) =>
23
+ /^(?:authorization|authentication|auth|bearer|cookie|credential|password|passwd|privatekey|secret|session|sessionid|token)$/.test(
24
+ candidate,
25
+ ) ||
26
+ /^(?:api|client|service|access|consumer)(?:key|secret|token)$/.test(candidate) ||
27
+ /(?:authorization|credential|password|passwd|privatekey|secret|sessionid|token)$/.test(
28
+ candidate,
29
+ ),
30
+ );
31
+ }
32
+
33
+ /**
34
+ * Returns JSON fixture data with credential-bearing keys and heuristic-confirmed string secrets
35
+ * replaced. Ordinary short prose and identifiers are retained.
36
+ */
37
+ export function sanitizeFixture(value: JsonValue): JsonValue {
38
+ if (Array.isArray(value)) {
39
+ return value.map((item) => sanitizeFixture(item));
40
+ }
41
+
42
+ if (typeof value === "string") return sanitizeFixtureString(value);
43
+ if (value === null || typeof value !== "object") return value;
44
+
45
+ return Object.fromEntries(
46
+ Object.entries(value).map(([key, entryValue]) => [
47
+ key,
48
+ isSensitiveFixtureKey(key) ? REDACTED_FIXTURE_VALUE : sanitizeFixture(entryValue),
49
+ ]),
50
+ );
51
+ }
52
+
53
+ /** Applies the shared credential-key policy to ordinary JSON fixtures. */
54
+ export function sanitizeOrdinaryFixture(value: JsonValue): JsonValue {
55
+ if (Array.isArray(value)) return value.map((item) => sanitizeOrdinaryFixture(item));
56
+ if (value === null || typeof value !== "object") return value;
57
+ return Object.fromEntries(
58
+ Object.entries(value).map(([key, entryValue]) => [
59
+ key,
60
+ isSensitiveFixtureKey(key) ? REDACTED_FIXTURE_VALUE : sanitizeOrdinaryFixture(entryValue),
61
+ ]),
62
+ );
63
+ }
64
+
65
+ /** Sanitizes a primitive fixture string only when textual-secret heuristics match. */
66
+ export function sanitizeFixtureString(value: string): string {
67
+ let sanitized = value.replace(PEM_PRIVATE_KEY, REDACTED_FIXTURE_VALUE);
68
+ const retainedUrls: string[] = [];
69
+ sanitized = sanitized.replace(URL_RUN, (url) => {
70
+ const index =
71
+ retainedUrls.push(isCredentialBearingUrl(url) ? sanitizeUrlForLogs(url) : url) - 1;
72
+ return `APIFUSEURL${index}X`;
73
+ });
74
+ sanitized = redactSensitiveAssignments(sanitized);
75
+ sanitized = sanitized.replace(OPAQUE_TOKEN_RUN, (candidate) =>
76
+ isSensitiveFixtureValue(candidate) ? REDACTED_FIXTURE_VALUE : candidate,
77
+ );
78
+ sanitized = sanitized.replace(
79
+ /APIFUSEURL(\d+)X/g,
80
+ (_match, index: string) => retainedUrls[Number(index)] ?? REDACTED_FIXTURE_VALUE,
81
+ );
82
+ return sanitized;
83
+ }
84
+
85
+ /** True for opaque values that are unsafe to retain in paths or unstructured text. */
86
+ export function isSensitiveFixtureValue(value: string): boolean {
87
+ const candidate = decodePathSegment(value);
88
+ if (/^bot(?:\d{6,}:)?[A-Za-z0-9_-]{16,}$/i.test(candidate)) return true;
89
+ if (/^\d{6,}:[A-Za-z0-9_-]{20,}$/.test(candidate)) return true;
90
+ if (/^(?:gh[opusr]_|sk[-_]|xox[baprs]-)[A-Za-z0-9_-]{16,}$/i.test(candidate)) return true;
91
+ if (!OPAQUE_TOKEN.test(candidate) || candidate.length < 24) return false;
92
+ if (/^[a-f0-9]{32,}$/i.test(candidate)) return true;
93
+ return shannonEntropy(candidate) >= 3.5;
94
+ }
95
+
96
+ /** Sanitizes every path segment and values following a credential-like segment name. */
97
+ export function sanitizePathname(pathname: string): string {
98
+ const segments = pathname.split("/");
99
+ return segments
100
+ .map((segment, index) => {
101
+ if (!segment) return segment;
102
+ const decoded = decodePathSegment(segment);
103
+ const previous = index > 0 ? decodePathSegment(segments[index - 1] as string) : "";
104
+ if (
105
+ isSensitivePathSegment(decoded) ||
106
+ isCredentialPathKey(previous) ||
107
+ isSensitiveFixtureValue(decoded)
108
+ ) {
109
+ return REDACTED_FIXTURE_VALUE;
110
+ }
111
+ return segment;
112
+ })
113
+ .join("/");
114
+ }
115
+
116
+ function isCredentialPathKey(key: string): boolean {
117
+ const finalPathPart = key.split("/").at(-1) ?? "";
118
+ const baseSegment = finalPathPart.split(";", 1)[0] ?? "";
119
+ return isSensitiveFixtureKey(baseSegment.split(/[=:]/, 1)[0] ?? "");
120
+ }
121
+
122
+ function isSensitivePathSegment(segment: string): boolean {
123
+ return segment
124
+ .split(/[;/]/)
125
+ .some((part) => isSensitiveFixtureKey(part.split(/[=:]/, 1)[0] ?? ""));
126
+ }
127
+
128
+ /** Removes userinfo, query values, fragments, and credential-like path segments from log URLs. */
129
+ export function sanitizeUrlForLogs(value: string): string {
130
+ try {
131
+ const parsed = new URL(value, "https://fixture.invalid");
132
+ const queryMarker = parsed.search ? `?${REDACTED_FIXTURE_VALUE}` : "";
133
+ const path = sanitizePathname(parsed.pathname);
134
+ if (parsed.origin === "https://fixture.invalid" && !hasExplicitOrigin(value)) {
135
+ return `${path}${queryMarker}`;
136
+ }
137
+ return `${parsed.origin}${path}${queryMarker}`;
138
+ } catch {
139
+ return sanitizePathname(value.split(/[?#]/, 1)[0]);
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Returns query-free request provenance with each path segment scrubbed for credential-like values.
145
+ * Origins, URL userinfo, query values, and fragments are never persisted in request provenance.
146
+ */
147
+ export function requestPathForFixture(value: string): string {
148
+ try {
149
+ return sanitizePathname(new URL(value, "https://fixture.invalid").pathname);
150
+ } catch {
151
+ const path = value.split(/[?#]/, 1)[0];
152
+ return sanitizePathname(path.startsWith("/") ? path : `/${path}`);
153
+ }
154
+ }
155
+
156
+ /** Scrubs secrets and terminal/log control characters before diagnostic text is emitted. */
157
+ export function sanitizeDiagnosticText(value: string): string {
158
+ const retainedUrls: string[] = [];
159
+ // Remove attacker-controlled NUL delimiters before introducing internal URL sentinels.
160
+ let sanitized = encodeDiagnosticControls(value)
161
+ .replace(URL_RUN, (url) => {
162
+ const index = retainedUrls.push(sanitizeUrlForLogs(url)) - 1;
163
+ return `${DIAGNOSTIC_URL_SENTINEL_DELIMITER}APIFUSE_URL${index}${DIAGNOSTIC_URL_SENTINEL_DELIMITER}`;
164
+ })
165
+ .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, `Bearer ${REDACTED_FIXTURE_VALUE}`)
166
+ .replace(EMAIL_ADDRESS_RUN, REDACTED_FIXTURE_VALUE);
167
+ sanitized = redactSensitiveAssignments(sanitized);
168
+ sanitized = sanitized.replace(OPAQUE_TOKEN_RUN, (candidate, offset: number, source: string) => {
169
+ if (/^(?:request|trace|correlation)[-_]?id[:=]/i.test(candidate)) return candidate;
170
+ const prefix = source.slice(Math.max(0, offset - 32), offset);
171
+ if (/(?:request|trace|correlation)[-_]?id\s*[:=]\s*$/i.test(prefix)) return candidate;
172
+ return isSensitiveFixtureValue(candidate) ? REDACTED_FIXTURE_VALUE : candidate;
173
+ });
174
+ sanitized = sanitized.replace(
175
+ DIAGNOSTIC_URL_SENTINEL_RUN,
176
+ (_match, index: string) => retainedUrls[Number(index)] ?? REDACTED_FIXTURE_VALUE,
177
+ );
178
+ return encodeDiagnosticControls(sanitized);
179
+ }
180
+
181
+ function redactSensitiveAssignments(value: string): string {
182
+ return value.replace(
183
+ /((["']?)([\w-]+)\2\s*[:=]\s*)("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;&]+)/gi,
184
+ (match, prefix: string, _quote: string, key: string, assignmentValue: string) => {
185
+ if (!isSensitiveFixtureKey(key) && key.toLowerCase() !== "key") return match;
186
+ const quote = assignmentValue.startsWith('"')
187
+ ? '"'
188
+ : assignmentValue.startsWith("'")
189
+ ? "'"
190
+ : "";
191
+ return `${prefix}${quote}${REDACTED_FIXTURE_VALUE}${quote}`;
192
+ },
193
+ );
194
+ }
195
+
196
+ function isCredentialBearingUrl(value: string): boolean {
197
+ try {
198
+ const parsed = new URL(value);
199
+ return (
200
+ parsed.username !== "" ||
201
+ parsed.password !== "" ||
202
+ parsed.hash !== "" ||
203
+ parsed.search !== "" ||
204
+ parsed.pathname.split("/").some((segment, index, segments) => {
205
+ const decoded = decodePathSegment(segment);
206
+ const previous = decodePathSegment(segments[index - 1] ?? "");
207
+ return (
208
+ isSensitiveFixtureKey(decoded) ||
209
+ isCredentialPathKey(previous) ||
210
+ isSensitiveFixtureValue(decoded)
211
+ );
212
+ })
213
+ );
214
+ } catch {
215
+ return false;
216
+ }
217
+ }
218
+
219
+ /** Encodes terminal/log control characters without applying value-level secret heuristics. */
220
+ export function encodeDiagnosticControls(value: string): string {
221
+ let result = "";
222
+ for (const character of value) {
223
+ const code = character.codePointAt(0) ?? 0;
224
+ if (code === 0x0a || code === 0x0d || code === 0x2028 || code === 0x2029) {
225
+ result += " ";
226
+ } else if (
227
+ (code >= 0 && code <= 0x1f) ||
228
+ (code >= 0x7f && code <= 0x9f) ||
229
+ code === 0x061c ||
230
+ code === 0x200e ||
231
+ code === 0x200f ||
232
+ (code >= 0x202a && code <= 0x202e) ||
233
+ (code >= 0x2066 && code <= 0x2069)
234
+ ) {
235
+ result += `\\u${code.toString(16).padStart(4, "0")}`;
236
+ } else {
237
+ result += character;
238
+ }
239
+ }
240
+ return result;
241
+ }
242
+
243
+ function decodePathSegment(value: string): string {
244
+ try {
245
+ return decodeURIComponent(value);
246
+ } catch {
247
+ return value;
248
+ }
249
+ }
250
+
251
+ function hasExplicitOrigin(value: string): boolean {
252
+ return /^[a-z][a-z\d+.-]*:\/\//i.test(value);
253
+ }
254
+
255
+ function shannonEntropy(value: string): number {
256
+ const counts = new Map<string, number>();
257
+ for (const character of value) counts.set(character, (counts.get(character) ?? 0) + 1);
258
+ let entropy = 0;
259
+ for (const count of counts.values()) {
260
+ const probability = count / value.length;
261
+ entropy -= probability * Math.log2(probability);
262
+ }
263
+ return entropy;
264
+ }