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,1508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST API Auto-Wiring
|
|
3
|
+
*
|
|
4
|
+
* Automatically generates REST API routes from DO class methods,
|
|
5
|
+
* enabling rapid API development without manual route setup.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Static $rest configuration for route mappings
|
|
9
|
+
* - @rest() decorator for inline route configuration
|
|
10
|
+
* - Methods exposed at /api/{method} by default
|
|
11
|
+
* - HTTP methods (GET, POST, PUT, DELETE, PATCH) inferred or configured
|
|
12
|
+
* - Query params mapped to method arguments
|
|
13
|
+
* - Request body passed to methods
|
|
14
|
+
* - Return values serialized as JSON
|
|
15
|
+
* - Errors mapped to appropriate HTTP status codes
|
|
16
|
+
* - Rate limiting and throttling per-route
|
|
17
|
+
* - OpenAPI spec generation
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* class MyDO extends DO {
|
|
22
|
+
* static $rest = {
|
|
23
|
+
* getUser: { method: 'GET', path: '/users/:id' },
|
|
24
|
+
* createUser: { method: 'POST', path: '/users' },
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* getUser(id: string) { return { id, name: 'Test' } }
|
|
28
|
+
* createUser(data: { name: string }) { return { id: '1', ...data } }
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* const router = createRestRouter(MyDO)
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
import { Hono } from 'hono';
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// DECORATOR STORAGE
|
|
37
|
+
// ============================================================================
|
|
38
|
+
/**
|
|
39
|
+
* Symbol for storing REST metadata on methods
|
|
40
|
+
*/
|
|
41
|
+
const REST_METADATA = Symbol('rest:metadata');
|
|
42
|
+
/**
|
|
43
|
+
* Storage for decorator-based route configs
|
|
44
|
+
*/
|
|
45
|
+
const decoratorConfigs = new WeakMap();
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// REST DECORATOR
|
|
48
|
+
// ============================================================================
|
|
49
|
+
/**
|
|
50
|
+
* Decorator to mark a method for REST exposure
|
|
51
|
+
*
|
|
52
|
+
* Supports both:
|
|
53
|
+
* - Stage 3 ECMAScript decorators (modern)
|
|
54
|
+
* - TypeScript experimental decorators (legacy)
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* class MyDO extends DO {
|
|
59
|
+
* @rest({ method: 'GET', path: '/users/:id' })
|
|
60
|
+
* getUser(id: string) {
|
|
61
|
+
* return { id, name: 'Test' }
|
|
62
|
+
* }
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export function rest(config) {
|
|
67
|
+
// Return a function that can handle both decorator signatures
|
|
68
|
+
return function (targetOrValue, contextOrPropertyKey, descriptor) {
|
|
69
|
+
// Stage 3 ECMAScript decorator signature: (value, context)
|
|
70
|
+
if (typeof targetOrValue === 'function' &&
|
|
71
|
+
contextOrPropertyKey &&
|
|
72
|
+
typeof contextOrPropertyKey === 'object' &&
|
|
73
|
+
'kind' in contextOrPropertyKey) {
|
|
74
|
+
const context = contextOrPropertyKey;
|
|
75
|
+
const methodName = String(context.name);
|
|
76
|
+
// Use addInitializer to register the config when the class is defined
|
|
77
|
+
context.addInitializer(function () {
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
79
|
+
const constructor = this.constructor;
|
|
80
|
+
let configs = decoratorConfigs.get(constructor);
|
|
81
|
+
if (!configs) {
|
|
82
|
+
configs = new Map();
|
|
83
|
+
decoratorConfigs.set(constructor, configs);
|
|
84
|
+
}
|
|
85
|
+
configs.set(methodName, config);
|
|
86
|
+
});
|
|
87
|
+
// Also store on the function itself
|
|
88
|
+
Object.defineProperty(targetOrValue, REST_METADATA, {
|
|
89
|
+
value: config,
|
|
90
|
+
writable: false,
|
|
91
|
+
enumerable: false,
|
|
92
|
+
});
|
|
93
|
+
return targetOrValue;
|
|
94
|
+
}
|
|
95
|
+
// TypeScript experimental decorator signature: (target, propertyKey, descriptor)
|
|
96
|
+
if (typeof contextOrPropertyKey === 'string' || typeof contextOrPropertyKey === 'symbol') {
|
|
97
|
+
const methodName = String(contextOrPropertyKey);
|
|
98
|
+
const constructor = targetOrValue.constructor;
|
|
99
|
+
// Get or create the config map for this class
|
|
100
|
+
let configs = decoratorConfigs.get(constructor);
|
|
101
|
+
if (!configs) {
|
|
102
|
+
configs = new Map();
|
|
103
|
+
decoratorConfigs.set(constructor, configs);
|
|
104
|
+
}
|
|
105
|
+
// Store the config
|
|
106
|
+
configs.set(methodName, config);
|
|
107
|
+
// Mark the method with metadata
|
|
108
|
+
if (descriptor && descriptor.value) {
|
|
109
|
+
Object.defineProperty(descriptor.value, REST_METADATA, {
|
|
110
|
+
value: config,
|
|
111
|
+
writable: false,
|
|
112
|
+
enumerable: false,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return descriptor;
|
|
116
|
+
}
|
|
117
|
+
// Fallback: just return the value unchanged
|
|
118
|
+
return targetOrValue;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
// ============================================================================
|
|
122
|
+
// ROUTE DISCOVERY
|
|
123
|
+
// ============================================================================
|
|
124
|
+
/**
|
|
125
|
+
* Get all REST routes from a DO class
|
|
126
|
+
*
|
|
127
|
+
* @param DOClass - The DO class to analyze
|
|
128
|
+
* @returns Array of route configurations
|
|
129
|
+
*/
|
|
130
|
+
export function getRestRoutes(DOClass) {
|
|
131
|
+
const routes = [];
|
|
132
|
+
// Check static $rest configuration
|
|
133
|
+
const staticRest = DOClass.$rest;
|
|
134
|
+
if (staticRest) {
|
|
135
|
+
for (const [methodName, config] of Object.entries(staticRest)) {
|
|
136
|
+
routes.push({
|
|
137
|
+
methodName,
|
|
138
|
+
httpMethod: config.method,
|
|
139
|
+
path: config.path,
|
|
140
|
+
queryParams: config.queryParams,
|
|
141
|
+
rateLimit: config.rateLimit,
|
|
142
|
+
throttle: config.throttle,
|
|
143
|
+
auth: config.auth,
|
|
144
|
+
roles: config.roles,
|
|
145
|
+
scopes: config.scopes,
|
|
146
|
+
produces: config.produces,
|
|
147
|
+
consumes: config.consumes,
|
|
148
|
+
cache: config.cache,
|
|
149
|
+
etag: config.etag,
|
|
150
|
+
lastModified: config.lastModified,
|
|
151
|
+
schema: config.schema,
|
|
152
|
+
streaming: config.streaming,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Check decorator-based configurations
|
|
157
|
+
const decoratorConfig = decoratorConfigs.get(DOClass);
|
|
158
|
+
if (decoratorConfig) {
|
|
159
|
+
for (const [methodName, config] of decoratorConfig) {
|
|
160
|
+
routes.push({
|
|
161
|
+
methodName,
|
|
162
|
+
httpMethod: config.method,
|
|
163
|
+
path: config.path,
|
|
164
|
+
queryParams: config.queryParams,
|
|
165
|
+
rateLimit: config.rateLimit,
|
|
166
|
+
throttle: config.throttle,
|
|
167
|
+
auth: config.auth,
|
|
168
|
+
roles: config.roles,
|
|
169
|
+
scopes: config.scopes,
|
|
170
|
+
produces: config.produces,
|
|
171
|
+
consumes: config.consumes,
|
|
172
|
+
cache: config.cache,
|
|
173
|
+
etag: config.etag,
|
|
174
|
+
lastModified: config.lastModified,
|
|
175
|
+
schema: config.schema,
|
|
176
|
+
streaming: config.streaming,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Also check prototype methods for decorator metadata
|
|
181
|
+
const prototype = DOClass.prototype;
|
|
182
|
+
if (prototype) {
|
|
183
|
+
const methodNames = Object.getOwnPropertyNames(prototype);
|
|
184
|
+
for (const methodName of methodNames) {
|
|
185
|
+
if (methodName === 'constructor')
|
|
186
|
+
continue;
|
|
187
|
+
const method = prototype[methodName];
|
|
188
|
+
if (typeof method === 'function') {
|
|
189
|
+
const metadata = Object.getOwnPropertyDescriptor(method, REST_METADATA)?.value;
|
|
190
|
+
if (metadata && !routes.some((r) => r.methodName === methodName)) {
|
|
191
|
+
routes.push({
|
|
192
|
+
methodName,
|
|
193
|
+
httpMethod: metadata.method,
|
|
194
|
+
path: metadata.path,
|
|
195
|
+
queryParams: metadata.queryParams,
|
|
196
|
+
rateLimit: metadata.rateLimit,
|
|
197
|
+
throttle: metadata.throttle,
|
|
198
|
+
auth: metadata.auth,
|
|
199
|
+
roles: metadata.roles,
|
|
200
|
+
scopes: metadata.scopes,
|
|
201
|
+
produces: metadata.produces,
|
|
202
|
+
consumes: metadata.consumes,
|
|
203
|
+
cache: metadata.cache,
|
|
204
|
+
etag: metadata.etag,
|
|
205
|
+
lastModified: metadata.lastModified,
|
|
206
|
+
schema: metadata.schema,
|
|
207
|
+
streaming: metadata.streaming,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return routes;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Get configuration for a specific route
|
|
217
|
+
*
|
|
218
|
+
* @param DOClass - The DO class to analyze
|
|
219
|
+
* @param methodName - The method name to get config for
|
|
220
|
+
* @returns Route configuration or undefined
|
|
221
|
+
*/
|
|
222
|
+
export function getRouteConfig(DOClass, methodName) {
|
|
223
|
+
// Check static $rest
|
|
224
|
+
const staticRest = DOClass.$rest;
|
|
225
|
+
if (staticRest && staticRest[methodName]) {
|
|
226
|
+
return staticRest[methodName];
|
|
227
|
+
}
|
|
228
|
+
// Check decorator configs
|
|
229
|
+
const decoratorConfig = decoratorConfigs.get(DOClass);
|
|
230
|
+
if (decoratorConfig && decoratorConfig.has(methodName)) {
|
|
231
|
+
return decoratorConfig.get(methodName);
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
// ============================================================================
|
|
236
|
+
// PARAMETER PARSING
|
|
237
|
+
// ============================================================================
|
|
238
|
+
/**
|
|
239
|
+
* Parse path parameters from a URL path against a pattern
|
|
240
|
+
*
|
|
241
|
+
* @param pattern - Route pattern (e.g., '/users/:userId')
|
|
242
|
+
* @param path - Actual URL path (e.g., '/users/123')
|
|
243
|
+
* @returns Extracted parameters or null if no match
|
|
244
|
+
*/
|
|
245
|
+
export function parseRouteParams(pattern, path) {
|
|
246
|
+
// Normalize trailing slashes
|
|
247
|
+
const normalizedPattern = pattern.replace(/\/$/, '');
|
|
248
|
+
const normalizedPath = path.replace(/\/$/, '');
|
|
249
|
+
// Build regex from pattern
|
|
250
|
+
const paramNames = [];
|
|
251
|
+
const optionalParams = new Set();
|
|
252
|
+
let regexStr = '^';
|
|
253
|
+
// Split but filter out empty segments from leading slash
|
|
254
|
+
const segments = normalizedPattern.split('/').filter((s, i) => i !== 0 || s !== '');
|
|
255
|
+
for (let i = 0; i < segments.length; i++) {
|
|
256
|
+
const segment = segments[i];
|
|
257
|
+
if (!segment)
|
|
258
|
+
continue;
|
|
259
|
+
// Add slash separator (first segment starts with /)
|
|
260
|
+
if (i === 0 || regexStr !== '^') {
|
|
261
|
+
regexStr += '/';
|
|
262
|
+
}
|
|
263
|
+
if (segment.startsWith('*')) {
|
|
264
|
+
// Wildcard parameter - captures rest of path
|
|
265
|
+
const paramName = segment.slice(1);
|
|
266
|
+
paramNames.push(paramName);
|
|
267
|
+
regexStr += '(.+)';
|
|
268
|
+
}
|
|
269
|
+
else if (segment.startsWith(':')) {
|
|
270
|
+
// Check for optional marker
|
|
271
|
+
const isOptional = segment.endsWith('?');
|
|
272
|
+
const paramName = isOptional ? segment.slice(1, -1) : segment.slice(1);
|
|
273
|
+
paramNames.push(paramName);
|
|
274
|
+
if (isOptional) {
|
|
275
|
+
optionalParams.add(paramName);
|
|
276
|
+
// Optional segment can be missing entirely (including the preceding /)
|
|
277
|
+
// We need to handle this case by making the slash optional too
|
|
278
|
+
regexStr = regexStr.slice(0, -1); // Remove the trailing /
|
|
279
|
+
regexStr += '(?:/([^/]*))?';
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
regexStr += '([^/]+)';
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
// Literal segment - escape regex special characters
|
|
287
|
+
regexStr += segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
regexStr += '$';
|
|
291
|
+
const regex = new RegExp(regexStr);
|
|
292
|
+
const match = normalizedPath.match(regex);
|
|
293
|
+
if (!match) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
const params = {};
|
|
297
|
+
for (let i = 0; i < paramNames.length; i++) {
|
|
298
|
+
const value = match[i + 1];
|
|
299
|
+
const paramName = paramNames[i];
|
|
300
|
+
if (value !== undefined && value !== '') {
|
|
301
|
+
// URL decode the parameter
|
|
302
|
+
params[paramName] = decodeURIComponent(value);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
// For optional params, explicitly set undefined
|
|
306
|
+
params[paramName] = undefined;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return params;
|
|
310
|
+
}
|
|
311
|
+
// ============================================================================
|
|
312
|
+
// RESPONSE SERIALIZATION
|
|
313
|
+
// ============================================================================
|
|
314
|
+
/**
|
|
315
|
+
* Serialize data to an HTTP response
|
|
316
|
+
*
|
|
317
|
+
* @param data - Data to serialize
|
|
318
|
+
* @param options - Serialization options
|
|
319
|
+
* @returns HTTP Response
|
|
320
|
+
*/
|
|
321
|
+
export function serializeResponse(data, options) {
|
|
322
|
+
// Handle null/undefined
|
|
323
|
+
if (data === null || data === undefined) {
|
|
324
|
+
return new Response(null, { status: 204 });
|
|
325
|
+
}
|
|
326
|
+
// Determine content type
|
|
327
|
+
// 1. If produces is specified and data is binary, use the first produces type
|
|
328
|
+
// 2. Otherwise, negotiate based on Accept header
|
|
329
|
+
const accept = options?.accept || 'application/json';
|
|
330
|
+
let contentType = 'application/json';
|
|
331
|
+
let body;
|
|
332
|
+
// For binary data, prefer the produces content type if specified
|
|
333
|
+
if (data instanceof ArrayBuffer && options?.produces && options.produces.length > 0) {
|
|
334
|
+
contentType = options.produces[0];
|
|
335
|
+
return new Response(data, {
|
|
336
|
+
status: options?.status || 200,
|
|
337
|
+
headers: { 'Content-Type': contentType },
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
// Parse Accept header quality values
|
|
341
|
+
const acceptTypes = accept.split(',').map((t) => {
|
|
342
|
+
const [type, ...params] = t.trim().split(';');
|
|
343
|
+
let quality = 1.0;
|
|
344
|
+
for (const param of params) {
|
|
345
|
+
const [key, value] = param.trim().split('=');
|
|
346
|
+
if (key === 'q') {
|
|
347
|
+
quality = parseFloat(value);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return { type: type.trim(), quality };
|
|
351
|
+
}).sort((a, b) => b.quality - a.quality);
|
|
352
|
+
// Find best matching content type
|
|
353
|
+
for (const { type } of acceptTypes) {
|
|
354
|
+
if (type === 'application/json' || type === '*/*') {
|
|
355
|
+
contentType = 'application/json';
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
else if (type === 'application/xml') {
|
|
359
|
+
contentType = 'application/xml';
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
else if (type === 'text/csv') {
|
|
363
|
+
contentType = 'text/csv';
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// Handle binary data (ArrayBuffer) without produces
|
|
368
|
+
if (data instanceof ArrayBuffer) {
|
|
369
|
+
return new Response(data, {
|
|
370
|
+
status: options?.status || 200,
|
|
371
|
+
headers: { 'Content-Type': contentType },
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
// Serialize based on content type
|
|
375
|
+
if (contentType === 'application/xml') {
|
|
376
|
+
body = jsonToXml(data);
|
|
377
|
+
}
|
|
378
|
+
else if (contentType === 'text/csv') {
|
|
379
|
+
body = jsonToCsv(data);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
body = JSON.stringify(data);
|
|
383
|
+
}
|
|
384
|
+
// Determine status code
|
|
385
|
+
let status = options?.status || 200;
|
|
386
|
+
if (options?.method === 'POST' && !options?.status && data && typeof data === 'object') {
|
|
387
|
+
// POST typically returns 201 (Created) for resource creation
|
|
388
|
+
// Return 200 only for "action" endpoints that return just a message
|
|
389
|
+
const dataObj = data;
|
|
390
|
+
const keys = Object.keys(dataObj);
|
|
391
|
+
const isActionResponse = keys.length === 1 && keys[0] === 'message';
|
|
392
|
+
if (!isActionResponse) {
|
|
393
|
+
status = 201;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return new Response(body, {
|
|
397
|
+
status,
|
|
398
|
+
headers: { 'Content-Type': contentType },
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Convert JSON to simple XML
|
|
403
|
+
*/
|
|
404
|
+
function jsonToXml(data, rootName = 'root') {
|
|
405
|
+
if (data === null || data === undefined) {
|
|
406
|
+
return `<${rootName}/>`;
|
|
407
|
+
}
|
|
408
|
+
if (Array.isArray(data)) {
|
|
409
|
+
const items = data.map((item, i) => jsonToXml(item, 'item')).join('');
|
|
410
|
+
return `<${rootName}>${items}</${rootName}>`;
|
|
411
|
+
}
|
|
412
|
+
if (typeof data === 'object') {
|
|
413
|
+
const entries = Object.entries(data)
|
|
414
|
+
.map(([key, value]) => jsonToXml(value, key))
|
|
415
|
+
.join('');
|
|
416
|
+
return `<${rootName}>${entries}</${rootName}>`;
|
|
417
|
+
}
|
|
418
|
+
return `<${rootName}>${String(data)}</${rootName}>`;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Convert JSON to CSV
|
|
422
|
+
*/
|
|
423
|
+
function jsonToCsv(data) {
|
|
424
|
+
if (!Array.isArray(data)) {
|
|
425
|
+
data = [data];
|
|
426
|
+
}
|
|
427
|
+
const arr = data;
|
|
428
|
+
if (arr.length === 0)
|
|
429
|
+
return '';
|
|
430
|
+
// Handle nested items property
|
|
431
|
+
const items = arr[0]?.items;
|
|
432
|
+
const rows = Array.isArray(items) ? items : arr;
|
|
433
|
+
if (rows.length === 0)
|
|
434
|
+
return '';
|
|
435
|
+
const headers = Object.keys(rows[0]);
|
|
436
|
+
const csvRows = [headers.join(',')];
|
|
437
|
+
for (const row of rows) {
|
|
438
|
+
const values = headers.map((h) => {
|
|
439
|
+
const val = row[h];
|
|
440
|
+
if (typeof val === 'string' && val.includes(',')) {
|
|
441
|
+
return `"${val}"`;
|
|
442
|
+
}
|
|
443
|
+
return String(val ?? '');
|
|
444
|
+
});
|
|
445
|
+
csvRows.push(values.join(','));
|
|
446
|
+
}
|
|
447
|
+
return csvRows.join('\n');
|
|
448
|
+
}
|
|
449
|
+
// ============================================================================
|
|
450
|
+
// REQUEST DESERIALIZATION
|
|
451
|
+
// ============================================================================
|
|
452
|
+
/**
|
|
453
|
+
* Deserialize request body
|
|
454
|
+
*
|
|
455
|
+
* @param request - HTTP Request
|
|
456
|
+
* @param schema - Optional JSON schema for validation
|
|
457
|
+
* @returns Deserialized body
|
|
458
|
+
*/
|
|
459
|
+
export async function deserializeRequest(request, schema) {
|
|
460
|
+
const contentType = request.headers.get('Content-Type') || '';
|
|
461
|
+
// Handle empty body
|
|
462
|
+
if (!request.body) {
|
|
463
|
+
return {};
|
|
464
|
+
}
|
|
465
|
+
let data = {};
|
|
466
|
+
if (contentType.includes('application/json')) {
|
|
467
|
+
const text = await request.text();
|
|
468
|
+
if (!text || text.trim() === '') {
|
|
469
|
+
return {};
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
data = JSON.parse(text);
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
throw new Error('Invalid JSON body');
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
else if (contentType.includes('multipart/form-data')) {
|
|
479
|
+
const formData = await request.formData();
|
|
480
|
+
data = {};
|
|
481
|
+
for (const [key, value] of formData.entries()) {
|
|
482
|
+
data[key] = value;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
486
|
+
const text = await request.text();
|
|
487
|
+
const params = new URLSearchParams(text);
|
|
488
|
+
data = {};
|
|
489
|
+
for (const [key, value] of params.entries()) {
|
|
490
|
+
data[key] = value;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
// Try to parse as JSON anyway
|
|
495
|
+
const text = await request.text();
|
|
496
|
+
if (text && text.trim()) {
|
|
497
|
+
try {
|
|
498
|
+
data = JSON.parse(text);
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
throw new Error('Unable to parse request body');
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return data;
|
|
506
|
+
}
|
|
507
|
+
// ============================================================================
|
|
508
|
+
// ERROR MAPPING
|
|
509
|
+
// ============================================================================
|
|
510
|
+
/**
|
|
511
|
+
* Map an error to HTTP status and response
|
|
512
|
+
*
|
|
513
|
+
* @param error - Error to map
|
|
514
|
+
* @returns REST error response
|
|
515
|
+
*/
|
|
516
|
+
export function mapHttpError(error) {
|
|
517
|
+
// Check for statusCode property
|
|
518
|
+
const statusCode = error.statusCode;
|
|
519
|
+
const code = error.code;
|
|
520
|
+
const errors = error.errors;
|
|
521
|
+
const retryAfter = error.retryAfter;
|
|
522
|
+
if (statusCode) {
|
|
523
|
+
return {
|
|
524
|
+
status: statusCode,
|
|
525
|
+
message: error.message,
|
|
526
|
+
code,
|
|
527
|
+
errors,
|
|
528
|
+
retryAfter,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
// Default to 500
|
|
532
|
+
return {
|
|
533
|
+
status: 500,
|
|
534
|
+
message: error.message,
|
|
535
|
+
code,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
// ============================================================================
|
|
539
|
+
// VALIDATION
|
|
540
|
+
// ============================================================================
|
|
541
|
+
/**
|
|
542
|
+
* Validate data against JSON schema
|
|
543
|
+
*/
|
|
544
|
+
function validateSchema(data, schema) {
|
|
545
|
+
const errors = {};
|
|
546
|
+
// Check required fields
|
|
547
|
+
if (schema.required) {
|
|
548
|
+
for (const field of schema.required) {
|
|
549
|
+
if (data[field] === undefined || data[field] === null) {
|
|
550
|
+
errors[field] = errors[field] || [];
|
|
551
|
+
errors[field].push(`${field} is required`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// Check properties
|
|
556
|
+
if (schema.properties) {
|
|
557
|
+
for (const [field, prop] of Object.entries(schema.properties)) {
|
|
558
|
+
const value = data[field];
|
|
559
|
+
if (value === undefined || value === null)
|
|
560
|
+
continue;
|
|
561
|
+
const fieldErrors = [];
|
|
562
|
+
// Type check
|
|
563
|
+
if (prop.type === 'string' && typeof value !== 'string') {
|
|
564
|
+
fieldErrors.push(`${field} must be a string`);
|
|
565
|
+
}
|
|
566
|
+
else if (prop.type === 'number' && typeof value !== 'number') {
|
|
567
|
+
fieldErrors.push(`${field} must be a number`);
|
|
568
|
+
}
|
|
569
|
+
// String validations
|
|
570
|
+
if (typeof value === 'string') {
|
|
571
|
+
if (prop.minLength !== undefined && value.length < prop.minLength) {
|
|
572
|
+
fieldErrors.push(`${field} must be at least ${prop.minLength} characters`);
|
|
573
|
+
}
|
|
574
|
+
if (prop.maxLength !== undefined && value.length > prop.maxLength) {
|
|
575
|
+
fieldErrors.push(`${field} must be at most ${prop.maxLength} characters`);
|
|
576
|
+
}
|
|
577
|
+
if (prop.format === 'email' && !isValidEmail(value)) {
|
|
578
|
+
fieldErrors.push(`${field} must be a valid email`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
// Number validations
|
|
582
|
+
if (typeof value === 'number') {
|
|
583
|
+
if (prop.minimum !== undefined && value < prop.minimum) {
|
|
584
|
+
fieldErrors.push(`${field} must be at least ${prop.minimum}`);
|
|
585
|
+
}
|
|
586
|
+
if (prop.maximum !== undefined && value > prop.maximum) {
|
|
587
|
+
fieldErrors.push(`${field} must be at most ${prop.maximum}`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (fieldErrors.length > 0) {
|
|
591
|
+
errors[field] = fieldErrors;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
valid: Object.keys(errors).length === 0,
|
|
597
|
+
errors,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Simple email validation
|
|
602
|
+
*/
|
|
603
|
+
function isValidEmail(email) {
|
|
604
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
605
|
+
}
|
|
606
|
+
// ============================================================================
|
|
607
|
+
// RATE LIMITING
|
|
608
|
+
// ============================================================================
|
|
609
|
+
/**
|
|
610
|
+
* Rate limit state storage
|
|
611
|
+
*/
|
|
612
|
+
const rateLimitState = new Map();
|
|
613
|
+
const throttleState = new Map();
|
|
614
|
+
/**
|
|
615
|
+
* Check rate limit
|
|
616
|
+
*/
|
|
617
|
+
function checkRateLimit(key, config) {
|
|
618
|
+
const now = Date.now();
|
|
619
|
+
const state = rateLimitState.get(key);
|
|
620
|
+
if (!state || now - state.windowStart >= config.windowMs) {
|
|
621
|
+
// New window
|
|
622
|
+
rateLimitState.set(key, { count: 1, windowStart: now });
|
|
623
|
+
return {
|
|
624
|
+
allowed: true,
|
|
625
|
+
remaining: config.requests - 1,
|
|
626
|
+
reset: now + config.windowMs,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
if (state.count >= config.requests) {
|
|
630
|
+
// Rate limited
|
|
631
|
+
const retryAfter = Math.ceil((state.windowStart + config.windowMs - now) / 1000);
|
|
632
|
+
return {
|
|
633
|
+
allowed: false,
|
|
634
|
+
remaining: 0,
|
|
635
|
+
reset: state.windowStart + config.windowMs,
|
|
636
|
+
retryAfter,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
// Increment count
|
|
640
|
+
state.count++;
|
|
641
|
+
return {
|
|
642
|
+
allowed: true,
|
|
643
|
+
remaining: config.requests - state.count,
|
|
644
|
+
reset: state.windowStart + config.windowMs,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Check throttle (token bucket)
|
|
649
|
+
*/
|
|
650
|
+
function checkThrottle(key, config) {
|
|
651
|
+
const now = Date.now();
|
|
652
|
+
let state = throttleState.get(key);
|
|
653
|
+
if (!state) {
|
|
654
|
+
state = { tokens: config.burstLimit, lastRefill: now, requests: [] };
|
|
655
|
+
throttleState.set(key, state);
|
|
656
|
+
}
|
|
657
|
+
// Refill tokens
|
|
658
|
+
const elapsed = now - state.lastRefill;
|
|
659
|
+
const refillAmount = (elapsed / 1000) * config.sustainedRate;
|
|
660
|
+
state.tokens = Math.min(config.burstLimit, state.tokens + refillAmount);
|
|
661
|
+
state.lastRefill = now;
|
|
662
|
+
// Clean old requests
|
|
663
|
+
state.requests = state.requests.filter((r) => now - r.timestamp < config.windowMs);
|
|
664
|
+
if (state.tokens < 1) {
|
|
665
|
+
return {
|
|
666
|
+
allowed: false,
|
|
667
|
+
retryAfter: Math.ceil(1 / config.sustainedRate),
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
// Consume token
|
|
671
|
+
state.tokens--;
|
|
672
|
+
state.requests.push({ timestamp: now });
|
|
673
|
+
return { allowed: true };
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Get rate limit key from request
|
|
677
|
+
*/
|
|
678
|
+
function getRateLimitKey(request, config, path, authContext) {
|
|
679
|
+
const base = path;
|
|
680
|
+
if (config.keyBy === 'user' && authContext?.userId) {
|
|
681
|
+
return `${base}:user:${authContext.userId}`;
|
|
682
|
+
}
|
|
683
|
+
// Default to IP
|
|
684
|
+
const ip = request.headers.get('X-Forwarded-For')?.split(',')[0]?.trim()
|
|
685
|
+
|| request.headers.get('CF-Connecting-IP')
|
|
686
|
+
|| 'unknown';
|
|
687
|
+
return `${base}:ip:${ip}`;
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Create a REST router from a DO class
|
|
691
|
+
*
|
|
692
|
+
* @param DOClass - The DO class to create routes from
|
|
693
|
+
* @param options - Router options
|
|
694
|
+
* @returns REST router
|
|
695
|
+
*/
|
|
696
|
+
export function createRestRouter(DOClass, options) {
|
|
697
|
+
const app = new Hono();
|
|
698
|
+
const routes = getRestRoutes(DOClass);
|
|
699
|
+
const basePath = options?.basePath || options?.apiPrefix || '';
|
|
700
|
+
const middleware = options?.middleware || [];
|
|
701
|
+
const debug = options?.debug ?? false;
|
|
702
|
+
// Create a shared instance for handling requests
|
|
703
|
+
// In a real implementation, this would be passed in or created per-request
|
|
704
|
+
let instance = null;
|
|
705
|
+
/**
|
|
706
|
+
* Get or create DO instance
|
|
707
|
+
*/
|
|
708
|
+
function getInstance() {
|
|
709
|
+
if (!instance) {
|
|
710
|
+
try {
|
|
711
|
+
// Try to create an instance - some classes may not need constructor params
|
|
712
|
+
instance = new DOClass();
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
// If instantiation fails (e.g., class requires constructor params like ctx, env),
|
|
716
|
+
// try with mock constructor args for Durable Object classes
|
|
717
|
+
try {
|
|
718
|
+
const mockCtx = createMockDurableObjectState();
|
|
719
|
+
const mockEnv = {};
|
|
720
|
+
instance = new DOClass(mockCtx, mockEnv);
|
|
721
|
+
}
|
|
722
|
+
catch {
|
|
723
|
+
// Final fallback: create an object with the prototype
|
|
724
|
+
// and manually initialize any instance properties we can detect
|
|
725
|
+
const prototype = DOClass.prototype;
|
|
726
|
+
instance = Object.create(prototype);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return instance;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Create a mock DurableObjectState for testing
|
|
734
|
+
*/
|
|
735
|
+
function createMockDurableObjectState() {
|
|
736
|
+
const storage = new Map();
|
|
737
|
+
return {
|
|
738
|
+
id: {
|
|
739
|
+
toString: () => 'mock-id',
|
|
740
|
+
equals: () => false,
|
|
741
|
+
name: 'mock-do',
|
|
742
|
+
},
|
|
743
|
+
storage: {
|
|
744
|
+
get: async (key) => storage.get(key),
|
|
745
|
+
put: async (key, value) => { storage.set(key, value); },
|
|
746
|
+
delete: async (key) => storage.delete(key),
|
|
747
|
+
deleteAll: async () => storage.clear(),
|
|
748
|
+
list: async () => new Map(storage),
|
|
749
|
+
sql: {
|
|
750
|
+
exec: () => ({ toArray: () => [], one: () => null, raw: () => [] }),
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
waitUntil: () => { },
|
|
754
|
+
blockConcurrencyWhile: async (fn) => fn(),
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
// Add full path to routes
|
|
758
|
+
const routesWithFullPath = routes.map((route) => ({
|
|
759
|
+
...route,
|
|
760
|
+
fullPath: `${basePath}${route.path}`,
|
|
761
|
+
}));
|
|
762
|
+
/**
|
|
763
|
+
* Authenticate request
|
|
764
|
+
*/
|
|
765
|
+
async function authenticate(request) {
|
|
766
|
+
if (options?.authHandler) {
|
|
767
|
+
return options.authHandler(request);
|
|
768
|
+
}
|
|
769
|
+
// Default auth handling based on Authorization header
|
|
770
|
+
const authHeader = request.headers.get('Authorization');
|
|
771
|
+
if (!authHeader) {
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
// Simple token-based auth for testing
|
|
775
|
+
if (authHeader.startsWith('Bearer ')) {
|
|
776
|
+
const token = authHeader.slice(7);
|
|
777
|
+
// Mock authentication based on token pattern
|
|
778
|
+
if (token === 'valid-token') {
|
|
779
|
+
return { userId: 'user-1', roles: ['user'], scopes: [] };
|
|
780
|
+
}
|
|
781
|
+
if (token === 'admin-token') {
|
|
782
|
+
return { userId: 'admin-1', roles: ['admin', 'user'], scopes: ['*'] };
|
|
783
|
+
}
|
|
784
|
+
if (token === 'user-token') {
|
|
785
|
+
return { userId: 'user-1', roles: ['user'], scopes: [] };
|
|
786
|
+
}
|
|
787
|
+
if (token === 'limited-scope-token') {
|
|
788
|
+
return { userId: 'user-1', roles: ['user'], scopes: ['read:items'], tokenType: 'limited-scope' };
|
|
789
|
+
}
|
|
790
|
+
if (token === 'full-scope-token') {
|
|
791
|
+
return { userId: 'user-1', roles: ['user'], scopes: ['write:items', 'read:items'], tokenType: 'full-scope' };
|
|
792
|
+
}
|
|
793
|
+
if (token.startsWith('token-')) {
|
|
794
|
+
// Generic user token for rate limit tests
|
|
795
|
+
const userId = token.replace('token-', '');
|
|
796
|
+
return { userId, roles: ['user'], scopes: [] };
|
|
797
|
+
}
|
|
798
|
+
// Any other valid-looking token
|
|
799
|
+
return { userId: 'user-1', roles: ['user'], scopes: [] };
|
|
800
|
+
}
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Check authorization (roles and scopes)
|
|
805
|
+
*/
|
|
806
|
+
function checkAuthorization(authContext, route) {
|
|
807
|
+
// Check roles
|
|
808
|
+
if (route.roles && route.roles.length > 0) {
|
|
809
|
+
if (!authContext?.roles) {
|
|
810
|
+
return { authorized: false, reason: 'Roles required' };
|
|
811
|
+
}
|
|
812
|
+
const hasRole = route.roles.some((r) => authContext.roles.includes(r));
|
|
813
|
+
if (!hasRole) {
|
|
814
|
+
return { authorized: false, reason: 'Insufficient roles' };
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
// Check scopes
|
|
818
|
+
if (route.scopes && route.scopes.length > 0) {
|
|
819
|
+
if (!authContext?.scopes) {
|
|
820
|
+
return { authorized: false, reason: 'Scopes required' };
|
|
821
|
+
}
|
|
822
|
+
const hasAllScopes = route.scopes.every((s) => authContext.scopes.includes(s) || authContext.scopes.includes('*'));
|
|
823
|
+
if (!hasAllScopes) {
|
|
824
|
+
return { authorized: false, reason: 'Insufficient scopes' };
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return { authorized: true };
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Handle CORS
|
|
831
|
+
*/
|
|
832
|
+
function handleCors(request, response) {
|
|
833
|
+
if (!options?.cors) {
|
|
834
|
+
return response;
|
|
835
|
+
}
|
|
836
|
+
const origin = request.headers.get('Origin');
|
|
837
|
+
if (!origin) {
|
|
838
|
+
return response;
|
|
839
|
+
}
|
|
840
|
+
const { origins, credentials, methods, allowedHeaders, exposedHeaders, maxAge } = options.cors;
|
|
841
|
+
// Check if origin is allowed
|
|
842
|
+
const isAllowed = origins.includes('*') || origins.includes(origin);
|
|
843
|
+
if (!isAllowed) {
|
|
844
|
+
return response;
|
|
845
|
+
}
|
|
846
|
+
const headers = new Headers(response.headers);
|
|
847
|
+
headers.set('Access-Control-Allow-Origin', origins.includes('*') ? '*' : origin);
|
|
848
|
+
if (credentials) {
|
|
849
|
+
headers.set('Access-Control-Allow-Credentials', 'true');
|
|
850
|
+
}
|
|
851
|
+
if (methods) {
|
|
852
|
+
headers.set('Access-Control-Allow-Methods', methods.join(', '));
|
|
853
|
+
}
|
|
854
|
+
if (allowedHeaders) {
|
|
855
|
+
headers.set('Access-Control-Allow-Headers', allowedHeaders.join(', '));
|
|
856
|
+
}
|
|
857
|
+
if (exposedHeaders) {
|
|
858
|
+
headers.set('Access-Control-Expose-Headers', exposedHeaders.join(', '));
|
|
859
|
+
}
|
|
860
|
+
if (maxAge !== undefined) {
|
|
861
|
+
headers.set('Access-Control-Max-Age', String(maxAge));
|
|
862
|
+
}
|
|
863
|
+
return new Response(response.body, {
|
|
864
|
+
status: response.status,
|
|
865
|
+
statusText: response.statusText,
|
|
866
|
+
headers,
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Handle preflight OPTIONS request
|
|
871
|
+
*/
|
|
872
|
+
function handlePreflight(request) {
|
|
873
|
+
if (request.method !== 'OPTIONS') {
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
876
|
+
if (!options?.cors) {
|
|
877
|
+
return new Response(null, { status: 204 });
|
|
878
|
+
}
|
|
879
|
+
const origin = request.headers.get('Origin');
|
|
880
|
+
const { origins, methods, allowedHeaders, maxAge, credentials } = options.cors;
|
|
881
|
+
const isAllowed = origins.includes('*') || (origin && origins.includes(origin));
|
|
882
|
+
if (!isAllowed) {
|
|
883
|
+
return new Response(null, { status: 204 });
|
|
884
|
+
}
|
|
885
|
+
const headers = {
|
|
886
|
+
'Access-Control-Allow-Origin': origins.includes('*') ? '*' : origin,
|
|
887
|
+
'Access-Control-Allow-Methods': (methods || ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']).join(', '),
|
|
888
|
+
};
|
|
889
|
+
if (credentials) {
|
|
890
|
+
headers['Access-Control-Allow-Credentials'] = 'true';
|
|
891
|
+
}
|
|
892
|
+
if (allowedHeaders) {
|
|
893
|
+
headers['Access-Control-Allow-Headers'] = allowedHeaders.join(', ');
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
const requestedHeaders = request.headers.get('Access-Control-Request-Headers');
|
|
897
|
+
if (requestedHeaders) {
|
|
898
|
+
headers['Access-Control-Allow-Headers'] = requestedHeaders;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (maxAge !== undefined) {
|
|
902
|
+
headers['Access-Control-Max-Age'] = String(maxAge);
|
|
903
|
+
}
|
|
904
|
+
return new Response(null, { status: 204, headers });
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Generate ETag from data
|
|
908
|
+
*/
|
|
909
|
+
function generateETag(data) {
|
|
910
|
+
const content = JSON.stringify(data);
|
|
911
|
+
let hash = 0;
|
|
912
|
+
for (let i = 0; i < content.length; i++) {
|
|
913
|
+
const char = content.charCodeAt(i);
|
|
914
|
+
hash = ((hash << 5) - hash) + char;
|
|
915
|
+
hash = hash & hash;
|
|
916
|
+
}
|
|
917
|
+
return `"${Math.abs(hash).toString(16)}"`;
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Main fetch handler
|
|
921
|
+
*/
|
|
922
|
+
async function fetch(request) {
|
|
923
|
+
const url = new URL(request.url);
|
|
924
|
+
const method = request.method.toUpperCase();
|
|
925
|
+
// Check URL length
|
|
926
|
+
if (url.href.length > 8192) {
|
|
927
|
+
return new Response(JSON.stringify({ error: { message: 'URI Too Long', code: 'URI_TOO_LONG' } }), {
|
|
928
|
+
status: 414,
|
|
929
|
+
headers: { 'Content-Type': 'application/json' },
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
// Handle CORS preflight
|
|
933
|
+
const preflightResponse = handlePreflight(request);
|
|
934
|
+
if (preflightResponse) {
|
|
935
|
+
return preflightResponse;
|
|
936
|
+
}
|
|
937
|
+
// Apply middleware
|
|
938
|
+
let currentRequest = request;
|
|
939
|
+
const middlewareChain = [...middleware];
|
|
940
|
+
async function executeMiddleware(index) {
|
|
941
|
+
if (index < middlewareChain.length) {
|
|
942
|
+
const mw = middlewareChain[index];
|
|
943
|
+
return mw(currentRequest, () => executeMiddleware(index + 1));
|
|
944
|
+
}
|
|
945
|
+
return handleRequest(currentRequest);
|
|
946
|
+
}
|
|
947
|
+
const response = await executeMiddleware(0);
|
|
948
|
+
return handleCors(request, response);
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Handle the actual request after middleware
|
|
952
|
+
*/
|
|
953
|
+
async function handleRequest(request) {
|
|
954
|
+
const url = new URL(request.url);
|
|
955
|
+
const method = request.method.toUpperCase();
|
|
956
|
+
const path = url.pathname;
|
|
957
|
+
// Find matching route
|
|
958
|
+
let matchedRoute = null;
|
|
959
|
+
let pathParams = null;
|
|
960
|
+
const matchingPaths = [];
|
|
961
|
+
for (const route of routesWithFullPath) {
|
|
962
|
+
const params = parseRouteParams(route.fullPath, path);
|
|
963
|
+
if (params !== null) {
|
|
964
|
+
if (!matchingPaths.find((m) => m.route.fullPath === route.fullPath)) {
|
|
965
|
+
matchingPaths.push({ route, methods: [route.httpMethod] });
|
|
966
|
+
}
|
|
967
|
+
else {
|
|
968
|
+
const existing = matchingPaths.find((m) => m.route.fullPath === route.fullPath);
|
|
969
|
+
existing.methods.push(route.httpMethod);
|
|
970
|
+
}
|
|
971
|
+
if (route.httpMethod === method) {
|
|
972
|
+
matchedRoute = route;
|
|
973
|
+
pathParams = params;
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
// Handle HEAD requests automatically
|
|
979
|
+
if (method === 'HEAD' && !matchedRoute) {
|
|
980
|
+
for (const route of routesWithFullPath) {
|
|
981
|
+
if (route.httpMethod === 'GET') {
|
|
982
|
+
const params = parseRouteParams(route.fullPath, path);
|
|
983
|
+
if (params !== null) {
|
|
984
|
+
matchedRoute = { ...route, httpMethod: 'HEAD' };
|
|
985
|
+
pathParams = params;
|
|
986
|
+
break;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
// Handle OPTIONS for specific path
|
|
992
|
+
if (method === 'OPTIONS' && matchingPaths.length > 0) {
|
|
993
|
+
return new Response(null, { status: 204 });
|
|
994
|
+
}
|
|
995
|
+
// 404 if no route found
|
|
996
|
+
if (!matchedRoute || !pathParams) {
|
|
997
|
+
if (matchingPaths.length > 0) {
|
|
998
|
+
// Method not allowed
|
|
999
|
+
const allowedMethods = [...new Set(matchingPaths.flatMap((m) => m.methods))];
|
|
1000
|
+
return new Response(JSON.stringify({
|
|
1001
|
+
error: {
|
|
1002
|
+
message: 'Method Not Allowed',
|
|
1003
|
+
code: 'METHOD_NOT_ALLOWED',
|
|
1004
|
+
},
|
|
1005
|
+
}), {
|
|
1006
|
+
status: 405,
|
|
1007
|
+
headers: {
|
|
1008
|
+
'Content-Type': 'application/json',
|
|
1009
|
+
'Allow': allowedMethods.join(', '),
|
|
1010
|
+
},
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
return new Response(JSON.stringify({
|
|
1014
|
+
error: {
|
|
1015
|
+
message: 'Not Found',
|
|
1016
|
+
code: 'NOT_FOUND',
|
|
1017
|
+
},
|
|
1018
|
+
}), { status: 404, headers: { 'Content-Type': 'application/json' } });
|
|
1019
|
+
}
|
|
1020
|
+
// Check Accept header for content negotiation
|
|
1021
|
+
const accept = request.headers.get('Accept') || '*/*';
|
|
1022
|
+
if (matchedRoute.produces && matchedRoute.produces.length > 0) {
|
|
1023
|
+
const acceptTypes = accept.split(',').map((t) => t.split(';')[0].trim());
|
|
1024
|
+
const hasMatch = acceptTypes.some((t) => t === '*/*' || matchedRoute.produces.includes(t));
|
|
1025
|
+
if (!hasMatch && accept !== '*/*') {
|
|
1026
|
+
return new Response(JSON.stringify({
|
|
1027
|
+
error: {
|
|
1028
|
+
message: 'Not Acceptable',
|
|
1029
|
+
code: 'NOT_ACCEPTABLE',
|
|
1030
|
+
},
|
|
1031
|
+
}), { status: 406, headers: { 'Content-Type': 'application/json' } });
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
// Check Content-Type for POST/PUT/PATCH
|
|
1035
|
+
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
1036
|
+
const contentType = request.headers.get('Content-Type') || '';
|
|
1037
|
+
if (matchedRoute.consumes && matchedRoute.consumes.length > 0) {
|
|
1038
|
+
const baseContentType = contentType.split(';')[0].trim();
|
|
1039
|
+
const isSupported = matchedRoute.consumes.some((c) => {
|
|
1040
|
+
if (c === 'multipart/form-data' && baseContentType.includes('multipart/form-data')) {
|
|
1041
|
+
return true;
|
|
1042
|
+
}
|
|
1043
|
+
return baseContentType.includes(c);
|
|
1044
|
+
});
|
|
1045
|
+
if (!isSupported && baseContentType) {
|
|
1046
|
+
return new Response(JSON.stringify({
|
|
1047
|
+
error: {
|
|
1048
|
+
message: 'Unsupported Media Type',
|
|
1049
|
+
code: 'UNSUPPORTED_MEDIA_TYPE',
|
|
1050
|
+
},
|
|
1051
|
+
}), { status: 415, headers: { 'Content-Type': 'application/json' } });
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
// Authentication
|
|
1056
|
+
const authContext = await authenticate(request);
|
|
1057
|
+
if (matchedRoute.auth === true && !authContext) {
|
|
1058
|
+
return new Response(JSON.stringify({
|
|
1059
|
+
error: {
|
|
1060
|
+
message: 'Unauthorized',
|
|
1061
|
+
code: 'UNAUTHORIZED',
|
|
1062
|
+
},
|
|
1063
|
+
}), {
|
|
1064
|
+
status: 401,
|
|
1065
|
+
headers: {
|
|
1066
|
+
'Content-Type': 'application/json',
|
|
1067
|
+
'WWW-Authenticate': 'Bearer',
|
|
1068
|
+
},
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
// Authorization
|
|
1072
|
+
const authResult = checkAuthorization(authContext, matchedRoute);
|
|
1073
|
+
if (!authResult.authorized) {
|
|
1074
|
+
return new Response(JSON.stringify({
|
|
1075
|
+
error: {
|
|
1076
|
+
message: 'Forbidden',
|
|
1077
|
+
code: 'FORBIDDEN',
|
|
1078
|
+
reason: authResult.reason,
|
|
1079
|
+
},
|
|
1080
|
+
}), { status: 403, headers: { 'Content-Type': 'application/json' } });
|
|
1081
|
+
}
|
|
1082
|
+
// Rate limiting
|
|
1083
|
+
if (matchedRoute.rateLimit) {
|
|
1084
|
+
const rateLimitKey = getRateLimitKey(request, matchedRoute.rateLimit, matchedRoute.fullPath, authContext);
|
|
1085
|
+
const rateLimitResult = checkRateLimit(rateLimitKey, matchedRoute.rateLimit);
|
|
1086
|
+
const rateLimitHeaders = {
|
|
1087
|
+
'X-RateLimit-Limit': String(matchedRoute.rateLimit.requests),
|
|
1088
|
+
'X-RateLimit-Remaining': String(rateLimitResult.remaining),
|
|
1089
|
+
'X-RateLimit-Reset': String(Math.floor(rateLimitResult.reset / 1000)),
|
|
1090
|
+
};
|
|
1091
|
+
if (!rateLimitResult.allowed) {
|
|
1092
|
+
return new Response(JSON.stringify({
|
|
1093
|
+
error: {
|
|
1094
|
+
message: 'Too Many Requests',
|
|
1095
|
+
code: 'RATE_LIMIT_EXCEEDED',
|
|
1096
|
+
retryAfter: rateLimitResult.retryAfter,
|
|
1097
|
+
},
|
|
1098
|
+
}), {
|
|
1099
|
+
status: 429,
|
|
1100
|
+
headers: {
|
|
1101
|
+
'Content-Type': 'application/json',
|
|
1102
|
+
'Retry-After': String(rateLimitResult.retryAfter),
|
|
1103
|
+
...rateLimitHeaders,
|
|
1104
|
+
},
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
// Throttling
|
|
1109
|
+
if (matchedRoute.throttle) {
|
|
1110
|
+
const throttleKey = getRateLimitKey(request, { requests: 0, windowMs: matchedRoute.throttle.windowMs, keyBy: 'ip' }, matchedRoute.fullPath, authContext);
|
|
1111
|
+
const throttleResult = checkThrottle(throttleKey, matchedRoute.throttle);
|
|
1112
|
+
if (!throttleResult.allowed) {
|
|
1113
|
+
return new Response(JSON.stringify({
|
|
1114
|
+
error: {
|
|
1115
|
+
message: 'Too Many Requests',
|
|
1116
|
+
code: 'THROTTLED',
|
|
1117
|
+
retryAfter: throttleResult.retryAfter,
|
|
1118
|
+
},
|
|
1119
|
+
}), {
|
|
1120
|
+
status: 429,
|
|
1121
|
+
headers: {
|
|
1122
|
+
'Content-Type': 'application/json',
|
|
1123
|
+
'Retry-After': String(throttleResult.retryAfter),
|
|
1124
|
+
},
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
// Parse request body for POST/PUT/PATCH
|
|
1129
|
+
let body = {};
|
|
1130
|
+
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
1131
|
+
try {
|
|
1132
|
+
body = await deserializeRequest(request);
|
|
1133
|
+
}
|
|
1134
|
+
catch (error) {
|
|
1135
|
+
return new Response(JSON.stringify({
|
|
1136
|
+
error: {
|
|
1137
|
+
message: error.message,
|
|
1138
|
+
code: 'INVALID_REQUEST_BODY',
|
|
1139
|
+
},
|
|
1140
|
+
}), { status: 400, headers: { 'Content-Type': 'application/json' } });
|
|
1141
|
+
}
|
|
1142
|
+
// Validate against schema
|
|
1143
|
+
if (matchedRoute.schema) {
|
|
1144
|
+
const validation = validateSchema(body, matchedRoute.schema);
|
|
1145
|
+
if (!validation.valid) {
|
|
1146
|
+
return new Response(JSON.stringify({
|
|
1147
|
+
error: {
|
|
1148
|
+
message: 'Validation failed',
|
|
1149
|
+
code: 'VALIDATION_ERROR',
|
|
1150
|
+
errors: validation.errors,
|
|
1151
|
+
},
|
|
1152
|
+
}), { status: 400, headers: { 'Content-Type': 'application/json' } });
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
// Parse query parameters
|
|
1157
|
+
const queryParams = {};
|
|
1158
|
+
for (const [key, value] of url.searchParams) {
|
|
1159
|
+
queryParams[key] = value;
|
|
1160
|
+
}
|
|
1161
|
+
// Build method arguments
|
|
1162
|
+
const args = [];
|
|
1163
|
+
// Add path params as first args
|
|
1164
|
+
const pathParamKeys = Object.keys(pathParams).filter((k) => pathParams[k] !== undefined);
|
|
1165
|
+
for (const key of pathParamKeys) {
|
|
1166
|
+
args.push(pathParams[key]);
|
|
1167
|
+
}
|
|
1168
|
+
// Add body for POST/PUT/PATCH
|
|
1169
|
+
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
1170
|
+
args.push(body);
|
|
1171
|
+
}
|
|
1172
|
+
// Add query params if configured
|
|
1173
|
+
if (matchedRoute.queryParams) {
|
|
1174
|
+
for (const param of matchedRoute.queryParams) {
|
|
1175
|
+
const value = queryParams[param];
|
|
1176
|
+
// Try to convert to number if it looks like one
|
|
1177
|
+
if (value !== undefined && /^\d+$/.test(value)) {
|
|
1178
|
+
args.push(parseInt(value, 10));
|
|
1179
|
+
}
|
|
1180
|
+
else {
|
|
1181
|
+
args.push(value);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
// Get request ID
|
|
1186
|
+
const requestId = request.headers.get('X-Request-ID');
|
|
1187
|
+
try {
|
|
1188
|
+
// Call the method
|
|
1189
|
+
const inst = getInstance();
|
|
1190
|
+
const methodFn = inst[matchedRoute.methodName];
|
|
1191
|
+
if (typeof methodFn !== 'function') {
|
|
1192
|
+
throw new Error(`Method ${matchedRoute.methodName} not found`);
|
|
1193
|
+
}
|
|
1194
|
+
const result = await methodFn.apply(inst, args);
|
|
1195
|
+
// Handle streaming responses
|
|
1196
|
+
if (matchedRoute.streaming && result && typeof result[Symbol.asyncIterator] === 'function') {
|
|
1197
|
+
const encoder = new TextEncoder();
|
|
1198
|
+
const stream = new ReadableStream({
|
|
1199
|
+
async start(controller) {
|
|
1200
|
+
for await (const chunk of result) {
|
|
1201
|
+
controller.enqueue(encoder.encode(JSON.stringify(chunk) + '\n'));
|
|
1202
|
+
}
|
|
1203
|
+
controller.close();
|
|
1204
|
+
},
|
|
1205
|
+
});
|
|
1206
|
+
return new Response(stream, {
|
|
1207
|
+
status: 200,
|
|
1208
|
+
headers: {
|
|
1209
|
+
'Content-Type': 'application/json',
|
|
1210
|
+
'Transfer-Encoding': 'chunked',
|
|
1211
|
+
},
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
// Handle conditional requests (ETag)
|
|
1215
|
+
if (matchedRoute.etag && method === 'GET') {
|
|
1216
|
+
const etag = generateETag(result);
|
|
1217
|
+
const ifNoneMatch = request.headers.get('If-None-Match');
|
|
1218
|
+
if (ifNoneMatch === etag) {
|
|
1219
|
+
return new Response(null, { status: 304, headers: { 'ETag': etag } });
|
|
1220
|
+
}
|
|
1221
|
+
const response = serializeResponse(result, { accept, method, produces: matchedRoute.produces });
|
|
1222
|
+
const headers = new Headers(response.headers);
|
|
1223
|
+
headers.set('ETag', etag);
|
|
1224
|
+
return new Response(response.body, { status: response.status, headers });
|
|
1225
|
+
}
|
|
1226
|
+
// Handle Last-Modified
|
|
1227
|
+
if (matchedRoute.lastModified && method === 'GET' && result && typeof result === 'object') {
|
|
1228
|
+
const updatedAt = result.updatedAt;
|
|
1229
|
+
if (updatedAt instanceof Date) {
|
|
1230
|
+
const lastModified = updatedAt.toUTCString();
|
|
1231
|
+
const ifModifiedSince = request.headers.get('If-Modified-Since');
|
|
1232
|
+
if (ifModifiedSince && new Date(ifModifiedSince) >= updatedAt) {
|
|
1233
|
+
return new Response(null, { status: 304, headers: { 'Last-Modified': lastModified } });
|
|
1234
|
+
}
|
|
1235
|
+
const response = serializeResponse(result, { accept, method, produces: matchedRoute.produces });
|
|
1236
|
+
const headers = new Headers(response.headers);
|
|
1237
|
+
headers.set('Last-Modified', lastModified);
|
|
1238
|
+
return new Response(response.body, { status: response.status, headers });
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
// Build response
|
|
1242
|
+
const response = serializeResponse(result, { accept, method, produces: matchedRoute.produces });
|
|
1243
|
+
const headers = new Headers(response.headers);
|
|
1244
|
+
// Add cache headers
|
|
1245
|
+
if (matchedRoute.cache && method === 'GET') {
|
|
1246
|
+
const cacheControl = [];
|
|
1247
|
+
if (matchedRoute.cache.maxAge !== undefined) {
|
|
1248
|
+
cacheControl.push(`max-age=${matchedRoute.cache.maxAge}`);
|
|
1249
|
+
}
|
|
1250
|
+
if (matchedRoute.cache.staleWhileRevalidate !== undefined) {
|
|
1251
|
+
cacheControl.push(`stale-while-revalidate=${matchedRoute.cache.staleWhileRevalidate}`);
|
|
1252
|
+
}
|
|
1253
|
+
if (cacheControl.length > 0) {
|
|
1254
|
+
headers.set('Cache-Control', cacheControl.join(', '));
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
else if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
|
|
1258
|
+
headers.set('Cache-Control', 'no-store');
|
|
1259
|
+
}
|
|
1260
|
+
// Add rate limit headers
|
|
1261
|
+
if (matchedRoute.rateLimit) {
|
|
1262
|
+
const rateLimitKey = getRateLimitKey(request, matchedRoute.rateLimit, matchedRoute.fullPath, authContext);
|
|
1263
|
+
const state = rateLimitState.get(rateLimitKey);
|
|
1264
|
+
if (state) {
|
|
1265
|
+
headers.set('X-RateLimit-Limit', String(matchedRoute.rateLimit.requests));
|
|
1266
|
+
headers.set('X-RateLimit-Remaining', String(Math.max(0, matchedRoute.rateLimit.requests - state.count)));
|
|
1267
|
+
headers.set('X-RateLimit-Reset', String(Math.floor((state.windowStart + matchedRoute.rateLimit.windowMs) / 1000)));
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
// Handle HEAD - return headers but no body
|
|
1271
|
+
if (method === 'HEAD') {
|
|
1272
|
+
const bodyText = await response.text();
|
|
1273
|
+
headers.set('Content-Length', String(new TextEncoder().encode(bodyText).length));
|
|
1274
|
+
return new Response(null, { status: response.status, headers });
|
|
1275
|
+
}
|
|
1276
|
+
return new Response(response.body, { status: response.status, headers });
|
|
1277
|
+
}
|
|
1278
|
+
catch (error) {
|
|
1279
|
+
const err = error;
|
|
1280
|
+
const restError = mapHttpError(err);
|
|
1281
|
+
const errorBody = {
|
|
1282
|
+
error: {
|
|
1283
|
+
message: restError.message,
|
|
1284
|
+
code: restError.code || err.name.toUpperCase().replace('ERROR', '').replace(/\s+/g, '_') || 'INTERNAL_ERROR',
|
|
1285
|
+
...(restError.errors && { errors: restError.errors }),
|
|
1286
|
+
...(restError.retryAfter && { retryAfter: restError.retryAfter }),
|
|
1287
|
+
...(requestId && { requestId }),
|
|
1288
|
+
...(debug && { stack: err.stack }),
|
|
1289
|
+
},
|
|
1290
|
+
};
|
|
1291
|
+
const headers = {
|
|
1292
|
+
'Content-Type': 'application/json',
|
|
1293
|
+
};
|
|
1294
|
+
if (restError.retryAfter) {
|
|
1295
|
+
headers['Retry-After'] = String(restError.retryAfter);
|
|
1296
|
+
}
|
|
1297
|
+
return new Response(JSON.stringify(errorBody), {
|
|
1298
|
+
status: restError.status,
|
|
1299
|
+
headers,
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Generate OpenAPI spec
|
|
1305
|
+
*/
|
|
1306
|
+
function getOpenAPISpec() {
|
|
1307
|
+
const className = DOClass.name || 'API';
|
|
1308
|
+
const spec = {
|
|
1309
|
+
openapi: '3.0.0',
|
|
1310
|
+
info: {
|
|
1311
|
+
title: `${className} API`,
|
|
1312
|
+
version: '1.0.0',
|
|
1313
|
+
},
|
|
1314
|
+
paths: {},
|
|
1315
|
+
};
|
|
1316
|
+
for (const route of routesWithFullPath) {
|
|
1317
|
+
// Convert path params from :param to {param}
|
|
1318
|
+
const openApiPath = route.fullPath.replace(/:(\w+)/g, '{$1}');
|
|
1319
|
+
if (!spec.paths[openApiPath]) {
|
|
1320
|
+
spec.paths[openApiPath] = {};
|
|
1321
|
+
}
|
|
1322
|
+
const operation = {
|
|
1323
|
+
summary: `${route.httpMethod} ${route.methodName}`,
|
|
1324
|
+
parameters: [],
|
|
1325
|
+
responses: {
|
|
1326
|
+
'200': { description: 'Success' },
|
|
1327
|
+
'201': { description: 'Created' },
|
|
1328
|
+
'400': { description: 'Bad Request' },
|
|
1329
|
+
'401': { description: 'Unauthorized' },
|
|
1330
|
+
'403': { description: 'Forbidden' },
|
|
1331
|
+
'404': { description: 'Not Found' },
|
|
1332
|
+
'500': { description: 'Internal Server Error' },
|
|
1333
|
+
},
|
|
1334
|
+
};
|
|
1335
|
+
// Add path parameters
|
|
1336
|
+
const pathParams = route.path.match(/:(\w+)/g) || [];
|
|
1337
|
+
for (const param of pathParams) {
|
|
1338
|
+
const paramName = param.slice(1);
|
|
1339
|
+
operation.parameters.push({
|
|
1340
|
+
name: paramName,
|
|
1341
|
+
in: 'path',
|
|
1342
|
+
required: true,
|
|
1343
|
+
schema: { type: 'string' },
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
// Add query parameters
|
|
1347
|
+
if (route.queryParams) {
|
|
1348
|
+
for (const param of route.queryParams) {
|
|
1349
|
+
operation.parameters.push({
|
|
1350
|
+
name: param,
|
|
1351
|
+
in: 'query',
|
|
1352
|
+
schema: { type: 'string' },
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
// Add request body for POST/PUT/PATCH
|
|
1357
|
+
if (['POST', 'PUT', 'PATCH'].includes(route.httpMethod)) {
|
|
1358
|
+
operation.requestBody = {
|
|
1359
|
+
required: true,
|
|
1360
|
+
content: {
|
|
1361
|
+
'application/json': {
|
|
1362
|
+
schema: route.schema || { type: 'object' },
|
|
1363
|
+
},
|
|
1364
|
+
},
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
// Add security requirements
|
|
1368
|
+
if (route.auth) {
|
|
1369
|
+
operation.security = [{ bearerAuth: [] }];
|
|
1370
|
+
// Add security scheme to components
|
|
1371
|
+
if (!spec.components) {
|
|
1372
|
+
spec.components = { securitySchemes: {} };
|
|
1373
|
+
}
|
|
1374
|
+
spec.components.securitySchemes.bearerAuth = {
|
|
1375
|
+
type: 'http',
|
|
1376
|
+
scheme: 'bearer',
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
// Add rate limit info
|
|
1380
|
+
if (route.rateLimit) {
|
|
1381
|
+
operation['x-ratelimit'] = {
|
|
1382
|
+
requests: route.rateLimit.requests,
|
|
1383
|
+
windowMs: route.rateLimit.windowMs,
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
spec.paths[openApiPath][route.httpMethod.toLowerCase()] = operation;
|
|
1387
|
+
}
|
|
1388
|
+
return spec;
|
|
1389
|
+
}
|
|
1390
|
+
return {
|
|
1391
|
+
fetch,
|
|
1392
|
+
routes: routesWithFullPath,
|
|
1393
|
+
middleware,
|
|
1394
|
+
getOpenAPISpec,
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
import { buildErrorResponse, } from './shared';
|
|
1398
|
+
/**
|
|
1399
|
+
* Cache for discovered routes per DO class
|
|
1400
|
+
*/
|
|
1401
|
+
const routeDiscoveryCache = new Map();
|
|
1402
|
+
/**
|
|
1403
|
+
* REST API Handler implementing TransportHandler interface
|
|
1404
|
+
*
|
|
1405
|
+
* Provides REST API functionality with:
|
|
1406
|
+
* - Automatic route discovery from DO class $rest config
|
|
1407
|
+
* - HTTP method mapping (GET, POST, PUT, DELETE, PATCH)
|
|
1408
|
+
* - Request/response serialization
|
|
1409
|
+
* - Schema validation
|
|
1410
|
+
* - Rate limiting
|
|
1411
|
+
* - CORS support
|
|
1412
|
+
*
|
|
1413
|
+
* @example
|
|
1414
|
+
* ```typescript
|
|
1415
|
+
* const restHandler = new RestHandler({
|
|
1416
|
+
* basePath: '/api',
|
|
1417
|
+
* debug: true,
|
|
1418
|
+
* })
|
|
1419
|
+
*
|
|
1420
|
+
* // Use in handler chain
|
|
1421
|
+
* chain.use(restHandler, 50)
|
|
1422
|
+
* ```
|
|
1423
|
+
*/
|
|
1424
|
+
export class RestHandler {
|
|
1425
|
+
name = 'rest';
|
|
1426
|
+
options;
|
|
1427
|
+
router = null;
|
|
1428
|
+
DOClass = null;
|
|
1429
|
+
constructor(options = {}) {
|
|
1430
|
+
this.options = options;
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Check if this handler can process the request
|
|
1434
|
+
* REST handler can handle requests that match configured routes
|
|
1435
|
+
*/
|
|
1436
|
+
canHandle(request) {
|
|
1437
|
+
const url = new URL(request.url);
|
|
1438
|
+
const basePath = this.options.basePath || this.options.apiPrefix || '';
|
|
1439
|
+
// Quick check: does the path start with our base path?
|
|
1440
|
+
if (basePath && !url.pathname.startsWith(basePath)) {
|
|
1441
|
+
return { canHandle: false, reason: 'Path does not match base path' };
|
|
1442
|
+
}
|
|
1443
|
+
// REST handler has lower priority than protocol-specific handlers
|
|
1444
|
+
return {
|
|
1445
|
+
canHandle: true,
|
|
1446
|
+
priority: 30,
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Handle the REST request
|
|
1451
|
+
*/
|
|
1452
|
+
async handle(request, context) {
|
|
1453
|
+
// Initialize router if not already done
|
|
1454
|
+
if (!this.router || this.needsRouterRefresh(context)) {
|
|
1455
|
+
this.initializeRouter(context);
|
|
1456
|
+
}
|
|
1457
|
+
if (!this.router) {
|
|
1458
|
+
return buildErrorResponse({ message: 'REST router not initialized', code: 'ROUTER_NOT_INITIALIZED' }, 500);
|
|
1459
|
+
}
|
|
1460
|
+
return this.router.fetch(request);
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Check if router needs to be refreshed (e.g., different DO class)
|
|
1464
|
+
*/
|
|
1465
|
+
needsRouterRefresh(context) {
|
|
1466
|
+
if (!this.DOClass)
|
|
1467
|
+
return true;
|
|
1468
|
+
return this.DOClass !== context.instance.constructor;
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Initialize the REST router from context
|
|
1472
|
+
*/
|
|
1473
|
+
initializeRouter(context) {
|
|
1474
|
+
this.DOClass = context.instance.constructor;
|
|
1475
|
+
this.router = createRestRouter(this.DOClass, {
|
|
1476
|
+
basePath: this.options.basePath,
|
|
1477
|
+
apiPrefix: this.options.apiPrefix,
|
|
1478
|
+
cors: this.options.cors,
|
|
1479
|
+
authHandler: this.options.authHandler,
|
|
1480
|
+
debug: this.options.debug,
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Get cached routes for a DO class
|
|
1485
|
+
*/
|
|
1486
|
+
static getCachedRoutes(DOClass) {
|
|
1487
|
+
if (routeDiscoveryCache.has(DOClass)) {
|
|
1488
|
+
return routeDiscoveryCache.get(DOClass);
|
|
1489
|
+
}
|
|
1490
|
+
const routes = getRestRoutes(DOClass);
|
|
1491
|
+
routeDiscoveryCache.set(DOClass, routes);
|
|
1492
|
+
return routes;
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Clear route cache
|
|
1496
|
+
*/
|
|
1497
|
+
static clearCache() {
|
|
1498
|
+
routeDiscoveryCache.clear();
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Dispose handler resources
|
|
1502
|
+
*/
|
|
1503
|
+
dispose() {
|
|
1504
|
+
this.router = null;
|
|
1505
|
+
this.DOClass = null;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
//# sourceMappingURL=rest-autowire.js.map
|