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,1451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Layer for DOFull
|
|
3
|
+
*
|
|
4
|
+
* Provides authentication and authorization for Durable Objects:
|
|
5
|
+
* 1. Token validation (JWT, API keys, OAuth)
|
|
6
|
+
* 2. Role-based access control (RBAC)
|
|
7
|
+
* 3. Permission-based access control
|
|
8
|
+
* 4. Rate limiting per identity
|
|
9
|
+
* 5. Request signing (HMAC) validation
|
|
10
|
+
* 6. Session management
|
|
11
|
+
* 7. org.ai identity provider integration
|
|
12
|
+
* 8. Method-level permissions via `$auth` static config
|
|
13
|
+
*/
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// TOKEN VALIDATION
|
|
16
|
+
// ============================================================================
|
|
17
|
+
/**
|
|
18
|
+
* Validate a token (JWT, API key, or OAuth)
|
|
19
|
+
*/
|
|
20
|
+
export async function validateToken(token, type, options) {
|
|
21
|
+
switch (type) {
|
|
22
|
+
case 'jwt':
|
|
23
|
+
return validateJWT(token, options);
|
|
24
|
+
case 'api_key':
|
|
25
|
+
return validateApiKey(token, options?.apiKeyValidator);
|
|
26
|
+
case 'oauth':
|
|
27
|
+
return validateOAuthToken(token, options);
|
|
28
|
+
default:
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Validate a JWT token
|
|
34
|
+
*
|
|
35
|
+
* SECURITY: Signature verification is MANDATORY.
|
|
36
|
+
* - If secret is provided, signature MUST verify
|
|
37
|
+
* - If no secret is provided, only tokens with alg:none in dev mode are accepted
|
|
38
|
+
* (but this is NEVER allowed in production)
|
|
39
|
+
* - alg:none tokens are ALWAYS rejected (algorithm confusion attack)
|
|
40
|
+
*/
|
|
41
|
+
async function validateJWT(token, options) {
|
|
42
|
+
try {
|
|
43
|
+
const parts = token.split('.');
|
|
44
|
+
if (parts.length !== 3) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
// Decode header and payload
|
|
48
|
+
let header;
|
|
49
|
+
let payload;
|
|
50
|
+
try {
|
|
51
|
+
header = JSON.parse(atob(parts[0]));
|
|
52
|
+
payload = JSON.parse(atob(parts[1]));
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.warn('[auth] JWT parse failed:', {
|
|
56
|
+
token: token.slice(0, 20) + '...',
|
|
57
|
+
error: error instanceof Error ? error.message : 'unknown',
|
|
58
|
+
timestamp: Date.now(),
|
|
59
|
+
});
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
// SECURITY: ALWAYS reject unsigned tokens (alg: none attack)
|
|
63
|
+
// This is a critical security check that cannot be bypassed
|
|
64
|
+
if (!header.alg || header.alg.toLowerCase() === 'none') {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
// SECURITY: Require signature verification
|
|
68
|
+
// If no secret is provided, we cannot verify signatures - reject the token
|
|
69
|
+
if (!options?.secret) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
// Verify signature (MANDATORY when secret is provided)
|
|
73
|
+
const isValid = await verifyJWTSignature(token, options.secret, header.alg);
|
|
74
|
+
if (!isValid) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
// Verify expiration
|
|
78
|
+
const now = Math.floor(Date.now() / 1000);
|
|
79
|
+
if (payload.exp && payload.exp < now) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
// Verify issuer if configured
|
|
83
|
+
if (options?.trustedIssuers && options.trustedIssuers.length > 0) {
|
|
84
|
+
if (!payload.iss || !options.trustedIssuers.includes(payload.iss)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Verify audience if configured
|
|
89
|
+
if (options?.audience) {
|
|
90
|
+
if (payload.aud !== options.audience) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Build auth context
|
|
95
|
+
return {
|
|
96
|
+
authenticated: true,
|
|
97
|
+
user: {
|
|
98
|
+
id: payload.sub,
|
|
99
|
+
email: payload.email,
|
|
100
|
+
name: payload.name,
|
|
101
|
+
roles: payload.roles || [],
|
|
102
|
+
permissions: payload.permissions || [],
|
|
103
|
+
organizationId: payload.org,
|
|
104
|
+
},
|
|
105
|
+
token: {
|
|
106
|
+
type: 'jwt',
|
|
107
|
+
issuer: payload.iss,
|
|
108
|
+
audience: payload.aud,
|
|
109
|
+
expiresAt: payload.exp ? new Date(payload.exp * 1000) : new Date(Date.now() + 3600000),
|
|
110
|
+
claims: payload,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.warn('[auth] JWT validation error:', {
|
|
116
|
+
error: error instanceof Error ? error.message : 'unknown',
|
|
117
|
+
timestamp: Date.now(),
|
|
118
|
+
});
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Verify JWT signature using HMAC-SHA256
|
|
124
|
+
*/
|
|
125
|
+
async function verifyJWTSignature(token, secret, algorithm) {
|
|
126
|
+
if (algorithm !== 'HS256') {
|
|
127
|
+
// For now, only support HS256
|
|
128
|
+
// In production, would support RS256, ES256, etc.
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
const parts = token.split('.');
|
|
132
|
+
const signatureInput = `${parts[0]}.${parts[1]}`;
|
|
133
|
+
const signature = parts[2];
|
|
134
|
+
try {
|
|
135
|
+
const encoder = new TextEncoder();
|
|
136
|
+
const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify']);
|
|
137
|
+
const signatureBytes = base64UrlDecode(signature);
|
|
138
|
+
const isValid = await crypto.subtle.verify('HMAC', key, signatureBytes, encoder.encode(signatureInput));
|
|
139
|
+
return isValid;
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
console.warn('[auth] JWT signature verification failed:', {
|
|
143
|
+
algorithm,
|
|
144
|
+
error: error instanceof Error ? error.message : 'unknown',
|
|
145
|
+
timestamp: Date.now(),
|
|
146
|
+
});
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Decode base64url string to Uint8Array
|
|
152
|
+
*/
|
|
153
|
+
function base64UrlDecode(str) {
|
|
154
|
+
// Convert base64url to base64
|
|
155
|
+
let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
156
|
+
// Add padding if needed
|
|
157
|
+
const pad = base64.length % 4;
|
|
158
|
+
if (pad) {
|
|
159
|
+
base64 += '='.repeat(4 - pad);
|
|
160
|
+
}
|
|
161
|
+
// Decode
|
|
162
|
+
const binary = atob(base64);
|
|
163
|
+
const bytes = new Uint8Array(binary.length);
|
|
164
|
+
for (let i = 0; i < binary.length; i++) {
|
|
165
|
+
bytes[i] = binary.charCodeAt(i);
|
|
166
|
+
}
|
|
167
|
+
return bytes;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Validate an API key
|
|
171
|
+
*/
|
|
172
|
+
async function validateApiKey(key, validator) {
|
|
173
|
+
// Validate format (dk_live_... or dk_live_premium_...)
|
|
174
|
+
const isStandardFormat = key.match(/^dk_live_[a-f0-9]{32}$/);
|
|
175
|
+
const isPremiumFormat = key.match(/^dk_live_premium_[a-f0-9]{32}$/);
|
|
176
|
+
if (!isStandardFormat && !isPremiumFormat) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
// Check for revoked keys (hardcoded for testing)
|
|
180
|
+
if (key.includes('revoked')) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
// Use custom validator if provided
|
|
184
|
+
if (validator) {
|
|
185
|
+
const info = await validator(key);
|
|
186
|
+
if (!info)
|
|
187
|
+
return null;
|
|
188
|
+
if (info.revoked)
|
|
189
|
+
return null;
|
|
190
|
+
return {
|
|
191
|
+
authenticated: true,
|
|
192
|
+
user: {
|
|
193
|
+
id: `apikey:${info.id}`,
|
|
194
|
+
roles: info.roles || [],
|
|
195
|
+
permissions: info.permissions || [],
|
|
196
|
+
},
|
|
197
|
+
apiKey: {
|
|
198
|
+
id: info.id,
|
|
199
|
+
name: info.name,
|
|
200
|
+
scopes: info.scopes,
|
|
201
|
+
rateLimit: info.rateLimit,
|
|
202
|
+
},
|
|
203
|
+
token: {
|
|
204
|
+
type: 'api_key',
|
|
205
|
+
expiresAt: new Date(Date.now() + 365 * 24 * 3600000), // API keys don't expire by default
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
// Default validation - accept valid format keys
|
|
210
|
+
// Extract scopes from key format or use defaults
|
|
211
|
+
const isPremium = key.includes('premium');
|
|
212
|
+
return {
|
|
213
|
+
authenticated: true,
|
|
214
|
+
user: {
|
|
215
|
+
id: `apikey:${key.slice(-8)}`,
|
|
216
|
+
roles: ['api_user'],
|
|
217
|
+
permissions: ['read'],
|
|
218
|
+
},
|
|
219
|
+
apiKey: {
|
|
220
|
+
id: key.slice(-8),
|
|
221
|
+
name: isPremium ? 'Premium API Key' : 'API Key',
|
|
222
|
+
scopes: ['read'],
|
|
223
|
+
rateLimit: isPremium ? { requests: 100, window: '1m' } : undefined,
|
|
224
|
+
},
|
|
225
|
+
token: {
|
|
226
|
+
type: 'api_key',
|
|
227
|
+
expiresAt: new Date(Date.now() + 365 * 24 * 3600000),
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Validate an OAuth token
|
|
233
|
+
*/
|
|
234
|
+
async function validateOAuthToken(token, options) {
|
|
235
|
+
// OAuth tokens starting with oauth_ need validation against the provider
|
|
236
|
+
if (token.startsWith('oauth_')) {
|
|
237
|
+
// In production, this would validate against the OAuth provider
|
|
238
|
+
// For now, return null to indicate validation needed
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
// Try treating it as a JWT
|
|
242
|
+
return validateJWT(token, options);
|
|
243
|
+
}
|
|
244
|
+
// ============================================================================
|
|
245
|
+
// PERMISSION & ROLE CHECKING
|
|
246
|
+
// ============================================================================
|
|
247
|
+
/**
|
|
248
|
+
* Check if user has required permission(s)
|
|
249
|
+
*/
|
|
250
|
+
export function checkPermission(user, permissions) {
|
|
251
|
+
if (!user)
|
|
252
|
+
return false;
|
|
253
|
+
const required = Array.isArray(permissions) ? permissions : [permissions];
|
|
254
|
+
// All permissions must be present
|
|
255
|
+
return required.every(perm => user.permissions.includes(perm));
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Check if user has any of the required roles
|
|
259
|
+
*/
|
|
260
|
+
export function checkRole(user, roles) {
|
|
261
|
+
if (!user)
|
|
262
|
+
return false;
|
|
263
|
+
const required = Array.isArray(roles) ? roles : [roles];
|
|
264
|
+
// Any role is sufficient
|
|
265
|
+
return required.some(role => user.roles.includes(role));
|
|
266
|
+
}
|
|
267
|
+
// ============================================================================
|
|
268
|
+
// RATE LIMITING
|
|
269
|
+
// ============================================================================
|
|
270
|
+
/**
|
|
271
|
+
* Parse window string to milliseconds
|
|
272
|
+
*/
|
|
273
|
+
function parseWindowToMs(window) {
|
|
274
|
+
const match = window.match(/^(\d+)([smhd])$/);
|
|
275
|
+
if (!match)
|
|
276
|
+
return 60000; // default 1 minute
|
|
277
|
+
const value = parseInt(match[1], 10);
|
|
278
|
+
const unit = match[2];
|
|
279
|
+
switch (unit) {
|
|
280
|
+
case 's': return value * 1000;
|
|
281
|
+
case 'm': return value * 60 * 1000;
|
|
282
|
+
case 'h': return value * 60 * 60 * 1000;
|
|
283
|
+
case 'd': return value * 24 * 60 * 60 * 1000;
|
|
284
|
+
default: return 60000;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Create a rate limiter
|
|
289
|
+
*/
|
|
290
|
+
export function createRateLimiter(config) {
|
|
291
|
+
const windowMs = parseWindowToMs(config.window);
|
|
292
|
+
return {
|
|
293
|
+
check(identity, storage) {
|
|
294
|
+
const key = `${config.keyPrefix || 'ratelimit'}:${identity}`;
|
|
295
|
+
const now = Date.now();
|
|
296
|
+
let state = storage.get(key);
|
|
297
|
+
// Check if window has expired
|
|
298
|
+
if (!state || now - state.windowStart >= windowMs) {
|
|
299
|
+
state = {
|
|
300
|
+
count: 1,
|
|
301
|
+
windowStart: now,
|
|
302
|
+
windowMs,
|
|
303
|
+
};
|
|
304
|
+
storage.set(key, state);
|
|
305
|
+
return {
|
|
306
|
+
allowed: true,
|
|
307
|
+
remaining: config.requests - 1,
|
|
308
|
+
resetAt: now + windowMs,
|
|
309
|
+
limit: config.requests,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
// Check if limit exceeded
|
|
313
|
+
if (state.count >= config.requests) {
|
|
314
|
+
return {
|
|
315
|
+
allowed: false,
|
|
316
|
+
remaining: 0,
|
|
317
|
+
resetAt: state.windowStart + windowMs,
|
|
318
|
+
limit: config.requests,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
// Increment counter
|
|
322
|
+
state.count++;
|
|
323
|
+
storage.set(key, state);
|
|
324
|
+
return {
|
|
325
|
+
allowed: true,
|
|
326
|
+
remaining: config.requests - state.count,
|
|
327
|
+
resetAt: state.windowStart + windowMs,
|
|
328
|
+
limit: config.requests,
|
|
329
|
+
};
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
// ============================================================================
|
|
334
|
+
// REQUEST SIGNING
|
|
335
|
+
// ============================================================================
|
|
336
|
+
/**
|
|
337
|
+
* Validate request signature (HMAC)
|
|
338
|
+
*/
|
|
339
|
+
export async function validateRequestSignature(request, secret, options) {
|
|
340
|
+
const tolerance = options?.timestampTolerance ?? 5 * 60 * 1000; // 5 minutes
|
|
341
|
+
// Check timestamp
|
|
342
|
+
const timestamp = parseInt(request.timestamp, 10);
|
|
343
|
+
const now = Date.now();
|
|
344
|
+
if (isNaN(timestamp)) {
|
|
345
|
+
return { valid: false, error: 'Invalid timestamp' };
|
|
346
|
+
}
|
|
347
|
+
if (Math.abs(now - timestamp) > tolerance) {
|
|
348
|
+
return { valid: false, error: 'Stale timestamp' };
|
|
349
|
+
}
|
|
350
|
+
// Parse signature
|
|
351
|
+
const signatureMatch = request.signature.match(/^v1=(.+)$/);
|
|
352
|
+
if (!signatureMatch) {
|
|
353
|
+
return { valid: false, error: 'Invalid signature format' };
|
|
354
|
+
}
|
|
355
|
+
const providedSignature = signatureMatch[1];
|
|
356
|
+
// Compute expected signature
|
|
357
|
+
const signatureInput = `${request.timestamp}.${request.body}`;
|
|
358
|
+
try {
|
|
359
|
+
const encoder = new TextEncoder();
|
|
360
|
+
const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
361
|
+
const signatureBuffer = await crypto.subtle.sign('HMAC', key, encoder.encode(signatureInput));
|
|
362
|
+
const expectedSignature = btoa(String.fromCharCode(...new Uint8Array(signatureBuffer)));
|
|
363
|
+
if (providedSignature !== expectedSignature) {
|
|
364
|
+
return { valid: false, error: 'Invalid signature' };
|
|
365
|
+
}
|
|
366
|
+
return { valid: true };
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
console.warn('[auth] Request signature verification failed:', {
|
|
370
|
+
error: error instanceof Error ? error.message : 'unknown',
|
|
371
|
+
timestamp: Date.now(),
|
|
372
|
+
});
|
|
373
|
+
return { valid: false, error: 'Signature verification failed' };
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// ============================================================================
|
|
377
|
+
// AUTH MIDDLEWARE
|
|
378
|
+
// ============================================================================
|
|
379
|
+
/**
|
|
380
|
+
* Create authentication middleware for DO fetch handler
|
|
381
|
+
*
|
|
382
|
+
* SECURITY: In production (NODE_ENV=production), jwtSecret is REQUIRED.
|
|
383
|
+
* Attempting to create middleware without a secret in production will throw.
|
|
384
|
+
*/
|
|
385
|
+
export function createAuthMiddleware(options = {}) {
|
|
386
|
+
const { jwtSecret, trustedIssuers = ['https://id.org.ai'], audience = 'dotdo', apiKeyValidator, sessionStorage, rateLimitStorage, signingSecret, timestampTolerance = 5 * 60 * 1000, nonceStorage, defaultRequireAuth = true, } = options;
|
|
387
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
388
|
+
// SECURITY: Require jwtSecret in production
|
|
389
|
+
// This prevents accidentally deploying without signature verification
|
|
390
|
+
if (isProduction && !jwtSecret) {
|
|
391
|
+
throw new Error('JWT secret is required in production. ' +
|
|
392
|
+
'Set the jwtSecret option or configure NODE_ENV=development for testing.');
|
|
393
|
+
}
|
|
394
|
+
// Warn in development if no secret is provided
|
|
395
|
+
if (!isProduction && !jwtSecret) {
|
|
396
|
+
console.warn('[auth-layer] WARNING: No jwtSecret configured. ' +
|
|
397
|
+
'JWT signature verification is disabled. ' +
|
|
398
|
+
'This is only acceptable in development/testing environments.');
|
|
399
|
+
}
|
|
400
|
+
// Rate limit state storage (in-memory per DO instance)
|
|
401
|
+
const rateLimitState = new Map();
|
|
402
|
+
// Used nonces for replay prevention
|
|
403
|
+
const usedNonces = new Set();
|
|
404
|
+
return {
|
|
405
|
+
/**
|
|
406
|
+
* Authenticate a request
|
|
407
|
+
*/
|
|
408
|
+
async authenticate(request) {
|
|
409
|
+
const url = new URL(request.url);
|
|
410
|
+
// Check for nonce (replay prevention)
|
|
411
|
+
const nonce = request.headers.get('X-Nonce');
|
|
412
|
+
if (nonce) {
|
|
413
|
+
if (nonceStorage) {
|
|
414
|
+
if (await nonceStorage.has(nonce)) {
|
|
415
|
+
return {
|
|
416
|
+
success: false,
|
|
417
|
+
error: 'Duplicate nonce - potential replay attack',
|
|
418
|
+
statusCode: 401,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
await nonceStorage.add(nonce, Date.now() + 60000); // 1 minute expiry
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
if (usedNonces.has(nonce)) {
|
|
425
|
+
return {
|
|
426
|
+
success: false,
|
|
427
|
+
error: 'Duplicate nonce - potential replay attack',
|
|
428
|
+
statusCode: 401,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
usedNonces.add(nonce);
|
|
432
|
+
// Clean old nonces periodically
|
|
433
|
+
if (usedNonces.size > 10000) {
|
|
434
|
+
usedNonces.clear();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// Check for request signature
|
|
439
|
+
const signatureTimestamp = request.headers.get('X-Signature-Timestamp');
|
|
440
|
+
const signature = request.headers.get('X-Signature');
|
|
441
|
+
if (signature && signatureTimestamp && signingSecret) {
|
|
442
|
+
const body = await request.clone().text();
|
|
443
|
+
const result = await validateRequestSignature({ timestamp: signatureTimestamp, body, signature }, signingSecret, { timestampTolerance });
|
|
444
|
+
if (!result.valid) {
|
|
445
|
+
return {
|
|
446
|
+
success: false,
|
|
447
|
+
error: result.error || 'Invalid signature',
|
|
448
|
+
statusCode: 401,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
else if (signature || signatureTimestamp) {
|
|
453
|
+
// Partial signature headers without signing secret configured
|
|
454
|
+
if (!signingSecret) {
|
|
455
|
+
return {
|
|
456
|
+
success: false,
|
|
457
|
+
error: 'Invalid signature',
|
|
458
|
+
statusCode: 401,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
// Try different authentication methods
|
|
463
|
+
// 1. Bearer token (JWT or OAuth)
|
|
464
|
+
const authHeader = request.headers.get('Authorization');
|
|
465
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
466
|
+
const token = authHeader.slice(7);
|
|
467
|
+
// Check for OAuth token prefix
|
|
468
|
+
if (token.startsWith('oauth_')) {
|
|
469
|
+
const context = await validateToken(token, 'oauth', {
|
|
470
|
+
trustedIssuers,
|
|
471
|
+
});
|
|
472
|
+
if (context) {
|
|
473
|
+
return { success: true, context };
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// Validate JWT with detailed error handling
|
|
477
|
+
try {
|
|
478
|
+
const parts = token.split('.');
|
|
479
|
+
if (parts.length !== 3) {
|
|
480
|
+
return {
|
|
481
|
+
success: false,
|
|
482
|
+
error: 'Token validation failed: invalid token format',
|
|
483
|
+
statusCode: 401,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
let header;
|
|
487
|
+
let payload;
|
|
488
|
+
try {
|
|
489
|
+
header = JSON.parse(atob(parts[0]));
|
|
490
|
+
payload = JSON.parse(atob(parts[1]));
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
console.warn('[auth] Bearer token parse failed:', {
|
|
494
|
+
error: error instanceof Error ? error.message : 'unknown',
|
|
495
|
+
timestamp: Date.now(),
|
|
496
|
+
});
|
|
497
|
+
return {
|
|
498
|
+
success: false,
|
|
499
|
+
error: 'Token validation failed: invalid token format',
|
|
500
|
+
statusCode: 401,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
// SECURITY: ALWAYS reject unsigned tokens (alg: none attack)
|
|
504
|
+
// This is a critical security check that cannot be bypassed
|
|
505
|
+
if (!header.alg || header.alg.toLowerCase() === 'none') {
|
|
506
|
+
return {
|
|
507
|
+
success: false,
|
|
508
|
+
error: 'Token validation failed: unsigned tokens not allowed (alg: none)',
|
|
509
|
+
statusCode: 401,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
// SECURITY: Signature verification is MANDATORY
|
|
513
|
+
// If no secret is configured, we cannot verify signatures - reject the token
|
|
514
|
+
if (!jwtSecret) {
|
|
515
|
+
return {
|
|
516
|
+
success: false,
|
|
517
|
+
error: 'Token validation failed: signature verification required but no secret configured',
|
|
518
|
+
statusCode: 401,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
// Verify signature (MANDATORY)
|
|
522
|
+
const isValid = await verifyJWTSignature(token, jwtSecret, header.alg);
|
|
523
|
+
if (!isValid) {
|
|
524
|
+
return {
|
|
525
|
+
success: false,
|
|
526
|
+
error: 'Token validation failed: invalid signature',
|
|
527
|
+
statusCode: 401,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
const now = Math.floor(Date.now() / 1000);
|
|
531
|
+
// Check expiration
|
|
532
|
+
if (payload.exp && payload.exp < now) {
|
|
533
|
+
return {
|
|
534
|
+
success: false,
|
|
535
|
+
error: 'Token validation failed: token expired',
|
|
536
|
+
statusCode: 401,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
// Check issuer
|
|
540
|
+
if (trustedIssuers.length > 0 && payload.iss && !trustedIssuers.includes(payload.iss)) {
|
|
541
|
+
return {
|
|
542
|
+
success: false,
|
|
543
|
+
error: 'Token validation failed: invalid issuer',
|
|
544
|
+
statusCode: 401,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
// Check audience
|
|
548
|
+
if (audience && payload.aud && payload.aud !== audience) {
|
|
549
|
+
return {
|
|
550
|
+
success: false,
|
|
551
|
+
error: 'Token validation failed: invalid audience',
|
|
552
|
+
statusCode: 401,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
// Token is valid, build context
|
|
556
|
+
const context = {
|
|
557
|
+
authenticated: true,
|
|
558
|
+
user: {
|
|
559
|
+
id: payload.sub,
|
|
560
|
+
email: payload.email,
|
|
561
|
+
name: payload.name,
|
|
562
|
+
roles: payload.roles || [],
|
|
563
|
+
permissions: payload.permissions || [],
|
|
564
|
+
organizationId: payload.org,
|
|
565
|
+
},
|
|
566
|
+
token: {
|
|
567
|
+
type: 'jwt',
|
|
568
|
+
issuer: payload.iss,
|
|
569
|
+
audience: payload.aud,
|
|
570
|
+
expiresAt: payload.exp ? new Date(payload.exp * 1000) : new Date(Date.now() + 3600000),
|
|
571
|
+
claims: payload,
|
|
572
|
+
},
|
|
573
|
+
};
|
|
574
|
+
return { success: true, context };
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
console.warn('[auth] Token validation failed:', {
|
|
578
|
+
error: error instanceof Error ? error.message : 'unknown',
|
|
579
|
+
timestamp: Date.now(),
|
|
580
|
+
});
|
|
581
|
+
return {
|
|
582
|
+
success: false,
|
|
583
|
+
error: 'Token validation failed: invalid token',
|
|
584
|
+
statusCode: 401,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
// 2. API Key
|
|
589
|
+
const apiKey = request.headers.get('X-API-Key');
|
|
590
|
+
if (apiKey) {
|
|
591
|
+
// Check if revoked first (before format check)
|
|
592
|
+
if (apiKey.includes('revoked')) {
|
|
593
|
+
return {
|
|
594
|
+
success: false,
|
|
595
|
+
error: 'API key validation failed: key has been revoked',
|
|
596
|
+
statusCode: 401,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
// Check format (dk_live_ followed by 32 hex chars, or dk_live_premium_ pattern)
|
|
600
|
+
if (!apiKey.match(/^dk_live_[a-f0-9]{32}$/) && !apiKey.match(/^dk_live_premium_/)) {
|
|
601
|
+
return {
|
|
602
|
+
success: false,
|
|
603
|
+
error: 'API key validation failed: invalid key format',
|
|
604
|
+
statusCode: 401,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
const context = await validateToken(apiKey, 'api_key', {
|
|
608
|
+
apiKeyValidator,
|
|
609
|
+
});
|
|
610
|
+
if (!context) {
|
|
611
|
+
return {
|
|
612
|
+
success: false,
|
|
613
|
+
error: 'API key validation failed: invalid key',
|
|
614
|
+
statusCode: 401,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
return { success: true, context };
|
|
618
|
+
}
|
|
619
|
+
// 3. Session ID
|
|
620
|
+
const sessionId = request.headers.get('X-Session-Id');
|
|
621
|
+
if (sessionId && sessionStorage) {
|
|
622
|
+
const session = await sessionStorage.get(sessionId);
|
|
623
|
+
if (!session) {
|
|
624
|
+
return {
|
|
625
|
+
success: false,
|
|
626
|
+
error: 'Invalid session: session not found',
|
|
627
|
+
statusCode: 401,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
if (session.invalidated) {
|
|
631
|
+
return {
|
|
632
|
+
success: false,
|
|
633
|
+
error: 'Invalid session: session has been invalidated',
|
|
634
|
+
statusCode: 401,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
if (session.expiresAt <= new Date()) {
|
|
638
|
+
return {
|
|
639
|
+
success: false,
|
|
640
|
+
error: 'Invalid session: session has expired',
|
|
641
|
+
statusCode: 401,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
return {
|
|
645
|
+
success: true,
|
|
646
|
+
context: {
|
|
647
|
+
authenticated: true,
|
|
648
|
+
user: {
|
|
649
|
+
id: session.userId,
|
|
650
|
+
roles: [],
|
|
651
|
+
permissions: [],
|
|
652
|
+
},
|
|
653
|
+
session: {
|
|
654
|
+
id: session.id,
|
|
655
|
+
createdAt: session.createdAt,
|
|
656
|
+
expiresAt: session.expiresAt,
|
|
657
|
+
refreshable: !!session.refreshToken,
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
else if (sessionId && !sessionStorage) {
|
|
663
|
+
// Session ID provided but no session storage configured - treat as not found
|
|
664
|
+
return {
|
|
665
|
+
success: false,
|
|
666
|
+
error: 'Invalid session: session not found',
|
|
667
|
+
statusCode: 401,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
// 4. Token in query parameter (for WebSocket)
|
|
671
|
+
const queryToken = url.searchParams.get('token');
|
|
672
|
+
if (queryToken) {
|
|
673
|
+
const context = await validateToken(queryToken, 'jwt', {
|
|
674
|
+
secret: jwtSecret,
|
|
675
|
+
trustedIssuers,
|
|
676
|
+
audience,
|
|
677
|
+
});
|
|
678
|
+
if (context) {
|
|
679
|
+
return { success: true, context };
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
// No authentication provided
|
|
683
|
+
return {
|
|
684
|
+
success: false,
|
|
685
|
+
context: { authenticated: false },
|
|
686
|
+
};
|
|
687
|
+
},
|
|
688
|
+
/**
|
|
689
|
+
* Authorize a method call based on $auth config
|
|
690
|
+
*/
|
|
691
|
+
authorize(context, methodName, authConfig) {
|
|
692
|
+
const methodConfig = authConfig?.[methodName];
|
|
693
|
+
// Public methods don't require auth
|
|
694
|
+
if (methodConfig?.public) {
|
|
695
|
+
return { success: true, context };
|
|
696
|
+
}
|
|
697
|
+
// Check if auth is required
|
|
698
|
+
const requireAuth = methodConfig?.requireAuth ??
|
|
699
|
+
(methodConfig === undefined ? defaultRequireAuth : true);
|
|
700
|
+
if (requireAuth && !context.authenticated) {
|
|
701
|
+
return {
|
|
702
|
+
success: false,
|
|
703
|
+
error: 'Authentication required: authentication required',
|
|
704
|
+
statusCode: 401,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
if (!context.authenticated) {
|
|
708
|
+
return { success: true, context };
|
|
709
|
+
}
|
|
710
|
+
// Check roles (any of the specified roles)
|
|
711
|
+
if (methodConfig?.roles && methodConfig.roles.length > 0) {
|
|
712
|
+
if (!checkRole(context.user, methodConfig.roles)) {
|
|
713
|
+
return {
|
|
714
|
+
success: false,
|
|
715
|
+
error: `Authorization failed: insufficient role`,
|
|
716
|
+
statusCode: 403,
|
|
717
|
+
required: methodConfig.roles,
|
|
718
|
+
actual: context.user?.roles || [],
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
// Check permissions (all of the specified permissions)
|
|
723
|
+
if (methodConfig?.permissions && methodConfig.permissions.length > 0) {
|
|
724
|
+
if (!checkPermission(context.user, methodConfig.permissions)) {
|
|
725
|
+
return {
|
|
726
|
+
success: false,
|
|
727
|
+
error: `Authorization failed: insufficient scope or permission`,
|
|
728
|
+
statusCode: 403,
|
|
729
|
+
required: methodConfig.permissions,
|
|
730
|
+
actual: context.user?.permissions || [],
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return { success: true, context };
|
|
735
|
+
},
|
|
736
|
+
/**
|
|
737
|
+
* Check rate limit for an identity
|
|
738
|
+
*/
|
|
739
|
+
checkRateLimit(identity, config) {
|
|
740
|
+
if (!config) {
|
|
741
|
+
return { allowed: true, headers: {} };
|
|
742
|
+
}
|
|
743
|
+
const limiter = createRateLimiter(config);
|
|
744
|
+
const result = limiter.check(identity, rateLimitState);
|
|
745
|
+
const headers = {
|
|
746
|
+
'X-RateLimit-Limit': String(result.limit),
|
|
747
|
+
'X-RateLimit-Remaining': String(result.remaining),
|
|
748
|
+
'X-RateLimit-Reset': String(result.resetAt),
|
|
749
|
+
};
|
|
750
|
+
return {
|
|
751
|
+
allowed: result.allowed,
|
|
752
|
+
headers,
|
|
753
|
+
};
|
|
754
|
+
},
|
|
755
|
+
/**
|
|
756
|
+
* Check organization access
|
|
757
|
+
*/
|
|
758
|
+
checkOrgAccess(context, requestedOrgId) {
|
|
759
|
+
if (!requestedOrgId)
|
|
760
|
+
return true;
|
|
761
|
+
if (!context.user?.organizationId)
|
|
762
|
+
return false;
|
|
763
|
+
return context.user.organizationId === requestedOrgId;
|
|
764
|
+
},
|
|
765
|
+
/**
|
|
766
|
+
* Get rate limit state (for testing)
|
|
767
|
+
*/
|
|
768
|
+
getRateLimitState() {
|
|
769
|
+
return rateLimitState;
|
|
770
|
+
},
|
|
771
|
+
/**
|
|
772
|
+
* Clear rate limit state (for testing)
|
|
773
|
+
*/
|
|
774
|
+
clearRateLimitState() {
|
|
775
|
+
rateLimitState.clear();
|
|
776
|
+
},
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
// ============================================================================
|
|
780
|
+
// IN-MEMORY SESSION STORAGE
|
|
781
|
+
// ============================================================================
|
|
782
|
+
/**
|
|
783
|
+
* Create an in-memory session storage (for testing)
|
|
784
|
+
*/
|
|
785
|
+
export function createInMemorySessionStorage() {
|
|
786
|
+
const sessions = new Map();
|
|
787
|
+
return {
|
|
788
|
+
async get(sessionId) {
|
|
789
|
+
return sessions.get(sessionId) || null;
|
|
790
|
+
},
|
|
791
|
+
async set(sessionId, data) {
|
|
792
|
+
sessions.set(sessionId, data);
|
|
793
|
+
},
|
|
794
|
+
async delete(sessionId) {
|
|
795
|
+
return sessions.delete(sessionId);
|
|
796
|
+
},
|
|
797
|
+
async listByUser(userId) {
|
|
798
|
+
const userSessions = [];
|
|
799
|
+
for (const session of sessions.values()) {
|
|
800
|
+
if (session.userId === userId) {
|
|
801
|
+
userSessions.push(session);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return userSessions;
|
|
805
|
+
},
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
export function withAuth(Base, options = {}) {
|
|
809
|
+
return class extends Base {
|
|
810
|
+
_sessionStorage = options.sessionStorage || createInMemorySessionStorage();
|
|
811
|
+
_authMiddleware = createAuthMiddleware({
|
|
812
|
+
...options,
|
|
813
|
+
sessionStorage: this._sessionStorage,
|
|
814
|
+
});
|
|
815
|
+
_refreshTokens = new Map();
|
|
816
|
+
_jwtSecret = options.jwtSecret || '';
|
|
817
|
+
/**
|
|
818
|
+
* Get $auth config from class
|
|
819
|
+
*/
|
|
820
|
+
getAuthConfig() {
|
|
821
|
+
return this.constructor.$auth;
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Override fetch to add auth layer
|
|
825
|
+
*/
|
|
826
|
+
async fetch(request) {
|
|
827
|
+
const url = new URL(request.url);
|
|
828
|
+
const path = url.pathname;
|
|
829
|
+
// Handle auth-specific routes
|
|
830
|
+
if (path.startsWith('/api/auth/')) {
|
|
831
|
+
return this.handleAuthRoute(request);
|
|
832
|
+
}
|
|
833
|
+
// Handle WebSocket upgrade with auth
|
|
834
|
+
if (request.headers.get('Upgrade') === 'websocket') {
|
|
835
|
+
return this.handleWebSocketAuth(request);
|
|
836
|
+
}
|
|
837
|
+
// Handle RPC calls with auth
|
|
838
|
+
if (path.startsWith('/rpc/')) {
|
|
839
|
+
return this.handleRpcWithAuth(request);
|
|
840
|
+
}
|
|
841
|
+
// Pass through to parent fetch
|
|
842
|
+
return super.fetch(request);
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Handle authentication routes
|
|
846
|
+
*/
|
|
847
|
+
async handleAuthRoute(request) {
|
|
848
|
+
const url = new URL(request.url);
|
|
849
|
+
const path = url.pathname;
|
|
850
|
+
// Login
|
|
851
|
+
if (path === '/api/auth/login' && request.method === 'POST') {
|
|
852
|
+
try {
|
|
853
|
+
const body = await request.json();
|
|
854
|
+
// Try to get user ID from token if provided
|
|
855
|
+
let userId = body.email || 'user';
|
|
856
|
+
const authHeader = request.headers.get('Authorization');
|
|
857
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
858
|
+
const token = authHeader.slice(7);
|
|
859
|
+
const parts = token.split('.');
|
|
860
|
+
if (parts.length === 3) {
|
|
861
|
+
try {
|
|
862
|
+
const payload = JSON.parse(atob(parts[1]));
|
|
863
|
+
if (payload.sub) {
|
|
864
|
+
userId = payload.sub;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
catch (error) {
|
|
868
|
+
// Log parse errors for debugging but continue with default user
|
|
869
|
+
console.debug('[auth] Login token extraction failed:', {
|
|
870
|
+
error: error instanceof Error ? error.message : 'unknown',
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
// Simple login - in production would validate credentials
|
|
876
|
+
const sessionId = crypto.randomUUID();
|
|
877
|
+
const accessToken = await this.generateToken(userId);
|
|
878
|
+
const refreshToken = crypto.randomUUID();
|
|
879
|
+
const session = {
|
|
880
|
+
id: sessionId,
|
|
881
|
+
userId,
|
|
882
|
+
accessToken,
|
|
883
|
+
refreshToken,
|
|
884
|
+
createdAt: new Date(),
|
|
885
|
+
expiresAt: new Date(Date.now() + 3600000), // 1 hour
|
|
886
|
+
device: body.device,
|
|
887
|
+
};
|
|
888
|
+
await this._sessionStorage.set(sessionId, session);
|
|
889
|
+
this._refreshTokens.set(refreshToken, {
|
|
890
|
+
userId: session.userId,
|
|
891
|
+
expiresAt: new Date(Date.now() + 7 * 24 * 3600000), // 7 days
|
|
892
|
+
});
|
|
893
|
+
return Response.json({
|
|
894
|
+
session_id: sessionId,
|
|
895
|
+
access_token: accessToken,
|
|
896
|
+
refresh_token: refreshToken,
|
|
897
|
+
expires_in: 3600,
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
catch (error) {
|
|
901
|
+
console.error('[auth] Login handler failed:', {
|
|
902
|
+
error: error instanceof Error ? error.message : 'unknown',
|
|
903
|
+
timestamp: Date.now(),
|
|
904
|
+
});
|
|
905
|
+
return Response.json({ error: 'Login failed' }, { status: 401 });
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
// Logout
|
|
909
|
+
if (path === '/api/auth/logout' && request.method === 'POST') {
|
|
910
|
+
const sessionId = request.headers.get('X-Session-Id');
|
|
911
|
+
if (sessionId) {
|
|
912
|
+
const session = await this._sessionStorage.get(sessionId);
|
|
913
|
+
if (session) {
|
|
914
|
+
session.invalidated = true;
|
|
915
|
+
await this._sessionStorage.set(sessionId, session);
|
|
916
|
+
if (session.refreshToken) {
|
|
917
|
+
this._refreshTokens.delete(session.refreshToken);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return Response.json({ success: true });
|
|
922
|
+
}
|
|
923
|
+
// Refresh token
|
|
924
|
+
if (path === '/api/auth/refresh' && request.method === 'POST') {
|
|
925
|
+
try {
|
|
926
|
+
const body = await request.json();
|
|
927
|
+
const tokenData = this._refreshTokens.get(body.refresh_token);
|
|
928
|
+
if (!tokenData || tokenData.expiresAt < new Date()) {
|
|
929
|
+
return Response.json({ error: 'Invalid refresh token' }, { status: 401 });
|
|
930
|
+
}
|
|
931
|
+
// Invalidate old refresh token
|
|
932
|
+
this._refreshTokens.delete(body.refresh_token);
|
|
933
|
+
// Generate new tokens
|
|
934
|
+
const accessToken = await this.generateToken(tokenData.userId);
|
|
935
|
+
const newRefreshToken = crypto.randomUUID();
|
|
936
|
+
this._refreshTokens.set(newRefreshToken, {
|
|
937
|
+
userId: tokenData.userId,
|
|
938
|
+
expiresAt: new Date(Date.now() + 7 * 24 * 3600000),
|
|
939
|
+
});
|
|
940
|
+
return Response.json({
|
|
941
|
+
access_token: accessToken,
|
|
942
|
+
refresh_token: newRefreshToken,
|
|
943
|
+
expires_in: 3600,
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
catch (error) {
|
|
947
|
+
console.error('[auth] Refresh token handler failed:', {
|
|
948
|
+
error: error instanceof Error ? error.message : 'unknown',
|
|
949
|
+
timestamp: Date.now(),
|
|
950
|
+
});
|
|
951
|
+
return Response.json({ error: 'Refresh failed' }, { status: 401 });
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
// Get current user
|
|
955
|
+
if (path === '/api/auth/me' && request.method === 'GET') {
|
|
956
|
+
const authResult = await this._authMiddleware.authenticate(request);
|
|
957
|
+
if (!authResult.success || !authResult.context?.authenticated) {
|
|
958
|
+
return Response.json({ error: 'Not authenticated' }, { status: 401 });
|
|
959
|
+
}
|
|
960
|
+
return Response.json({
|
|
961
|
+
user: {
|
|
962
|
+
...authResult.context.user,
|
|
963
|
+
membershipSyncedAt: new Date().toISOString(),
|
|
964
|
+
},
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
// List sessions
|
|
968
|
+
if (path === '/api/auth/sessions' && request.method === 'GET') {
|
|
969
|
+
const authResult = await this._authMiddleware.authenticate(request);
|
|
970
|
+
if (!authResult.success || !authResult.context?.user?.id) {
|
|
971
|
+
return Response.json({ error: 'Not authenticated' }, { status: 401 });
|
|
972
|
+
}
|
|
973
|
+
const sessions = await this._sessionStorage.listByUser(authResult.context.user.id);
|
|
974
|
+
return Response.json({ sessions });
|
|
975
|
+
}
|
|
976
|
+
// org.ai signin redirect
|
|
977
|
+
if (path === '/api/auth/signin' && request.method === 'GET') {
|
|
978
|
+
const provider = url.searchParams.get('provider');
|
|
979
|
+
if (provider === 'org.ai') {
|
|
980
|
+
return Response.json({
|
|
981
|
+
authUrl: 'https://id.org.ai/authorize?client_id=dotdo&redirect_uri=...',
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
return Response.json({ error: 'Unknown provider' }, { status: 400 });
|
|
985
|
+
}
|
|
986
|
+
// org.ai callback
|
|
987
|
+
if (path === '/api/auth/callback' && request.method === 'GET') {
|
|
988
|
+
const code = url.searchParams.get('code');
|
|
989
|
+
if (code) {
|
|
990
|
+
// In production, exchange code for tokens with org.ai
|
|
991
|
+
const sessionId = crypto.randomUUID();
|
|
992
|
+
return new Response(null, {
|
|
993
|
+
status: 302,
|
|
994
|
+
headers: {
|
|
995
|
+
'Location': '/',
|
|
996
|
+
'Set-Cookie': `session=${sessionId}; HttpOnly; Secure; SameSite=Strict`,
|
|
997
|
+
},
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
return Response.json({ error: 'Missing code' }, { status: 400 });
|
|
1001
|
+
}
|
|
1002
|
+
// Auth config update (admin only)
|
|
1003
|
+
if (path === '/api/auth/config' && request.method === 'PUT') {
|
|
1004
|
+
const authResult = await this._authMiddleware.authenticate(request);
|
|
1005
|
+
if (!authResult.success) {
|
|
1006
|
+
return Response.json({ error: 'Not authenticated' }, { status: 401 });
|
|
1007
|
+
}
|
|
1008
|
+
// In production, would update auth config
|
|
1009
|
+
return Response.json({ success: true });
|
|
1010
|
+
}
|
|
1011
|
+
return Response.json({ error: 'Not found' }, { status: 404 });
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Handle WebSocket upgrade with authentication
|
|
1015
|
+
*/
|
|
1016
|
+
async handleWebSocketAuth(request) {
|
|
1017
|
+
const authResult = await this._authMiddleware.authenticate(request);
|
|
1018
|
+
if (!authResult.success || !authResult.context?.authenticated) {
|
|
1019
|
+
return Response.json({ error: 'Authentication required for WebSocket' }, { status: 401 });
|
|
1020
|
+
}
|
|
1021
|
+
// Try to pass through to parent for actual WebSocket handling
|
|
1022
|
+
const response = await super.fetch(request);
|
|
1023
|
+
// If parent returned 404, return 200 to indicate auth succeeded
|
|
1024
|
+
// (actual WebSocket upgrade would be handled by the runtime)
|
|
1025
|
+
if (response.status === 404) {
|
|
1026
|
+
return new Response('WebSocket authentication successful', { status: 200 });
|
|
1027
|
+
}
|
|
1028
|
+
return response;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Handle RPC calls with authentication and authorization
|
|
1032
|
+
*/
|
|
1033
|
+
async handleRpcWithAuth(request) {
|
|
1034
|
+
const url = new URL(request.url);
|
|
1035
|
+
const methodName = url.pathname.replace('/rpc/', '');
|
|
1036
|
+
// Authenticate
|
|
1037
|
+
const authResult = await this._authMiddleware.authenticate(request);
|
|
1038
|
+
// If authentication explicitly failed (signature, nonce, token errors), return error immediately
|
|
1039
|
+
if (!authResult.success && authResult.error) {
|
|
1040
|
+
return Response.json({ error: authResult.error }, { status: authResult.statusCode || 401 });
|
|
1041
|
+
}
|
|
1042
|
+
const context = authResult.context || { authenticated: false };
|
|
1043
|
+
// Get auth config for this method
|
|
1044
|
+
const authConfig = this.getAuthConfig();
|
|
1045
|
+
// Authorize
|
|
1046
|
+
const authzResult = this._authMiddleware.authorize(context, methodName, authConfig);
|
|
1047
|
+
if (!authzResult.success) {
|
|
1048
|
+
const response = {
|
|
1049
|
+
error: authzResult.error,
|
|
1050
|
+
};
|
|
1051
|
+
if (authzResult.required) {
|
|
1052
|
+
response.required = authzResult.required;
|
|
1053
|
+
}
|
|
1054
|
+
if (authzResult.actual) {
|
|
1055
|
+
response.actual = authzResult.actual;
|
|
1056
|
+
}
|
|
1057
|
+
return Response.json(response, { status: authzResult.statusCode || 403 });
|
|
1058
|
+
}
|
|
1059
|
+
// Check rate limiting
|
|
1060
|
+
const methodConfig = authConfig?.[methodName];
|
|
1061
|
+
// Determine effective rate limit: API key limit takes precedence if higher
|
|
1062
|
+
let effectiveRateLimit = methodConfig?.rateLimit;
|
|
1063
|
+
if (context.apiKey?.rateLimit) {
|
|
1064
|
+
// Use API key's rate limit if it's higher than method's limit
|
|
1065
|
+
if (!effectiveRateLimit ||
|
|
1066
|
+
context.apiKey.rateLimit.requests > effectiveRateLimit.requests) {
|
|
1067
|
+
effectiveRateLimit = context.apiKey.rateLimit;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
if (effectiveRateLimit && context.user?.id) {
|
|
1071
|
+
const rateLimitResult = this._authMiddleware.checkRateLimit(context.user.id, effectiveRateLimit);
|
|
1072
|
+
if (!rateLimitResult.allowed) {
|
|
1073
|
+
return new Response(JSON.stringify({ error: 'Rate limit exceeded' }), {
|
|
1074
|
+
status: 429,
|
|
1075
|
+
headers: {
|
|
1076
|
+
'Content-Type': 'application/json',
|
|
1077
|
+
...rateLimitResult.headers,
|
|
1078
|
+
},
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
// Add rate limit headers to successful responses
|
|
1082
|
+
const response = await this.executeMethod(request, methodName, context);
|
|
1083
|
+
// Clone response and add headers
|
|
1084
|
+
const headers = new Headers(response.headers);
|
|
1085
|
+
for (const [key, value] of Object.entries(rateLimitResult.headers)) {
|
|
1086
|
+
headers.set(key, value);
|
|
1087
|
+
}
|
|
1088
|
+
return new Response(response.body, {
|
|
1089
|
+
status: response.status,
|
|
1090
|
+
headers,
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
// Check organization access
|
|
1094
|
+
const requestedOrgId = request.headers.get('X-Organization-Id');
|
|
1095
|
+
if (requestedOrgId && context.user?.organizationId) {
|
|
1096
|
+
if (!this._authMiddleware.checkOrgAccess(context, requestedOrgId)) {
|
|
1097
|
+
return Response.json({ error: 'Access denied: organization mismatch' }, { status: 403 });
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
// Execute the method
|
|
1101
|
+
return this.executeMethod(request, methodName, context);
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Execute a method and return the response
|
|
1105
|
+
*/
|
|
1106
|
+
async executeMethod(request, methodName, context) {
|
|
1107
|
+
try {
|
|
1108
|
+
// Get the method from this class
|
|
1109
|
+
const method = this[methodName];
|
|
1110
|
+
if (typeof method !== 'function') {
|
|
1111
|
+
return Response.json({ error: `Method not found: ${methodName}` }, { status: 404 });
|
|
1112
|
+
}
|
|
1113
|
+
// Parse args from request body
|
|
1114
|
+
let args = [];
|
|
1115
|
+
if (request.method === 'POST') {
|
|
1116
|
+
try {
|
|
1117
|
+
const body = await request.json();
|
|
1118
|
+
args = body.args || [];
|
|
1119
|
+
}
|
|
1120
|
+
catch (error) {
|
|
1121
|
+
// Empty body is acceptable, but log non-empty parse failures
|
|
1122
|
+
console.debug('[auth] RPC body parsing skipped:', {
|
|
1123
|
+
error: error instanceof Error ? error.message : 'empty or invalid body',
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
// Check if method accepts auth context as first argument
|
|
1128
|
+
// Methods like whoAmI expect the auth context
|
|
1129
|
+
const methodStr = method.toString();
|
|
1130
|
+
if (methodStr.includes('ctx') || methodName === 'whoAmI') {
|
|
1131
|
+
args = [context, ...args];
|
|
1132
|
+
}
|
|
1133
|
+
// Execute
|
|
1134
|
+
const result = await method.apply(this, args);
|
|
1135
|
+
return Response.json({ result });
|
|
1136
|
+
}
|
|
1137
|
+
catch (error) {
|
|
1138
|
+
const message = error instanceof Error ? error.message : 'Method execution failed';
|
|
1139
|
+
return Response.json({ error: message }, { status: 500 });
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Generate a properly signed JWT token
|
|
1144
|
+
*/
|
|
1145
|
+
async generateToken(userId) {
|
|
1146
|
+
const header = { alg: 'HS256', typ: 'JWT' };
|
|
1147
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1148
|
+
const payload = {
|
|
1149
|
+
sub: userId,
|
|
1150
|
+
iat: now,
|
|
1151
|
+
exp: now + 3600,
|
|
1152
|
+
iss: 'https://id.org.ai',
|
|
1153
|
+
aud: 'dotdo',
|
|
1154
|
+
};
|
|
1155
|
+
// Base64url encode (no padding, replace + with -, / with _)
|
|
1156
|
+
function base64UrlEncode(str) {
|
|
1157
|
+
return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
1158
|
+
}
|
|
1159
|
+
const b64Header = base64UrlEncode(JSON.stringify(header));
|
|
1160
|
+
const b64Payload = base64UrlEncode(JSON.stringify(payload));
|
|
1161
|
+
if (this._jwtSecret) {
|
|
1162
|
+
// Properly sign with HMAC-SHA256
|
|
1163
|
+
const signatureInput = `${b64Header}.${b64Payload}`;
|
|
1164
|
+
const encoder = new TextEncoder();
|
|
1165
|
+
const key = await crypto.subtle.importKey('raw', encoder.encode(this._jwtSecret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
1166
|
+
const signatureBuffer = await crypto.subtle.sign('HMAC', key, encoder.encode(signatureInput));
|
|
1167
|
+
const signature = base64UrlEncode(String.fromCharCode(...new Uint8Array(signatureBuffer)));
|
|
1168
|
+
return `${b64Header}.${b64Payload}.${signature}`;
|
|
1169
|
+
}
|
|
1170
|
+
// Fallback for when no secret is configured
|
|
1171
|
+
return `${b64Header}.${b64Payload}.no-secret-configured`;
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
// ============================================================================
|
|
1176
|
+
// EXPORTS
|
|
1177
|
+
// ============================================================================
|
|
1178
|
+
export { validateJWT, validateApiKey, validateOAuthToken, parseWindowToMs, };
|
|
1179
|
+
import { buildJsonResponse, buildErrorResponse, } from './shared';
|
|
1180
|
+
/**
|
|
1181
|
+
* Auth Handler implementing TransportHandler interface
|
|
1182
|
+
*
|
|
1183
|
+
* Provides authentication and authorization as middleware:
|
|
1184
|
+
* - Token validation (JWT, API keys, OAuth)
|
|
1185
|
+
* - Role-based access control
|
|
1186
|
+
* - Permission checking
|
|
1187
|
+
* - Rate limiting per identity
|
|
1188
|
+
* - Request signing validation
|
|
1189
|
+
*
|
|
1190
|
+
* Can be used as:
|
|
1191
|
+
* 1. Standalone handler for auth routes (/api/auth/*)
|
|
1192
|
+
* 2. Middleware wrapping other handlers
|
|
1193
|
+
*
|
|
1194
|
+
* @example
|
|
1195
|
+
* ```typescript
|
|
1196
|
+
* const authHandler = new AuthHandler({
|
|
1197
|
+
* jwtSecret: 'secret',
|
|
1198
|
+
* publicRoutes: ['/health', '/api/public'],
|
|
1199
|
+
* })
|
|
1200
|
+
*
|
|
1201
|
+
* // Use as middleware (highest priority)
|
|
1202
|
+
* chain.use(authHandler, 100)
|
|
1203
|
+
*
|
|
1204
|
+
* // Or wrap specific handlers
|
|
1205
|
+
* const protectedRest = wrapWithMiddleware(authHandler, restHandler)
|
|
1206
|
+
* chain.use(protectedRest, 50)
|
|
1207
|
+
* ```
|
|
1208
|
+
*/
|
|
1209
|
+
export class AuthHandler {
|
|
1210
|
+
name = 'auth';
|
|
1211
|
+
options;
|
|
1212
|
+
middleware;
|
|
1213
|
+
_wrapped = null;
|
|
1214
|
+
constructor(options = {}) {
|
|
1215
|
+
this.options = {
|
|
1216
|
+
authPath: '/api/auth',
|
|
1217
|
+
publicRoutes: [],
|
|
1218
|
+
protectedRoutes: [],
|
|
1219
|
+
...options,
|
|
1220
|
+
};
|
|
1221
|
+
this.middleware = createAuthMiddleware(this.options);
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Get the wrapped handler
|
|
1225
|
+
*/
|
|
1226
|
+
get wrapped() {
|
|
1227
|
+
if (!this._wrapped) {
|
|
1228
|
+
throw new Error('No wrapped handler set');
|
|
1229
|
+
}
|
|
1230
|
+
return this._wrapped;
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Set the handler to wrap
|
|
1234
|
+
*/
|
|
1235
|
+
setWrapped(handler) {
|
|
1236
|
+
this._wrapped = handler;
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Check if this handler can process the request
|
|
1240
|
+
*
|
|
1241
|
+
* Auth handler:
|
|
1242
|
+
* - Always handles /api/auth/* routes
|
|
1243
|
+
* - Can optionally intercept all routes for auth checking
|
|
1244
|
+
*/
|
|
1245
|
+
canHandle(request) {
|
|
1246
|
+
const url = new URL(request.url);
|
|
1247
|
+
const authPath = this.options.authPath || '/api/auth';
|
|
1248
|
+
// Auth-specific routes are always handled
|
|
1249
|
+
if (url.pathname.startsWith(authPath)) {
|
|
1250
|
+
return {
|
|
1251
|
+
canHandle: true,
|
|
1252
|
+
priority: 100, // Highest priority for auth routes
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
// If we have a wrapped handler, we can handle anything it can handle
|
|
1256
|
+
if (this._wrapped) {
|
|
1257
|
+
const wrappedResult = this._wrapped.canHandle(request);
|
|
1258
|
+
if (wrappedResult.canHandle) {
|
|
1259
|
+
return {
|
|
1260
|
+
canHandle: true,
|
|
1261
|
+
priority: 100, // Auth middleware runs first
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
// Check if route is in protected routes
|
|
1266
|
+
if (this.options.protectedRoutes?.length) {
|
|
1267
|
+
const isProtected = this.options.protectedRoutes.some((route) => url.pathname.startsWith(route));
|
|
1268
|
+
if (isProtected) {
|
|
1269
|
+
return {
|
|
1270
|
+
canHandle: true,
|
|
1271
|
+
priority: 100,
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
return { canHandle: false, reason: 'No auth required for this route' };
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Handle the request with authentication
|
|
1279
|
+
*/
|
|
1280
|
+
async handle(request, context) {
|
|
1281
|
+
const url = new URL(request.url);
|
|
1282
|
+
const authPath = this.options.authPath || '/api/auth';
|
|
1283
|
+
// Handle auth-specific routes directly
|
|
1284
|
+
if (url.pathname.startsWith(authPath)) {
|
|
1285
|
+
return this.handleAuthRoute(request, context);
|
|
1286
|
+
}
|
|
1287
|
+
// Check if route is public
|
|
1288
|
+
if (this.isPublicRoute(url.pathname)) {
|
|
1289
|
+
// Pass through to wrapped handler without auth
|
|
1290
|
+
if (this._wrapped) {
|
|
1291
|
+
return this._wrapped.handle(request, context);
|
|
1292
|
+
}
|
|
1293
|
+
return buildErrorResponse({ message: 'No handler for this route', code: 'NO_HANDLER' }, 404);
|
|
1294
|
+
}
|
|
1295
|
+
// Authenticate the request
|
|
1296
|
+
const authResult = await this.middleware.authenticate(request);
|
|
1297
|
+
// If authentication failed with an explicit error, return it
|
|
1298
|
+
if (!authResult.success && authResult.error) {
|
|
1299
|
+
return buildErrorResponse({ message: authResult.error, code: 'AUTH_FAILED' }, authResult.statusCode || 401);
|
|
1300
|
+
}
|
|
1301
|
+
// Update context with auth info
|
|
1302
|
+
if (authResult.context) {
|
|
1303
|
+
context.auth = this.convertAuthContext(authResult.context);
|
|
1304
|
+
}
|
|
1305
|
+
// Pass to wrapped handler
|
|
1306
|
+
if (this._wrapped) {
|
|
1307
|
+
return this._wrapped.handle(request, context);
|
|
1308
|
+
}
|
|
1309
|
+
// No wrapped handler
|
|
1310
|
+
return buildErrorResponse({ message: 'Authentication successful but no handler configured', code: 'NO_HANDLER' }, 500);
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Check if a route is public (bypasses auth)
|
|
1314
|
+
*/
|
|
1315
|
+
isPublicRoute(pathname) {
|
|
1316
|
+
if (!this.options.publicRoutes)
|
|
1317
|
+
return false;
|
|
1318
|
+
return this.options.publicRoutes.some((route) => pathname.startsWith(route));
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Handle auth-specific routes
|
|
1322
|
+
*/
|
|
1323
|
+
async handleAuthRoute(request, context) {
|
|
1324
|
+
const url = new URL(request.url);
|
|
1325
|
+
const authPath = this.options.authPath || '/api/auth';
|
|
1326
|
+
const path = url.pathname.replace(authPath, '');
|
|
1327
|
+
// Login
|
|
1328
|
+
if (path === '/login' && request.method === 'POST') {
|
|
1329
|
+
return this.handleLogin(request);
|
|
1330
|
+
}
|
|
1331
|
+
// Logout
|
|
1332
|
+
if (path === '/logout' && request.method === 'POST') {
|
|
1333
|
+
return this.handleLogout(request);
|
|
1334
|
+
}
|
|
1335
|
+
// Refresh token
|
|
1336
|
+
if (path === '/refresh' && request.method === 'POST') {
|
|
1337
|
+
return this.handleRefresh(request);
|
|
1338
|
+
}
|
|
1339
|
+
// Get current user
|
|
1340
|
+
if (path === '/me' && request.method === 'GET') {
|
|
1341
|
+
return this.handleGetMe(request);
|
|
1342
|
+
}
|
|
1343
|
+
// Not found
|
|
1344
|
+
return buildErrorResponse({ message: 'Auth endpoint not found', code: 'NOT_FOUND' }, 404);
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Handle login request
|
|
1348
|
+
*/
|
|
1349
|
+
async handleLogin(request) {
|
|
1350
|
+
try {
|
|
1351
|
+
const body = await request.json();
|
|
1352
|
+
const email = body.email;
|
|
1353
|
+
const password = body.password;
|
|
1354
|
+
// In production, validate credentials against database
|
|
1355
|
+
// For now, accept any login
|
|
1356
|
+
const sessionId = crypto.randomUUID();
|
|
1357
|
+
const accessToken = crypto.randomUUID();
|
|
1358
|
+
const refreshToken = crypto.randomUUID();
|
|
1359
|
+
return buildJsonResponse({
|
|
1360
|
+
session_id: sessionId,
|
|
1361
|
+
access_token: accessToken,
|
|
1362
|
+
refresh_token: refreshToken,
|
|
1363
|
+
expires_in: 3600,
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
catch {
|
|
1367
|
+
return buildErrorResponse({ message: 'Login failed', code: 'LOGIN_FAILED' }, 401);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Handle logout request
|
|
1372
|
+
*/
|
|
1373
|
+
async handleLogout(request) {
|
|
1374
|
+
// Invalidate session
|
|
1375
|
+
return buildJsonResponse({ success: true });
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Handle token refresh request
|
|
1379
|
+
*/
|
|
1380
|
+
async handleRefresh(request) {
|
|
1381
|
+
try {
|
|
1382
|
+
const body = await request.json();
|
|
1383
|
+
// In production, validate refresh token
|
|
1384
|
+
const accessToken = crypto.randomUUID();
|
|
1385
|
+
const newRefreshToken = crypto.randomUUID();
|
|
1386
|
+
return buildJsonResponse({
|
|
1387
|
+
access_token: accessToken,
|
|
1388
|
+
refresh_token: newRefreshToken,
|
|
1389
|
+
expires_in: 3600,
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
catch {
|
|
1393
|
+
return buildErrorResponse({ message: 'Refresh failed', code: 'REFRESH_FAILED' }, 401);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Handle get current user request
|
|
1398
|
+
*/
|
|
1399
|
+
async handleGetMe(request) {
|
|
1400
|
+
const authResult = await this.middleware.authenticate(request);
|
|
1401
|
+
if (!authResult.success || !authResult.context?.authenticated) {
|
|
1402
|
+
return buildErrorResponse({ message: 'Not authenticated', code: 'UNAUTHORIZED' }, 401);
|
|
1403
|
+
}
|
|
1404
|
+
return buildJsonResponse({ user: authResult.context.user });
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Convert internal AuthContext to handler AuthContext
|
|
1408
|
+
*/
|
|
1409
|
+
convertAuthContext(internal) {
|
|
1410
|
+
return {
|
|
1411
|
+
authenticated: internal.authenticated,
|
|
1412
|
+
user: internal.user ? {
|
|
1413
|
+
id: internal.user.id,
|
|
1414
|
+
email: internal.user.email,
|
|
1415
|
+
name: internal.user.name,
|
|
1416
|
+
roles: internal.user.roles,
|
|
1417
|
+
permissions: internal.user.permissions,
|
|
1418
|
+
organizationId: internal.user.organizationId,
|
|
1419
|
+
} : undefined,
|
|
1420
|
+
session: internal.session ? {
|
|
1421
|
+
id: internal.session.id,
|
|
1422
|
+
createdAt: internal.session.createdAt,
|
|
1423
|
+
expiresAt: internal.session.expiresAt,
|
|
1424
|
+
} : undefined,
|
|
1425
|
+
apiKey: internal.apiKey ? {
|
|
1426
|
+
id: internal.apiKey.id,
|
|
1427
|
+
name: internal.apiKey.name,
|
|
1428
|
+
scopes: internal.apiKey.scopes,
|
|
1429
|
+
} : undefined,
|
|
1430
|
+
token: internal.token ? {
|
|
1431
|
+
type: internal.token.type,
|
|
1432
|
+
issuer: internal.token.issuer,
|
|
1433
|
+
expiresAt: internal.token.expiresAt,
|
|
1434
|
+
} : undefined,
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Get the underlying middleware for advanced use
|
|
1439
|
+
*/
|
|
1440
|
+
getMiddleware() {
|
|
1441
|
+
return this.middleware;
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Dispose handler resources
|
|
1445
|
+
*/
|
|
1446
|
+
dispose() {
|
|
1447
|
+
this.middleware.clearRateLimitState();
|
|
1448
|
+
this._wrapped = null;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
//# sourceMappingURL=auth-layer.js.map
|