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,1035 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Artifact Serve Snippet
|
|
3
|
+
*
|
|
4
|
+
* Serves artifacts from R2 Iceberg with SWR caching.
|
|
5
|
+
* Handles path parsing, extension mapping, and cache control.
|
|
6
|
+
*
|
|
7
|
+
* @module snippets/artifacts-serve
|
|
8
|
+
* @see docs/plans/2026-01-10-artifact-storage-design.md
|
|
9
|
+
*/
|
|
10
|
+
import { noopMetrics } from './artifacts-ingest';
|
|
11
|
+
// Re-export metrics types for external use
|
|
12
|
+
export { noopMetrics, createDefaultMetrics } from './artifacts-ingest';
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Constants
|
|
15
|
+
// ============================================================================
|
|
16
|
+
/**
|
|
17
|
+
* Supported file extensions and their compound variants.
|
|
18
|
+
* Order matters: compound extensions must be checked first.
|
|
19
|
+
*/
|
|
20
|
+
const COMPOUND_EXTENSIONS = ['mdast.json', 'hast.json', 'estree.json', 'd.ts'];
|
|
21
|
+
const SIMPLE_EXTENSIONS = ['md', 'mdx', 'html', 'js', 'mjs', 'css', 'json'];
|
|
22
|
+
const ALL_EXTENSIONS = [...COMPOUND_EXTENSIONS, ...SIMPLE_EXTENSIONS];
|
|
23
|
+
/**
|
|
24
|
+
* Extension to database column mapping.
|
|
25
|
+
*/
|
|
26
|
+
const EXTENSION_TO_COLUMN = {
|
|
27
|
+
'md': 'markdown',
|
|
28
|
+
'mdx': 'mdx',
|
|
29
|
+
'html': 'html',
|
|
30
|
+
'js': 'esm',
|
|
31
|
+
'mjs': 'esm',
|
|
32
|
+
'd.ts': 'dts',
|
|
33
|
+
'css': 'css',
|
|
34
|
+
'json': 'frontmatter',
|
|
35
|
+
'mdast.json': 'mdast',
|
|
36
|
+
'hast.json': 'hast',
|
|
37
|
+
'estree.json': 'estree',
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Extension to Content-Type mapping.
|
|
41
|
+
*/
|
|
42
|
+
const EXTENSION_TO_CONTENT_TYPE = {
|
|
43
|
+
'md': 'text/markdown; charset=utf-8',
|
|
44
|
+
'mdx': 'text/mdx; charset=utf-8',
|
|
45
|
+
'html': 'text/html; charset=utf-8',
|
|
46
|
+
'js': 'application/javascript; charset=utf-8',
|
|
47
|
+
'mjs': 'application/javascript; charset=utf-8',
|
|
48
|
+
'd.ts': 'application/typescript; charset=utf-8',
|
|
49
|
+
'css': 'text/css; charset=utf-8',
|
|
50
|
+
'json': 'application/json',
|
|
51
|
+
'mdast.json': 'application/json',
|
|
52
|
+
'hast.json': 'application/json',
|
|
53
|
+
'estree.json': 'application/json',
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Default tenant config used when no config is provided.
|
|
57
|
+
*/
|
|
58
|
+
const DEFAULT_CONFIG = {
|
|
59
|
+
ns: 'default',
|
|
60
|
+
cache: {
|
|
61
|
+
defaultMaxAge: 300,
|
|
62
|
+
defaultStaleWhileRevalidate: 60,
|
|
63
|
+
minMaxAge: 10,
|
|
64
|
+
allowFreshBypass: true,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// Path Parsing
|
|
69
|
+
// ============================================================================
|
|
70
|
+
/**
|
|
71
|
+
* Parse URL path into artifact components.
|
|
72
|
+
*
|
|
73
|
+
* URL Pattern: /$.content/{ns}/{type}/{id}.{ext}
|
|
74
|
+
*
|
|
75
|
+
* Supports:
|
|
76
|
+
* - Simple extensions: .md, .html, .js, .css, .json
|
|
77
|
+
* - Compound extensions: .d.ts, .mdast.json, .hast.json, .estree.json
|
|
78
|
+
* - Nested namespaces: myorg.app.do
|
|
79
|
+
* - URL-encoded characters in id
|
|
80
|
+
*
|
|
81
|
+
* @param url - The request URL to parse
|
|
82
|
+
* @returns Parsed path components or null if invalid
|
|
83
|
+
*/
|
|
84
|
+
export function parsePath(url) {
|
|
85
|
+
// Remove /$.content/ prefix and get the path
|
|
86
|
+
const pathname = decodeURIComponent(url.pathname);
|
|
87
|
+
const prefix = '/$.content/';
|
|
88
|
+
if (!pathname.startsWith(prefix)) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const path = pathname.slice(prefix.length);
|
|
92
|
+
const parts = path.split('/');
|
|
93
|
+
// Need at least 3 parts: ns, type, id.ext
|
|
94
|
+
if (parts.length < 3) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const ns = parts[0];
|
|
98
|
+
const type = parts[1];
|
|
99
|
+
const filenamePart = parts.slice(2).join('/');
|
|
100
|
+
// Try compound extensions first (order matters)
|
|
101
|
+
let ext = null;
|
|
102
|
+
let id = null;
|
|
103
|
+
for (const compoundExt of COMPOUND_EXTENSIONS) {
|
|
104
|
+
const suffix = '.' + compoundExt;
|
|
105
|
+
if (filenamePart.endsWith(suffix)) {
|
|
106
|
+
ext = compoundExt;
|
|
107
|
+
id = filenamePart.slice(0, -suffix.length);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// If no compound extension matched, try simple extensions
|
|
112
|
+
if (!ext) {
|
|
113
|
+
const lastDotIdx = filenamePart.lastIndexOf('.');
|
|
114
|
+
if (lastDotIdx === -1 || lastDotIdx === filenamePart.length - 1) {
|
|
115
|
+
return null; // No extension
|
|
116
|
+
}
|
|
117
|
+
const potentialExt = filenamePart.slice(lastDotIdx + 1).toLowerCase();
|
|
118
|
+
if (!SIMPLE_EXTENSIONS.includes(potentialExt)) {
|
|
119
|
+
return null; // Unknown extension
|
|
120
|
+
}
|
|
121
|
+
ext = potentialExt;
|
|
122
|
+
id = filenamePart.slice(0, lastDotIdx);
|
|
123
|
+
}
|
|
124
|
+
// Validate required parts
|
|
125
|
+
if (!ns || !type || !id) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return { ns, type, id, ext };
|
|
129
|
+
}
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// Extension Mapping
|
|
132
|
+
// ============================================================================
|
|
133
|
+
/**
|
|
134
|
+
* Map file extension to database column name.
|
|
135
|
+
*
|
|
136
|
+
* @param ext - File extension (without leading dot)
|
|
137
|
+
* @returns Database column name
|
|
138
|
+
* @throws Error if extension is unknown
|
|
139
|
+
*/
|
|
140
|
+
export function getColumnForExtension(ext) {
|
|
141
|
+
const normalizedExt = ext.toLowerCase();
|
|
142
|
+
const column = EXTENSION_TO_COLUMN[normalizedExt];
|
|
143
|
+
if (!column) {
|
|
144
|
+
throw new Error(`Unsupported extension: ${ext}`);
|
|
145
|
+
}
|
|
146
|
+
return column;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Get Content-Type header for file extension.
|
|
150
|
+
*
|
|
151
|
+
* @param ext - File extension (without leading dot)
|
|
152
|
+
* @returns Content-Type header value
|
|
153
|
+
*/
|
|
154
|
+
export function getContentType(ext) {
|
|
155
|
+
const normalizedExt = ext.toLowerCase();
|
|
156
|
+
return EXTENSION_TO_CONTENT_TYPE[normalizedExt] ?? 'application/octet-stream';
|
|
157
|
+
}
|
|
158
|
+
// ============================================================================
|
|
159
|
+
// Cache Control
|
|
160
|
+
// ============================================================================
|
|
161
|
+
/**
|
|
162
|
+
* Build Cache-Control header from config and overrides.
|
|
163
|
+
*
|
|
164
|
+
* @param config - Tenant configuration
|
|
165
|
+
* @param overrides - Optional overrides for max-age, visibility, fresh bypass
|
|
166
|
+
* @returns Cache-Control header value
|
|
167
|
+
*/
|
|
168
|
+
export function buildCacheControl(config, overrides) {
|
|
169
|
+
// Fresh bypass returns no-store immediately
|
|
170
|
+
if (overrides?.fresh) {
|
|
171
|
+
return 'no-store';
|
|
172
|
+
}
|
|
173
|
+
// Determine max-age
|
|
174
|
+
let maxAge = overrides?.maxAge ?? config.cache.defaultMaxAge;
|
|
175
|
+
// Enforce minimum
|
|
176
|
+
if (maxAge < config.cache.minMaxAge) {
|
|
177
|
+
maxAge = config.cache.minMaxAge;
|
|
178
|
+
}
|
|
179
|
+
// Build header parts
|
|
180
|
+
const visibility = overrides?.visibility ?? 'public';
|
|
181
|
+
const parts = [`${visibility}`, `max-age=${maxAge}`];
|
|
182
|
+
// Add stale-while-revalidate if > 0
|
|
183
|
+
const swr = config.cache.defaultStaleWhileRevalidate;
|
|
184
|
+
if (swr > 0) {
|
|
185
|
+
parts.push(`stale-while-revalidate=${swr}`);
|
|
186
|
+
}
|
|
187
|
+
return parts.join(', ');
|
|
188
|
+
}
|
|
189
|
+
// ============================================================================
|
|
190
|
+
// Auth Context Extraction
|
|
191
|
+
// ============================================================================
|
|
192
|
+
/**
|
|
193
|
+
* Parse a base64url-encoded JWT payload.
|
|
194
|
+
* Does NOT verify signature - verification should be done by auth middleware.
|
|
195
|
+
*
|
|
196
|
+
* @param token - Bearer token string
|
|
197
|
+
* @returns Decoded payload or null if invalid
|
|
198
|
+
*/
|
|
199
|
+
function parseJwtPayload(token) {
|
|
200
|
+
try {
|
|
201
|
+
const parts = token.split('.');
|
|
202
|
+
if (parts.length !== 3)
|
|
203
|
+
return null;
|
|
204
|
+
const payload = parts[1];
|
|
205
|
+
// Base64url to base64 conversion
|
|
206
|
+
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
|
|
207
|
+
const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
|
|
208
|
+
const decoded = atob(padded);
|
|
209
|
+
return JSON.parse(decoded);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Check if a JWT token has expired.
|
|
217
|
+
*
|
|
218
|
+
* @param payload - Parsed JWT payload
|
|
219
|
+
* @returns true if expired, false otherwise
|
|
220
|
+
*/
|
|
221
|
+
function isTokenExpired(payload) {
|
|
222
|
+
const exp = payload.exp;
|
|
223
|
+
if (typeof exp !== 'number')
|
|
224
|
+
return false;
|
|
225
|
+
// exp is in seconds since epoch
|
|
226
|
+
return Date.now() / 1000 > exp;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Extract auth context from request headers.
|
|
230
|
+
*
|
|
231
|
+
* Extracts:
|
|
232
|
+
* - userId from JWT payload or X-User-Id header
|
|
233
|
+
* - orgId from X-Org-Id header or JWT payload
|
|
234
|
+
* - roles from JWT payload
|
|
235
|
+
*
|
|
236
|
+
* Supports both:
|
|
237
|
+
* - JWT tokens with claims
|
|
238
|
+
* - Non-JWT tokens with X-User-Id/X-Org-Id headers for testing
|
|
239
|
+
*
|
|
240
|
+
* @param request - The HTTP request
|
|
241
|
+
* @returns AuthContext or undefined if no auth present
|
|
242
|
+
*/
|
|
243
|
+
export function extractAuthContext(request) {
|
|
244
|
+
const authHeader = request.headers.get('Authorization');
|
|
245
|
+
// Check for X-User-Id and X-Org-Id headers (can work with or without auth)
|
|
246
|
+
const userIdHeader = request.headers.get('X-User-Id');
|
|
247
|
+
const orgIdHeader = request.headers.get('X-Org-Id');
|
|
248
|
+
// If there's no auth header and no X- headers, return undefined
|
|
249
|
+
if (!authHeader && !userIdHeader && !orgIdHeader) {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
const authContext = {};
|
|
253
|
+
// Try to extract from JWT if present
|
|
254
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
255
|
+
const token = authHeader.slice(7);
|
|
256
|
+
const payload = parseJwtPayload(token);
|
|
257
|
+
if (payload && !isTokenExpired(payload)) {
|
|
258
|
+
// Extract userId from JWT payload
|
|
259
|
+
if (typeof payload.userId === 'string') {
|
|
260
|
+
authContext.userId = payload.userId;
|
|
261
|
+
}
|
|
262
|
+
// Extract orgId from JWT payload
|
|
263
|
+
if (typeof payload.orgId === 'string') {
|
|
264
|
+
authContext.orgId = payload.orgId;
|
|
265
|
+
}
|
|
266
|
+
// Extract roles from JWT payload
|
|
267
|
+
if (Array.isArray(payload.roles)) {
|
|
268
|
+
authContext.roles = payload.roles.filter((r) => typeof r === 'string');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// Override/supplement with X- headers (headers take precedence)
|
|
273
|
+
if (userIdHeader) {
|
|
274
|
+
authContext.userId = userIdHeader;
|
|
275
|
+
}
|
|
276
|
+
if (orgIdHeader) {
|
|
277
|
+
authContext.orgId = orgIdHeader;
|
|
278
|
+
}
|
|
279
|
+
// If no properties were extracted, return undefined
|
|
280
|
+
if (!authContext.userId && !authContext.orgId && !authContext.roles) {
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
return authContext;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Determine the visibility level to request based on auth context.
|
|
287
|
+
*
|
|
288
|
+
* Rules:
|
|
289
|
+
* - No auth context: 'public'
|
|
290
|
+
* - Auth with orgId: 'org'
|
|
291
|
+
* - Auth with userId only: 'user'
|
|
292
|
+
* - For unlisted artifacts: return undefined (accessible by direct ID only)
|
|
293
|
+
*
|
|
294
|
+
* @param authContext - The extracted auth context
|
|
295
|
+
* @param isUnlistedAccess - Whether this is accessing an unlisted artifact
|
|
296
|
+
* @returns Visibility level or undefined for unlisted access
|
|
297
|
+
*/
|
|
298
|
+
export function determineVisibility(authContext, isUnlistedAccess = false) {
|
|
299
|
+
// Unlisted artifacts don't filter by visibility - they're accessible by direct ID
|
|
300
|
+
if (isUnlistedAccess)
|
|
301
|
+
return undefined;
|
|
302
|
+
if (!authContext)
|
|
303
|
+
return 'public';
|
|
304
|
+
// If orgId is present, use org visibility
|
|
305
|
+
if (authContext.orgId)
|
|
306
|
+
return 'org';
|
|
307
|
+
// If userId is present, use user visibility
|
|
308
|
+
if (authContext.userId)
|
|
309
|
+
return 'user';
|
|
310
|
+
// Default to public
|
|
311
|
+
return 'public';
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Get the columns to request based on file extension.
|
|
315
|
+
* Always includes 'id' for record identification.
|
|
316
|
+
*
|
|
317
|
+
* @param ext - File extension
|
|
318
|
+
* @returns Array of column names to request
|
|
319
|
+
*/
|
|
320
|
+
export function getColumnsForExtension(ext) {
|
|
321
|
+
const column = getColumnForExtension(ext);
|
|
322
|
+
return ['id', column];
|
|
323
|
+
}
|
|
324
|
+
// ============================================================================
|
|
325
|
+
// Hash Utilities
|
|
326
|
+
// ============================================================================
|
|
327
|
+
/**
|
|
328
|
+
* Generate a simple hash for ETag from content.
|
|
329
|
+
*/
|
|
330
|
+
async function generateETag(content) {
|
|
331
|
+
const text = typeof content === 'string' ? content : JSON.stringify(content);
|
|
332
|
+
const encoder = new TextEncoder();
|
|
333
|
+
const data = encoder.encode(text);
|
|
334
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
335
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
336
|
+
const hashHex = hashArray.slice(0, 8).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
337
|
+
return `"${hashHex}"`;
|
|
338
|
+
}
|
|
339
|
+
// ============================================================================
|
|
340
|
+
// Response Helpers
|
|
341
|
+
// ============================================================================
|
|
342
|
+
function jsonError(message, status, cacheControl) {
|
|
343
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
344
|
+
if (cacheControl) {
|
|
345
|
+
headers['Cache-Control'] = cacheControl;
|
|
346
|
+
}
|
|
347
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
348
|
+
status,
|
|
349
|
+
headers,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
function methodNotAllowed() {
|
|
353
|
+
return new Response(JSON.stringify({ error: 'Method not allowed' }), {
|
|
354
|
+
status: 405,
|
|
355
|
+
headers: {
|
|
356
|
+
'Content-Type': 'application/json',
|
|
357
|
+
'Allow': 'GET, HEAD',
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
function optionsResponse() {
|
|
362
|
+
return new Response(null, {
|
|
363
|
+
status: 204,
|
|
364
|
+
headers: {
|
|
365
|
+
'Allow': 'GET, HEAD, OPTIONS',
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Check if a cached response is stale based on its Date header.
|
|
371
|
+
*/
|
|
372
|
+
function getResponseAge(response) {
|
|
373
|
+
const dateHeader = response.headers.get('Date');
|
|
374
|
+
if (!dateHeader)
|
|
375
|
+
return 0;
|
|
376
|
+
const cachedAt = new Date(dateHeader).getTime();
|
|
377
|
+
return Math.floor((Date.now() - cachedAt) / 1000);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Check if response is stale (past max-age but potentially within SWR window).
|
|
381
|
+
*/
|
|
382
|
+
function isResponseStale(response) {
|
|
383
|
+
const cacheControl = response.headers.get('Cache-Control');
|
|
384
|
+
if (!cacheControl)
|
|
385
|
+
return false;
|
|
386
|
+
const maxAgeMatch = cacheControl.match(/max-age=(\d+)/);
|
|
387
|
+
if (!maxAgeMatch)
|
|
388
|
+
return false;
|
|
389
|
+
const maxAge = parseInt(maxAgeMatch[1]);
|
|
390
|
+
const age = getResponseAge(response);
|
|
391
|
+
return age > maxAge;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Revalidate cache entry in the background.
|
|
395
|
+
*/
|
|
396
|
+
async function revalidateCache(cacheKey, parsed, config, reader, overrides) {
|
|
397
|
+
try {
|
|
398
|
+
const record = await reader.getRecord({
|
|
399
|
+
table: 'do_resources',
|
|
400
|
+
partition: { ns: parsed.ns, type: parsed.type },
|
|
401
|
+
id: parsed.id,
|
|
402
|
+
});
|
|
403
|
+
if (!record)
|
|
404
|
+
return;
|
|
405
|
+
const column = getColumnForExtension(parsed.ext);
|
|
406
|
+
const content = record[column];
|
|
407
|
+
if (content === null || content === undefined)
|
|
408
|
+
return;
|
|
409
|
+
const isJson = parsed.ext.endsWith('.json') || parsed.ext === 'json';
|
|
410
|
+
const body = isJson ? JSON.stringify(content) : String(content);
|
|
411
|
+
const etag = await generateETag(body);
|
|
412
|
+
const contentType = getContentType(parsed.ext);
|
|
413
|
+
const cacheControl = buildCacheControl(config, overrides);
|
|
414
|
+
const contentLength = new TextEncoder().encode(body).length;
|
|
415
|
+
const headers = new Headers({
|
|
416
|
+
'Content-Type': contentType,
|
|
417
|
+
'Cache-Control': cacheControl,
|
|
418
|
+
'ETag': etag,
|
|
419
|
+
'X-Artifact-Source': 'cache',
|
|
420
|
+
'Vary': 'Accept-Encoding',
|
|
421
|
+
'Content-Length': String(contentLength),
|
|
422
|
+
'Date': new Date().toUTCString(),
|
|
423
|
+
});
|
|
424
|
+
const response = new Response(body, { status: 200, headers });
|
|
425
|
+
await caches.default.put(cacheKey, response);
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
// Ignore revalidation errors
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// ============================================================================
|
|
432
|
+
// Integration Test Handler
|
|
433
|
+
// ============================================================================
|
|
434
|
+
/**
|
|
435
|
+
* Check if options are for integration test mode.
|
|
436
|
+
*/
|
|
437
|
+
function isIntegrationOptions(options) {
|
|
438
|
+
return 'cache' in options && typeof options.cache === 'object' && 'match' in options.cache;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Handle artifact serve request - overloaded for integration tests.
|
|
442
|
+
* This version accepts (request, env, ctx, options) signature.
|
|
443
|
+
*/
|
|
444
|
+
export async function handleServe(request, envOrCtx, ctxOrOptions, maybeOptions) {
|
|
445
|
+
// Determine which signature was used
|
|
446
|
+
const isIntegrationMode = maybeOptions !== undefined && isIntegrationOptions(maybeOptions);
|
|
447
|
+
if (isIntegrationMode) {
|
|
448
|
+
// Integration test mode: (request, env, ctx, options)
|
|
449
|
+
const ctx = ctxOrOptions;
|
|
450
|
+
const options = maybeOptions;
|
|
451
|
+
return handleServeIntegration(request, ctx, options);
|
|
452
|
+
}
|
|
453
|
+
// Original mode: (request, ctx, options)
|
|
454
|
+
const ctx = envOrCtx;
|
|
455
|
+
const options = ctxOrOptions;
|
|
456
|
+
return handleServeOriginal(request, ctx, options);
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Original handleServe implementation returning Response.
|
|
460
|
+
*/
|
|
461
|
+
async function handleServeOriginal(request, ctx, options) {
|
|
462
|
+
const method = request.method.toUpperCase();
|
|
463
|
+
const metrics = options.metrics ?? noopMetrics;
|
|
464
|
+
const startTime = Date.now();
|
|
465
|
+
// Handle OPTIONS for CORS preflight
|
|
466
|
+
if (method === 'OPTIONS') {
|
|
467
|
+
return optionsResponse();
|
|
468
|
+
}
|
|
469
|
+
// Only allow GET and HEAD
|
|
470
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
471
|
+
return methodNotAllowed();
|
|
472
|
+
}
|
|
473
|
+
const url = new URL(request.url);
|
|
474
|
+
// Parse path
|
|
475
|
+
const parsed = parsePath(url);
|
|
476
|
+
if (!parsed) {
|
|
477
|
+
return jsonError('Invalid path format', 400);
|
|
478
|
+
}
|
|
479
|
+
const { ns, type, id, ext } = parsed;
|
|
480
|
+
// Load config (use provided, load from loader, or default)
|
|
481
|
+
let config;
|
|
482
|
+
if (options.config) {
|
|
483
|
+
config = options.config;
|
|
484
|
+
}
|
|
485
|
+
else if (options.configLoader) {
|
|
486
|
+
try {
|
|
487
|
+
config = await options.configLoader(ns);
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
// Fall back to default config on loader error
|
|
491
|
+
config = DEFAULT_CONFIG;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
config = DEFAULT_CONFIG;
|
|
496
|
+
}
|
|
497
|
+
// Parse query params
|
|
498
|
+
const maxAgeParam = url.searchParams.get('max_age');
|
|
499
|
+
const freshParam = url.searchParams.get('fresh');
|
|
500
|
+
// Determine cache overrides
|
|
501
|
+
const overrides = {};
|
|
502
|
+
// Handle fresh bypass
|
|
503
|
+
if (freshParam === 'true' && config.cache.allowFreshBypass) {
|
|
504
|
+
overrides.fresh = true;
|
|
505
|
+
}
|
|
506
|
+
// Handle max_age override
|
|
507
|
+
if (maxAgeParam && !overrides.fresh) {
|
|
508
|
+
const parsedMaxAge = parseInt(maxAgeParam);
|
|
509
|
+
if (!isNaN(parsedMaxAge)) {
|
|
510
|
+
overrides.maxAge = Math.max(parsedMaxAge, 0);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// Build cache key (without query params)
|
|
514
|
+
const cacheKey = `${url.origin}${url.pathname}`;
|
|
515
|
+
// Try cache first (unless fresh bypass)
|
|
516
|
+
if (!overrides.fresh) {
|
|
517
|
+
try {
|
|
518
|
+
const cachedResponse = await caches.default.match(cacheKey);
|
|
519
|
+
if (cachedResponse) {
|
|
520
|
+
const age = getResponseAge(cachedResponse);
|
|
521
|
+
const isStale = isResponseStale(cachedResponse);
|
|
522
|
+
const responseHeaders = new Headers(cachedResponse.headers);
|
|
523
|
+
responseHeaders.set('X-Artifact-Age', String(age));
|
|
524
|
+
responseHeaders.set('X-Artifact-Source', 'cache');
|
|
525
|
+
const cachedETag = cachedResponse.headers.get('ETag');
|
|
526
|
+
const ifNoneMatch = request.headers.get('If-None-Match');
|
|
527
|
+
if (cachedETag && ifNoneMatch === cachedETag) {
|
|
528
|
+
return new Response(null, {
|
|
529
|
+
status: 304,
|
|
530
|
+
headers: {
|
|
531
|
+
'ETag': cachedETag,
|
|
532
|
+
'Cache-Control': cachedResponse.headers.get('Cache-Control') || buildCacheControl(config, overrides),
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
if (isStale) {
|
|
537
|
+
ctx.waitUntil(revalidateCache(cacheKey, parsed, config, options.reader, overrides));
|
|
538
|
+
// Record SWR revalidation
|
|
539
|
+
metrics.recordMetric('serve.swr_revalidation', 1, { ns, type, ext });
|
|
540
|
+
metrics.recordMetric('serve.cache_hit', 1, { ns, type, ext, stale: 'true' });
|
|
541
|
+
metrics.recordLatency('serve.latency', Date.now() - startTime, { ns, type, ext, source: 'cache' });
|
|
542
|
+
return new Response(method === 'HEAD' ? null : cachedResponse.body, {
|
|
543
|
+
status: cachedResponse.status,
|
|
544
|
+
headers: responseHeaders,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
// Fresh cache hit
|
|
548
|
+
metrics.recordMetric('serve.cache_hit', 1, { ns, type, ext, stale: 'false' });
|
|
549
|
+
metrics.recordLatency('serve.latency', Date.now() - startTime, { ns, type, ext, source: 'cache' });
|
|
550
|
+
return new Response(method === 'HEAD' ? null : cachedResponse.body, {
|
|
551
|
+
status: cachedResponse.status,
|
|
552
|
+
headers: responseHeaders,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
// Cache API error, continue to fetch from origin
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
// Fetch from IcebergReader
|
|
561
|
+
let record;
|
|
562
|
+
try {
|
|
563
|
+
record = await options.reader.getRecord({
|
|
564
|
+
table: 'do_resources',
|
|
565
|
+
partition: { ns, type },
|
|
566
|
+
id,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
if (error instanceof Error && error.message.includes('connection')) {
|
|
571
|
+
return jsonError('Storage backend unavailable', 502);
|
|
572
|
+
}
|
|
573
|
+
return jsonError('Internal server error', 500);
|
|
574
|
+
}
|
|
575
|
+
// Handle not found
|
|
576
|
+
if (!record) {
|
|
577
|
+
return jsonError('Artifact not found', 404);
|
|
578
|
+
}
|
|
579
|
+
// Get content from the appropriate column
|
|
580
|
+
const column = getColumnForExtension(ext);
|
|
581
|
+
const content = record[column];
|
|
582
|
+
// Handle missing column content
|
|
583
|
+
if (content === null || content === undefined) {
|
|
584
|
+
return jsonError('Artifact format not available', 404);
|
|
585
|
+
}
|
|
586
|
+
// Prepare response body
|
|
587
|
+
const isJson = ext.endsWith('.json') || ext === 'json';
|
|
588
|
+
const body = isJson ? JSON.stringify(content) : String(content);
|
|
589
|
+
// Generate ETag
|
|
590
|
+
const etag = await generateETag(body);
|
|
591
|
+
// Check If-None-Match
|
|
592
|
+
const ifNoneMatch = request.headers.get('If-None-Match');
|
|
593
|
+
if (ifNoneMatch === etag) {
|
|
594
|
+
return new Response(null, {
|
|
595
|
+
status: 304,
|
|
596
|
+
headers: {
|
|
597
|
+
'ETag': etag,
|
|
598
|
+
'Cache-Control': buildCacheControl(config, overrides),
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
// Build response headers
|
|
603
|
+
const contentType = getContentType(ext);
|
|
604
|
+
const cacheControl = buildCacheControl(config, overrides);
|
|
605
|
+
const contentLength = new TextEncoder().encode(body).length;
|
|
606
|
+
const headers = new Headers({
|
|
607
|
+
'Content-Type': contentType,
|
|
608
|
+
'Cache-Control': cacheControl,
|
|
609
|
+
'ETag': etag,
|
|
610
|
+
'X-Artifact-Source': 'parquet',
|
|
611
|
+
'Vary': 'Accept-Encoding',
|
|
612
|
+
'Content-Length': String(contentLength),
|
|
613
|
+
'Date': new Date().toUTCString(),
|
|
614
|
+
});
|
|
615
|
+
const response = new Response(method === 'HEAD' ? null : body, {
|
|
616
|
+
status: 200,
|
|
617
|
+
headers,
|
|
618
|
+
});
|
|
619
|
+
// Cache the response
|
|
620
|
+
if (!overrides.fresh) {
|
|
621
|
+
try {
|
|
622
|
+
const putResult = caches.default.put(cacheKey, response.clone());
|
|
623
|
+
if (putResult && typeof putResult.then === 'function') {
|
|
624
|
+
ctx.waitUntil(putResult.catch(() => { }));
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
// Ignore cache put errors
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// Record cache miss and latency for parquet fetch
|
|
632
|
+
metrics.recordMetric('serve.cache_miss', 1, { ns, type, ext });
|
|
633
|
+
metrics.recordLatency('serve.latency', Date.now() - startTime, { ns, type, ext, source: 'parquet' });
|
|
634
|
+
return response;
|
|
635
|
+
}
|
|
636
|
+
// ============================================================================
|
|
637
|
+
// In-Flight Request Management with TTL
|
|
638
|
+
// ============================================================================
|
|
639
|
+
/**
|
|
640
|
+
* TTL for in-flight request entries in milliseconds.
|
|
641
|
+
* Entries older than this are considered stale and cleaned up.
|
|
642
|
+
*/
|
|
643
|
+
const IN_FLIGHT_TTL_MS = 30_000;
|
|
644
|
+
/**
|
|
645
|
+
* Maximum number of entries in the in-flight requests map.
|
|
646
|
+
* Prevents unbounded growth under high load.
|
|
647
|
+
*/
|
|
648
|
+
const IN_FLIGHT_MAX_SIZE = 10_000;
|
|
649
|
+
/**
|
|
650
|
+
* In-flight requests map for request coalescing.
|
|
651
|
+
* Uses TTL-based cleanup to prevent memory leaks from hung requests.
|
|
652
|
+
*/
|
|
653
|
+
const inFlightRequests = new Map();
|
|
654
|
+
/**
|
|
655
|
+
* Cleans up stale entries from the in-flight requests map.
|
|
656
|
+
* Removes entries older than IN_FLIGHT_TTL_MS.
|
|
657
|
+
*/
|
|
658
|
+
function cleanupStaleEntries() {
|
|
659
|
+
const now = Date.now();
|
|
660
|
+
for (const [key, entry] of inFlightRequests) {
|
|
661
|
+
if (now - entry.createdAt > IN_FLIGHT_TTL_MS) {
|
|
662
|
+
inFlightRequests.delete(key);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Gets the current count of in-flight requests.
|
|
668
|
+
* Exported for testing to verify cleanup behavior.
|
|
669
|
+
*/
|
|
670
|
+
export function getInFlightRequestCount() {
|
|
671
|
+
return inFlightRequests.size;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Clears all in-flight requests.
|
|
675
|
+
* Exported for testing to reset state between tests.
|
|
676
|
+
*/
|
|
677
|
+
export function clearInFlightRequests() {
|
|
678
|
+
inFlightRequests.clear();
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Integration test handleServe implementation returning ServeResult.
|
|
682
|
+
* Uses request coalescing to ensure concurrent requests for the same resource
|
|
683
|
+
* share a single reader call.
|
|
684
|
+
*
|
|
685
|
+
* Includes TTL-based cleanup to prevent memory leaks from hung requests.
|
|
686
|
+
*/
|
|
687
|
+
async function handleServeIntegration(request, ctx, options) {
|
|
688
|
+
const url = new URL(request.url);
|
|
689
|
+
const cacheKey = url.toString();
|
|
690
|
+
// Lazy cleanup: purge stale entries on each access
|
|
691
|
+
cleanupStaleEntries();
|
|
692
|
+
// Check if there's an in-flight request for the same resource
|
|
693
|
+
const inFlight = inFlightRequests.get(cacheKey);
|
|
694
|
+
if (inFlight) {
|
|
695
|
+
// Check if the entry is still valid (within TTL)
|
|
696
|
+
const now = Date.now();
|
|
697
|
+
if (now - inFlight.createdAt <= IN_FLIGHT_TTL_MS) {
|
|
698
|
+
// Wait for the in-flight request to complete and return its result
|
|
699
|
+
return inFlight.promise;
|
|
700
|
+
}
|
|
701
|
+
// Entry is stale, remove it and proceed with a new request
|
|
702
|
+
inFlightRequests.delete(cacheKey);
|
|
703
|
+
}
|
|
704
|
+
// Enforce max size limit - if at capacity, clean up oldest entries
|
|
705
|
+
if (inFlightRequests.size >= IN_FLIGHT_MAX_SIZE) {
|
|
706
|
+
// Find and remove the oldest entries (LRU-style eviction)
|
|
707
|
+
const entriesToRemove = Math.max(1, Math.floor(IN_FLIGHT_MAX_SIZE * 0.1));
|
|
708
|
+
const sortedEntries = Array.from(inFlightRequests.entries())
|
|
709
|
+
.sort((a, b) => a[1].createdAt - b[1].createdAt);
|
|
710
|
+
for (let i = 0; i < entriesToRemove && i < sortedEntries.length; i++) {
|
|
711
|
+
inFlightRequests.delete(sortedEntries[i][0]);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
// Create a promise for this request and store it with timestamp
|
|
715
|
+
const requestPromise = handleServeIntegrationInner(request, ctx, options);
|
|
716
|
+
const entry = {
|
|
717
|
+
promise: requestPromise,
|
|
718
|
+
createdAt: Date.now(),
|
|
719
|
+
};
|
|
720
|
+
inFlightRequests.set(cacheKey, entry);
|
|
721
|
+
try {
|
|
722
|
+
const result = await requestPromise;
|
|
723
|
+
return result;
|
|
724
|
+
}
|
|
725
|
+
finally {
|
|
726
|
+
// Clean up the in-flight request
|
|
727
|
+
inFlightRequests.delete(cacheKey);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Inner implementation of handleServeIntegration.
|
|
732
|
+
* Uses real IcebergReader interface with visibility, auth, and columns.
|
|
733
|
+
*/
|
|
734
|
+
async function handleServeIntegrationInner(request, ctx, options) {
|
|
735
|
+
const url = new URL(request.url);
|
|
736
|
+
const metrics = options.metrics ?? noopMetrics;
|
|
737
|
+
const startTime = Date.now();
|
|
738
|
+
// Parse path
|
|
739
|
+
const parsed = parsePath(url);
|
|
740
|
+
if (!parsed) {
|
|
741
|
+
return {
|
|
742
|
+
status: 400,
|
|
743
|
+
body: JSON.stringify({ error: 'Invalid path format' }),
|
|
744
|
+
contentType: 'application/json',
|
|
745
|
+
headers: {},
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
const { ns, type, id, ext } = parsed;
|
|
749
|
+
// Check extension validity and get column
|
|
750
|
+
let column;
|
|
751
|
+
try {
|
|
752
|
+
column = getColumnForExtension(ext);
|
|
753
|
+
}
|
|
754
|
+
catch {
|
|
755
|
+
return {
|
|
756
|
+
status: 400,
|
|
757
|
+
body: JSON.stringify({ error: `Unsupported extension: ${ext}` }),
|
|
758
|
+
contentType: 'application/json',
|
|
759
|
+
headers: {},
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
// Use provided config or default
|
|
763
|
+
const config = options.tenantConfig || DEFAULT_CONFIG;
|
|
764
|
+
// Parse query params
|
|
765
|
+
const maxAgeParam = url.searchParams.get('max_age');
|
|
766
|
+
const freshParam = url.searchParams.get('fresh');
|
|
767
|
+
// Determine cache overrides
|
|
768
|
+
const overrides = {};
|
|
769
|
+
if (freshParam === 'true' && config.cache.allowFreshBypass) {
|
|
770
|
+
overrides.fresh = true;
|
|
771
|
+
}
|
|
772
|
+
if (maxAgeParam && !overrides.fresh) {
|
|
773
|
+
const parsedMaxAge = parseInt(maxAgeParam);
|
|
774
|
+
if (!isNaN(parsedMaxAge)) {
|
|
775
|
+
overrides.maxAge = Math.max(parsedMaxAge, 0);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
// Build cache key
|
|
779
|
+
const cacheKey = url.toString();
|
|
780
|
+
// Extract auth context from request headers
|
|
781
|
+
const authContext = extractAuthContext(request);
|
|
782
|
+
// Determine visibility based on auth context
|
|
783
|
+
// For unlisted resources, we don't filter by visibility (undefined)
|
|
784
|
+
// This allows direct ID access while still hiding from listings
|
|
785
|
+
const visibility = determineVisibility(authContext);
|
|
786
|
+
// Get columns to request (id + requested column)
|
|
787
|
+
const columns = getColumnsForExtension(ext);
|
|
788
|
+
// Build partition filter with visibility
|
|
789
|
+
const partition = { ns, type };
|
|
790
|
+
if (visibility !== undefined) {
|
|
791
|
+
partition.visibility = visibility;
|
|
792
|
+
}
|
|
793
|
+
// Check if this is a protected visibility that requires auth
|
|
794
|
+
const requiresAuth = visibility === 'org' || visibility === 'user';
|
|
795
|
+
// Try cache first (unless fresh bypass)
|
|
796
|
+
if (!overrides.fresh) {
|
|
797
|
+
try {
|
|
798
|
+
const cachedResponse = await options.cache.match(cacheKey);
|
|
799
|
+
if (cachedResponse) {
|
|
800
|
+
const age = getResponseAge(cachedResponse);
|
|
801
|
+
const isStale = cachedResponse.headers.get('X-Cache-Stale') === 'true';
|
|
802
|
+
const cacheControl = cachedResponse.headers.get('Cache-Control') || buildCacheControl(config, overrides);
|
|
803
|
+
const headers = {
|
|
804
|
+
'Cache-Control': cacheControl,
|
|
805
|
+
'X-Artifact-Source': 'cache',
|
|
806
|
+
'X-Artifact-Age': String(age),
|
|
807
|
+
};
|
|
808
|
+
if (isStale) {
|
|
809
|
+
headers['X-Cache-Stale'] = 'true';
|
|
810
|
+
// Trigger background revalidation
|
|
811
|
+
const revalidatePromise = (async () => {
|
|
812
|
+
const freshRecord = await options.reader.getRecord({
|
|
813
|
+
table: 'do_resources',
|
|
814
|
+
partition,
|
|
815
|
+
id,
|
|
816
|
+
auth: authContext,
|
|
817
|
+
columns,
|
|
818
|
+
});
|
|
819
|
+
if (freshRecord) {
|
|
820
|
+
const content = freshRecord[column];
|
|
821
|
+
if (content !== null && content !== undefined) {
|
|
822
|
+
const isJson = ext.endsWith('.json') || ext === 'json';
|
|
823
|
+
const body = isJson ? JSON.stringify(content) : String(content);
|
|
824
|
+
const cacheControl = buildCacheControl(config, overrides);
|
|
825
|
+
const response = new Response(body, {
|
|
826
|
+
headers: {
|
|
827
|
+
'Content-Type': getContentType(ext),
|
|
828
|
+
'Cache-Control': cacheControl,
|
|
829
|
+
},
|
|
830
|
+
});
|
|
831
|
+
await options.cache.put(cacheKey, response);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
})();
|
|
835
|
+
ctx.waitUntil(revalidatePromise);
|
|
836
|
+
// Record SWR revalidation
|
|
837
|
+
metrics.recordMetric('serve.swr_revalidation', 1, { ns, type, ext });
|
|
838
|
+
}
|
|
839
|
+
const cachedBody = await cachedResponse.text();
|
|
840
|
+
const contentType = cachedResponse.headers.get('Content-Type') || getContentType(ext);
|
|
841
|
+
// Record cache hit metrics
|
|
842
|
+
metrics.recordMetric('serve.cache_hit', 1, { ns, type, ext, stale: String(isStale) });
|
|
843
|
+
metrics.recordLatency('serve.latency', Date.now() - startTime, { ns, type, ext, source: 'cache' });
|
|
844
|
+
return {
|
|
845
|
+
status: cachedResponse.status,
|
|
846
|
+
body: cachedBody,
|
|
847
|
+
contentType: contentType.split(';')[0].trim(),
|
|
848
|
+
headers,
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
catch {
|
|
853
|
+
// Cache error, continue to fetch from origin
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
// Fetch from IcebergReader using real interface
|
|
857
|
+
let record = null;
|
|
858
|
+
try {
|
|
859
|
+
record = await options.reader.getRecord({
|
|
860
|
+
table: 'do_resources',
|
|
861
|
+
partition,
|
|
862
|
+
id,
|
|
863
|
+
auth: authContext,
|
|
864
|
+
columns,
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
catch (error) {
|
|
868
|
+
return {
|
|
869
|
+
status: 500,
|
|
870
|
+
body: JSON.stringify({ error: 'Internal server error' }),
|
|
871
|
+
contentType: 'application/json',
|
|
872
|
+
headers: {},
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
// Handle visibility-based auth checks
|
|
876
|
+
// The record might be null because visibility doesn't match
|
|
877
|
+
// or because the record doesn't exist
|
|
878
|
+
// If record not found with visibility filter, try without filter
|
|
879
|
+
// This handles:
|
|
880
|
+
// 1. Unlisted artifacts (accessible by direct ID regardless of visibility)
|
|
881
|
+
// 2. Protected artifacts (need to check auth)
|
|
882
|
+
if (!record) {
|
|
883
|
+
// Check if auth is required but not present
|
|
884
|
+
if (requiresAuth && !authContext) {
|
|
885
|
+
return {
|
|
886
|
+
status: 401,
|
|
887
|
+
body: JSON.stringify({ error: 'Authentication required' }),
|
|
888
|
+
contentType: 'application/json',
|
|
889
|
+
headers: {
|
|
890
|
+
'WWW-Authenticate': 'Bearer realm="artifacts"',
|
|
891
|
+
},
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
// Try fetching with no visibility filter to check if resource exists
|
|
895
|
+
// Include visibility column to check access control
|
|
896
|
+
const unfilteredColumns = [...columns, 'visibility', 'orgId', 'userId'];
|
|
897
|
+
const unfiltered = await options.reader.getRecord({
|
|
898
|
+
table: 'do_resources',
|
|
899
|
+
partition: { ns, type },
|
|
900
|
+
id,
|
|
901
|
+
columns: unfilteredColumns,
|
|
902
|
+
});
|
|
903
|
+
if (unfiltered) {
|
|
904
|
+
const actualVisibility = unfiltered.visibility;
|
|
905
|
+
// Unlisted artifacts are accessible by direct ID lookup
|
|
906
|
+
if (actualVisibility === 'unlisted') {
|
|
907
|
+
// Use the unfiltered record
|
|
908
|
+
record = unfiltered;
|
|
909
|
+
}
|
|
910
|
+
else if (actualVisibility === 'org' || actualVisibility === 'user') {
|
|
911
|
+
// Resource exists but is protected
|
|
912
|
+
if (!authContext) {
|
|
913
|
+
return {
|
|
914
|
+
status: 401,
|
|
915
|
+
body: JSON.stringify({ error: 'Authentication required' }),
|
|
916
|
+
contentType: 'application/json',
|
|
917
|
+
headers: {
|
|
918
|
+
'WWW-Authenticate': 'Bearer realm="artifacts"',
|
|
919
|
+
},
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
// Auth present but doesn't match - access denied
|
|
923
|
+
return {
|
|
924
|
+
status: 403,
|
|
925
|
+
body: JSON.stringify({ error: 'Access denied' }),
|
|
926
|
+
contentType: 'application/json',
|
|
927
|
+
headers: {},
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
else {
|
|
931
|
+
// Public artifact should have been found earlier - this is unexpected
|
|
932
|
+
record = unfiltered;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
// Still no record found
|
|
937
|
+
if (!record) {
|
|
938
|
+
return {
|
|
939
|
+
status: 404,
|
|
940
|
+
body: JSON.stringify({ error: 'Artifact not found' }),
|
|
941
|
+
contentType: 'application/json',
|
|
942
|
+
headers: {},
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
const recordVisibility = record.visibility;
|
|
946
|
+
// Double-check visibility matches auth
|
|
947
|
+
if (recordVisibility === 'org') {
|
|
948
|
+
const recordOrgId = record.orgId;
|
|
949
|
+
if (!authContext?.orgId || (recordOrgId && authContext.orgId !== recordOrgId)) {
|
|
950
|
+
return {
|
|
951
|
+
status: 403,
|
|
952
|
+
body: JSON.stringify({ error: 'Access denied' }),
|
|
953
|
+
contentType: 'application/json',
|
|
954
|
+
headers: {},
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
if (recordVisibility === 'user') {
|
|
959
|
+
const recordUserId = record.userId;
|
|
960
|
+
if (!authContext?.userId || (recordUserId && authContext.userId !== recordUserId)) {
|
|
961
|
+
return {
|
|
962
|
+
status: 403,
|
|
963
|
+
body: JSON.stringify({ error: 'Access denied' }),
|
|
964
|
+
contentType: 'application/json',
|
|
965
|
+
headers: {},
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
// Check legacy visibility === 'private' handling (backward compatibility)
|
|
970
|
+
const legacyVisibility = record.visibility;
|
|
971
|
+
if (legacyVisibility === 'private') {
|
|
972
|
+
if (!options.authenticatedNs) {
|
|
973
|
+
return {
|
|
974
|
+
status: 401,
|
|
975
|
+
body: JSON.stringify({ error: 'Authentication required' }),
|
|
976
|
+
contentType: 'application/json',
|
|
977
|
+
headers: {},
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
if (options.authenticatedNs !== ns) {
|
|
981
|
+
return {
|
|
982
|
+
status: 403,
|
|
983
|
+
body: JSON.stringify({ error: 'Access denied' }),
|
|
984
|
+
contentType: 'application/json',
|
|
985
|
+
headers: {},
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
// Get content from the appropriate column (column already defined above)
|
|
990
|
+
const content = record[column];
|
|
991
|
+
// Handle missing column content
|
|
992
|
+
if (content === null || content === undefined) {
|
|
993
|
+
return {
|
|
994
|
+
status: 404,
|
|
995
|
+
body: JSON.stringify({ error: 'Artifact format not available' }),
|
|
996
|
+
contentType: 'application/json',
|
|
997
|
+
headers: {},
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
// Prepare response body
|
|
1001
|
+
const isJson = ext.endsWith('.json') || ext === 'json';
|
|
1002
|
+
const body = isJson ? JSON.stringify(content) : String(content);
|
|
1003
|
+
// Build response headers
|
|
1004
|
+
const contentType = getContentType(ext);
|
|
1005
|
+
const cacheControl = buildCacheControl(config, overrides);
|
|
1006
|
+
const headers = {
|
|
1007
|
+
'Cache-Control': cacheControl,
|
|
1008
|
+
'X-Artifact-Source': 'parquet',
|
|
1009
|
+
};
|
|
1010
|
+
// Cache the response
|
|
1011
|
+
if (!overrides.fresh) {
|
|
1012
|
+
try {
|
|
1013
|
+
const cacheResponse = new Response(body, {
|
|
1014
|
+
headers: {
|
|
1015
|
+
'Content-Type': contentType,
|
|
1016
|
+
'Cache-Control': cacheControl,
|
|
1017
|
+
},
|
|
1018
|
+
});
|
|
1019
|
+
await options.cache.put(cacheKey, cacheResponse);
|
|
1020
|
+
}
|
|
1021
|
+
catch {
|
|
1022
|
+
// Ignore cache put errors
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
// Record cache miss metrics
|
|
1026
|
+
metrics.recordMetric('serve.cache_miss', 1, { ns, type, ext });
|
|
1027
|
+
metrics.recordLatency('serve.latency', Date.now() - startTime, { ns, type, ext, source: 'parquet' });
|
|
1028
|
+
return {
|
|
1029
|
+
status: 200,
|
|
1030
|
+
body,
|
|
1031
|
+
contentType: contentType.split(';')[0].trim(),
|
|
1032
|
+
headers,
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
//# sourceMappingURL=artifacts-serve.js.map
|