@fgv/ts-extras 5.1.0-5 → 5.1.0-50
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.
- package/LICENSE +1 -1
- package/README.md +90 -0
- package/dist/index.browser.js +6 -2
- package/dist/index.browser.js.map +1 -0
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -0
- package/dist/packlets/ai-assist/chatRequestBuilders.js +282 -0
- package/dist/packlets/ai-assist/chatRequestBuilders.js.map +1 -0
- package/dist/packlets/ai-assist/{apiClient.js → completionClient.js} +165 -214
- package/dist/packlets/ai-assist/completionClient.js.map +1 -0
- package/dist/packlets/ai-assist/converters.js +47 -3
- package/dist/packlets/ai-assist/converters.js.map +1 -0
- package/dist/packlets/ai-assist/embeddingClient.js +345 -0
- package/dist/packlets/ai-assist/embeddingClient.js.map +1 -0
- package/dist/packlets/ai-assist/endpoint.js +107 -0
- package/dist/packlets/ai-assist/endpoint.js.map +1 -0
- package/dist/packlets/ai-assist/http.js +104 -0
- package/dist/packlets/ai-assist/http.js.map +1 -0
- package/dist/packlets/ai-assist/imageGenerationClient.js +454 -0
- package/dist/packlets/ai-assist/imageGenerationClient.js.map +1 -0
- package/dist/packlets/ai-assist/imageOptionsResolver.js +193 -0
- package/dist/packlets/ai-assist/imageOptionsResolver.js.map +1 -0
- package/dist/packlets/ai-assist/index.js +12 -4
- package/dist/packlets/ai-assist/index.js.map +1 -0
- package/dist/packlets/ai-assist/jsonCompletion.js +93 -0
- package/dist/packlets/ai-assist/jsonCompletion.js.map +1 -0
- package/dist/packlets/ai-assist/jsonResponse.js +423 -0
- package/dist/packlets/ai-assist/jsonResponse.js.map +1 -0
- package/dist/packlets/ai-assist/listModelsClient.js +294 -0
- package/dist/packlets/ai-assist/listModelsClient.js.map +1 -0
- package/dist/packlets/ai-assist/model.js +301 -9
- package/dist/packlets/ai-assist/model.js.map +1 -0
- package/dist/packlets/ai-assist/registry.js +376 -12
- package/dist/packlets/ai-assist/registry.js.map +1 -0
- package/dist/packlets/ai-assist/sseParser.js +123 -0
- package/dist/packlets/ai-assist/sseParser.js.map +1 -0
- package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +376 -0
- package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -0
- package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +687 -0
- package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -0
- package/dist/packlets/ai-assist/streamingAdapters/common.js +187 -0
- package/dist/packlets/ai-assist/streamingAdapters/common.js.map +1 -0
- package/dist/packlets/ai-assist/streamingAdapters/gemini.js +208 -0
- package/dist/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -0
- package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js +175 -0
- package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -0
- package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +390 -0
- package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -0
- package/dist/packlets/ai-assist/streamingAdapters/proxy.js +178 -0
- package/dist/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -0
- package/dist/packlets/ai-assist/streamingClient.js +140 -0
- package/dist/packlets/ai-assist/streamingClient.js.map +1 -0
- package/dist/packlets/ai-assist/thinkingOptionsResolver.js +288 -0
- package/dist/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -0
- package/dist/packlets/ai-assist/toolFormats.js +106 -10
- package/dist/packlets/ai-assist/toolFormats.js.map +1 -0
- package/dist/packlets/conversion/converters.js +35 -1
- package/dist/packlets/conversion/converters.js.map +1 -0
- package/dist/packlets/conversion/index.js.map +1 -0
- package/dist/packlets/crypto-utils/constants.js.map +1 -0
- package/dist/packlets/crypto-utils/converters.js +42 -4
- package/dist/packlets/crypto-utils/converters.js.map +1 -0
- package/dist/packlets/crypto-utils/directEncryptionProvider.js.map +1 -0
- package/dist/packlets/crypto-utils/encryptedFile.js +37 -0
- package/dist/packlets/crypto-utils/encryptedFile.js.map +1 -0
- package/dist/packlets/crypto-utils/hpkeProvider.js +355 -0
- package/dist/packlets/crypto-utils/hpkeProvider.js.map +1 -0
- package/dist/packlets/crypto-utils/index.browser.js +13 -2
- package/dist/packlets/crypto-utils/index.browser.js.map +1 -0
- package/dist/packlets/crypto-utils/index.js +9 -1
- package/dist/packlets/crypto-utils/index.js.map +1 -0
- package/dist/packlets/crypto-utils/keyPairAlgorithmParams.js +71 -0
- package/dist/packlets/crypto-utils/keyPairAlgorithmParams.js.map +1 -0
- package/dist/packlets/crypto-utils/keystore/converters.js +111 -16
- package/dist/packlets/crypto-utils/keystore/converters.js.map +1 -0
- package/dist/packlets/crypto-utils/keystore/encryptedFilePrivateKeyStorage.js +287 -0
- package/dist/packlets/crypto-utils/keystore/encryptedFilePrivateKeyStorage.js.map +1 -0
- package/dist/packlets/crypto-utils/keystore/index.browser.js +36 -0
- package/dist/packlets/crypto-utils/keystore/index.browser.js.map +1 -0
- package/dist/packlets/crypto-utils/keystore/index.js +3 -0
- package/dist/packlets/crypto-utils/keystore/index.js.map +1 -0
- package/dist/packlets/crypto-utils/keystore/keyStore.js +872 -119
- package/dist/packlets/crypto-utils/keystore/keyStore.js.map +1 -0
- package/dist/packlets/crypto-utils/keystore/model.js +35 -4
- package/dist/packlets/crypto-utils/keystore/model.js.map +1 -0
- package/dist/packlets/crypto-utils/keystore/privateKeyStorage.js +21 -0
- package/dist/packlets/crypto-utils/keystore/privateKeyStorage.js.map +1 -0
- package/dist/packlets/crypto-utils/model.js +32 -0
- package/dist/packlets/crypto-utils/model.js.map +1 -0
- package/dist/packlets/crypto-utils/nodeCryptoProvider.js +347 -1
- package/dist/packlets/crypto-utils/nodeCryptoProvider.js.map +1 -0
- package/dist/packlets/crypto-utils/seedDerivedKeyPair.js +120 -0
- package/dist/packlets/crypto-utils/seedDerivedKeyPair.js.map +1 -0
- package/dist/packlets/crypto-utils/spkiHelpers.js +284 -0
- package/dist/packlets/crypto-utils/spkiHelpers.js.map +1 -0
- package/dist/packlets/csv/csvFileHelpers.js +0 -14
- package/dist/packlets/csv/csvFileHelpers.js.map +1 -0
- package/dist/packlets/csv/csvHelpers.js +14 -0
- package/dist/packlets/csv/csvHelpers.js.map +1 -0
- package/dist/packlets/csv/index.browser.js +1 -3
- package/dist/packlets/csv/index.browser.js.map +1 -0
- package/dist/packlets/csv/index.js.map +1 -0
- package/dist/packlets/experimental/extendedArray.js.map +1 -0
- package/dist/packlets/experimental/formatter.js.map +1 -0
- package/dist/packlets/experimental/index.js.map +1 -0
- package/dist/packlets/experimental/rangeOf.js.map +1 -0
- package/dist/packlets/hash/index.browser.js.map +1 -0
- package/dist/packlets/hash/index.js.map +1 -0
- package/dist/packlets/hash/index.node.js.map +1 -0
- package/dist/packlets/hash/md5Normalizer.browser.js.map +1 -0
- package/dist/packlets/hash/md5Normalizer.js.map +1 -0
- package/dist/packlets/mustache/index.js.map +1 -0
- package/dist/packlets/mustache/interfaces.js.map +1 -0
- package/dist/packlets/mustache/mustacheTemplate.js +42 -4
- package/dist/packlets/mustache/mustacheTemplate.js.map +1 -0
- package/dist/packlets/record-jar/index.browser.js +1 -3
- package/dist/packlets/record-jar/index.browser.js.map +1 -0
- package/dist/packlets/record-jar/index.js.map +1 -0
- package/dist/packlets/record-jar/recordJarFileHelpers.js +0 -18
- package/dist/packlets/record-jar/recordJarFileHelpers.js.map +1 -0
- package/dist/packlets/record-jar/recordJarHelpers.js +18 -0
- package/dist/packlets/record-jar/recordJarHelpers.js.map +1 -0
- package/dist/packlets/safer-fetch/addressClassification.js +438 -0
- package/dist/packlets/safer-fetch/addressClassification.js.map +1 -0
- package/dist/packlets/safer-fetch/addressPolicy.js +119 -0
- package/dist/packlets/safer-fetch/addressPolicy.js.map +1 -0
- package/dist/packlets/safer-fetch/contentType.js +120 -0
- package/dist/packlets/safer-fetch/contentType.js.map +1 -0
- package/dist/packlets/safer-fetch/deadline.js +254 -0
- package/dist/packlets/safer-fetch/deadline.js.map +1 -0
- package/dist/packlets/safer-fetch/defaults.js +131 -0
- package/dist/packlets/safer-fetch/defaults.js.map +1 -0
- package/dist/packlets/safer-fetch/failureReason.js +21 -0
- package/dist/packlets/safer-fetch/failureReason.js.map +1 -0
- package/dist/packlets/safer-fetch/guards.js +148 -0
- package/dist/packlets/safer-fetch/guards.js.map +1 -0
- package/dist/packlets/safer-fetch/index.browser.js +58 -0
- package/dist/packlets/safer-fetch/index.browser.js.map +1 -0
- package/dist/packlets/safer-fetch/index.js +52 -0
- package/dist/packlets/safer-fetch/index.js.map +1 -0
- package/dist/packlets/safer-fetch/model.js +21 -0
- package/dist/packlets/safer-fetch/model.js.map +1 -0
- package/dist/packlets/safer-fetch/nodeAddressGuard.js +191 -0
- package/dist/packlets/safer-fetch/nodeAddressGuard.js.map +1 -0
- package/dist/packlets/safer-fetch/redirect.js +93 -0
- package/dist/packlets/safer-fetch/redirect.js.map +1 -0
- package/dist/packlets/safer-fetch/retry.js +188 -0
- package/dist/packlets/safer-fetch/retry.js.map +1 -0
- package/dist/packlets/safer-fetch/saferFetch.js +755 -0
- package/dist/packlets/safer-fetch/saferFetch.js.map +1 -0
- package/dist/packlets/safer-fetch/transport.js +50 -0
- package/dist/packlets/safer-fetch/transport.js.map +1 -0
- package/dist/packlets/yaml/converters.js.map +1 -0
- package/dist/packlets/yaml/index.js +1 -0
- package/dist/packlets/yaml/index.js.map +1 -0
- package/dist/packlets/yaml/serializers.js +48 -0
- package/dist/packlets/yaml/serializers.js.map +1 -0
- package/dist/packlets/zip-file-tree/index.js.map +1 -0
- package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js +99 -9
- package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js.map +1 -0
- package/dist/packlets/zip-file-tree/zipFileTreeWriter.js +16 -1
- package/dist/packlets/zip-file-tree/zipFileTreeWriter.js.map +1 -0
- package/dist/ts-extras.d.ts +6648 -553
- package/dist/tsdoc-metadata.json +1 -1
- package/lib/index.browser.d.ts +5 -2
- package/lib/index.browser.d.ts.map +1 -0
- package/lib/index.browser.js +11 -3
- package/lib/index.browser.js.map +1 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +3 -1
- package/lib/index.js.map +1 -0
- package/lib/packlets/ai-assist/chatRequestBuilders.d.ts +149 -0
- package/lib/packlets/ai-assist/chatRequestBuilders.d.ts.map +1 -0
- package/lib/packlets/ai-assist/chatRequestBuilders.js +293 -0
- package/lib/packlets/ai-assist/chatRequestBuilders.js.map +1 -0
- package/lib/packlets/ai-assist/completionClient.d.ts +74 -0
- package/lib/packlets/ai-assist/completionClient.d.ts.map +1 -0
- package/lib/packlets/ai-assist/{apiClient.js → completionClient.js} +164 -213
- package/lib/packlets/ai-assist/completionClient.js.map +1 -0
- package/lib/packlets/ai-assist/converters.d.ts +15 -1
- package/lib/packlets/ai-assist/converters.d.ts.map +1 -0
- package/lib/packlets/ai-assist/converters.js +47 -3
- package/lib/packlets/ai-assist/converters.js.map +1 -0
- package/lib/packlets/ai-assist/embeddingClient.d.ts +69 -0
- package/lib/packlets/ai-assist/embeddingClient.d.ts.map +1 -0
- package/lib/packlets/ai-assist/embeddingClient.js +349 -0
- package/lib/packlets/ai-assist/embeddingClient.js.map +1 -0
- package/lib/packlets/ai-assist/endpoint.d.ts +49 -0
- package/lib/packlets/ai-assist/endpoint.d.ts.map +1 -0
- package/lib/packlets/ai-assist/endpoint.js +113 -0
- package/lib/packlets/ai-assist/endpoint.js.map +1 -0
- package/lib/packlets/ai-assist/http.d.ts +34 -0
- package/lib/packlets/ai-assist/http.d.ts.map +1 -0
- package/lib/packlets/ai-assist/http.js +109 -0
- package/lib/packlets/ai-assist/http.js.map +1 -0
- package/lib/packlets/ai-assist/imageGenerationClient.d.ts +46 -0
- package/lib/packlets/ai-assist/imageGenerationClient.d.ts.map +1 -0
- package/lib/packlets/ai-assist/imageGenerationClient.js +458 -0
- package/lib/packlets/ai-assist/imageGenerationClient.js.map +1 -0
- package/lib/packlets/ai-assist/imageOptionsResolver.d.ts +66 -0
- package/lib/packlets/ai-assist/imageOptionsResolver.d.ts.map +1 -0
- package/lib/packlets/ai-assist/imageOptionsResolver.js +197 -0
- package/lib/packlets/ai-assist/imageOptionsResolver.js.map +1 -0
- package/lib/packlets/ai-assist/index.d.ts +12 -4
- package/lib/packlets/ai-assist/index.d.ts.map +1 -0
- package/lib/packlets/ai-assist/index.js +47 -4
- package/lib/packlets/ai-assist/index.js.map +1 -0
- package/lib/packlets/ai-assist/jsonCompletion.d.ts +93 -0
- package/lib/packlets/ai-assist/jsonCompletion.d.ts.map +1 -0
- package/lib/packlets/ai-assist/jsonCompletion.js +97 -0
- package/lib/packlets/ai-assist/jsonCompletion.js.map +1 -0
- package/lib/packlets/ai-assist/jsonResponse.d.ts +194 -0
- package/lib/packlets/ai-assist/jsonResponse.d.ts.map +1 -0
- package/lib/packlets/ai-assist/jsonResponse.js +429 -0
- package/lib/packlets/ai-assist/jsonResponse.js.map +1 -0
- package/lib/packlets/ai-assist/listModelsClient.d.ts +46 -0
- package/lib/packlets/ai-assist/listModelsClient.d.ts.map +1 -0
- package/lib/packlets/ai-assist/listModelsClient.js +298 -0
- package/lib/packlets/ai-assist/listModelsClient.js.map +1 -0
- package/lib/packlets/ai-assist/model.d.ts +1346 -12
- package/lib/packlets/ai-assist/model.d.ts.map +1 -0
- package/lib/packlets/ai-assist/model.js +309 -10
- package/lib/packlets/ai-assist/model.js.map +1 -0
- package/lib/packlets/ai-assist/registry.d.ts +76 -1
- package/lib/packlets/ai-assist/registry.d.ts.map +1 -0
- package/lib/packlets/ai-assist/registry.js +381 -13
- package/lib/packlets/ai-assist/registry.js.map +1 -0
- package/lib/packlets/ai-assist/sseParser.d.ts +45 -0
- package/lib/packlets/ai-assist/sseParser.d.ts.map +1 -0
- package/lib/packlets/ai-assist/sseParser.js +128 -0
- package/lib/packlets/ai-assist/sseParser.js.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts +72 -0
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +379 -0
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts +269 -0
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +694 -0
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +163 -0
- package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/common.js +193 -0
- package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts +43 -0
- package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/gemini.js +211 -0
- package/lib/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts +19 -0
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +178 -0
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +33 -0
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +393 -0
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts +34 -0
- package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/proxy.js +181 -0
- package/lib/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -0
- package/lib/packlets/ai-assist/streamingClient.d.ts +50 -0
- package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -0
- package/lib/packlets/ai-assist/streamingClient.js +146 -0
- package/lib/packlets/ai-assist/streamingClient.js.map +1 -0
- package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts +87 -0
- package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts.map +1 -0
- package/lib/packlets/ai-assist/thinkingOptionsResolver.js +294 -0
- package/lib/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -0
- package/lib/packlets/ai-assist/toolFormats.d.ts +40 -9
- package/lib/packlets/ai-assist/toolFormats.d.ts.map +1 -0
- package/lib/packlets/ai-assist/toolFormats.js +107 -10
- package/lib/packlets/ai-assist/toolFormats.js.map +1 -0
- package/lib/packlets/conversion/converters.d.ts +8 -1
- package/lib/packlets/conversion/converters.d.ts.map +1 -0
- package/lib/packlets/conversion/converters.js +36 -2
- package/lib/packlets/conversion/converters.js.map +1 -0
- package/lib/packlets/conversion/index.d.ts.map +1 -0
- package/lib/packlets/conversion/index.js.map +1 -0
- package/lib/packlets/crypto-utils/constants.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/constants.js.map +1 -0
- package/lib/packlets/crypto-utils/converters.d.ts +20 -1
- package/lib/packlets/crypto-utils/converters.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/converters.js +43 -5
- package/lib/packlets/crypto-utils/converters.js.map +1 -0
- package/lib/packlets/crypto-utils/directEncryptionProvider.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/directEncryptionProvider.js.map +1 -0
- package/lib/packlets/crypto-utils/encryptedFile.d.ts +25 -0
- package/lib/packlets/crypto-utils/encryptedFile.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/encryptedFile.js +38 -0
- package/lib/packlets/crypto-utils/encryptedFile.js.map +1 -0
- package/lib/packlets/crypto-utils/hpkeProvider.d.ts +150 -0
- package/lib/packlets/crypto-utils/hpkeProvider.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/hpkeProvider.js +359 -0
- package/lib/packlets/crypto-utils/hpkeProvider.js.map +1 -0
- package/lib/packlets/crypto-utils/index.browser.d.ts +5 -1
- package/lib/packlets/crypto-utils/index.browser.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/index.browser.js +28 -3
- package/lib/packlets/crypto-utils/index.browser.js.map +1 -0
- package/lib/packlets/crypto-utils/index.d.ts +5 -1
- package/lib/packlets/crypto-utils/index.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/index.js +24 -1
- package/lib/packlets/crypto-utils/index.js.map +1 -0
- package/lib/packlets/crypto-utils/keyPairAlgorithmParams.d.ts +54 -0
- package/lib/packlets/crypto-utils/keyPairAlgorithmParams.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/keyPairAlgorithmParams.js +74 -0
- package/lib/packlets/crypto-utils/keyPairAlgorithmParams.js.map +1 -0
- package/lib/packlets/crypto-utils/keystore/converters.d.ts +68 -6
- package/lib/packlets/crypto-utils/keystore/converters.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/keystore/converters.js +109 -14
- package/lib/packlets/crypto-utils/keystore/converters.js.map +1 -0
- package/lib/packlets/crypto-utils/keystore/encryptedFilePrivateKeyStorage.d.ts +148 -0
- package/lib/packlets/crypto-utils/keystore/encryptedFilePrivateKeyStorage.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/keystore/encryptedFilePrivateKeyStorage.js +324 -0
- package/lib/packlets/crypto-utils/keystore/encryptedFilePrivateKeyStorage.js.map +1 -0
- package/lib/packlets/crypto-utils/keystore/index.browser.d.ts +10 -0
- package/lib/packlets/crypto-utils/keystore/index.browser.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/keystore/index.browser.js +76 -0
- package/lib/packlets/crypto-utils/keystore/index.browser.js.map +1 -0
- package/lib/packlets/crypto-utils/keystore/index.d.ts +2 -0
- package/lib/packlets/crypto-utils/keystore/index.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/keystore/index.js +5 -1
- package/lib/packlets/crypto-utils/keystore/index.js.map +1 -0
- package/lib/packlets/crypto-utils/keystore/keyStore.d.ts +314 -13
- package/lib/packlets/crypto-utils/keystore/keyStore.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/keystore/keyStore.js +877 -124
- package/lib/packlets/crypto-utils/keystore/keyStore.js.map +1 -0
- package/lib/packlets/crypto-utils/keystore/model.d.ts +404 -21
- package/lib/packlets/crypto-utils/keystore/model.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/keystore/model.js +37 -5
- package/lib/packlets/crypto-utils/keystore/model.js.map +1 -0
- package/lib/packlets/crypto-utils/keystore/privateKeyStorage.d.ts +53 -0
- package/lib/packlets/crypto-utils/keystore/privateKeyStorage.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/keystore/privateKeyStorage.js +22 -0
- package/lib/packlets/crypto-utils/keystore/privateKeyStorage.js.map +1 -0
- package/lib/packlets/crypto-utils/model.d.ts +518 -10
- package/lib/packlets/crypto-utils/model.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/model.js +33 -1
- package/lib/packlets/crypto-utils/model.js.map +1 -0
- package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts +142 -2
- package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/nodeCryptoProvider.js +346 -0
- package/lib/packlets/crypto-utils/nodeCryptoProvider.js.map +1 -0
- package/lib/packlets/crypto-utils/seedDerivedKeyPair.d.ts +42 -0
- package/lib/packlets/crypto-utils/seedDerivedKeyPair.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/seedDerivedKeyPair.js +123 -0
- package/lib/packlets/crypto-utils/seedDerivedKeyPair.js.map +1 -0
- package/lib/packlets/crypto-utils/spkiHelpers.d.ts +151 -0
- package/lib/packlets/crypto-utils/spkiHelpers.d.ts.map +1 -0
- package/lib/packlets/crypto-utils/spkiHelpers.js +297 -0
- package/lib/packlets/crypto-utils/spkiHelpers.js.map +1 -0
- package/lib/packlets/csv/csvFileHelpers.d.ts +0 -10
- package/lib/packlets/csv/csvFileHelpers.d.ts.map +1 -0
- package/lib/packlets/csv/csvFileHelpers.js +0 -15
- package/lib/packlets/csv/csvFileHelpers.js.map +1 -0
- package/lib/packlets/csv/csvHelpers.d.ts +10 -0
- package/lib/packlets/csv/csvHelpers.d.ts.map +1 -0
- package/lib/packlets/csv/csvHelpers.js +15 -0
- package/lib/packlets/csv/csvHelpers.js.map +1 -0
- package/lib/packlets/csv/index.browser.d.ts +0 -1
- package/lib/packlets/csv/index.browser.d.ts.map +1 -0
- package/lib/packlets/csv/index.browser.js +1 -5
- package/lib/packlets/csv/index.browser.js.map +1 -0
- package/lib/packlets/csv/index.d.ts.map +1 -0
- package/lib/packlets/csv/index.js.map +1 -0
- package/lib/packlets/experimental/extendedArray.d.ts.map +1 -0
- package/lib/packlets/experimental/extendedArray.js.map +1 -0
- package/lib/packlets/experimental/formatter.d.ts.map +1 -0
- package/lib/packlets/experimental/formatter.js.map +1 -0
- package/lib/packlets/experimental/index.d.ts.map +1 -0
- package/lib/packlets/experimental/index.js.map +1 -0
- package/lib/packlets/experimental/rangeOf.d.ts.map +1 -0
- package/lib/packlets/experimental/rangeOf.js.map +1 -0
- package/lib/packlets/hash/index.browser.d.ts.map +1 -0
- package/lib/packlets/hash/index.browser.js.map +1 -0
- package/lib/packlets/hash/index.d.ts.map +1 -0
- package/lib/packlets/hash/index.js.map +1 -0
- package/lib/packlets/hash/index.node.d.ts.map +1 -0
- package/lib/packlets/hash/index.node.js.map +1 -0
- package/lib/packlets/hash/md5Normalizer.browser.d.ts.map +1 -0
- package/lib/packlets/hash/md5Normalizer.browser.js.map +1 -0
- package/lib/packlets/hash/md5Normalizer.d.ts.map +1 -0
- package/lib/packlets/hash/md5Normalizer.js.map +1 -0
- package/lib/packlets/mustache/index.d.ts +1 -1
- package/lib/packlets/mustache/index.d.ts.map +1 -0
- package/lib/packlets/mustache/index.js.map +1 -0
- package/lib/packlets/mustache/interfaces.d.ts +34 -0
- package/lib/packlets/mustache/interfaces.d.ts.map +1 -0
- package/lib/packlets/mustache/interfaces.js.map +1 -0
- package/lib/packlets/mustache/mustacheTemplate.d.ts +2 -0
- package/lib/packlets/mustache/mustacheTemplate.d.ts.map +1 -0
- package/lib/packlets/mustache/mustacheTemplate.js +42 -4
- package/lib/packlets/mustache/mustacheTemplate.js.map +1 -0
- package/lib/packlets/record-jar/index.browser.d.ts +0 -1
- package/lib/packlets/record-jar/index.browser.d.ts.map +1 -0
- package/lib/packlets/record-jar/index.browser.js +1 -5
- package/lib/packlets/record-jar/index.browser.js.map +1 -0
- package/lib/packlets/record-jar/index.d.ts.map +1 -0
- package/lib/packlets/record-jar/index.js.map +1 -0
- package/lib/packlets/record-jar/recordJarFileHelpers.d.ts +0 -11
- package/lib/packlets/record-jar/recordJarFileHelpers.d.ts.map +1 -0
- package/lib/packlets/record-jar/recordJarFileHelpers.js +0 -19
- package/lib/packlets/record-jar/recordJarFileHelpers.js.map +1 -0
- package/lib/packlets/record-jar/recordJarHelpers.d.ts +11 -0
- package/lib/packlets/record-jar/recordJarHelpers.d.ts.map +1 -0
- package/lib/packlets/record-jar/recordJarHelpers.js +19 -0
- package/lib/packlets/record-jar/recordJarHelpers.js.map +1 -0
- package/lib/packlets/safer-fetch/addressClassification.d.ts +144 -0
- package/lib/packlets/safer-fetch/addressClassification.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/addressClassification.js +441 -0
- package/lib/packlets/safer-fetch/addressClassification.js.map +1 -0
- package/lib/packlets/safer-fetch/addressPolicy.d.ts +113 -0
- package/lib/packlets/safer-fetch/addressPolicy.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/addressPolicy.js +123 -0
- package/lib/packlets/safer-fetch/addressPolicy.js.map +1 -0
- package/lib/packlets/safer-fetch/contentType.d.ts +51 -0
- package/lib/packlets/safer-fetch/contentType.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/contentType.js +127 -0
- package/lib/packlets/safer-fetch/contentType.js.map +1 -0
- package/lib/packlets/safer-fetch/deadline.d.ts +138 -0
- package/lib/packlets/safer-fetch/deadline.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/deadline.js +258 -0
- package/lib/packlets/safer-fetch/deadline.js.map +1 -0
- package/lib/packlets/safer-fetch/defaults.d.ts +108 -0
- package/lib/packlets/safer-fetch/defaults.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/defaults.js +134 -0
- package/lib/packlets/safer-fetch/defaults.js.map +1 -0
- package/lib/packlets/safer-fetch/failureReason.d.ts +148 -0
- package/lib/packlets/safer-fetch/failureReason.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/failureReason.js +22 -0
- package/lib/packlets/safer-fetch/failureReason.js.map +1 -0
- package/lib/packlets/safer-fetch/guards.d.ts +62 -0
- package/lib/packlets/safer-fetch/guards.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/guards.js +153 -0
- package/lib/packlets/safer-fetch/guards.js.map +1 -0
- package/lib/packlets/safer-fetch/index.browser.d.ts +33 -0
- package/lib/packlets/safer-fetch/index.browser.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/index.browser.js +82 -0
- package/lib/packlets/safer-fetch/index.browser.js.map +1 -0
- package/lib/packlets/safer-fetch/index.d.ts +27 -0
- package/lib/packlets/safer-fetch/index.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/index.js +78 -0
- package/lib/packlets/safer-fetch/index.js.map +1 -0
- package/lib/packlets/safer-fetch/model.d.ts +373 -0
- package/lib/packlets/safer-fetch/model.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/model.js +22 -0
- package/lib/packlets/safer-fetch/model.js.map +1 -0
- package/lib/packlets/safer-fetch/nodeAddressGuard.d.ts +129 -0
- package/lib/packlets/safer-fetch/nodeAddressGuard.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/nodeAddressGuard.js +196 -0
- package/lib/packlets/safer-fetch/nodeAddressGuard.js.map +1 -0
- package/lib/packlets/safer-fetch/redirect.d.ts +62 -0
- package/lib/packlets/safer-fetch/redirect.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/redirect.js +98 -0
- package/lib/packlets/safer-fetch/redirect.js.map +1 -0
- package/lib/packlets/safer-fetch/retry.d.ts +162 -0
- package/lib/packlets/safer-fetch/retry.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/retry.js +197 -0
- package/lib/packlets/safer-fetch/retry.js.map +1 -0
- package/lib/packlets/safer-fetch/saferFetch.d.ts +108 -0
- package/lib/packlets/safer-fetch/saferFetch.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/saferFetch.js +760 -0
- package/lib/packlets/safer-fetch/saferFetch.js.map +1 -0
- package/lib/packlets/safer-fetch/transport.d.ts +18 -0
- package/lib/packlets/safer-fetch/transport.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/transport.js +53 -0
- package/lib/packlets/safer-fetch/transport.js.map +1 -0
- package/lib/packlets/yaml/converters.d.ts.map +1 -0
- package/lib/packlets/yaml/converters.js.map +1 -0
- package/lib/packlets/yaml/index.d.ts +1 -0
- package/lib/packlets/yaml/index.d.ts.map +1 -0
- package/lib/packlets/yaml/index.js +1 -0
- package/lib/packlets/yaml/index.js.map +1 -0
- package/lib/packlets/yaml/serializers.d.ts +45 -0
- package/lib/packlets/yaml/serializers.d.ts.map +1 -0
- package/lib/packlets/yaml/serializers.js +84 -0
- package/lib/packlets/yaml/serializers.js.map +1 -0
- package/lib/packlets/zip-file-tree/index.d.ts.map +1 -0
- package/lib/packlets/zip-file-tree/index.js.map +1 -0
- package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts +84 -8
- package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts.map +1 -0
- package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js +99 -9
- package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js.map +1 -0
- package/lib/packlets/zip-file-tree/zipFileTreeWriter.d.ts +26 -1
- package/lib/packlets/zip-file-tree/zipFileTreeWriter.d.ts.map +1 -0
- package/lib/packlets/zip-file-tree/zipFileTreeWriter.js +17 -1
- package/lib/packlets/zip-file-tree/zipFileTreeWriter.js.map +1 -0
- package/package.json +30 -15
- package/lib/packlets/ai-assist/apiClient.d.ts +0 -60
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { type Result } from '@fgv/ts-utils';
|
|
2
|
+
import type { FetchFailureReason } from './failureReason';
|
|
3
|
+
import type { SaferFetchMethod } from './model';
|
|
4
|
+
/**
|
|
5
|
+
* Opt-in retry policy.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* **Retry is off by default and has to be asked for.** A primitive that silently retries
|
|
9
|
+
* changes the semantics of every call site and amplifies load against a service that is already
|
|
10
|
+
* struggling.
|
|
11
|
+
*
|
|
12
|
+
* Two rules are load-bearing rather than incidental, and are worth reading before enabling this:
|
|
13
|
+
*
|
|
14
|
+
* **Every attempt re-runs the address guard from hop 0 — a full re-walk, never a resume and
|
|
15
|
+
* never a cached verdict.** Reusing an earlier attempt's verdict would make retry its own
|
|
16
|
+
* DNS-rebinding vector: an attacker needs the guard to pass once and a connect to land on a
|
|
17
|
+
* private address, so `N` connects against **one** check multiplies their odds by the retry
|
|
18
|
+
* count — and a retry delay is precisely when a short-TTL rebind lands. A resume is
|
|
19
|
+
* independently wrong because the redirect chain is not stable across attempts: attempt 2's
|
|
20
|
+
* server may answer with a different `Location`, so revalidating only the current hop would
|
|
21
|
+
* validate a chain the request no longer follows. The consequence is that retries cost more
|
|
22
|
+
* than a naive implementation would suggest — each carries full re-resolution — and that cost
|
|
23
|
+
* is deliberate.
|
|
24
|
+
*
|
|
25
|
+
* **The overall deadline is the ceiling.** Retries consume `timeoutMs` and never reset it, so
|
|
26
|
+
* `{ timeoutMs: 30_000, retry: { attempts: 3 } }` is a thirty-second call, not a two-minute
|
|
27
|
+
* one. When the remaining budget is shorter than the computed delay, the call fails with the
|
|
28
|
+
* attempt's own failure rather than sleeping past its deadline.
|
|
29
|
+
* @public
|
|
30
|
+
*/
|
|
31
|
+
export interface IRetryPolicy {
|
|
32
|
+
/**
|
|
33
|
+
* Additional attempts after the first, **not** a total. `attempts: 2` means up to three
|
|
34
|
+
* requests. Must be a non-negative integer; `0` disables retry.
|
|
35
|
+
*/
|
|
36
|
+
readonly attempts: number;
|
|
37
|
+
/**
|
|
38
|
+
* First backoff delay, in milliseconds, doubled per attempt before jitter. Default
|
|
39
|
+
* {@link SaferFetch.DEFAULT_RETRY_BASE_DELAY_MS}.
|
|
40
|
+
*/
|
|
41
|
+
readonly baseDelayMs?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Ceiling on any single delay, in milliseconds. Default
|
|
44
|
+
* {@link SaferFetch.DEFAULT_RETRY_MAX_DELAY_MS}. Also clamps a server-supplied
|
|
45
|
+
* `Retry-After`, which is the security-relevant half of its job.
|
|
46
|
+
*/
|
|
47
|
+
readonly maxDelayMs?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Retries `POST` / `PUT` / `PATCH` / `DELETE` as well as `GET` / `HEAD`. Default `false`.
|
|
50
|
+
*
|
|
51
|
+
* @remarks
|
|
52
|
+
* Off by default because a timeout does not tell you whether the server processed the
|
|
53
|
+
* request: a retried `POST` after a timeout can double-charge. Turn it on for endpoints you
|
|
54
|
+
* know are idempotent — an idempotency key, a `PUT` of a whole resource — and not otherwise.
|
|
55
|
+
*/
|
|
56
|
+
readonly retryNonIdempotent?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Honors a `Retry-After` header on `429` and `503`. Default `true`.
|
|
59
|
+
*
|
|
60
|
+
* @remarks
|
|
61
|
+
* Always clamped to `maxDelayMs`, whatever this is set to. The header is chosen by the
|
|
62
|
+
* server, which in this threat model may be the adversary.
|
|
63
|
+
*/
|
|
64
|
+
readonly respectRetryAfter?: boolean;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A {@link SaferFetch.IRetryPolicy} with every default applied, so no downstream code path
|
|
68
|
+
* branches on a field's absence.
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
export interface IResolvedRetryPolicy {
|
|
72
|
+
readonly attempts: number;
|
|
73
|
+
readonly baseDelayMs: number;
|
|
74
|
+
readonly maxDelayMs: number;
|
|
75
|
+
readonly retryNonIdempotent: boolean;
|
|
76
|
+
readonly respectRetryAfter: boolean;
|
|
77
|
+
}
|
|
78
|
+
/** No retry. The resolved shape of an absent `retry` option. @internal */
|
|
79
|
+
export declare const NO_RETRY: IResolvedRetryPolicy;
|
|
80
|
+
/**
|
|
81
|
+
* Applies the retry defaults, rejecting a policy that cannot mean anything.
|
|
82
|
+
*
|
|
83
|
+
* @remarks
|
|
84
|
+
* Validated for the same reason `redirectPolicy` and `sensitiveHeaders` are: a JavaScript
|
|
85
|
+
* caller can pass anything, and a `NaN` delay silently becomes a `setTimeout(NaN)` that fires
|
|
86
|
+
* immediately — turning a backoff into a tight retry loop against a struggling service.
|
|
87
|
+
* @internal
|
|
88
|
+
*/
|
|
89
|
+
export declare function resolveRetryPolicy(retry?: IRetryPolicy): Result<IResolvedRetryPolicy>;
|
|
90
|
+
/**
|
|
91
|
+
* Decides whether a failure is one a later attempt could plausibly answer differently.
|
|
92
|
+
*
|
|
93
|
+
* @remarks
|
|
94
|
+
* The `switch` is exhaustive over the taxonomy on purpose: a new failure kind should have to be
|
|
95
|
+
* classified deliberately rather than inherit "retryable" or "not" from a default branch.
|
|
96
|
+
*
|
|
97
|
+
* `'blocked-by-guard'` can never succeed on a retry — the guard's answer is about the
|
|
98
|
+
* destination, not about the moment. `'too-large'` is the sharper case: retrying it
|
|
99
|
+
* re-downloads up to the cap every time, which turns a defense into an amplifier aimed at the
|
|
100
|
+
* caller's own bandwidth.
|
|
101
|
+
* @internal
|
|
102
|
+
*/
|
|
103
|
+
export declare function isRetryableFailure(reason: FetchFailureReason | undefined): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Whether this method may be retried under the supplied policy.
|
|
106
|
+
* @internal
|
|
107
|
+
*/
|
|
108
|
+
export declare function isRetryableMethod(method: SaferFetchMethod, policy: IResolvedRetryPolicy): boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Whether a `Retry-After` header on this failure is one to honor.
|
|
111
|
+
*
|
|
112
|
+
* @remarks
|
|
113
|
+
* Only on `429` and `503`, and only when the policy asks for it. A `500` carrying a
|
|
114
|
+
* `Retry-After` does not get to dictate the caller's schedule.
|
|
115
|
+
* @internal
|
|
116
|
+
*/
|
|
117
|
+
export declare function honorsRetryAfter(reason: FetchFailureReason | undefined, policy: IResolvedRetryPolicy): boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Parses a `Retry-After` header value into milliseconds.
|
|
120
|
+
*
|
|
121
|
+
* @remarks
|
|
122
|
+
* Both RFC 9110 forms: delta-seconds, and an HTTP-date which is converted to a delay relative
|
|
123
|
+
* to `nowMs`. An unparseable value, a negative delta, or a date already in the past yields
|
|
124
|
+
* `undefined` — "the server said nothing usable", which falls back to ordinary backoff rather
|
|
125
|
+
* than to zero delay. Returning `0` for a past date would let a stale clock produce a hot loop.
|
|
126
|
+
*
|
|
127
|
+
* The value is **not** clamped here. Clamping belongs with the rest of the delay arithmetic in
|
|
128
|
+
* {@link computeRetryDelayMs}, so there is exactly one place the ceiling is applied.
|
|
129
|
+
* @internal
|
|
130
|
+
*/
|
|
131
|
+
export declare function parseRetryAfterMs(value: string | undefined, nowMs: number): number | undefined;
|
|
132
|
+
/** Inputs to {@link computeRetryDelayMs}. @internal */
|
|
133
|
+
export interface IRetryDelayParams {
|
|
134
|
+
/** 0-based index of the retry about to be scheduled: `0` is the first retry. */
|
|
135
|
+
readonly attempt: number;
|
|
136
|
+
readonly policy: IResolvedRetryPolicy;
|
|
137
|
+
/** A honored, parsed `Retry-After`, in milliseconds, when the server sent a usable one. */
|
|
138
|
+
readonly retryAfterMs?: number;
|
|
139
|
+
/** Injected for determinism in tests; the call path passes `Math.random`. */
|
|
140
|
+
readonly random: () => number;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Computes how long to wait before the next attempt.
|
|
144
|
+
*
|
|
145
|
+
* @remarks
|
|
146
|
+
* **Exponential backoff with full jitter**: the delay is uniform on `[0, cap)` where `cap` is
|
|
147
|
+
* `baseDelayMs * 2^attempt` bounded by `maxDelayMs`. Full jitter rather than a fixed backoff
|
|
148
|
+
* because every caller that failed against the same struggling host would otherwise retry in
|
|
149
|
+
* the same instant, which is the thundering herd the backoff was supposed to prevent.
|
|
150
|
+
*
|
|
151
|
+
* **A honored `Retry-After` replaces the computed backoff and is clamped to `maxDelayMs`.** The
|
|
152
|
+
* clamp is not a rounding detail: `Retry-After` is chosen by the server, which in this threat
|
|
153
|
+
* model may be the adversary, so an unclamped `Retry-After: 86400` is a day-long sleep the
|
|
154
|
+
* caller never agreed to. Jitter is not applied to it — the server named an instant, and
|
|
155
|
+
* scattering around it is the caller's herd problem, not the server's instruction.
|
|
156
|
+
*
|
|
157
|
+
* `2 ** attempt` overflows to `Infinity` for a large enough `attempt`; `Math.min` with a finite
|
|
158
|
+
* ceiling absorbs that, which is why the cap is computed in this order.
|
|
159
|
+
* @internal
|
|
160
|
+
*/
|
|
161
|
+
export declare function computeRetryDelayMs(params: IRetryDelayParams): number;
|
|
162
|
+
//# sourceMappingURL=retry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../../src/packlets/safer-fetch/retry.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAiB,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAS3D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAE9B;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAE7B;;;;;;;OAOG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAEtC;;;;;;OAMG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAC;CACtC;AAED;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IACrC,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;CACrC;AAED,0EAA0E;AAC1E,eAAO,MAAM,QAAQ,EAAE,oBAMtB,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC,oBAAoB,CAAC,CAkCrF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,GAAG,SAAS,GAAG,OAAO,CAyBlF;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,oBAAoB,GAAG,OAAO,CAEjG;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,kBAAkB,GAAG,SAAS,EACtC,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAIT;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAgB9F;AAED,uDAAuD;AACvD,MAAM,WAAW,iBAAiB;IAChC,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IACtC,2FAA2F;IAC3F,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAQrE"}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright (c) 2026 Erik Fortune
|
|
3
|
+
//
|
|
4
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
// of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
// in the Software without restriction, including without limitation the rights
|
|
7
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
// copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
// furnished to do so, subject to the following conditions:
|
|
10
|
+
//
|
|
11
|
+
// The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
// copies or substantial portions of the Software.
|
|
13
|
+
//
|
|
14
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
// SOFTWARE.
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.NO_RETRY = void 0;
|
|
23
|
+
exports.resolveRetryPolicy = resolveRetryPolicy;
|
|
24
|
+
exports.isRetryableFailure = isRetryableFailure;
|
|
25
|
+
exports.isRetryableMethod = isRetryableMethod;
|
|
26
|
+
exports.honorsRetryAfter = honorsRetryAfter;
|
|
27
|
+
exports.parseRetryAfterMs = parseRetryAfterMs;
|
|
28
|
+
exports.computeRetryDelayMs = computeRetryDelayMs;
|
|
29
|
+
const ts_utils_1 = require("@fgv/ts-utils");
|
|
30
|
+
const defaults_1 = require("./defaults");
|
|
31
|
+
/** No retry. The resolved shape of an absent `retry` option. @internal */
|
|
32
|
+
exports.NO_RETRY = {
|
|
33
|
+
attempts: 0,
|
|
34
|
+
baseDelayMs: defaults_1.DEFAULT_RETRY_BASE_DELAY_MS,
|
|
35
|
+
maxDelayMs: defaults_1.DEFAULT_RETRY_MAX_DELAY_MS,
|
|
36
|
+
retryNonIdempotent: false,
|
|
37
|
+
respectRetryAfter: true
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Applies the retry defaults, rejecting a policy that cannot mean anything.
|
|
41
|
+
*
|
|
42
|
+
* @remarks
|
|
43
|
+
* Validated for the same reason `redirectPolicy` and `sensitiveHeaders` are: a JavaScript
|
|
44
|
+
* caller can pass anything, and a `NaN` delay silently becomes a `setTimeout(NaN)` that fires
|
|
45
|
+
* immediately — turning a backoff into a tight retry loop against a struggling service.
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
function resolveRetryPolicy(retry) {
|
|
49
|
+
var _a, _b, _c;
|
|
50
|
+
// `?? undefined` rather than `!== undefined`, matching every other option: a `null` from a
|
|
51
|
+
// JavaScript caller means "absent", never "installed".
|
|
52
|
+
const policy = retry !== null && retry !== void 0 ? retry : undefined;
|
|
53
|
+
if (policy === undefined) {
|
|
54
|
+
return (0, ts_utils_1.succeed)(exports.NO_RETRY);
|
|
55
|
+
}
|
|
56
|
+
const attempts = policy.attempts;
|
|
57
|
+
if (!Number.isInteger(attempts) || attempts < 0) {
|
|
58
|
+
return (0, ts_utils_1.fail)(`retry.attempts must be a non-negative integer; got ${attempts}.`);
|
|
59
|
+
}
|
|
60
|
+
const baseDelayMs = (_a = policy.baseDelayMs) !== null && _a !== void 0 ? _a : defaults_1.DEFAULT_RETRY_BASE_DELAY_MS;
|
|
61
|
+
const maxDelayMs = (_b = policy.maxDelayMs) !== null && _b !== void 0 ? _b : defaults_1.DEFAULT_RETRY_MAX_DELAY_MS;
|
|
62
|
+
const delays = [
|
|
63
|
+
['retry.baseDelayMs', baseDelayMs],
|
|
64
|
+
['retry.maxDelayMs', maxDelayMs]
|
|
65
|
+
];
|
|
66
|
+
for (const [name, value] of delays) {
|
|
67
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
68
|
+
return (0, ts_utils_1.fail)(`${name} must be a non-negative finite number; got ${value}.`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return (0, ts_utils_1.succeed)({
|
|
72
|
+
attempts,
|
|
73
|
+
baseDelayMs,
|
|
74
|
+
maxDelayMs,
|
|
75
|
+
retryNonIdempotent: policy.retryNonIdempotent === true,
|
|
76
|
+
// Defaults to honoring the header, so `?? true` rather than the `=== true` the other two
|
|
77
|
+
// flags use.
|
|
78
|
+
respectRetryAfter: (_c = policy.respectRetryAfter) !== null && _c !== void 0 ? _c : true
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Decides whether a failure is one a later attempt could plausibly answer differently.
|
|
83
|
+
*
|
|
84
|
+
* @remarks
|
|
85
|
+
* The `switch` is exhaustive over the taxonomy on purpose: a new failure kind should have to be
|
|
86
|
+
* classified deliberately rather than inherit "retryable" or "not" from a default branch.
|
|
87
|
+
*
|
|
88
|
+
* `'blocked-by-guard'` can never succeed on a retry — the guard's answer is about the
|
|
89
|
+
* destination, not about the moment. `'too-large'` is the sharper case: retrying it
|
|
90
|
+
* re-downloads up to the cap every time, which turns a defense into an amplifier aimed at the
|
|
91
|
+
* caller's own bandwidth.
|
|
92
|
+
* @internal
|
|
93
|
+
*/
|
|
94
|
+
function isRetryableFailure(reason) {
|
|
95
|
+
// A failure with no detail is one this primitive could not classify, and an unclassifiable
|
|
96
|
+
// failure is not one to retry — the whole basis for retrying is knowing what went wrong.
|
|
97
|
+
if (reason === undefined) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
switch (reason.kind) {
|
|
101
|
+
case 'network':
|
|
102
|
+
case 'timeout':
|
|
103
|
+
return true;
|
|
104
|
+
case 'http-status':
|
|
105
|
+
return defaults_1.RETRYABLE_HTTP_STATUSES.includes(reason.status);
|
|
106
|
+
case 'invalid-url':
|
|
107
|
+
case 'blocked-by-guard':
|
|
108
|
+
case 'redirect-rejected':
|
|
109
|
+
case 'redirect-opaque':
|
|
110
|
+
case 'too-many-redirects':
|
|
111
|
+
case 'aborted':
|
|
112
|
+
case 'too-large':
|
|
113
|
+
case 'unsupported-content-type':
|
|
114
|
+
case 'decode':
|
|
115
|
+
case 'parse':
|
|
116
|
+
case 'unknown':
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Whether this method may be retried under the supplied policy.
|
|
122
|
+
* @internal
|
|
123
|
+
*/
|
|
124
|
+
function isRetryableMethod(method, policy) {
|
|
125
|
+
return policy.retryNonIdempotent || defaults_1.IDEMPOTENT_METHODS.includes(method);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Whether a `Retry-After` header on this failure is one to honor.
|
|
129
|
+
*
|
|
130
|
+
* @remarks
|
|
131
|
+
* Only on `429` and `503`, and only when the policy asks for it. A `500` carrying a
|
|
132
|
+
* `Retry-After` does not get to dictate the caller's schedule.
|
|
133
|
+
* @internal
|
|
134
|
+
*/
|
|
135
|
+
function honorsRetryAfter(reason, policy) {
|
|
136
|
+
return (policy.respectRetryAfter && (reason === null || reason === void 0 ? void 0 : reason.kind) === 'http-status' && defaults_1.RETRY_AFTER_STATUSES.includes(reason.status));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Parses a `Retry-After` header value into milliseconds.
|
|
140
|
+
*
|
|
141
|
+
* @remarks
|
|
142
|
+
* Both RFC 9110 forms: delta-seconds, and an HTTP-date which is converted to a delay relative
|
|
143
|
+
* to `nowMs`. An unparseable value, a negative delta, or a date already in the past yields
|
|
144
|
+
* `undefined` — "the server said nothing usable", which falls back to ordinary backoff rather
|
|
145
|
+
* than to zero delay. Returning `0` for a past date would let a stale clock produce a hot loop.
|
|
146
|
+
*
|
|
147
|
+
* The value is **not** clamped here. Clamping belongs with the rest of the delay arithmetic in
|
|
148
|
+
* {@link computeRetryDelayMs}, so there is exactly one place the ceiling is applied.
|
|
149
|
+
* @internal
|
|
150
|
+
*/
|
|
151
|
+
function parseRetryAfterMs(value, nowMs) {
|
|
152
|
+
const text = (value !== null && value !== void 0 ? value : '').trim();
|
|
153
|
+
if (text.length === 0) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
// Digits only: a delta-seconds value. Tested before `Date.parse`, which accepts bare numbers
|
|
157
|
+
// as years in some engines and would read `Retry-After: 120` as the year 120.
|
|
158
|
+
if (/^\d+$/.test(text)) {
|
|
159
|
+
return Number(text) * 1000;
|
|
160
|
+
}
|
|
161
|
+
const at = Date.parse(text);
|
|
162
|
+
if (Number.isNaN(at)) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
const delay = at - nowMs;
|
|
166
|
+
return delay > 0 ? delay : undefined;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Computes how long to wait before the next attempt.
|
|
170
|
+
*
|
|
171
|
+
* @remarks
|
|
172
|
+
* **Exponential backoff with full jitter**: the delay is uniform on `[0, cap)` where `cap` is
|
|
173
|
+
* `baseDelayMs * 2^attempt` bounded by `maxDelayMs`. Full jitter rather than a fixed backoff
|
|
174
|
+
* because every caller that failed against the same struggling host would otherwise retry in
|
|
175
|
+
* the same instant, which is the thundering herd the backoff was supposed to prevent.
|
|
176
|
+
*
|
|
177
|
+
* **A honored `Retry-After` replaces the computed backoff and is clamped to `maxDelayMs`.** The
|
|
178
|
+
* clamp is not a rounding detail: `Retry-After` is chosen by the server, which in this threat
|
|
179
|
+
* model may be the adversary, so an unclamped `Retry-After: 86400` is a day-long sleep the
|
|
180
|
+
* caller never agreed to. Jitter is not applied to it — the server named an instant, and
|
|
181
|
+
* scattering around it is the caller's herd problem, not the server's instruction.
|
|
182
|
+
*
|
|
183
|
+
* `2 ** attempt` overflows to `Infinity` for a large enough `attempt`; `Math.min` with a finite
|
|
184
|
+
* ceiling absorbs that, which is why the cap is computed in this order.
|
|
185
|
+
* @internal
|
|
186
|
+
*/
|
|
187
|
+
function computeRetryDelayMs(params) {
|
|
188
|
+
var _a;
|
|
189
|
+
const { attempt, policy, random } = params;
|
|
190
|
+
const retryAfterMs = (_a = params.retryAfterMs) !== null && _a !== void 0 ? _a : undefined;
|
|
191
|
+
if (retryAfterMs !== undefined) {
|
|
192
|
+
return Math.min(retryAfterMs, policy.maxDelayMs);
|
|
193
|
+
}
|
|
194
|
+
const cap = Math.min(policy.baseDelayMs * 2 ** attempt, policy.maxDelayMs);
|
|
195
|
+
return random() * cap;
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=retry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.js","sourceRoot":"","sources":["../../../src/packlets/safer-fetch/retry.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\nimport { fail, succeed, type Result } from '@fgv/ts-utils';\n\nimport {\n DEFAULT_RETRY_BASE_DELAY_MS,\n DEFAULT_RETRY_MAX_DELAY_MS,\n IDEMPOTENT_METHODS,\n RETRY_AFTER_STATUSES,\n RETRYABLE_HTTP_STATUSES\n} from './defaults';\nimport type { FetchFailureReason } from './failureReason';\nimport type { SaferFetchMethod } from './model';\n\n/**\n * Opt-in retry policy.\n *\n * @remarks\n * **Retry is off by default and has to be asked for.** A primitive that silently retries\n * changes the semantics of every call site and amplifies load against a service that is already\n * struggling.\n *\n * Two rules are load-bearing rather than incidental, and are worth reading before enabling this:\n *\n * **Every attempt re-runs the address guard from hop 0 — a full re-walk, never a resume and\n * never a cached verdict.** Reusing an earlier attempt's verdict would make retry its own\n * DNS-rebinding vector: an attacker needs the guard to pass once and a connect to land on a\n * private address, so `N` connects against **one** check multiplies their odds by the retry\n * count — and a retry delay is precisely when a short-TTL rebind lands. A resume is\n * independently wrong because the redirect chain is not stable across attempts: attempt 2's\n * server may answer with a different `Location`, so revalidating only the current hop would\n * validate a chain the request no longer follows. The consequence is that retries cost more\n * than a naive implementation would suggest — each carries full re-resolution — and that cost\n * is deliberate.\n *\n * **The overall deadline is the ceiling.** Retries consume `timeoutMs` and never reset it, so\n * `{ timeoutMs: 30_000, retry: { attempts: 3 } }` is a thirty-second call, not a two-minute\n * one. When the remaining budget is shorter than the computed delay, the call fails with the\n * attempt's own failure rather than sleeping past its deadline.\n * @public\n */\nexport interface IRetryPolicy {\n /**\n * Additional attempts after the first, **not** a total. `attempts: 2` means up to three\n * requests. Must be a non-negative integer; `0` disables retry.\n */\n readonly attempts: number;\n\n /**\n * First backoff delay, in milliseconds, doubled per attempt before jitter. Default\n * {@link SaferFetch.DEFAULT_RETRY_BASE_DELAY_MS}.\n */\n readonly baseDelayMs?: number;\n\n /**\n * Ceiling on any single delay, in milliseconds. Default\n * {@link SaferFetch.DEFAULT_RETRY_MAX_DELAY_MS}. Also clamps a server-supplied\n * `Retry-After`, which is the security-relevant half of its job.\n */\n readonly maxDelayMs?: number;\n\n /**\n * Retries `POST` / `PUT` / `PATCH` / `DELETE` as well as `GET` / `HEAD`. Default `false`.\n *\n * @remarks\n * Off by default because a timeout does not tell you whether the server processed the\n * request: a retried `POST` after a timeout can double-charge. Turn it on for endpoints you\n * know are idempotent — an idempotency key, a `PUT` of a whole resource — and not otherwise.\n */\n readonly retryNonIdempotent?: boolean;\n\n /**\n * Honors a `Retry-After` header on `429` and `503`. Default `true`.\n *\n * @remarks\n * Always clamped to `maxDelayMs`, whatever this is set to. The header is chosen by the\n * server, which in this threat model may be the adversary.\n */\n readonly respectRetryAfter?: boolean;\n}\n\n/**\n * A {@link SaferFetch.IRetryPolicy} with every default applied, so no downstream code path\n * branches on a field's absence.\n * @internal\n */\nexport interface IResolvedRetryPolicy {\n readonly attempts: number;\n readonly baseDelayMs: number;\n readonly maxDelayMs: number;\n readonly retryNonIdempotent: boolean;\n readonly respectRetryAfter: boolean;\n}\n\n/** No retry. The resolved shape of an absent `retry` option. @internal */\nexport const NO_RETRY: IResolvedRetryPolicy = {\n attempts: 0,\n baseDelayMs: DEFAULT_RETRY_BASE_DELAY_MS,\n maxDelayMs: DEFAULT_RETRY_MAX_DELAY_MS,\n retryNonIdempotent: false,\n respectRetryAfter: true\n};\n\n/**\n * Applies the retry defaults, rejecting a policy that cannot mean anything.\n *\n * @remarks\n * Validated for the same reason `redirectPolicy` and `sensitiveHeaders` are: a JavaScript\n * caller can pass anything, and a `NaN` delay silently becomes a `setTimeout(NaN)` that fires\n * immediately — turning a backoff into a tight retry loop against a struggling service.\n * @internal\n */\nexport function resolveRetryPolicy(retry?: IRetryPolicy): Result<IResolvedRetryPolicy> {\n // `?? undefined` rather than `!== undefined`, matching every other option: a `null` from a\n // JavaScript caller means \"absent\", never \"installed\".\n const policy = retry ?? undefined;\n if (policy === undefined) {\n return succeed(NO_RETRY);\n }\n\n const attempts = policy.attempts;\n if (!Number.isInteger(attempts) || attempts < 0) {\n return fail(`retry.attempts must be a non-negative integer; got ${attempts}.`);\n }\n\n const baseDelayMs = policy.baseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS;\n const maxDelayMs = policy.maxDelayMs ?? DEFAULT_RETRY_MAX_DELAY_MS;\n const delays: ReadonlyArray<readonly [string, number]> = [\n ['retry.baseDelayMs', baseDelayMs],\n ['retry.maxDelayMs', maxDelayMs]\n ];\n for (const [name, value] of delays) {\n if (!Number.isFinite(value) || value < 0) {\n return fail(`${name} must be a non-negative finite number; got ${value}.`);\n }\n }\n\n return succeed({\n attempts,\n baseDelayMs,\n maxDelayMs,\n retryNonIdempotent: policy.retryNonIdempotent === true,\n // Defaults to honoring the header, so `?? true` rather than the `=== true` the other two\n // flags use.\n respectRetryAfter: policy.respectRetryAfter ?? true\n });\n}\n\n/**\n * Decides whether a failure is one a later attempt could plausibly answer differently.\n *\n * @remarks\n * The `switch` is exhaustive over the taxonomy on purpose: a new failure kind should have to be\n * classified deliberately rather than inherit \"retryable\" or \"not\" from a default branch.\n *\n * `'blocked-by-guard'` can never succeed on a retry — the guard's answer is about the\n * destination, not about the moment. `'too-large'` is the sharper case: retrying it\n * re-downloads up to the cap every time, which turns a defense into an amplifier aimed at the\n * caller's own bandwidth.\n * @internal\n */\nexport function isRetryableFailure(reason: FetchFailureReason | undefined): boolean {\n // A failure with no detail is one this primitive could not classify, and an unclassifiable\n // failure is not one to retry — the whole basis for retrying is knowing what went wrong.\n if (reason === undefined) {\n return false;\n }\n switch (reason.kind) {\n case 'network':\n case 'timeout':\n return true;\n case 'http-status':\n return RETRYABLE_HTTP_STATUSES.includes(reason.status);\n case 'invalid-url':\n case 'blocked-by-guard':\n case 'redirect-rejected':\n case 'redirect-opaque':\n case 'too-many-redirects':\n case 'aborted':\n case 'too-large':\n case 'unsupported-content-type':\n case 'decode':\n case 'parse':\n case 'unknown':\n return false;\n }\n}\n\n/**\n * Whether this method may be retried under the supplied policy.\n * @internal\n */\nexport function isRetryableMethod(method: SaferFetchMethod, policy: IResolvedRetryPolicy): boolean {\n return policy.retryNonIdempotent || IDEMPOTENT_METHODS.includes(method);\n}\n\n/**\n * Whether a `Retry-After` header on this failure is one to honor.\n *\n * @remarks\n * Only on `429` and `503`, and only when the policy asks for it. A `500` carrying a\n * `Retry-After` does not get to dictate the caller's schedule.\n * @internal\n */\nexport function honorsRetryAfter(\n reason: FetchFailureReason | undefined,\n policy: IResolvedRetryPolicy\n): boolean {\n return (\n policy.respectRetryAfter && reason?.kind === 'http-status' && RETRY_AFTER_STATUSES.includes(reason.status)\n );\n}\n\n/**\n * Parses a `Retry-After` header value into milliseconds.\n *\n * @remarks\n * Both RFC 9110 forms: delta-seconds, and an HTTP-date which is converted to a delay relative\n * to `nowMs`. An unparseable value, a negative delta, or a date already in the past yields\n * `undefined` — \"the server said nothing usable\", which falls back to ordinary backoff rather\n * than to zero delay. Returning `0` for a past date would let a stale clock produce a hot loop.\n *\n * The value is **not** clamped here. Clamping belongs with the rest of the delay arithmetic in\n * {@link computeRetryDelayMs}, so there is exactly one place the ceiling is applied.\n * @internal\n */\nexport function parseRetryAfterMs(value: string | undefined, nowMs: number): number | undefined {\n const text = (value ?? '').trim();\n if (text.length === 0) {\n return undefined;\n }\n // Digits only: a delta-seconds value. Tested before `Date.parse`, which accepts bare numbers\n // as years in some engines and would read `Retry-After: 120` as the year 120.\n if (/^\\d+$/.test(text)) {\n return Number(text) * 1000;\n }\n const at = Date.parse(text);\n if (Number.isNaN(at)) {\n return undefined;\n }\n const delay = at - nowMs;\n return delay > 0 ? delay : undefined;\n}\n\n/** Inputs to {@link computeRetryDelayMs}. @internal */\nexport interface IRetryDelayParams {\n /** 0-based index of the retry about to be scheduled: `0` is the first retry. */\n readonly attempt: number;\n readonly policy: IResolvedRetryPolicy;\n /** A honored, parsed `Retry-After`, in milliseconds, when the server sent a usable one. */\n readonly retryAfterMs?: number;\n /** Injected for determinism in tests; the call path passes `Math.random`. */\n readonly random: () => number;\n}\n\n/**\n * Computes how long to wait before the next attempt.\n *\n * @remarks\n * **Exponential backoff with full jitter**: the delay is uniform on `[0, cap)` where `cap` is\n * `baseDelayMs * 2^attempt` bounded by `maxDelayMs`. Full jitter rather than a fixed backoff\n * because every caller that failed against the same struggling host would otherwise retry in\n * the same instant, which is the thundering herd the backoff was supposed to prevent.\n *\n * **A honored `Retry-After` replaces the computed backoff and is clamped to `maxDelayMs`.** The\n * clamp is not a rounding detail: `Retry-After` is chosen by the server, which in this threat\n * model may be the adversary, so an unclamped `Retry-After: 86400` is a day-long sleep the\n * caller never agreed to. Jitter is not applied to it — the server named an instant, and\n * scattering around it is the caller's herd problem, not the server's instruction.\n *\n * `2 ** attempt` overflows to `Infinity` for a large enough `attempt`; `Math.min` with a finite\n * ceiling absorbs that, which is why the cap is computed in this order.\n * @internal\n */\nexport function computeRetryDelayMs(params: IRetryDelayParams): number {\n const { attempt, policy, random } = params;\n const retryAfterMs = params.retryAfterMs ?? undefined;\n if (retryAfterMs !== undefined) {\n return Math.min(retryAfterMs, policy.maxDelayMs);\n }\n const cap = Math.min(policy.baseDelayMs * 2 ** attempt, policy.maxDelayMs);\n return random() * cap;\n}\n"]}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { type Converter, type DetailedResult, type Validator } from '@fgv/ts-utils';
|
|
2
|
+
import { type JsonValue } from '@fgv/ts-json-base';
|
|
3
|
+
import type { FetchFailureReason } from './failureReason';
|
|
4
|
+
import type { ISaferFetchOptions, ISaferFetchResponse } from './model';
|
|
5
|
+
/**
|
|
6
|
+
* Fetches a URL and returns the raw response bytes, subject to the scheme check, the address
|
|
7
|
+
* guard, the request and response guards, the deadlines, and the response size cap.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* **What this does not protect against.** It is not a sandbox: a caller who wants to reach
|
|
11
|
+
* `127.0.0.1` can call `globalThis.fetch` directly, so this guards untrusted *data*, never
|
|
12
|
+
* untrusted *code in the same process*. It does no certificate pinning and adds nothing
|
|
13
|
+
* against a network-position attacker — TLS is that control. It does not resist DNS rebinding:
|
|
14
|
+
* an address guard validates a resolved address and the transport then re-resolves, so a
|
|
15
|
+
* hostile resolver can answer the two lookups differently. And it inspects nothing in the
|
|
16
|
+
* response body. If your deployment has an egress proxy or firewall, that control is strictly
|
|
17
|
+
* stronger than this one; this is defense in depth for deployments that do not.
|
|
18
|
+
*
|
|
19
|
+
* **The failure detail is an internal-network scanning oracle.** Log it; do not echo it, or
|
|
20
|
+
* any string derived from it, to an untrusted caller. The detail is structured precisely so
|
|
21
|
+
* that mapping it to a coarse public code is trivial.
|
|
22
|
+
*
|
|
23
|
+
* **Redirects are rejected unless you ask for them.** `redirectPolicy: 'validate-each-hop'`
|
|
24
|
+
* follows them, runs the address guard on every hop before any connection, and drops credential
|
|
25
|
+
* headers the first time the chain leaves an origin — and never restores them, so an
|
|
26
|
+
* `A` → `B` → `A` chain does not hand the token back to `A`.
|
|
27
|
+
*
|
|
28
|
+
* @param url - The URL to fetch. Only `http:` and `https:` are ever requested; every other
|
|
29
|
+
* scheme fails as `'invalid-url'`.
|
|
30
|
+
* @param options - Call options. `addressGuard` is required and has no default — use
|
|
31
|
+
* `blockPrivateNetworks()` on Node, or `allowAnyAddress()` to name the absence of the
|
|
32
|
+
* guarantee at the call site. `maxResponseBytes` defaults to 5 MiB and is meant to be tuned
|
|
33
|
+
* per call: real documents clear that, and raising it for the calls that need it is the
|
|
34
|
+
* intended use, not a workaround.
|
|
35
|
+
* @public
|
|
36
|
+
*/
|
|
37
|
+
export declare function saferFetchBytes(url: string | URL, options: ISaferFetchOptions): Promise<DetailedResult<ISaferFetchResponse<Uint8Array>, FetchFailureReason>>;
|
|
38
|
+
/**
|
|
39
|
+
* Fetches a URL and decodes the response body as text.
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* The charset comes from the `Content-Type` parameter and defaults to UTF-8. Decoding is
|
|
43
|
+
* strict: an unknown charset, or a byte sequence that is not valid in the declared one, fails
|
|
44
|
+
* as `'decode'` rather than silently producing mojibake that some caller downstream treats as
|
|
45
|
+
* data.
|
|
46
|
+
*
|
|
47
|
+
* See {@link SaferFetch.saferFetchBytes} for what this primitive does **not** protect against,
|
|
48
|
+
* and for the warning about echoing failure detail to untrusted callers.
|
|
49
|
+
*
|
|
50
|
+
* @param url - The URL to fetch.
|
|
51
|
+
* @param options - Call options. `addressGuard` is required. `maxResponseBytes` defaults to
|
|
52
|
+
* 5 MiB and is meant to be tuned per call.
|
|
53
|
+
* @public
|
|
54
|
+
*/
|
|
55
|
+
export declare function saferFetchText(url: string | URL, options: ISaferFetchOptions): Promise<DetailedResult<ISaferFetchResponse<string>, FetchFailureReason>>;
|
|
56
|
+
/**
|
|
57
|
+
* Options for the validating form of {@link SaferFetch.saferFetchJson}.
|
|
58
|
+
*
|
|
59
|
+
* @remarks
|
|
60
|
+
* `converter` is **required** here, and `T` is inferred from it. There is deliberately no way
|
|
61
|
+
* to assert a `T` without supplying the converter that evidences it at runtime: a caller-named
|
|
62
|
+
* type with nothing checking it is a claim the primitive cannot keep. Omit the whole options
|
|
63
|
+
* type and the value comes back as `JsonValue`, which is what the wire actually guarantees.
|
|
64
|
+
* @public
|
|
65
|
+
*/
|
|
66
|
+
export interface ISaferFetchJsonOptions<T> extends ISaferFetchOptions {
|
|
67
|
+
/** Applied to the parsed JSON, taking the caller from wire to validated `T` in one step. */
|
|
68
|
+
readonly converter: Converter<T> | Validator<T>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Fetches a URL and parses the response body as JSON, yielding the raw `JsonValue` for the
|
|
72
|
+
* caller to validate.
|
|
73
|
+
*
|
|
74
|
+
* @remarks
|
|
75
|
+
* **This does not gate on `Content-Type` by itself.** A server returning an HTML error page
|
|
76
|
+
* with a `200` will be decoded and parsed, and fail as `'parse'` — twenty frames from where it
|
|
77
|
+
* actually went wrong. Pass `responseHeadersGuard: allowContentTypes(['application/json'])`
|
|
78
|
+
* to reject on the header instead: it costs a header comparison rather than a body transfer,
|
|
79
|
+
* and the failure names the type. Content-type gating is a guard rather than an option so that
|
|
80
|
+
* there is exactly one mechanism for it.
|
|
81
|
+
*
|
|
82
|
+
* See {@link SaferFetch.saferFetchBytes} for what this primitive does **not** protect against,
|
|
83
|
+
* and for the warning about echoing failure detail to untrusted callers.
|
|
84
|
+
*
|
|
85
|
+
* @param url - The URL to fetch.
|
|
86
|
+
* @param options - Call options. `addressGuard` is required. `maxResponseBytes` defaults to
|
|
87
|
+
* 5 MiB and is meant to be tuned per call.
|
|
88
|
+
* @public
|
|
89
|
+
*/
|
|
90
|
+
export declare function saferFetchJson(url: string | URL, options: ISaferFetchOptions): Promise<DetailedResult<ISaferFetchResponse<JsonValue>, FetchFailureReason>>;
|
|
91
|
+
/**
|
|
92
|
+
* Fetches a URL, parses the response body as JSON, and runs it through the supplied converter
|
|
93
|
+
* or validator so the caller reaches a validated `T` in one step.
|
|
94
|
+
*
|
|
95
|
+
* @remarks
|
|
96
|
+
* `T` is inferred from `converter` and is never caller-asserted, so the returned type is
|
|
97
|
+
* evidenced at runtime rather than claimed.
|
|
98
|
+
*
|
|
99
|
+
* See {@link SaferFetch.saferFetchBytes} for what this primitive does **not** protect against,
|
|
100
|
+
* and for the warning about echoing failure detail to untrusted callers.
|
|
101
|
+
*
|
|
102
|
+
* @param url - The URL to fetch.
|
|
103
|
+
* @param options - Call options plus the required `converter`. `addressGuard` is required.
|
|
104
|
+
* `maxResponseBytes` defaults to 5 MiB and is meant to be tuned per call.
|
|
105
|
+
* @public
|
|
106
|
+
*/
|
|
107
|
+
export declare function saferFetchJson<T>(url: string | URL, options: ISaferFetchJsonOptions<T>): Promise<DetailedResult<ISaferFetchResponse<T>, FetchFailureReason>>;
|
|
108
|
+
//# sourceMappingURL=saferFetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"saferFetch.d.ts","sourceRoot":"","sources":["../../../src/packlets/safer-fetch/saferFetch.ts"],"names":[],"mappings":"AAoBA,OAAO,EAOL,KAAK,SAAS,EAEd,KAAK,cAAc,EAEnB,KAAK,SAAS,EACf,MAAM,eAAe,CAAC;AACvB,OAAO,EAAoC,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAYrF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAW1D,OAAO,KAAK,EAIV,kBAAkB,EAElB,mBAAmB,EAIpB,MAAM,SAAS,CAAC;AAk+BjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,cAAc,CAAC,mBAAmB,CAAC,UAAU,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAE9E;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,cAAc,CAAC,mBAAmB,CAAC,MAAM,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAM1E;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,sBAAsB,CAAC,CAAC,CAAE,SAAQ,kBAAkB;IACnE,4FAA4F;IAC5F,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;CACjD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,cAAc,CAAC,mBAAmB,CAAC,SAAS,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAAC;AAC/E;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAAC,CAAC,EACpC,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,OAAO,EAAE,sBAAsB,CAAC,CAAC,CAAC,GACjC,OAAO,CAAC,cAAc,CAAC,mBAAmB,CAAC,CAAC,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAAC"}
|