dotdo 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +446 -315
- package/cli/README.md +238 -0
- package/cli/agent.ts +72 -0
- package/cli/bin.js +44 -0
- package/cli/bin.ts +38 -0
- package/cli/build.ts +157 -0
- package/cli/commands/auth/login.ts +14 -0
- package/cli/commands/auth/logout.ts +6 -0
- package/cli/commands/auth/whoami.ts +16 -0
- package/cli/commands/deploy-multi.ts +245 -0
- package/cli/commands/dev/deploy.ts +100 -0
- package/cli/commands/dev/dev.ts +95 -0
- package/cli/commands/dev/logs.ts +91 -0
- package/cli/commands/dev-local.ts +88 -0
- package/cli/commands/do-ops.ts +314 -0
- package/cli/commands/index.ts +100 -0
- package/cli/commands/init.ts +247 -0
- package/cli/commands/introspect/emitter.ts +315 -0
- package/cli/commands/introspect/index.ts +193 -0
- package/cli/commands/link.ts +598 -0
- package/cli/commands/snippets.ts +415 -0
- package/cli/commands/tunnel.ts +239 -0
- package/cli/device-auth.ts +289 -0
- package/cli/fallback.ts +12 -0
- package/cli/index.ts +121 -0
- package/cli/main.ts +246 -0
- package/cli/mcp-stdio.ts +790 -0
- package/cli/package.json +62 -0
- package/cli/runtime/do-registry.ts +193 -0
- package/cli/runtime/embedded-db.ts +344 -0
- package/cli/runtime/index.ts +9 -0
- package/cli/runtime/miniflare-adapter.ts +162 -0
- package/cli/sandbox.ts +82 -0
- package/cli/src/args.ts +174 -0
- package/cli/src/auth.ts +55 -0
- package/cli/src/commands/call.ts +84 -0
- package/cli/src/commands/charge.ts +96 -0
- package/cli/src/commands/config.ts +115 -0
- package/cli/src/commands/email.ts +112 -0
- package/cli/src/commands/llm.ts +115 -0
- package/cli/src/commands/queue.ts +134 -0
- package/cli/src/commands/text.ts +86 -0
- package/cli/src/config.ts +185 -0
- package/cli/src/output.ts +246 -0
- package/cli/src/rpc.ts +192 -0
- package/cli/utils/config.ts +282 -0
- package/cli/utils/detect.ts +73 -0
- package/cli/utils/index.ts +15 -0
- package/cli/utils/logger.ts +232 -0
- package/dist/ai/index.js +19 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/template-literals.js +852 -0
- package/dist/ai/template-literals.js.map +1 -0
- package/dist/api/middleware/auth-federation.js +573 -0
- package/dist/api/middleware/auth-federation.js.map +1 -0
- package/dist/api/middleware/auth.js +545 -0
- package/dist/api/middleware/auth.js.map +1 -0
- package/dist/db/actions.js +212 -0
- package/dist/db/actions.js.map +1 -0
- package/dist/db/auth.js +506 -0
- package/dist/db/auth.js.map +1 -0
- package/dist/db/branches.js +65 -0
- package/dist/db/branches.js.map +1 -0
- package/dist/db/clickhouse.js +1074 -0
- package/dist/db/clickhouse.js.map +1 -0
- package/dist/db/dlq.js +39 -0
- package/dist/db/dlq.js.map +1 -0
- package/dist/db/events.js +28 -0
- package/dist/db/events.js.map +1 -0
- package/dist/db/exec.js +64 -0
- package/dist/db/exec.js.map +1 -0
- package/dist/db/files.js +85 -0
- package/dist/db/files.js.map +1 -0
- package/dist/db/flags.js +24 -0
- package/dist/db/flags.js.map +1 -0
- package/dist/db/git.js +116 -0
- package/dist/db/git.js.map +1 -0
- package/dist/db/iceberg/inverted-index.js +862 -0
- package/dist/db/iceberg/inverted-index.js.map +1 -0
- package/dist/db/iceberg/puffin.js +878 -0
- package/dist/db/iceberg/puffin.js.map +1 -0
- package/dist/db/iceberg/search-manifest.js +422 -0
- package/dist/db/iceberg/search-manifest.js.map +1 -0
- package/dist/db/iceberg/types.js +8 -0
- package/dist/db/iceberg/types.js.map +1 -0
- package/dist/db/index.js +121 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/integrations.js +368 -0
- package/dist/db/integrations.js.map +1 -0
- package/dist/db/json-indexes.js +332 -0
- package/dist/db/json-indexes.js.map +1 -0
- package/dist/db/linked-accounts.js +287 -0
- package/dist/db/linked-accounts.js.map +1 -0
- package/dist/db/nouns.js +183 -0
- package/dist/db/nouns.js.map +1 -0
- package/dist/db/objects.js +170 -0
- package/dist/db/objects.js.map +1 -0
- package/dist/db/primitives/dag-scheduler/index.js +869 -0
- package/dist/db/primitives/dag-scheduler/index.js.map +1 -0
- package/dist/db/primitives/exactly-once-context.js +237 -0
- package/dist/db/primitives/exactly-once-context.js.map +1 -0
- package/dist/db/primitives/index.js +62 -0
- package/dist/db/primitives/index.js.map +1 -0
- package/dist/db/primitives/keyed-router.js +145 -0
- package/dist/db/primitives/keyed-router.js.map +1 -0
- package/dist/db/primitives/observability.js +162 -0
- package/dist/db/primitives/observability.js.map +1 -0
- package/dist/db/primitives/schema-evolution.js +643 -0
- package/dist/db/primitives/schema-evolution.js.map +1 -0
- package/dist/db/primitives/stateful-operator/index.js +770 -0
- package/dist/db/primitives/stateful-operator/index.js.map +1 -0
- package/dist/db/primitives/temporal-store.js +306 -0
- package/dist/db/primitives/temporal-store.js.map +1 -0
- package/dist/db/primitives/typed-column-store.js +1229 -0
- package/dist/db/primitives/typed-column-store.js.map +1 -0
- package/dist/db/primitives/utils/duration.js +162 -0
- package/dist/db/primitives/utils/duration.js.map +1 -0
- package/dist/db/primitives/utils/murmur3.js +116 -0
- package/dist/db/primitives/utils/murmur3.js.map +1 -0
- package/dist/db/primitives/watermark-service.js +136 -0
- package/dist/db/primitives/watermark-service.js.map +1 -0
- package/dist/db/primitives/window-manager.js +764 -0
- package/dist/db/primitives/window-manager.js.map +1 -0
- package/dist/db/relationships.js +66 -0
- package/dist/db/relationships.js.map +1 -0
- package/dist/db/schema-minimal.js +61 -0
- package/dist/db/schema-minimal.js.map +1 -0
- package/dist/db/search.js +28 -0
- package/dist/db/search.js.map +1 -0
- package/dist/db/stores.js +1665 -0
- package/dist/db/stores.js.map +1 -0
- package/dist/db/things.js +297 -0
- package/dist/db/things.js.map +1 -0
- package/dist/db/vault.js +171 -0
- package/dist/db/vault.js.map +1 -0
- package/dist/db/verbs.js +102 -0
- package/dist/db/verbs.js.map +1 -0
- package/dist/do/base.js +48 -0
- package/dist/do/base.js.map +1 -0
- package/dist/do/tiny.js +31 -0
- package/dist/do/tiny.js.map +1 -0
- package/dist/lib/DOAuth.js +261 -0
- package/dist/lib/DOAuth.js.map +1 -0
- package/dist/lib/DODispatcher.js +72 -0
- package/dist/lib/DODispatcher.js.map +1 -0
- package/dist/lib/Modifier.js +189 -0
- package/dist/lib/Modifier.js.map +1 -0
- package/dist/lib/StateStorage.js +403 -0
- package/dist/lib/StateStorage.js.map +1 -0
- package/dist/lib/TypeRegistry.js +122 -0
- package/dist/lib/TypeRegistry.js.map +1 -0
- package/dist/lib/ai/gateway.js +247 -0
- package/dist/lib/ai/gateway.js.map +1 -0
- package/dist/lib/ai/tool-loop-agent.js +591 -0
- package/dist/lib/ai/tool-loop-agent.js.map +1 -0
- package/dist/lib/auto-wiring.js +439 -0
- package/dist/lib/auto-wiring.js.map +1 -0
- package/dist/lib/browse/browserbase.js +163 -0
- package/dist/lib/browse/browserbase.js.map +1 -0
- package/dist/lib/browse/cloudflare.js +144 -0
- package/dist/lib/browse/cloudflare.js.map +1 -0
- package/dist/lib/browse/index.js +62 -0
- package/dist/lib/browse/index.js.map +1 -0
- package/dist/lib/browse/types.js +13 -0
- package/dist/lib/browse/types.js.map +1 -0
- package/dist/lib/cache/index.js +37 -0
- package/dist/lib/cache/index.js.map +1 -0
- package/dist/lib/cache/visibility.js +638 -0
- package/dist/lib/cache/visibility.js.map +1 -0
- package/dist/lib/capabilities.js +268 -0
- package/dist/lib/capabilities.js.map +1 -0
- package/dist/lib/channels/base.js +106 -0
- package/dist/lib/channels/base.js.map +1 -0
- package/dist/lib/channels/discord.js +94 -0
- package/dist/lib/channels/discord.js.map +1 -0
- package/dist/lib/channels/email.js +204 -0
- package/dist/lib/channels/email.js.map +1 -0
- package/dist/lib/channels/index.js +90 -0
- package/dist/lib/channels/index.js.map +1 -0
- package/dist/lib/channels/mdxui-chat.js +95 -0
- package/dist/lib/channels/mdxui-chat.js.map +1 -0
- package/dist/lib/channels/slack-blockkit.js +121 -0
- package/dist/lib/channels/slack-blockkit.js.map +1 -0
- package/dist/lib/channels/types.js +7 -0
- package/dist/lib/channels/types.js.map +1 -0
- package/dist/lib/cloudflare/ai.js +654 -0
- package/dist/lib/cloudflare/ai.js.map +1 -0
- package/dist/lib/cloudflare/index.js +88 -0
- package/dist/lib/cloudflare/index.js.map +1 -0
- package/dist/lib/cloudflare/kv.js +342 -0
- package/dist/lib/cloudflare/kv.js.map +1 -0
- package/dist/lib/cloudflare/queues.js +434 -0
- package/dist/lib/cloudflare/queues.js.map +1 -0
- package/dist/lib/cloudflare/r2.js +604 -0
- package/dist/lib/cloudflare/r2.js.map +1 -0
- package/dist/lib/cloudflare/vectorize.js +494 -0
- package/dist/lib/cloudflare/vectorize.js.map +1 -0
- package/dist/lib/cloudflare/workflows.js +569 -0
- package/dist/lib/cloudflare/workflows.js.map +1 -0
- package/dist/lib/colo/caching.js +196 -0
- package/dist/lib/colo/caching.js.map +1 -0
- package/dist/lib/colo/detection.js +194 -0
- package/dist/lib/colo/detection.js.map +1 -0
- package/dist/lib/colo/external-data.js +219 -0
- package/dist/lib/colo/external-data.js.map +1 -0
- package/dist/lib/colo/globe-data.js +179 -0
- package/dist/lib/colo/globe-data.js.map +1 -0
- package/dist/lib/colo/index.js +16 -0
- package/dist/lib/colo/index.js.map +1 -0
- package/dist/lib/decorators.js +37 -0
- package/dist/lib/decorators.js.map +1 -0
- package/dist/lib/discovery.js +81 -0
- package/dist/lib/discovery.js.map +1 -0
- package/dist/lib/executors/AgenticFunctionExecutor.js +619 -0
- package/dist/lib/executors/AgenticFunctionExecutor.js.map +1 -0
- package/dist/lib/executors/BaseFunctionExecutor.js +328 -0
- package/dist/lib/executors/BaseFunctionExecutor.js.map +1 -0
- package/dist/lib/executors/CascadeExecutor.js +418 -0
- package/dist/lib/executors/CascadeExecutor.js.map +1 -0
- package/dist/lib/executors/CodeFunctionExecutor.js +904 -0
- package/dist/lib/executors/CodeFunctionExecutor.js.map +1 -0
- package/dist/lib/executors/GenerativeFunctionExecutor.js +904 -0
- package/dist/lib/executors/GenerativeFunctionExecutor.js.map +1 -0
- package/dist/lib/executors/HumanFunctionExecutor.js +884 -0
- package/dist/lib/executors/HumanFunctionExecutor.js.map +1 -0
- package/dist/lib/executors/ParallelStepExecutor.js +308 -0
- package/dist/lib/executors/ParallelStepExecutor.js.map +1 -0
- package/dist/lib/executors/types.js +12 -0
- package/dist/lib/executors/types.js.map +1 -0
- package/dist/lib/experiments.js +89 -0
- package/dist/lib/experiments.js.map +1 -0
- package/dist/lib/flags/store.js +262 -0
- package/dist/lib/flags/store.js.map +1 -0
- package/dist/lib/functions/FunctionComposition.js +467 -0
- package/dist/lib/functions/FunctionComposition.js.map +1 -0
- package/dist/lib/functions/FunctionMiddleware.js +457 -0
- package/dist/lib/functions/FunctionMiddleware.js.map +1 -0
- package/dist/lib/functions/FunctionRegistry.js +426 -0
- package/dist/lib/functions/FunctionRegistry.js.map +1 -0
- package/dist/lib/functions/createFunction.js +1048 -0
- package/dist/lib/functions/createFunction.js.map +1 -0
- package/dist/lib/humans/index.js +68 -0
- package/dist/lib/humans/index.js.map +1 -0
- package/dist/lib/humans/templates.js +117 -0
- package/dist/lib/humans/templates.js.map +1 -0
- package/dist/lib/identity.js +98 -0
- package/dist/lib/identity.js.map +1 -0
- package/dist/lib/index.js +9 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/logging/error-logger.js +163 -0
- package/dist/lib/logging/error-logger.js.map +1 -0
- package/dist/lib/logging/index.js +160 -0
- package/dist/lib/logging/index.js.map +1 -0
- package/dist/lib/mixins/bash.js +753 -0
- package/dist/lib/mixins/bash.js.map +1 -0
- package/dist/lib/mixins/fs.js +648 -0
- package/dist/lib/mixins/fs.js.map +1 -0
- package/dist/lib/mixins/git.js +1006 -0
- package/dist/lib/mixins/git.js.map +1 -0
- package/dist/lib/mixins/npm.js +662 -0
- package/dist/lib/mixins/npm.js.map +1 -0
- package/dist/lib/noun-id.js +278 -0
- package/dist/lib/noun-id.js.map +1 -0
- package/dist/lib/rate-limit/sliding-window.js +148 -0
- package/dist/lib/rate-limit/sliding-window.js.map +1 -0
- package/dist/lib/rate-limit.js +110 -0
- package/dist/lib/rate-limit.js.map +1 -0
- package/dist/lib/rpc/bindings.js +548 -0
- package/dist/lib/rpc/bindings.js.map +1 -0
- package/dist/lib/rpc/index.js +64 -0
- package/dist/lib/rpc/index.js.map +1 -0
- package/dist/lib/safe-stringify.js +223 -0
- package/dist/lib/safe-stringify.js.map +1 -0
- package/dist/lib/sandbox/miniflare-sandbox.js +1007 -0
- package/dist/lib/sandbox/miniflare-sandbox.js.map +1 -0
- package/dist/lib/sqids.js +110 -0
- package/dist/lib/sqids.js.map +1 -0
- package/dist/lib/sql/adapters/index.js +10 -0
- package/dist/lib/sql/adapters/index.js.map +1 -0
- package/dist/lib/sql/adapters/node-sql-parser.js +552 -0
- package/dist/lib/sql/adapters/node-sql-parser.js.map +1 -0
- package/dist/lib/sql/adapters/pgsql-parser.js +1190 -0
- package/dist/lib/sql/adapters/pgsql-parser.js.map +1 -0
- package/dist/lib/sql/index.js +277 -0
- package/dist/lib/sql/index.js.map +1 -0
- package/dist/lib/sql/types.js +56 -0
- package/dist/lib/sql/types.js.map +1 -0
- package/dist/lib/type-classifier.js +126 -0
- package/dist/lib/type-classifier.js.map +1 -0
- package/dist/lib/utils/html.js +47 -0
- package/dist/lib/utils/html.js.map +1 -0
- package/dist/lib/validation.js +48 -0
- package/dist/lib/validation.js.map +1 -0
- package/dist/lib/vault/store.js +411 -0
- package/dist/lib/vault/store.js.map +1 -0
- package/dist/metrics/hunch.js +739 -0
- package/dist/metrics/hunch.js.map +1 -0
- package/dist/objects/API.js +302 -0
- package/dist/objects/API.js.map +1 -0
- package/dist/objects/Agent.js +179 -0
- package/dist/objects/Agent.js.map +1 -0
- package/dist/objects/AgenticFunctionExecutor.js +8 -0
- package/dist/objects/AgenticFunctionExecutor.js.map +1 -0
- package/dist/objects/App.js +83 -0
- package/dist/objects/App.js.map +1 -0
- package/dist/objects/Browser.js +884 -0
- package/dist/objects/Browser.js.map +1 -0
- package/dist/objects/Business.js +107 -0
- package/dist/objects/Business.js.map +1 -0
- package/dist/objects/CLI.js +221 -0
- package/dist/objects/CLI.js.map +1 -0
- package/dist/objects/CodeFunctionExecutor.js +8 -0
- package/dist/objects/CodeFunctionExecutor.js.map +1 -0
- package/dist/objects/Collection.js +161 -0
- package/dist/objects/Collection.js.map +1 -0
- package/dist/objects/DO.js +41 -0
- package/dist/objects/DO.js.map +1 -0
- package/dist/objects/DOBase.js +2309 -0
- package/dist/objects/DOBase.js.map +1 -0
- package/dist/objects/DOCache.js +153 -0
- package/dist/objects/DOCache.js.map +1 -0
- package/dist/objects/DOFull.js +1676 -0
- package/dist/objects/DOFull.js.map +1 -0
- package/dist/objects/DOTiny.js +207 -0
- package/dist/objects/DOTiny.js.map +1 -0
- package/dist/objects/Directory.js +199 -0
- package/dist/objects/Directory.js.map +1 -0
- package/dist/objects/Entity.js +413 -0
- package/dist/objects/Entity.js.map +1 -0
- package/dist/objects/Function.js +116 -0
- package/dist/objects/Function.js.map +1 -0
- package/dist/objects/Human.js +231 -0
- package/dist/objects/Human.js.map +1 -0
- package/dist/objects/HumanFunctionExecutor.js +8 -0
- package/dist/objects/HumanFunctionExecutor.js.map +1 -0
- package/dist/objects/IcebergMetadataDO.js +938 -0
- package/dist/objects/IcebergMetadataDO.js.map +1 -0
- package/dist/objects/IntegrationsDO.js +1174 -0
- package/dist/objects/IntegrationsDO.js.map +1 -0
- package/dist/objects/ObservabilityBroadcaster.js +149 -0
- package/dist/objects/ObservabilityBroadcaster.js.map +1 -0
- package/dist/objects/Package.js +154 -0
- package/dist/objects/Package.js.map +1 -0
- package/dist/objects/Product.js +193 -0
- package/dist/objects/Product.js.map +1 -0
- package/dist/objects/SDK.js +152 -0
- package/dist/objects/SDK.js.map +1 -0
- package/dist/objects/SaaS.js +235 -0
- package/dist/objects/SaaS.js.map +1 -0
- package/dist/objects/SandboxDO.js +759 -0
- package/dist/objects/SandboxDO.js.map +1 -0
- package/dist/objects/Service.js +337 -0
- package/dist/objects/Service.js.map +1 -0
- package/dist/objects/Site.js +80 -0
- package/dist/objects/Site.js.map +1 -0
- package/dist/objects/Startup.js +479 -0
- package/dist/objects/Startup.js.map +1 -0
- package/dist/objects/ThingsDO.js +170 -0
- package/dist/objects/ThingsDO.js.map +1 -0
- package/dist/objects/VectorShardDO.js +650 -0
- package/dist/objects/VectorShardDO.js.map +1 -0
- package/dist/objects/Worker.js +144 -0
- package/dist/objects/Worker.js.map +1 -0
- package/dist/objects/Workflow.js +196 -0
- package/dist/objects/Workflow.js.map +1 -0
- package/dist/objects/WorkflowFactory.js +313 -0
- package/dist/objects/WorkflowFactory.js.map +1 -0
- package/dist/objects/WorkflowRuntime.js +863 -0
- package/dist/objects/WorkflowRuntime.js.map +1 -0
- package/dist/objects/circuit-breaker-bulkhead.js +178 -0
- package/dist/objects/circuit-breaker-bulkhead.js.map +1 -0
- package/dist/objects/createFunction.js +934 -0
- package/dist/objects/createFunction.js.map +1 -0
- package/dist/objects/index.js +80 -0
- package/dist/objects/index.js.map +1 -0
- package/dist/objects/lifecycle/Branch.js +275 -0
- package/dist/objects/lifecycle/Branch.js.map +1 -0
- package/dist/objects/lifecycle/Clone.js +1499 -0
- package/dist/objects/lifecycle/Clone.js.map +1 -0
- package/dist/objects/lifecycle/Compact.js +237 -0
- package/dist/objects/lifecycle/Compact.js.map +1 -0
- package/dist/objects/lifecycle/Promote.js +476 -0
- package/dist/objects/lifecycle/Promote.js.map +1 -0
- package/dist/objects/lifecycle/Shard.js +560 -0
- package/dist/objects/lifecycle/Shard.js.map +1 -0
- package/dist/objects/lifecycle/index.js +15 -0
- package/dist/objects/lifecycle/index.js.map +1 -0
- package/dist/objects/lifecycle/types.js +33 -0
- package/dist/objects/lifecycle/types.js.map +1 -0
- package/dist/objects/mixins/infrastructure.js +171 -0
- package/dist/objects/mixins/infrastructure.js.map +1 -0
- package/dist/objects/modules/StoresModule.js +153 -0
- package/dist/objects/modules/StoresModule.js.map +1 -0
- package/dist/objects/persistence/checkpoint-manager.js +606 -0
- package/dist/objects/persistence/checkpoint-manager.js.map +1 -0
- package/dist/objects/persistence/index.js +72 -0
- package/dist/objects/persistence/index.js.map +1 -0
- package/dist/objects/persistence/migration-runner.js +562 -0
- package/dist/objects/persistence/migration-runner.js.map +1 -0
- package/dist/objects/persistence/replication-manager.js +501 -0
- package/dist/objects/persistence/replication-manager.js.map +1 -0
- package/dist/objects/persistence/tiered-storage-manager.js +595 -0
- package/dist/objects/persistence/tiered-storage-manager.js.map +1 -0
- package/dist/objects/persistence/types.js +14 -0
- package/dist/objects/persistence/types.js.map +1 -0
- package/dist/objects/persistence/wal-manager.js +653 -0
- package/dist/objects/persistence/wal-manager.js.map +1 -0
- package/dist/objects/presets/index.js +20 -0
- package/dist/objects/presets/index.js.map +1 -0
- package/dist/objects/presets/primitives.js +188 -0
- package/dist/objects/presets/primitives.js.map +1 -0
- package/dist/objects/primitives/alarm-adapter.js +141 -0
- package/dist/objects/primitives/alarm-adapter.js.map +1 -0
- package/dist/objects/primitives/index.js +337 -0
- package/dist/objects/primitives/index.js.map +1 -0
- package/dist/objects/primitives/storage-adapter.js +182 -0
- package/dist/objects/primitives/storage-adapter.js.map +1 -0
- package/dist/objects/primitives/with-primitives.js +102 -0
- package/dist/objects/primitives/with-primitives.js.map +1 -0
- package/dist/objects/services/StoreManager.js +227 -0
- package/dist/objects/services/StoreManager.js.map +1 -0
- package/dist/objects/services/index.js +13 -0
- package/dist/objects/services/index.js.map +1 -0
- package/dist/objects/transport/auth-layer.js +1451 -0
- package/dist/objects/transport/auth-layer.js.map +1 -0
- package/dist/objects/transport/capnweb-target.js +355 -0
- package/dist/objects/transport/capnweb-target.js.map +1 -0
- package/dist/objects/transport/chain.js +441 -0
- package/dist/objects/transport/chain.js.map +1 -0
- package/dist/objects/transport/handler.js +58 -0
- package/dist/objects/transport/handler.js.map +1 -0
- package/dist/objects/transport/index.js +53 -0
- package/dist/objects/transport/index.js.map +1 -0
- package/dist/objects/transport/mcp-server.js +691 -0
- package/dist/objects/transport/mcp-server.js.map +1 -0
- package/dist/objects/transport/rest-autowire.js +1508 -0
- package/dist/objects/transport/rest-autowire.js.map +1 -0
- package/dist/objects/transport/rest-router.js +440 -0
- package/dist/objects/transport/rest-router.js.map +1 -0
- package/dist/objects/transport/rpc-server.js +1539 -0
- package/dist/objects/transport/rpc-server.js.map +1 -0
- package/dist/objects/transport/shared.js +576 -0
- package/dist/objects/transport/shared.js.map +1 -0
- package/dist/objects/transport/sync-engine.js +291 -0
- package/dist/objects/transport/sync-engine.js.map +1 -0
- package/dist/objects/transport/types.js +8 -0
- package/dist/objects/transport/types.js.map +1 -0
- package/dist/sandbox/index.js +258 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/snippets/artifacts-config.js +241 -0
- package/dist/snippets/artifacts-config.js.map +1 -0
- package/dist/snippets/artifacts-ingest.js +832 -0
- package/dist/snippets/artifacts-ingest.js.map +1 -0
- package/dist/snippets/artifacts-serve.js +1035 -0
- package/dist/snippets/artifacts-serve.js.map +1 -0
- package/dist/snippets/artifacts-types.js +161 -0
- package/dist/snippets/artifacts-types.js.map +1 -0
- package/dist/snippets/cache-probe.js +376 -0
- package/dist/snippets/cache-probe.js.map +1 -0
- package/dist/snippets/cache.js +10 -0
- package/dist/snippets/cache.js.map +1 -0
- package/dist/snippets/events.js +469 -0
- package/dist/snippets/events.js.map +1 -0
- package/dist/snippets/index.js +7 -0
- package/dist/snippets/index.js.map +1 -0
- package/dist/snippets/proxy.js +495 -0
- package/dist/snippets/proxy.js.map +1 -0
- package/dist/snippets/search.js +1759 -0
- package/dist/snippets/search.js.map +1 -0
- package/dist/streams/index.js +30 -0
- package/dist/streams/index.js.map +1 -0
- package/dist/streams/observability.js +68 -0
- package/dist/streams/observability.js.map +1 -0
- package/dist/types/AI.js +92 -0
- package/dist/types/AI.js.map +1 -0
- package/dist/types/AIFunction.js +171 -0
- package/dist/types/AIFunction.js.map +1 -0
- package/dist/types/BrowseVerb.js +89 -0
- package/dist/types/BrowseVerb.js.map +1 -0
- package/dist/types/Browser.js +31 -0
- package/dist/types/Browser.js.map +1 -0
- package/dist/types/Chaos.js +15 -0
- package/dist/types/Chaos.js.map +1 -0
- package/dist/types/CloudflareBindings.js +109 -0
- package/dist/types/CloudflareBindings.js.map +1 -0
- package/dist/types/Collection.js +50 -0
- package/dist/types/Collection.js.map +1 -0
- package/dist/types/DO.js +2 -0
- package/dist/types/DO.js.map +1 -0
- package/dist/types/DOLocation.js +63 -0
- package/dist/types/DOLocation.js.map +1 -0
- package/dist/types/EventHandler.js +57 -0
- package/dist/types/EventHandler.js.map +1 -0
- package/dist/types/Experiment.js +33 -0
- package/dist/types/Experiment.js.map +1 -0
- package/dist/types/Flag.js +57 -0
- package/dist/types/Flag.js.map +1 -0
- package/dist/types/Lifecycle.js +13 -0
- package/dist/types/Lifecycle.js.map +1 -0
- package/dist/types/Location.js +169 -0
- package/dist/types/Location.js.map +1 -0
- package/dist/types/Noun.js +66 -0
- package/dist/types/Noun.js.map +1 -0
- package/dist/types/SessionEvent.js +194 -0
- package/dist/types/SessionEvent.js.map +1 -0
- package/dist/types/Thing.js +55 -0
- package/dist/types/Thing.js.map +1 -0
- package/dist/types/ThingDO.js +153 -0
- package/dist/types/ThingDO.js.map +1 -0
- package/dist/types/Things.js +2 -0
- package/dist/types/Things.js.map +1 -0
- package/dist/types/Verb.js +119 -0
- package/dist/types/Verb.js.map +1 -0
- package/dist/types/WorkflowContext.js +70 -0
- package/dist/types/WorkflowContext.js.map +1 -0
- package/dist/types/analytics-api.js +13 -0
- package/dist/types/analytics-api.js.map +1 -0
- package/dist/types/capabilities.js +135 -0
- package/dist/types/capabilities.js.map +1 -0
- package/dist/types/drizzle.js +12 -0
- package/dist/types/drizzle.js.map +1 -0
- package/dist/types/event.js +201 -0
- package/dist/types/event.js.map +1 -0
- package/dist/types/fn.js +12 -0
- package/dist/types/fn.js.map +1 -0
- package/dist/types/iceberg.js +48 -0
- package/dist/types/iceberg.js.map +1 -0
- package/dist/types/ids.js +170 -0
- package/dist/types/ids.js.map +1 -0
- package/dist/types/index.js +41 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/introspect.js +54 -0
- package/dist/types/introspect.js.map +1 -0
- package/dist/types/observability.js +124 -0
- package/dist/types/observability.js.map +1 -0
- package/dist/types/sync-protocol.js +175 -0
- package/dist/types/sync-protocol.js.map +1 -0
- package/dist/types/vector.js +13 -0
- package/dist/types/vector.js.map +1 -0
- package/dist/workflows/ScheduleManager.js +473 -0
- package/dist/workflows/ScheduleManager.js.map +1 -0
- package/dist/workflows/StepDOBridge.js +149 -0
- package/dist/workflows/StepDOBridge.js.map +1 -0
- package/dist/workflows/StepResultStorage.js +232 -0
- package/dist/workflows/StepResultStorage.js.map +1 -0
- package/dist/workflows/WaitForEventManager.js +461 -0
- package/dist/workflows/WaitForEventManager.js.map +1 -0
- package/dist/workflows/analyzer.js +332 -0
- package/dist/workflows/analyzer.js.map +1 -0
- package/dist/workflows/compat/activity-router.js +484 -0
- package/dist/workflows/compat/activity-router.js.map +1 -0
- package/dist/workflows/compat/backends/cloudflare-workflows.js +431 -0
- package/dist/workflows/compat/backends/cloudflare-workflows.js.map +1 -0
- package/dist/workflows/compat/backends/index.js +14 -0
- package/dist/workflows/compat/backends/index.js.map +1 -0
- package/dist/workflows/compat/errors/index.js +375 -0
- package/dist/workflows/compat/errors/index.js.map +1 -0
- package/dist/workflows/compat/index.js +79 -0
- package/dist/workflows/compat/index.js.map +1 -0
- package/dist/workflows/compat/inngest/index.js +989 -0
- package/dist/workflows/compat/inngest/index.js.map +1 -0
- package/dist/workflows/compat/qstash/index.js +1263 -0
- package/dist/workflows/compat/qstash/index.js.map +1 -0
- package/dist/workflows/compat/temporal/activities.js +739 -0
- package/dist/workflows/compat/temporal/activities.js.map +1 -0
- package/dist/workflows/compat/temporal/child-workflows.js +154 -0
- package/dist/workflows/compat/temporal/child-workflows.js.map +1 -0
- package/dist/workflows/compat/temporal/client.js +381 -0
- package/dist/workflows/compat/temporal/client.js.map +1 -0
- package/dist/workflows/compat/temporal/context.js +309 -0
- package/dist/workflows/compat/temporal/context.js.map +1 -0
- package/dist/workflows/compat/temporal/determinism.js +216 -0
- package/dist/workflows/compat/temporal/determinism.js.map +1 -0
- package/dist/workflows/compat/temporal/errors.js +128 -0
- package/dist/workflows/compat/temporal/errors.js.map +1 -0
- package/dist/workflows/compat/temporal/index.js +2464 -0
- package/dist/workflows/compat/temporal/index.js.map +1 -0
- package/dist/workflows/compat/temporal/saga.js +504 -0
- package/dist/workflows/compat/temporal/saga.js.map +1 -0
- package/dist/workflows/compat/temporal/signals.js +364 -0
- package/dist/workflows/compat/temporal/signals.js.map +1 -0
- package/dist/workflows/compat/temporal/storage.js +271 -0
- package/dist/workflows/compat/temporal/storage.js.map +1 -0
- package/dist/workflows/compat/temporal/timers.js +347 -0
- package/dist/workflows/compat/temporal/timers.js.map +1 -0
- package/dist/workflows/compat/temporal/types.js +7 -0
- package/dist/workflows/compat/temporal/types.js.map +1 -0
- package/dist/workflows/compat/temporal/unified-primitives.js +339 -0
- package/dist/workflows/compat/temporal/unified-primitives.js.map +1 -0
- package/dist/workflows/compat/trigger/index.js +468 -0
- package/dist/workflows/compat/trigger/index.js.map +1 -0
- package/dist/workflows/compat/utils/index.js +69 -0
- package/dist/workflows/compat/utils/index.js.map +1 -0
- package/dist/workflows/context/correlation-capability.js +266 -0
- package/dist/workflows/context/correlation-capability.js.map +1 -0
- package/dist/workflows/context/correlation.js +484 -0
- package/dist/workflows/context/correlation.js.map +1 -0
- package/dist/workflows/context/experiment.js +289 -0
- package/dist/workflows/context/experiment.js.map +1 -0
- package/dist/workflows/context/flag.js +244 -0
- package/dist/workflows/context/flag.js.map +1 -0
- package/dist/workflows/context/foundation.js +648 -0
- package/dist/workflows/context/foundation.js.map +1 -0
- package/dist/workflows/context/human-base.js +106 -0
- package/dist/workflows/context/human-base.js.map +1 -0
- package/dist/workflows/context/human.js +368 -0
- package/dist/workflows/context/human.js.map +1 -0
- package/dist/workflows/context/measure.js +354 -0
- package/dist/workflows/context/measure.js.map +1 -0
- package/dist/workflows/context/rate-limit.js +358 -0
- package/dist/workflows/context/rate-limit.js.map +1 -0
- package/dist/workflows/context/user.js +117 -0
- package/dist/workflows/context/user.js.map +1 -0
- package/dist/workflows/context/vault.js +360 -0
- package/dist/workflows/context/vault.js.map +1 -0
- package/dist/workflows/data/entity-events/entity-events.js +489 -0
- package/dist/workflows/data/entity-events/entity-events.js.map +1 -0
- package/dist/workflows/data/experiment/index.js +599 -0
- package/dist/workflows/data/experiment/index.js.map +1 -0
- package/dist/workflows/data/goal/context.js +558 -0
- package/dist/workflows/data/goal/context.js.map +1 -0
- package/dist/workflows/data/goal/index.js +32 -0
- package/dist/workflows/data/goal/index.js.map +1 -0
- package/dist/workflows/data/measure/index.js +840 -0
- package/dist/workflows/data/measure/index.js.map +1 -0
- package/dist/workflows/data/stream/index.js +1149 -0
- package/dist/workflows/data/stream/index.js.map +1 -0
- package/dist/workflows/data/track/context.js +883 -0
- package/dist/workflows/data/track/context.js.map +1 -0
- package/dist/workflows/data/track/index.js +15 -0
- package/dist/workflows/data/track/index.js.map +1 -0
- package/dist/workflows/data/view/context.js +864 -0
- package/dist/workflows/data/view/context.js.map +1 -0
- package/dist/workflows/domain.js +93 -0
- package/dist/workflows/domain.js.map +1 -0
- package/dist/workflows/flag.js +176 -0
- package/dist/workflows/flag.js.map +1 -0
- package/dist/workflows/flags.js +217 -0
- package/dist/workflows/flags.js.map +1 -0
- package/dist/workflows/hash.js +209 -0
- package/dist/workflows/hash.js.map +1 -0
- package/dist/workflows/index.js +50 -0
- package/dist/workflows/index.js.map +1 -0
- package/dist/workflows/on.js +378 -0
- package/dist/workflows/on.js.map +1 -0
- package/dist/workflows/pipeline-promise.js +481 -0
- package/dist/workflows/pipeline-promise.js.map +1 -0
- package/dist/workflows/pipeline-types.js +20 -0
- package/dist/workflows/pipeline-types.js.map +1 -0
- package/dist/workflows/proxy.js +76 -0
- package/dist/workflows/proxy.js.map +1 -0
- package/dist/workflows/runtime.js +310 -0
- package/dist/workflows/runtime.js.map +1 -0
- package/dist/workflows/schedule-builder.js +327 -0
- package/dist/workflows/schedule-builder.js.map +1 -0
- package/dist/workflows/visibility/index.js +146 -0
- package/dist/workflows/visibility/index.js.map +1 -0
- package/dist/workflows/visibility/query-parser.js +150 -0
- package/dist/workflows/visibility/query-parser.js.map +1 -0
- package/dist/workflows/visibility/store.js +223 -0
- package/dist/workflows/visibility/store.js.map +1 -0
- package/dist/workflows/visibility/types.js +30 -0
- package/dist/workflows/visibility/types.js.map +1 -0
- package/dist/workflows/workflow.js +53 -0
- package/dist/workflows/workflow.js.map +1 -0
- package/package.json +294 -46
|
@@ -0,0 +1,1263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QStash Compat Layer - 100% API Compatible with @upstash/qstash
|
|
3
|
+
*
|
|
4
|
+
* Drop-in replacement for Upstash QStash that runs on dotdo's
|
|
5
|
+
* durable execution infrastructure.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Real HTTP delivery with fetch()
|
|
9
|
+
* - Retry logic with exponential backoff (5xx only, not 4xx)
|
|
10
|
+
* - Upstash-Retried header on retry attempts
|
|
11
|
+
* - Dead letter queue support (both internal and URL-based)
|
|
12
|
+
* - URL Groups for fan-out delivery (concurrent with Promise.all)
|
|
13
|
+
* - Callback URLs on success/failure
|
|
14
|
+
* - HMAC-SHA256 signature generation and verification
|
|
15
|
+
* - Request timeout handling with AbortController
|
|
16
|
+
* - Content-based deduplication (1 hour window)
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import { Client } from '@dotdo/qstash'
|
|
21
|
+
*
|
|
22
|
+
* const client = new Client({ token: 'xxx' })
|
|
23
|
+
* await client.publishJSON({
|
|
24
|
+
* url: 'https://example.com.ai/api/webhook',
|
|
25
|
+
* body: { hello: 'world' },
|
|
26
|
+
* delay: '5m',
|
|
27
|
+
* })
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @see https://upstash.com/docs/qstash for the original QStash documentation
|
|
31
|
+
*
|
|
32
|
+
* Note: For production use with Cloudflare Workers, consider integrating
|
|
33
|
+
* with CF Workflows for durable retry scheduling (free waits vs setTimeout).
|
|
34
|
+
*/
|
|
35
|
+
import { ScheduleManager, parseCronExpression, getNextRunTime } from '../../ScheduleManager';
|
|
36
|
+
import { DurableWorkflowRuntime, InMemoryStepStorage } from '../../runtime';
|
|
37
|
+
import { parseDuration, ensureError } from '../utils';
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// UTILITIES
|
|
40
|
+
// Note: parseDuration and ensureError are imported from '../utils'
|
|
41
|
+
// ============================================================================
|
|
42
|
+
function generateMessageId() {
|
|
43
|
+
return `msg_${crypto.randomUUID().replace(/-/g, '')}`;
|
|
44
|
+
}
|
|
45
|
+
function generateScheduleId() {
|
|
46
|
+
return `sched_${crypto.randomUUID().replace(/-/g, '')}`;
|
|
47
|
+
}
|
|
48
|
+
function generateTopicId() {
|
|
49
|
+
return `topic_${crypto.randomUUID().replace(/-/g, '')}`;
|
|
50
|
+
}
|
|
51
|
+
function generateSubscriptionId() {
|
|
52
|
+
return `sub_${crypto.randomUUID().replace(/-/g, '')}`;
|
|
53
|
+
}
|
|
54
|
+
function generateEventId() {
|
|
55
|
+
return `evt_${crypto.randomUUID().replace(/-/g, '')}`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Valid HTTP methods for QStash requests (matching PublishRequest.method)
|
|
59
|
+
*/
|
|
60
|
+
const validHttpMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
|
|
61
|
+
/**
|
|
62
|
+
* Check if a string is a valid HTTP method for publishing
|
|
63
|
+
*/
|
|
64
|
+
function isValidHttpMethod(method) {
|
|
65
|
+
return validHttpMethods.includes(method);
|
|
66
|
+
}
|
|
67
|
+
async function hashBody(body) {
|
|
68
|
+
const encoder = new TextEncoder();
|
|
69
|
+
const data = encoder.encode(body);
|
|
70
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
71
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
72
|
+
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Calculate exponential backoff delay with jitter
|
|
76
|
+
*/
|
|
77
|
+
function calculateBackoffDelay(attempt, initialDelayMs = 1000, maxDelayMs = 30000) {
|
|
78
|
+
// Exponential backoff: initialDelay * 2^(attempt-1)
|
|
79
|
+
let delay = initialDelayMs * Math.pow(2, attempt - 1);
|
|
80
|
+
// Cap at max delay
|
|
81
|
+
delay = Math.min(delay, maxDelayMs);
|
|
82
|
+
// Add jitter (0-25% of delay)
|
|
83
|
+
const jitter = delay * 0.25 * Math.random();
|
|
84
|
+
delay += jitter;
|
|
85
|
+
return Math.floor(delay);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Check if HTTP status code is retryable (5xx errors only)
|
|
89
|
+
*/
|
|
90
|
+
function isRetryableStatus(status) {
|
|
91
|
+
return status >= 500 && status < 600;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Generate HMAC-SHA256 signature for a payload
|
|
95
|
+
*/
|
|
96
|
+
async function generateSignature(signingKey, body) {
|
|
97
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
98
|
+
const payload = `${timestamp}.${body}`;
|
|
99
|
+
const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(signingKey), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
100
|
+
const signatureBuffer = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(payload));
|
|
101
|
+
const signature = Array.from(new Uint8Array(signatureBuffer))
|
|
102
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
103
|
+
.join('');
|
|
104
|
+
return `t=${timestamp},v1=${signature}`;
|
|
105
|
+
}
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// SCHEDULES API
|
|
108
|
+
// ============================================================================
|
|
109
|
+
class Schedules {
|
|
110
|
+
manager;
|
|
111
|
+
inMemorySchedules = new Map();
|
|
112
|
+
constructor(state) {
|
|
113
|
+
this.manager = state ? new ScheduleManager(state) : null;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Create a new schedule
|
|
117
|
+
*/
|
|
118
|
+
async create(request) {
|
|
119
|
+
const scheduleId = request.scheduleId || generateScheduleId();
|
|
120
|
+
const schedule = {
|
|
121
|
+
scheduleId,
|
|
122
|
+
cron: request.cron,
|
|
123
|
+
destination: request.destination,
|
|
124
|
+
body: typeof request.body === 'object' ? JSON.stringify(request.body) : request.body,
|
|
125
|
+
method: request.method || 'POST',
|
|
126
|
+
headers: request.headers,
|
|
127
|
+
contentType: request.contentType,
|
|
128
|
+
retries: request.retries ?? 3,
|
|
129
|
+
callback: request.callback,
|
|
130
|
+
failureCallback: request.failureCallback,
|
|
131
|
+
timeout: request.timeout,
|
|
132
|
+
createdAt: Date.now(),
|
|
133
|
+
isPaused: false,
|
|
134
|
+
};
|
|
135
|
+
if (this.manager) {
|
|
136
|
+
// Use DO-based schedule manager
|
|
137
|
+
await this.manager.schedule(request.cron, scheduleId, {
|
|
138
|
+
metadata: {
|
|
139
|
+
destination: request.destination,
|
|
140
|
+
body: schedule.body,
|
|
141
|
+
method: schedule.method,
|
|
142
|
+
headers: schedule.headers,
|
|
143
|
+
contentType: schedule.contentType,
|
|
144
|
+
retries: schedule.retries,
|
|
145
|
+
callback: schedule.callback,
|
|
146
|
+
failureCallback: schedule.failureCallback,
|
|
147
|
+
timeout: schedule.timeout,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// Always store in memory for compatibility
|
|
152
|
+
this.inMemorySchedules.set(scheduleId, schedule);
|
|
153
|
+
return { scheduleId };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get a schedule by ID
|
|
157
|
+
*/
|
|
158
|
+
async get(scheduleId) {
|
|
159
|
+
// Check in-memory first
|
|
160
|
+
const schedule = this.inMemorySchedules.get(scheduleId);
|
|
161
|
+
if (schedule)
|
|
162
|
+
return schedule;
|
|
163
|
+
// Check DO storage
|
|
164
|
+
if (this.manager) {
|
|
165
|
+
const stored = await this.manager.getSchedule(scheduleId);
|
|
166
|
+
if (stored) {
|
|
167
|
+
const meta = stored.metadata;
|
|
168
|
+
return {
|
|
169
|
+
scheduleId,
|
|
170
|
+
cron: stored.cronExpression,
|
|
171
|
+
destination: meta?.destination || '',
|
|
172
|
+
body: meta?.body,
|
|
173
|
+
method: meta?.method || 'POST',
|
|
174
|
+
headers: meta?.headers,
|
|
175
|
+
contentType: meta?.contentType,
|
|
176
|
+
retries: meta?.retries || 3,
|
|
177
|
+
callback: meta?.callback,
|
|
178
|
+
failureCallback: meta?.failureCallback,
|
|
179
|
+
timeout: meta?.timeout,
|
|
180
|
+
createdAt: stored.createdAt.getTime(),
|
|
181
|
+
isPaused: stored.status === 'paused',
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* List all schedules
|
|
189
|
+
*/
|
|
190
|
+
async list() {
|
|
191
|
+
const schedules = [];
|
|
192
|
+
// Add in-memory schedules
|
|
193
|
+
for (const schedule of Array.from(this.inMemorySchedules.values())) {
|
|
194
|
+
schedules.push(schedule);
|
|
195
|
+
}
|
|
196
|
+
// Add DO schedules (avoiding duplicates)
|
|
197
|
+
if (this.manager) {
|
|
198
|
+
const stored = await this.manager.listSchedules();
|
|
199
|
+
for (const s of stored) {
|
|
200
|
+
if (!this.inMemorySchedules.has(s.name)) {
|
|
201
|
+
const meta = s.metadata;
|
|
202
|
+
schedules.push({
|
|
203
|
+
scheduleId: s.name,
|
|
204
|
+
cron: s.cronExpression,
|
|
205
|
+
destination: meta?.destination || '',
|
|
206
|
+
body: meta?.body,
|
|
207
|
+
method: meta?.method || 'POST',
|
|
208
|
+
headers: meta?.headers,
|
|
209
|
+
contentType: meta?.contentType,
|
|
210
|
+
retries: meta?.retries || 3,
|
|
211
|
+
callback: meta?.callback,
|
|
212
|
+
failureCallback: meta?.failureCallback,
|
|
213
|
+
timeout: meta?.timeout,
|
|
214
|
+
createdAt: s.createdAt.getTime(),
|
|
215
|
+
isPaused: s.status === 'paused',
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return schedules;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Delete a schedule
|
|
224
|
+
*/
|
|
225
|
+
async delete(scheduleId) {
|
|
226
|
+
this.inMemorySchedules.delete(scheduleId);
|
|
227
|
+
if (this.manager) {
|
|
228
|
+
try {
|
|
229
|
+
await this.manager.deleteSchedule(scheduleId);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Schedule may not exist in DO storage
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Pause a schedule
|
|
238
|
+
*/
|
|
239
|
+
async pause(scheduleId) {
|
|
240
|
+
const schedule = this.inMemorySchedules.get(scheduleId);
|
|
241
|
+
if (schedule) {
|
|
242
|
+
schedule.isPaused = true;
|
|
243
|
+
}
|
|
244
|
+
if (this.manager) {
|
|
245
|
+
await this.manager.updateSchedule(scheduleId, { enabled: false });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Resume a schedule
|
|
250
|
+
*/
|
|
251
|
+
async resume(scheduleId) {
|
|
252
|
+
const schedule = this.inMemorySchedules.get(scheduleId);
|
|
253
|
+
if (schedule) {
|
|
254
|
+
schedule.isPaused = false;
|
|
255
|
+
}
|
|
256
|
+
if (this.manager) {
|
|
257
|
+
await this.manager.updateSchedule(scheduleId, { enabled: true });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// ============================================================================
|
|
262
|
+
// URL GROUPS API
|
|
263
|
+
// ============================================================================
|
|
264
|
+
export class URLGroups {
|
|
265
|
+
groups = new Map();
|
|
266
|
+
/**
|
|
267
|
+
* Create a new URL group
|
|
268
|
+
* @param nameOrRequest - Either a group name (string) or a CreateURLGroupRequest object
|
|
269
|
+
* @param endpoints - Array of endpoint URLs (only used when first arg is a string)
|
|
270
|
+
*/
|
|
271
|
+
async create(nameOrRequest, endpoints) {
|
|
272
|
+
let name;
|
|
273
|
+
let endpointList;
|
|
274
|
+
if (typeof nameOrRequest === 'string') {
|
|
275
|
+
// Simple signature: create('name', ['url1', 'url2'])
|
|
276
|
+
name = nameOrRequest;
|
|
277
|
+
endpointList = (endpoints || []).map((url) => ({ url }));
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
// Object signature: create({ name: 'name', endpoints: [...] })
|
|
281
|
+
name = nameOrRequest.name;
|
|
282
|
+
endpointList = [...nameOrRequest.endpoints];
|
|
283
|
+
}
|
|
284
|
+
const group = {
|
|
285
|
+
name,
|
|
286
|
+
endpoints: endpointList,
|
|
287
|
+
createdAt: Date.now(),
|
|
288
|
+
};
|
|
289
|
+
this.groups.set(name, group);
|
|
290
|
+
return group;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Get a URL group by name
|
|
294
|
+
*/
|
|
295
|
+
async get(name) {
|
|
296
|
+
return this.groups.get(name) ?? null;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* List all URL groups
|
|
300
|
+
*/
|
|
301
|
+
async list() {
|
|
302
|
+
return Array.from(this.groups.values());
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Delete a URL group
|
|
306
|
+
*/
|
|
307
|
+
async delete(name) {
|
|
308
|
+
this.groups.delete(name);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Add endpoints to an existing group
|
|
312
|
+
*/
|
|
313
|
+
async addEndpoints(name, endpoints) {
|
|
314
|
+
const group = this.groups.get(name);
|
|
315
|
+
if (!group) {
|
|
316
|
+
throw new Error(`URL group '${name}' not found`);
|
|
317
|
+
}
|
|
318
|
+
group.endpoints.push(...endpoints);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Remove endpoints from an existing group
|
|
322
|
+
*/
|
|
323
|
+
async removeEndpoints(name, endpoints) {
|
|
324
|
+
const group = this.groups.get(name);
|
|
325
|
+
if (!group) {
|
|
326
|
+
throw new Error(`URL group '${name}' not found`);
|
|
327
|
+
}
|
|
328
|
+
const urlsToRemove = new Set(endpoints.map((e) => e.url));
|
|
329
|
+
group.endpoints = group.endpoints.filter((e) => !urlsToRemove.has(e.url));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// ============================================================================
|
|
333
|
+
// DEAD LETTER QUEUE API
|
|
334
|
+
// ============================================================================
|
|
335
|
+
export class DLQ {
|
|
336
|
+
messages = new Map();
|
|
337
|
+
messageOrder = [];
|
|
338
|
+
/**
|
|
339
|
+
* Add a message to the DLQ (internal use)
|
|
340
|
+
*/
|
|
341
|
+
_addMessage(message) {
|
|
342
|
+
this.messages.set(message.messageId, message);
|
|
343
|
+
this.messageOrder.push(message.messageId);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* List DLQ messages
|
|
347
|
+
*/
|
|
348
|
+
async list(options) {
|
|
349
|
+
const limit = options?.limit ?? 100;
|
|
350
|
+
let startIndex = 0;
|
|
351
|
+
if (options?.cursor) {
|
|
352
|
+
const cursorIndex = this.messageOrder.indexOf(options.cursor);
|
|
353
|
+
if (cursorIndex !== -1) {
|
|
354
|
+
startIndex = cursorIndex + 1;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const result = [];
|
|
358
|
+
for (let i = startIndex; i < Math.min(startIndex + limit, this.messageOrder.length); i++) {
|
|
359
|
+
const id = this.messageOrder[i];
|
|
360
|
+
const msg = this.messages.get(id);
|
|
361
|
+
if (msg) {
|
|
362
|
+
result.push({
|
|
363
|
+
...msg,
|
|
364
|
+
cursor: id,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return result;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Get a specific DLQ message
|
|
372
|
+
*/
|
|
373
|
+
async get(messageId) {
|
|
374
|
+
return this.messages.get(messageId) ?? null;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Replay a DLQ message
|
|
378
|
+
*/
|
|
379
|
+
async replay(messageId, options, publishFn) {
|
|
380
|
+
const message = this.messages.get(messageId);
|
|
381
|
+
if (!message) {
|
|
382
|
+
return { success: false };
|
|
383
|
+
}
|
|
384
|
+
const url = options?.url ?? message.url;
|
|
385
|
+
if (publishFn) {
|
|
386
|
+
try {
|
|
387
|
+
// Validate HTTP method before casting
|
|
388
|
+
if (!isValidHttpMethod(message.method)) {
|
|
389
|
+
throw new Error(`Invalid HTTP method in DLQ message: ${message.method}`);
|
|
390
|
+
}
|
|
391
|
+
const result = await publishFn({
|
|
392
|
+
url,
|
|
393
|
+
body: message.body,
|
|
394
|
+
method: message.method,
|
|
395
|
+
headers: message.headers,
|
|
396
|
+
});
|
|
397
|
+
// Remove from DLQ on successful replay
|
|
398
|
+
this.messages.delete(messageId);
|
|
399
|
+
this.messageOrder = this.messageOrder.filter((id) => id !== messageId);
|
|
400
|
+
return {
|
|
401
|
+
success: true,
|
|
402
|
+
newMessageId: result.messageId,
|
|
403
|
+
url,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
return { success: false };
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// If no publish function provided, just remove from DLQ
|
|
411
|
+
this.messages.delete(messageId);
|
|
412
|
+
this.messageOrder = this.messageOrder.filter((id) => id !== messageId);
|
|
413
|
+
return {
|
|
414
|
+
success: true,
|
|
415
|
+
newMessageId: generateMessageId(),
|
|
416
|
+
url,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Delete a DLQ message
|
|
421
|
+
*/
|
|
422
|
+
async delete(messageId) {
|
|
423
|
+
this.messages.delete(messageId);
|
|
424
|
+
this.messageOrder = this.messageOrder.filter((id) => id !== messageId);
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Delete multiple DLQ messages
|
|
428
|
+
*/
|
|
429
|
+
async deleteMany(messageIds) {
|
|
430
|
+
for (const id of messageIds) {
|
|
431
|
+
this.messages.delete(id);
|
|
432
|
+
}
|
|
433
|
+
const idsSet = new Set(messageIds);
|
|
434
|
+
this.messageOrder = this.messageOrder.filter((id) => !idsSet.has(id));
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Purge all DLQ messages
|
|
438
|
+
*/
|
|
439
|
+
async purge() {
|
|
440
|
+
this.messages.clear();
|
|
441
|
+
this.messageOrder = [];
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// ============================================================================
|
|
445
|
+
// TOPICS API
|
|
446
|
+
// ============================================================================
|
|
447
|
+
export class Topics {
|
|
448
|
+
topics = new Map();
|
|
449
|
+
subscriptions = new Map();
|
|
450
|
+
topicSubscriptions = new Map(); // topicName -> subscriptionIds
|
|
451
|
+
/**
|
|
452
|
+
* Create a new topic
|
|
453
|
+
*/
|
|
454
|
+
async create(request) {
|
|
455
|
+
const topic = {
|
|
456
|
+
topicId: generateTopicId(),
|
|
457
|
+
name: request.name,
|
|
458
|
+
createdAt: Date.now(),
|
|
459
|
+
};
|
|
460
|
+
this.topics.set(request.name, topic);
|
|
461
|
+
this.topicSubscriptions.set(request.name, new Set());
|
|
462
|
+
return topic;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Get a topic by name
|
|
466
|
+
*/
|
|
467
|
+
async get(name) {
|
|
468
|
+
return this.topics.get(name) ?? null;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* List all topics
|
|
472
|
+
*/
|
|
473
|
+
async list() {
|
|
474
|
+
return Array.from(this.topics.values());
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Delete a topic
|
|
478
|
+
*/
|
|
479
|
+
async delete(name) {
|
|
480
|
+
this.topics.delete(name);
|
|
481
|
+
// Remove all subscriptions for this topic
|
|
482
|
+
const subIds = this.topicSubscriptions.get(name);
|
|
483
|
+
if (subIds) {
|
|
484
|
+
for (const subId of Array.from(subIds)) {
|
|
485
|
+
this.subscriptions.delete(subId);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
this.topicSubscriptions.delete(name);
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Subscribe to a topic
|
|
492
|
+
*/
|
|
493
|
+
async subscribe(topicName, request) {
|
|
494
|
+
const subscription = {
|
|
495
|
+
subscriptionId: generateSubscriptionId(),
|
|
496
|
+
topicName,
|
|
497
|
+
url: request.url,
|
|
498
|
+
urlGroup: request.urlGroup,
|
|
499
|
+
createdAt: Date.now(),
|
|
500
|
+
};
|
|
501
|
+
this.subscriptions.set(subscription.subscriptionId, subscription);
|
|
502
|
+
let topicSubs = this.topicSubscriptions.get(topicName);
|
|
503
|
+
if (!topicSubs) {
|
|
504
|
+
topicSubs = new Set();
|
|
505
|
+
this.topicSubscriptions.set(topicName, topicSubs);
|
|
506
|
+
}
|
|
507
|
+
topicSubs.add(subscription.subscriptionId);
|
|
508
|
+
return subscription;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Unsubscribe from a topic
|
|
512
|
+
*/
|
|
513
|
+
async unsubscribe(subscriptionId) {
|
|
514
|
+
const sub = this.subscriptions.get(subscriptionId);
|
|
515
|
+
if (sub) {
|
|
516
|
+
this.subscriptions.delete(subscriptionId);
|
|
517
|
+
const topicSubs = this.topicSubscriptions.get(sub.topicName);
|
|
518
|
+
if (topicSubs) {
|
|
519
|
+
topicSubs.delete(subscriptionId);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* List subscriptions for a topic
|
|
525
|
+
*/
|
|
526
|
+
async listSubscriptions(topicName) {
|
|
527
|
+
const subIds = this.topicSubscriptions.get(topicName);
|
|
528
|
+
if (!subIds)
|
|
529
|
+
return [];
|
|
530
|
+
const result = [];
|
|
531
|
+
for (const id of Array.from(subIds)) {
|
|
532
|
+
const sub = this.subscriptions.get(id);
|
|
533
|
+
if (sub) {
|
|
534
|
+
result.push(sub);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return result;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Get all subscribers for a topic (internal use)
|
|
541
|
+
*/
|
|
542
|
+
_getSubscribers(topicName) {
|
|
543
|
+
return this.listSubscriptions(topicName);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
export class Events {
|
|
547
|
+
events = [];
|
|
548
|
+
eventMap = new Map();
|
|
549
|
+
/**
|
|
550
|
+
* Record an event (internal use)
|
|
551
|
+
*/
|
|
552
|
+
_recordEvent(event) {
|
|
553
|
+
const eventId = generateEventId();
|
|
554
|
+
const fullEvent = {
|
|
555
|
+
...event,
|
|
556
|
+
eventId,
|
|
557
|
+
cursor: eventId,
|
|
558
|
+
};
|
|
559
|
+
this.events.push(fullEvent);
|
|
560
|
+
this.eventMap.set(eventId, fullEvent);
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* List events with optional filters
|
|
564
|
+
*/
|
|
565
|
+
async list(options) {
|
|
566
|
+
let result = [...this.events];
|
|
567
|
+
// Filter by type
|
|
568
|
+
if (options?.type) {
|
|
569
|
+
result = result.filter((e) => e.type === options.type);
|
|
570
|
+
}
|
|
571
|
+
// Filter by messageId
|
|
572
|
+
if (options?.messageId) {
|
|
573
|
+
result = result.filter((e) => e.messageId === options.messageId);
|
|
574
|
+
}
|
|
575
|
+
// Handle pagination
|
|
576
|
+
if (options?.cursor) {
|
|
577
|
+
const cursorIndex = result.findIndex((e) => e.eventId === options.cursor);
|
|
578
|
+
if (cursorIndex !== -1) {
|
|
579
|
+
result = result.slice(cursorIndex + 1);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
// Apply limit
|
|
583
|
+
if (options?.limit) {
|
|
584
|
+
result = result.slice(0, options.limit);
|
|
585
|
+
}
|
|
586
|
+
return result;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Get a specific event by ID
|
|
590
|
+
*/
|
|
591
|
+
async get(eventId) {
|
|
592
|
+
return this.eventMap.get(eventId) ?? null;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
// ============================================================================
|
|
596
|
+
// CLIENT
|
|
597
|
+
// ============================================================================
|
|
598
|
+
export class Client {
|
|
599
|
+
schedules;
|
|
600
|
+
urlGroups;
|
|
601
|
+
dlq;
|
|
602
|
+
topics;
|
|
603
|
+
events;
|
|
604
|
+
runtime;
|
|
605
|
+
deduplicationCache = new Map();
|
|
606
|
+
config;
|
|
607
|
+
cleanupIntervalId = null;
|
|
608
|
+
// Deduplication window is 1 hour, evict entries older than 2 hours
|
|
609
|
+
static DEDUP_EVICTION_AGE_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
610
|
+
static DEDUP_CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
611
|
+
constructor(config = {}) {
|
|
612
|
+
this.config = config;
|
|
613
|
+
this.runtime = new DurableWorkflowRuntime({
|
|
614
|
+
storage: config.storage ?? new InMemoryStepStorage(),
|
|
615
|
+
retryPolicy: config.retry
|
|
616
|
+
? {
|
|
617
|
+
maxAttempts: config.retry.retries ?? 3,
|
|
618
|
+
initialDelayMs: 1000,
|
|
619
|
+
maxDelayMs: 30000,
|
|
620
|
+
backoffMultiplier: 2,
|
|
621
|
+
jitter: true,
|
|
622
|
+
}
|
|
623
|
+
: undefined,
|
|
624
|
+
});
|
|
625
|
+
this.schedules = new Schedules(config.state);
|
|
626
|
+
this.urlGroups = new URLGroups();
|
|
627
|
+
this.dlq = new DLQ();
|
|
628
|
+
this.topics = new Topics();
|
|
629
|
+
this.events = new Events();
|
|
630
|
+
// Start periodic cleanup of deduplication cache
|
|
631
|
+
this._startDeduplicationCleanup();
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Start periodic cleanup of the deduplication cache to prevent memory leaks
|
|
635
|
+
*/
|
|
636
|
+
_startDeduplicationCleanup() {
|
|
637
|
+
this.cleanupIntervalId = setInterval(() => {
|
|
638
|
+
this._cleanupDeduplicationCache();
|
|
639
|
+
}, Client.DEDUP_CLEANUP_INTERVAL_MS);
|
|
640
|
+
// Ensure cleanup doesn't prevent process from exiting (Node.js)
|
|
641
|
+
if (typeof this.cleanupIntervalId === 'object' && 'unref' in this.cleanupIntervalId) {
|
|
642
|
+
this.cleanupIntervalId.unref();
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Clean up expired entries from the deduplication cache
|
|
647
|
+
*/
|
|
648
|
+
_cleanupDeduplicationCache() {
|
|
649
|
+
const now = Date.now();
|
|
650
|
+
this.deduplicationCache.forEach((entry, key) => {
|
|
651
|
+
if (now - entry.timestamp > Client.DEDUP_EVICTION_AGE_MS) {
|
|
652
|
+
this.deduplicationCache.delete(key);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Stop the cleanup interval (useful for testing or shutdown)
|
|
658
|
+
*/
|
|
659
|
+
destroy() {
|
|
660
|
+
if (this.cleanupIntervalId !== null) {
|
|
661
|
+
clearInterval(this.cleanupIntervalId);
|
|
662
|
+
this.cleanupIntervalId = null;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Handle callback failure - logs the error and emits a callback_failed event
|
|
667
|
+
*/
|
|
668
|
+
_handleCallbackFailure(messageId, callbackUrl, error) {
|
|
669
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
670
|
+
console.error(`[QStash] Callback failed for ${callbackUrl}:`, error);
|
|
671
|
+
this.events._recordEvent({
|
|
672
|
+
type: 'callback_failed',
|
|
673
|
+
messageId,
|
|
674
|
+
url: callbackUrl,
|
|
675
|
+
error: errorMessage,
|
|
676
|
+
timestamp: Date.now(),
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Publish a message to a URL, URL group, or topic
|
|
681
|
+
*/
|
|
682
|
+
async publish(request) {
|
|
683
|
+
const messageId = generateMessageId();
|
|
684
|
+
const bodyStr = typeof request.body === 'object' ? JSON.stringify(request.body) : (request.body ?? '');
|
|
685
|
+
const maxRetries = request.retries ?? 3;
|
|
686
|
+
// Check for deduplication
|
|
687
|
+
if (request.deduplicationId || request.contentBasedDeduplication) {
|
|
688
|
+
const dedupKey = request.deduplicationId ?? (await hashBody(bodyStr));
|
|
689
|
+
const cached = this.deduplicationCache.get(dedupKey);
|
|
690
|
+
// QStash deduplication window is 1 hour
|
|
691
|
+
if (cached && Date.now() - cached.timestamp < 60 * 60 * 1000) {
|
|
692
|
+
return {
|
|
693
|
+
messageId: cached.messageId,
|
|
694
|
+
url: request.url,
|
|
695
|
+
deduplicated: true,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
this.deduplicationCache.set(dedupKey, { messageId, timestamp: Date.now() });
|
|
699
|
+
}
|
|
700
|
+
// Record created event
|
|
701
|
+
this.events._recordEvent({
|
|
702
|
+
type: 'created',
|
|
703
|
+
messageId,
|
|
704
|
+
timestamp: Date.now(),
|
|
705
|
+
url: request.url,
|
|
706
|
+
});
|
|
707
|
+
// Calculate delay
|
|
708
|
+
const delayMs = request.delay ? parseDuration(request.delay) : 0;
|
|
709
|
+
// Handle URL group fan-out
|
|
710
|
+
if (request.urlGroup) {
|
|
711
|
+
return this._publishToUrlGroup(request, messageId, bodyStr, delayMs);
|
|
712
|
+
}
|
|
713
|
+
// Handle topic pub/sub (also check if it's a URL group name for simpler fan-out)
|
|
714
|
+
if (request.topic) {
|
|
715
|
+
const group = await this.urlGroups.get(request.topic);
|
|
716
|
+
if (group) {
|
|
717
|
+
// Topic name matches a URL group - do fan-out to all group endpoints
|
|
718
|
+
return this._publishToUrlGroupByName(request, messageId, bodyStr, delayMs, request.topic);
|
|
719
|
+
}
|
|
720
|
+
return this._publishToTopic(request, messageId, bodyStr, delayMs);
|
|
721
|
+
}
|
|
722
|
+
// Get the destination URL
|
|
723
|
+
const url = request.url ?? (request.api ? `https://qstash.upstash.io/v2/api/${request.api.name}` : '');
|
|
724
|
+
if (!url) {
|
|
725
|
+
throw new Error('Either url, urlGroup, topic, or api must be provided');
|
|
726
|
+
}
|
|
727
|
+
// Execute the delivery
|
|
728
|
+
const attemptHistory = [];
|
|
729
|
+
const executeDelivery = async () => {
|
|
730
|
+
let lastError;
|
|
731
|
+
let lastStatus;
|
|
732
|
+
let attempt = 0;
|
|
733
|
+
let shouldRetry = true;
|
|
734
|
+
for (attempt = 1; attempt <= maxRetries + 1 && shouldRetry; attempt++) {
|
|
735
|
+
try {
|
|
736
|
+
const response = await this._deliverMessage(url, bodyStr, request, messageId, attempt);
|
|
737
|
+
// Record delivered event
|
|
738
|
+
this.events._recordEvent({
|
|
739
|
+
type: 'delivered',
|
|
740
|
+
messageId,
|
|
741
|
+
timestamp: Date.now(),
|
|
742
|
+
url,
|
|
743
|
+
statusCode: response.status,
|
|
744
|
+
});
|
|
745
|
+
// Call success callback
|
|
746
|
+
if (request.callback) {
|
|
747
|
+
const responseBody = await response.clone().text();
|
|
748
|
+
const responseHeaders = {};
|
|
749
|
+
response.headers.forEach((value, key) => {
|
|
750
|
+
responseHeaders[key] = value;
|
|
751
|
+
});
|
|
752
|
+
await fetch(request.callback, {
|
|
753
|
+
method: 'POST',
|
|
754
|
+
headers: { 'Content-Type': 'application/json' },
|
|
755
|
+
body: JSON.stringify({
|
|
756
|
+
messageId,
|
|
757
|
+
url,
|
|
758
|
+
status: 'success',
|
|
759
|
+
statusCode: response.status,
|
|
760
|
+
body: responseBody,
|
|
761
|
+
response: {
|
|
762
|
+
body: responseBody,
|
|
763
|
+
headers: responseHeaders,
|
|
764
|
+
},
|
|
765
|
+
}),
|
|
766
|
+
}).catch((error) => {
|
|
767
|
+
this._handleCallbackFailure(messageId, request.callback, error);
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
return; // Success - exit the function
|
|
771
|
+
}
|
|
772
|
+
catch (error) {
|
|
773
|
+
lastError = error;
|
|
774
|
+
// Track attempt history
|
|
775
|
+
attemptHistory.push({
|
|
776
|
+
attempt,
|
|
777
|
+
timestamp: Date.now(),
|
|
778
|
+
error: lastError.message,
|
|
779
|
+
});
|
|
780
|
+
// Extract status code from error message if present
|
|
781
|
+
const statusMatch = lastError.message.match(/HTTP (\d+):/);
|
|
782
|
+
if (statusMatch) {
|
|
783
|
+
lastStatus = parseInt(statusMatch[1], 10);
|
|
784
|
+
// Don't retry on 4xx errors (client errors)
|
|
785
|
+
if (!isRetryableStatus(lastStatus)) {
|
|
786
|
+
shouldRetry = false;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
// Record retry event (if not the last attempt and should retry)
|
|
790
|
+
if (attempt < maxRetries + 1 && shouldRetry) {
|
|
791
|
+
this.events._recordEvent({
|
|
792
|
+
type: 'retry',
|
|
793
|
+
messageId,
|
|
794
|
+
timestamp: Date.now(),
|
|
795
|
+
url,
|
|
796
|
+
error: lastError.message,
|
|
797
|
+
attempt,
|
|
798
|
+
});
|
|
799
|
+
// Wait before next retry with exponential backoff
|
|
800
|
+
const backoffDelay = calculateBackoffDelay(attempt);
|
|
801
|
+
await new Promise((resolve) => setTimeout(resolve, backoffDelay));
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
// All retries exhausted or non-retryable error
|
|
806
|
+
this.events._recordEvent({
|
|
807
|
+
type: 'failed',
|
|
808
|
+
messageId,
|
|
809
|
+
timestamp: Date.now(),
|
|
810
|
+
url,
|
|
811
|
+
error: lastError?.message,
|
|
812
|
+
});
|
|
813
|
+
// Send to DLQ URL if provided in request
|
|
814
|
+
if (request.deadLetterQueue) {
|
|
815
|
+
await fetch(request.deadLetterQueue, {
|
|
816
|
+
method: 'POST',
|
|
817
|
+
headers: { 'Content-Type': 'application/json' },
|
|
818
|
+
body: JSON.stringify({
|
|
819
|
+
messageId,
|
|
820
|
+
originalUrl: url,
|
|
821
|
+
body: bodyStr,
|
|
822
|
+
method: request.method ?? 'POST',
|
|
823
|
+
headers: request.headers,
|
|
824
|
+
error: lastError?.message ?? 'Unknown error',
|
|
825
|
+
attempts: attempt - 1,
|
|
826
|
+
timestamp: Date.now(),
|
|
827
|
+
}),
|
|
828
|
+
}).catch((error) => {
|
|
829
|
+
// Log DLQ delivery failure
|
|
830
|
+
console.error(`[QStash] DLQ delivery failed for ${request.deadLetterQueue}:`, error);
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
// Add to internal DLQ storage
|
|
834
|
+
this.dlq._addMessage({
|
|
835
|
+
messageId,
|
|
836
|
+
url,
|
|
837
|
+
body: bodyStr,
|
|
838
|
+
method: request.method ?? 'POST',
|
|
839
|
+
headers: request.headers,
|
|
840
|
+
failureReason: lastError?.message ?? 'Unknown error',
|
|
841
|
+
attempts: attempt - 1,
|
|
842
|
+
dlqTimestamp: Date.now(),
|
|
843
|
+
createdAt: Date.now(),
|
|
844
|
+
});
|
|
845
|
+
// Record DLQ event
|
|
846
|
+
this.events._recordEvent({
|
|
847
|
+
type: 'dlq',
|
|
848
|
+
messageId,
|
|
849
|
+
timestamp: Date.now(),
|
|
850
|
+
url,
|
|
851
|
+
error: lastError?.message,
|
|
852
|
+
});
|
|
853
|
+
// Call failure callback
|
|
854
|
+
if (request.failureCallback) {
|
|
855
|
+
await fetch(request.failureCallback, {
|
|
856
|
+
method: 'POST',
|
|
857
|
+
headers: { 'Content-Type': 'application/json' },
|
|
858
|
+
body: JSON.stringify({
|
|
859
|
+
messageId,
|
|
860
|
+
url,
|
|
861
|
+
status: 'failed',
|
|
862
|
+
error: lastError?.message,
|
|
863
|
+
attempts: attempt - 1,
|
|
864
|
+
attemptHistory,
|
|
865
|
+
}),
|
|
866
|
+
}).catch((error) => {
|
|
867
|
+
this._handleCallbackFailure(messageId, request.failureCallback, error);
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
// Execute with delay if needed
|
|
872
|
+
if (delayMs > 0) {
|
|
873
|
+
setTimeout(executeDelivery, delayMs);
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
// Execute immediately (non-blocking)
|
|
877
|
+
executeDelivery().catch(() => {
|
|
878
|
+
// Errors handled within executeDelivery
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
return {
|
|
882
|
+
messageId,
|
|
883
|
+
url,
|
|
884
|
+
deduplicated: false,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Deliver a message to a single URL
|
|
889
|
+
*/
|
|
890
|
+
async _deliverMessage(url, bodyStr, request, messageId, attempt = 1) {
|
|
891
|
+
const headers = {
|
|
892
|
+
'Content-Type': request.contentType ?? 'application/json',
|
|
893
|
+
...request.headers,
|
|
894
|
+
'Upstash-Message-Id': messageId,
|
|
895
|
+
};
|
|
896
|
+
// Add Upstash-Retried header on retries (attempt > 1)
|
|
897
|
+
if (attempt > 1) {
|
|
898
|
+
headers['Upstash-Retried'] = String(attempt - 1);
|
|
899
|
+
}
|
|
900
|
+
// Add signature if signing key is configured
|
|
901
|
+
if (this.config.signingKey) {
|
|
902
|
+
headers['Upstash-Signature'] = await generateSignature(this.config.signingKey, bodyStr);
|
|
903
|
+
}
|
|
904
|
+
// Setup timeout with AbortController
|
|
905
|
+
let controller;
|
|
906
|
+
let timeoutId;
|
|
907
|
+
if (request.timeout) {
|
|
908
|
+
controller = new AbortController();
|
|
909
|
+
const timeoutMs = typeof request.timeout === 'number' ? request.timeout * 1000 : parseDuration(request.timeout);
|
|
910
|
+
timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
911
|
+
}
|
|
912
|
+
try {
|
|
913
|
+
const response = await fetch(url, {
|
|
914
|
+
method: request.method ?? 'POST',
|
|
915
|
+
headers,
|
|
916
|
+
body: bodyStr || undefined,
|
|
917
|
+
signal: controller?.signal,
|
|
918
|
+
});
|
|
919
|
+
if (timeoutId)
|
|
920
|
+
clearTimeout(timeoutId);
|
|
921
|
+
if (!response.ok) {
|
|
922
|
+
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
|
923
|
+
}
|
|
924
|
+
return response;
|
|
925
|
+
}
|
|
926
|
+
catch (error) {
|
|
927
|
+
if (timeoutId)
|
|
928
|
+
clearTimeout(timeoutId);
|
|
929
|
+
throw error;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Publish to a URL group by name (for topic-based fan-out)
|
|
934
|
+
*/
|
|
935
|
+
async _publishToUrlGroupByName(request, messageId, bodyStr, delayMs, groupName) {
|
|
936
|
+
const group = await this.urlGroups.get(groupName);
|
|
937
|
+
if (!group) {
|
|
938
|
+
throw new Error(`URL group '${groupName}' not found`);
|
|
939
|
+
}
|
|
940
|
+
const deliveries = group.endpoints.map((endpoint) => ({
|
|
941
|
+
url: endpoint.url,
|
|
942
|
+
status: 'pending',
|
|
943
|
+
timestamp: Date.now(),
|
|
944
|
+
}));
|
|
945
|
+
const executeDelivery = async () => {
|
|
946
|
+
const deliveryPromises = group.endpoints.map(async (endpoint, index) => {
|
|
947
|
+
try {
|
|
948
|
+
const response = await this._deliverMessage(endpoint.url, bodyStr, request, messageId, 1);
|
|
949
|
+
deliveries[index] = {
|
|
950
|
+
url: endpoint.url,
|
|
951
|
+
status: 'success',
|
|
952
|
+
statusCode: response.status,
|
|
953
|
+
timestamp: Date.now(),
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
catch (error) {
|
|
957
|
+
deliveries[index] = {
|
|
958
|
+
url: endpoint.url,
|
|
959
|
+
status: 'failed',
|
|
960
|
+
error: ensureError(error).message,
|
|
961
|
+
timestamp: Date.now(),
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
await Promise.all(deliveryPromises);
|
|
966
|
+
};
|
|
967
|
+
if (delayMs > 0) {
|
|
968
|
+
setTimeout(executeDelivery, delayMs);
|
|
969
|
+
}
|
|
970
|
+
else {
|
|
971
|
+
executeDelivery().catch(() => { });
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
messageId,
|
|
975
|
+
topic: groupName,
|
|
976
|
+
deliveries,
|
|
977
|
+
deduplicated: false,
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Publish to a URL group (fan-out)
|
|
982
|
+
*/
|
|
983
|
+
async _publishToUrlGroup(request, messageId, bodyStr, delayMs) {
|
|
984
|
+
const group = await this.urlGroups.get(request.urlGroup);
|
|
985
|
+
if (!group) {
|
|
986
|
+
throw new Error(`URL group '${request.urlGroup}' not found`);
|
|
987
|
+
}
|
|
988
|
+
const deliveries = group.endpoints.map((endpoint) => ({
|
|
989
|
+
url: endpoint.url,
|
|
990
|
+
status: 'pending',
|
|
991
|
+
timestamp: Date.now(),
|
|
992
|
+
}));
|
|
993
|
+
const executeDelivery = async () => {
|
|
994
|
+
const deliveryPromises = group.endpoints.map(async (endpoint, index) => {
|
|
995
|
+
try {
|
|
996
|
+
const response = await this._deliverMessage(endpoint.url, bodyStr, request, messageId);
|
|
997
|
+
deliveries[index] = {
|
|
998
|
+
url: endpoint.url,
|
|
999
|
+
status: 'success',
|
|
1000
|
+
statusCode: response.status,
|
|
1001
|
+
timestamp: Date.now(),
|
|
1002
|
+
};
|
|
1003
|
+
// Call callback for each successful delivery
|
|
1004
|
+
if (request.callback) {
|
|
1005
|
+
await fetch(request.callback, {
|
|
1006
|
+
method: 'POST',
|
|
1007
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1008
|
+
body: JSON.stringify({
|
|
1009
|
+
messageId,
|
|
1010
|
+
destinationUrl: endpoint.url,
|
|
1011
|
+
status: 'success',
|
|
1012
|
+
statusCode: response.status,
|
|
1013
|
+
}),
|
|
1014
|
+
}).catch((err) => {
|
|
1015
|
+
this._handleCallbackFailure(messageId, request.callback, err);
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
catch (error) {
|
|
1020
|
+
const errorMessage = ensureError(error).message;
|
|
1021
|
+
deliveries[index] = {
|
|
1022
|
+
url: endpoint.url,
|
|
1023
|
+
status: 'failed',
|
|
1024
|
+
error: errorMessage,
|
|
1025
|
+
timestamp: Date.now(),
|
|
1026
|
+
};
|
|
1027
|
+
// Call callback for each failed delivery
|
|
1028
|
+
if (request.callback) {
|
|
1029
|
+
await fetch(request.callback, {
|
|
1030
|
+
method: 'POST',
|
|
1031
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1032
|
+
body: JSON.stringify({
|
|
1033
|
+
messageId,
|
|
1034
|
+
destinationUrl: endpoint.url,
|
|
1035
|
+
status: 'failed',
|
|
1036
|
+
error: errorMessage,
|
|
1037
|
+
}),
|
|
1038
|
+
}).catch((err) => {
|
|
1039
|
+
this._handleCallbackFailure(messageId, request.callback, err);
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
await Promise.all(deliveryPromises);
|
|
1045
|
+
};
|
|
1046
|
+
if (delayMs > 0) {
|
|
1047
|
+
setTimeout(executeDelivery, delayMs);
|
|
1048
|
+
}
|
|
1049
|
+
else {
|
|
1050
|
+
executeDelivery().catch(() => { });
|
|
1051
|
+
}
|
|
1052
|
+
return {
|
|
1053
|
+
messageId,
|
|
1054
|
+
urlGroup: request.urlGroup,
|
|
1055
|
+
deliveries,
|
|
1056
|
+
deduplicated: false,
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Publish to a topic (pub/sub)
|
|
1061
|
+
*/
|
|
1062
|
+
async _publishToTopic(request, messageId, bodyStr, delayMs) {
|
|
1063
|
+
const subscriptions = await this.topics.listSubscriptions(request.topic);
|
|
1064
|
+
const deliveries = [];
|
|
1065
|
+
const executeDelivery = async () => {
|
|
1066
|
+
for (const sub of subscriptions) {
|
|
1067
|
+
if (sub.url) {
|
|
1068
|
+
// Direct URL subscription
|
|
1069
|
+
try {
|
|
1070
|
+
await this._deliverMessage(sub.url, bodyStr, request, messageId);
|
|
1071
|
+
deliveries.push({
|
|
1072
|
+
url: sub.url,
|
|
1073
|
+
status: 'success',
|
|
1074
|
+
timestamp: Date.now(),
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
catch (error) {
|
|
1078
|
+
deliveries.push({
|
|
1079
|
+
url: sub.url,
|
|
1080
|
+
status: 'failed',
|
|
1081
|
+
error: ensureError(error).message,
|
|
1082
|
+
timestamp: Date.now(),
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
else if (sub.urlGroup) {
|
|
1087
|
+
// URL group subscription
|
|
1088
|
+
const group = await this.urlGroups.get(sub.urlGroup);
|
|
1089
|
+
if (group) {
|
|
1090
|
+
for (const endpoint of group.endpoints) {
|
|
1091
|
+
try {
|
|
1092
|
+
await this._deliverMessage(endpoint.url, bodyStr, request, messageId);
|
|
1093
|
+
deliveries.push({
|
|
1094
|
+
url: endpoint.url,
|
|
1095
|
+
status: 'success',
|
|
1096
|
+
timestamp: Date.now(),
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
catch (error) {
|
|
1100
|
+
deliveries.push({
|
|
1101
|
+
url: endpoint.url,
|
|
1102
|
+
status: 'failed',
|
|
1103
|
+
error: ensureError(error).message,
|
|
1104
|
+
timestamp: Date.now(),
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
if (delayMs > 0) {
|
|
1113
|
+
setTimeout(executeDelivery, delayMs);
|
|
1114
|
+
}
|
|
1115
|
+
else {
|
|
1116
|
+
executeDelivery().catch(() => { });
|
|
1117
|
+
}
|
|
1118
|
+
return {
|
|
1119
|
+
messageId,
|
|
1120
|
+
topic: request.topic,
|
|
1121
|
+
deliveries,
|
|
1122
|
+
deduplicated: false,
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Publish JSON data to a URL (convenience method)
|
|
1127
|
+
*/
|
|
1128
|
+
async publishJSON(request) {
|
|
1129
|
+
return this.publish({
|
|
1130
|
+
...request,
|
|
1131
|
+
body: request.body,
|
|
1132
|
+
contentType: 'application/json',
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Publish multiple messages in a batch
|
|
1137
|
+
* Uses Promise.allSettled to return partial results even if some messages fail
|
|
1138
|
+
*/
|
|
1139
|
+
async batch(messages) {
|
|
1140
|
+
const results = await Promise.allSettled(messages.map((msg) => this.publish(msg)));
|
|
1141
|
+
const responses = results.map((result, index) => {
|
|
1142
|
+
if (result.status === 'fulfilled') {
|
|
1143
|
+
return result.value;
|
|
1144
|
+
}
|
|
1145
|
+
// For rejected promises, return a minimal response with error info
|
|
1146
|
+
return {
|
|
1147
|
+
messageId: `failed_${index}`,
|
|
1148
|
+
url: messages[index].url,
|
|
1149
|
+
deduplicated: false,
|
|
1150
|
+
error: result.reason?.message || 'Unknown error',
|
|
1151
|
+
};
|
|
1152
|
+
});
|
|
1153
|
+
return { responses };
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Enqueue a message (alias for publish)
|
|
1157
|
+
*/
|
|
1158
|
+
async enqueue(request) {
|
|
1159
|
+
return this.publish(request);
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Get a message by ID (stub - QStash doesn't actually support this well)
|
|
1163
|
+
*/
|
|
1164
|
+
async getMessage(_messageId) {
|
|
1165
|
+
// QStash doesn't have a direct getMessage API
|
|
1166
|
+
// This would need to be implemented via DLQ or logs
|
|
1167
|
+
return null;
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Publish to a URL group (fan-out) by topic name
|
|
1171
|
+
*/
|
|
1172
|
+
async publishToGroup(request) {
|
|
1173
|
+
const group = await this.urlGroups.get(request.topic);
|
|
1174
|
+
if (!group) {
|
|
1175
|
+
throw new Error(`URL group '${request.topic}' not found`);
|
|
1176
|
+
}
|
|
1177
|
+
const messageId = generateMessageId();
|
|
1178
|
+
const bodyStr = typeof request.body === 'object' ? JSON.stringify(request.body) : (request.body ?? '');
|
|
1179
|
+
const responses = group.endpoints.map((endpoint) => ({
|
|
1180
|
+
messageId,
|
|
1181
|
+
url: endpoint.url,
|
|
1182
|
+
deduplicated: false,
|
|
1183
|
+
}));
|
|
1184
|
+
// Calculate delay
|
|
1185
|
+
const delayMs = request.delay ? parseDuration(request.delay) : 0;
|
|
1186
|
+
const executeDelivery = async () => {
|
|
1187
|
+
await Promise.all(group.endpoints.map(async (endpoint) => {
|
|
1188
|
+
try {
|
|
1189
|
+
await this._deliverMessage(endpoint.url, bodyStr, request, messageId, 1);
|
|
1190
|
+
}
|
|
1191
|
+
catch {
|
|
1192
|
+
// Errors logged elsewhere
|
|
1193
|
+
}
|
|
1194
|
+
}));
|
|
1195
|
+
};
|
|
1196
|
+
if (delayMs > 0) {
|
|
1197
|
+
setTimeout(executeDelivery, delayMs);
|
|
1198
|
+
}
|
|
1199
|
+
else {
|
|
1200
|
+
executeDelivery().catch(() => { });
|
|
1201
|
+
}
|
|
1202
|
+
return { responses };
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
// ============================================================================
|
|
1206
|
+
// RECEIVER
|
|
1207
|
+
// ============================================================================
|
|
1208
|
+
export class Receiver {
|
|
1209
|
+
currentSigningKey;
|
|
1210
|
+
nextSigningKey;
|
|
1211
|
+
constructor(config) {
|
|
1212
|
+
this.currentSigningKey = config.currentSigningKey;
|
|
1213
|
+
this.nextSigningKey = config.nextSigningKey;
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Verify a QStash webhook signature
|
|
1217
|
+
*/
|
|
1218
|
+
async verify(request) {
|
|
1219
|
+
const { signature, body, clockTolerance = 0 } = request;
|
|
1220
|
+
// QStash signature format: t=<timestamp>,v1=<signature>
|
|
1221
|
+
const parts = signature.split(',');
|
|
1222
|
+
const timestampPart = parts.find((p) => p.startsWith('t='));
|
|
1223
|
+
const signaturePart = parts.find((p) => p.startsWith('v1='));
|
|
1224
|
+
if (!timestampPart || !signaturePart) {
|
|
1225
|
+
return false;
|
|
1226
|
+
}
|
|
1227
|
+
const timestamp = parseInt(timestampPart.slice(2), 10);
|
|
1228
|
+
const expectedSignature = signaturePart.slice(3);
|
|
1229
|
+
// Check timestamp tolerance
|
|
1230
|
+
if (clockTolerance > 0) {
|
|
1231
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1232
|
+
if (Math.abs(now - timestamp) > clockTolerance) {
|
|
1233
|
+
return false;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
// Verify signature with both keys
|
|
1237
|
+
const payload = `${timestamp}.${body}`;
|
|
1238
|
+
for (const key of [this.currentSigningKey, this.nextSigningKey]) {
|
|
1239
|
+
if (!key)
|
|
1240
|
+
continue;
|
|
1241
|
+
try {
|
|
1242
|
+
const cryptoKey = await crypto.subtle.importKey('raw', new TextEncoder().encode(key), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
1243
|
+
const signatureBuffer = await crypto.subtle.sign('HMAC', cryptoKey, new TextEncoder().encode(payload));
|
|
1244
|
+
const computedSignature = Array.from(new Uint8Array(signatureBuffer))
|
|
1245
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
1246
|
+
.join('');
|
|
1247
|
+
if (computedSignature === expectedSignature) {
|
|
1248
|
+
return true;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
catch {
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return false;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
// ============================================================================
|
|
1259
|
+
// EXPORTS
|
|
1260
|
+
// ============================================================================
|
|
1261
|
+
export default Client;
|
|
1262
|
+
export { parseCronExpression, getNextRunTime };
|
|
1263
|
+
//# sourceMappingURL=index.js.map
|