@pattern-stack/codegen 0.10.0 → 0.11.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/CHANGELOG.md +135 -0
- package/README.md +5 -5
- package/consumer-skills/codegen/SKILL.md +2 -2
- package/consumer-skills/events/typed-bus-and-outbox.md +1 -1
- package/consumer-skills/{sync → integration}/SKILL.md +29 -29
- package/consumer-skills/{sync → integration}/audit-and-detection.md +22 -22
- package/consumer-skills/{sync → integration}/change-sources-and-sinks.md +60 -60
- package/consumer-skills/subsystems/SKILL.md +64 -8
- package/consumer-skills/subsystems/wiring-and-order.md +7 -7
- package/dist/runtime/base-classes/index.d.ts +4 -4
- package/dist/runtime/base-classes/index.js +35 -35
- package/dist/runtime/base-classes/index.js.map +1 -1
- package/dist/runtime/base-classes/{synced-entity-repository.d.ts → integrated-entity-repository.d.ts} +15 -15
- package/dist/runtime/base-classes/{synced-entity-repository.js → integrated-entity-repository.js} +21 -21
- package/dist/runtime/base-classes/integrated-entity-repository.js.map +1 -0
- package/dist/runtime/base-classes/{synced-entity-service.d.ts → integrated-entity-service.d.ts} +6 -6
- package/dist/runtime/base-classes/{synced-entity-service.js → integrated-entity-service.js} +4 -4
- package/dist/runtime/base-classes/integrated-entity-service.js.map +1 -0
- package/dist/runtime/base-classes/{sync-upsert-config.d.ts → integration-upsert-config.d.ts} +13 -13
- package/dist/runtime/base-classes/integration-upsert-config.js +1 -0
- package/dist/runtime/base-classes/{junction-sync-repository.d.ts → junction-integration-repository.d.ts} +11 -11
- package/dist/runtime/base-classes/{junction-sync-repository.js → junction-integration-repository.js} +15 -15
- package/dist/runtime/base-classes/junction-integration-repository.js.map +1 -0
- package/dist/runtime/subsystems/auth/auth-oauth-state.schema.js.map +1 -1
- package/dist/runtime/subsystems/auth/auth.module.d.ts +4 -4
- package/dist/runtime/subsystems/auth/auth.module.js +3 -3
- package/dist/runtime/subsystems/auth/auth.module.js.map +1 -1
- package/dist/runtime/subsystems/auth/auth.tokens.d.ts +8 -8
- package/dist/runtime/subsystems/auth/auth.tokens.js +6 -6
- package/dist/runtime/subsystems/auth/auth.tokens.js.map +1 -1
- package/dist/runtime/subsystems/auth/backends/state-store.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/auth/controllers/auth.controller.d.ts +2 -2
- package/dist/runtime/subsystems/auth/controllers/auth.controller.js +3 -3
- package/dist/runtime/subsystems/auth/controllers/auth.controller.js.map +1 -1
- package/dist/runtime/subsystems/auth/index.d.ts +3 -3
- package/dist/runtime/subsystems/auth/index.js +40 -40
- package/dist/runtime/subsystems/auth/index.js.map +1 -1
- package/dist/runtime/subsystems/auth/middleware/requester-context.js.map +1 -1
- package/dist/runtime/subsystems/auth/protocols/auth-strategy.d.ts +3 -3
- package/dist/runtime/subsystems/auth/protocols/{integration-store.d.ts → connection-store.d.ts} +20 -20
- package/dist/runtime/subsystems/auth/protocols/connection-store.js +1 -0
- package/dist/runtime/subsystems/auth/protocols/provider-strategy.d.ts +3 -3
- package/dist/runtime/subsystems/auth/runtime/{integration-broken.error.d.ts → connection-broken.error.d.ts} +5 -5
- package/dist/runtime/subsystems/auth/runtime/connection-broken.error.js +19 -0
- package/dist/runtime/subsystems/auth/runtime/connection-broken.error.js.map +1 -0
- package/dist/runtime/subsystems/auth/runtime/oauth2-refresh.strategy.d.ts +10 -10
- package/dist/runtime/subsystems/auth/runtime/oauth2-refresh.strategy.js +28 -28
- package/dist/runtime/subsystems/auth/runtime/oauth2-refresh.strategy.js.map +1 -1
- package/dist/runtime/subsystems/auth/runtime/with-auth-retry.d.ts +1 -1
- package/dist/runtime/subsystems/auth/runtime/with-auth-retry.js +3 -3
- package/dist/runtime/subsystems/auth/runtime/with-auth-retry.js.map +1 -1
- package/dist/runtime/subsystems/bridge/bridge.module.d.ts +0 -1
- package/dist/runtime/subsystems/bridge/bridge.module.js +294 -710
- package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
- package/dist/runtime/subsystems/bridge/index.d.ts +0 -1
- package/dist/runtime/subsystems/bridge/index.js +248 -664
- package/dist/runtime/subsystems/bridge/index.js.map +1 -1
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +18 -10
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/events/events.module.js +43 -244
- package/dist/runtime/subsystems/events/events.module.js.map +1 -1
- package/dist/runtime/subsystems/events/index.d.ts +0 -1
- package/dist/runtime/subsystems/events/index.js +39 -241
- package/dist/runtime/subsystems/events/index.js.map +1 -1
- package/dist/runtime/subsystems/index.d.ts +7 -7
- package/dist/runtime/subsystems/index.js +222 -839
- package/dist/runtime/subsystems/index.js.map +1 -1
- package/dist/runtime/subsystems/{sync → integration}/build-change-source.d.ts +3 -3
- package/dist/runtime/subsystems/{sync → integration}/build-change-source.js +3 -3
- package/dist/runtime/subsystems/integration/build-change-source.js.map +1 -0
- package/dist/runtime/subsystems/{sync → integration}/deep-equal.differ.d.ts +2 -2
- package/dist/runtime/subsystems/{sync → integration}/deep-equal.differ.js +1 -1
- package/dist/runtime/subsystems/integration/deep-equal.differ.js.map +1 -0
- package/dist/runtime/subsystems/{sync → integration}/detection-config.schema.d.ts +3 -3
- package/dist/runtime/subsystems/{sync → integration}/detection-config.schema.js +1 -1
- package/dist/runtime/subsystems/integration/detection-config.schema.js.map +1 -0
- package/dist/runtime/subsystems/{sync/execute-sync.use-case.d.ts → integration/execute-integration.use-case.d.ts} +13 -13
- package/dist/runtime/subsystems/{sync/execute-sync.use-case.js → integration/execute-integration.use-case.js} +30 -30
- package/dist/runtime/subsystems/integration/execute-integration.use-case.js.map +1 -0
- package/dist/runtime/subsystems/integration/index.d.ts +28 -0
- package/dist/runtime/subsystems/{sync → integration}/index.js +171 -171
- package/dist/runtime/subsystems/integration/index.js.map +1 -0
- package/dist/runtime/subsystems/{sync/sync-audit.schema.d.ts → integration/integration-audit.schema.d.ts} +64 -64
- package/dist/runtime/subsystems/{sync/sync-audit.schema.js → integration/integration-audit.schema.js} +47 -47
- package/dist/runtime/subsystems/integration/integration-audit.schema.js.map +1 -0
- package/dist/runtime/subsystems/{sync/sync-change-source.protocol.d.ts → integration/integration-change-source.protocol.d.ts} +10 -10
- package/dist/runtime/subsystems/integration/integration-change-source.protocol.js +1 -0
- package/dist/runtime/subsystems/{sync/sync-cursor-store.drizzle-backend.d.ts → integration/integration-cursor-store.drizzle-backend.d.ts} +1 -1
- package/dist/runtime/subsystems/{sync/sync-cursor-store.drizzle-backend.js → integration/integration-cursor-store.drizzle-backend.js} +65 -65
- package/dist/runtime/subsystems/integration/integration-cursor-store.drizzle-backend.js.map +1 -0
- package/dist/runtime/subsystems/{sync/sync-cursor-store.memory-backend.d.ts → integration/integration-cursor-store.memory-backend.d.ts} +6 -6
- package/dist/runtime/subsystems/{sync/sync-cursor-store.memory-backend.js → integration/integration-cursor-store.memory-backend.js} +5 -5
- package/dist/runtime/subsystems/integration/integration-cursor-store.memory-backend.js.map +1 -0
- package/dist/runtime/subsystems/{sync/sync-cursor-store.protocol.d.ts → integration/integration-cursor-store.protocol.d.ts} +13 -13
- package/dist/runtime/subsystems/integration/integration-cursor-store.protocol.js +1 -0
- package/dist/runtime/subsystems/{sync/sync-errors.d.ts → integration/integration-errors.d.ts} +2 -2
- package/dist/runtime/subsystems/{sync/sync-errors.js → integration/integration-errors.js} +3 -3
- package/dist/runtime/subsystems/integration/integration-errors.js.map +1 -0
- package/dist/runtime/subsystems/{sync/sync-field-diff.protocol.d.ts → integration/integration-field-diff.protocol.d.ts} +2 -2
- package/dist/runtime/subsystems/{sync/sync-field-diff.protocol.js → integration/integration-field-diff.protocol.js} +2 -2
- package/dist/runtime/subsystems/integration/integration-field-diff.protocol.js.map +1 -0
- package/dist/runtime/subsystems/{sync/sync-loopback.protocol.d.ts → integration/integration-loopback.protocol.d.ts} +2 -2
- package/dist/runtime/subsystems/integration/integration-loopback.protocol.js +1 -0
- package/dist/runtime/subsystems/{sync/sync-middleware.protocol.d.ts → integration/integration-middleware.protocol.d.ts} +5 -5
- package/dist/runtime/subsystems/integration/integration-middleware.protocol.js +1 -0
- package/dist/runtime/subsystems/{sync/sync-run-recorder.drizzle-backend.d.ts → integration/integration-run-recorder.drizzle-backend.d.ts} +5 -5
- package/dist/runtime/subsystems/{sync/sync-run-recorder.drizzle-backend.js → integration/integration-run-recorder.drizzle-backend.js} +73 -73
- package/dist/runtime/subsystems/integration/integration-run-recorder.drizzle-backend.js.map +1 -0
- package/dist/runtime/subsystems/{sync/sync-run-recorder.memory-backend.d.ts → integration/integration-run-recorder.memory-backend.d.ts} +15 -15
- package/dist/runtime/subsystems/{sync/sync-run-recorder.memory-backend.js → integration/integration-run-recorder.memory-backend.js} +11 -11
- package/dist/runtime/subsystems/integration/integration-run-recorder.memory-backend.js.map +1 -0
- package/dist/runtime/subsystems/{sync/sync-run-recorder.protocol.d.ts → integration/integration-run-recorder.protocol.d.ts} +25 -25
- package/dist/runtime/subsystems/integration/integration-run-recorder.protocol.js +1 -0
- package/dist/runtime/subsystems/{sync/sync-sink.protocol.d.ts → integration/integration-sink.protocol.d.ts} +5 -5
- package/dist/runtime/subsystems/integration/integration-sink.protocol.js +1 -0
- package/dist/runtime/subsystems/{sync/sync.module.d.ts → integration/integration.module.d.ts} +24 -24
- package/dist/runtime/subsystems/{sync/sync.module.js → integration/integration.module.js} +132 -132
- package/dist/runtime/subsystems/integration/integration.module.js.map +1 -0
- package/dist/runtime/subsystems/integration/integration.tokens.d.ts +47 -0
- package/dist/runtime/subsystems/integration/integration.tokens.js +18 -0
- package/dist/runtime/subsystems/integration/integration.tokens.js.map +1 -0
- package/dist/runtime/subsystems/{sync → integration}/loopback.middleware.d.ts +5 -5
- package/dist/runtime/subsystems/{sync → integration}/loopback.middleware.js +1 -1
- package/dist/runtime/subsystems/integration/loopback.middleware.js.map +1 -0
- package/dist/runtime/subsystems/{sync → integration}/poll-change-source.d.ts +5 -5
- package/dist/runtime/subsystems/{sync → integration}/poll-change-source.js +1 -1
- package/dist/runtime/subsystems/integration/poll-change-source.js.map +1 -0
- package/dist/runtime/subsystems/{sync → integration}/webhook-change-source.d.ts +5 -5
- package/dist/runtime/subsystems/{sync → integration}/webhook-change-source.js +1 -1
- package/dist/runtime/subsystems/integration/webhook-change-source.js.map +1 -0
- package/dist/runtime/subsystems/jobs/bullmq.config.d.ts +22 -3
- package/dist/runtime/subsystems/jobs/bullmq.config.js.map +1 -1
- package/dist/runtime/subsystems/jobs/index.d.ts +1 -4
- package/dist/runtime/subsystems/jobs/index.js +87 -506
- package/dist/runtime/subsystems/jobs/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +3 -0
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +10 -3
- package/dist/runtime/subsystems/jobs/job-worker.module.js +248 -664
- package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
- package/dist/runtime/subsystems/jobs/jobs-domain.module.d.ts +0 -1
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js +89 -391
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
- package/dist/runtime/subsystems/observability/index.d.ts +4 -4
- package/dist/runtime/subsystems/observability/index.js +11 -11
- package/dist/runtime/subsystems/observability/index.js.map +1 -1
- package/dist/runtime/subsystems/observability/observability.module.d.ts +2 -2
- package/dist/runtime/subsystems/observability/observability.module.js +11 -11
- package/dist/runtime/subsystems/observability/observability.module.js.map +1 -1
- package/dist/runtime/subsystems/observability/observability.protocol.d.ts +11 -11
- package/dist/runtime/subsystems/observability/observability.service.d.ts +6 -6
- package/dist/runtime/subsystems/observability/observability.service.js +11 -11
- package/dist/runtime/subsystems/observability/observability.service.js.map +1 -1
- package/dist/runtime/subsystems/observability/observability.tokens.d.ts +1 -1
- package/dist/runtime/subsystems/observability/observability.tokens.js.map +1 -1
- package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.d.ts +3 -3
- package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.js.map +1 -1
- package/dist/runtime/subsystems/observability/reporters/index.d.ts +3 -3
- package/dist/runtime/subsystems/observability/reporters/index.js.map +1 -1
- package/dist/src/cli/index.js +412 -302
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/index.d.ts +22 -22
- package/dist/src/index.js +191 -191
- package/dist/src/index.js.map +1 -1
- package/examples/auth-integrations/README.md +32 -32
- package/examples/auth-integrations/definitions/entities/{integration.yaml → connection.yaml} +10 -10
- package/examples/auth-integrations/runtime/{integrations/adapters/integration-grant-sink.adapter.ts → connections/adapters/connection-grant-sink.adapter.ts} +7 -7
- package/examples/auth-integrations/runtime/{integrations/adapters/integration-reader.adapter.ts → connections/adapters/connection-reader.adapter.ts} +10 -10
- package/examples/auth-integrations/runtime/{integrations/adapters/integration-token-writer.adapter.ts → connections/adapters/connection-token-writer.adapter.ts} +11 -11
- package/examples/auth-integrations/runtime/connections/connections-auth.module.ts +81 -0
- package/examples/auth-integrations/runtime/{integrations/facade/integrations.service.ts → connections/facade/connections.service.ts} +35 -35
- package/examples/auth-integrations/runtime/{integrations → connections}/oauth/use-cases/create-or-update-from-oauth-grant.use-case.ts +11 -11
- package/examples/auth-integrations/runtime/{integrations/oauth/use-cases/disconnect-integration.use-case.ts → connections/oauth/use-cases/disconnect-connection.use-case.ts} +6 -6
- package/examples/auth-integrations/runtime/connections/oauth/use-cases/list-user-connections.use-case.ts +21 -0
- package/examples/auth-integrations/runtime/connections/oauth/use-cases/mark-connection-requires-reauth.use-case.ts +21 -0
- package/package.json +1 -1
- package/runtime/base-classes/index.ts +8 -8
- package/runtime/base-classes/{synced-entity-repository.ts → integrated-entity-repository.ts} +36 -36
- package/runtime/base-classes/{synced-entity-service.ts → integrated-entity-service.ts} +6 -6
- package/runtime/base-classes/{sync-upsert-config.ts → integration-upsert-config.ts} +12 -12
- package/runtime/base-classes/{junction-sync-repository.ts → junction-integration-repository.ts} +28 -28
- package/runtime/subsystems/auth/auth-oauth-state.schema.ts +1 -1
- package/runtime/subsystems/auth/auth.module.ts +4 -4
- package/runtime/subsystems/auth/auth.tokens.ts +7 -7
- package/runtime/subsystems/auth/controllers/auth.controller.ts +7 -7
- package/runtime/subsystems/auth/index.ts +19 -19
- package/runtime/subsystems/auth/protocols/auth-strategy.ts +3 -3
- package/runtime/subsystems/auth/protocols/{integration-store.ts → connection-store.ts} +19 -19
- package/runtime/subsystems/auth/protocols/provider-strategy.ts +2 -2
- package/runtime/subsystems/auth/runtime/{integration-broken.error.ts → connection-broken.error.ts} +5 -5
- package/runtime/subsystems/auth/runtime/oauth2-refresh.strategy.ts +35 -35
- package/runtime/subsystems/auth/runtime/with-auth-retry.ts +3 -3
- package/runtime/subsystems/events/event-bus.drizzle-backend.ts +32 -10
- package/runtime/subsystems/events/events.module.ts +38 -6
- package/runtime/subsystems/events/index.ts +7 -1
- package/runtime/subsystems/index.ts +11 -11
- package/runtime/subsystems/{sync → integration}/build-change-source.ts +3 -3
- package/runtime/subsystems/{sync → integration}/deep-equal.differ.ts +7 -7
- package/runtime/subsystems/{sync → integration}/detection-config.schema.ts +3 -3
- package/runtime/subsystems/{sync/execute-sync.use-case.ts → integration/execute-integration.use-case.ts} +40 -40
- package/runtime/subsystems/{sync → integration}/index.ts +47 -47
- package/runtime/subsystems/{sync/sync-audit.schema.ts → integration/integration-audit.schema.ts} +61 -61
- package/runtime/subsystems/{sync/sync-change-source.protocol.ts → integration/integration-change-source.protocol.ts} +9 -9
- package/runtime/subsystems/{sync/sync-cursor-store.drizzle-backend.ts → integration/integration-cursor-store.drizzle-backend.ts} +30 -30
- package/runtime/subsystems/{sync/sync-cursor-store.memory-backend.ts → integration/integration-cursor-store.memory-backend.ts} +9 -9
- package/runtime/subsystems/{sync/sync-cursor-store.protocol.ts → integration/integration-cursor-store.protocol.ts} +13 -13
- package/runtime/subsystems/{sync/sync-errors.ts → integration/integration-errors.ts} +3 -3
- package/runtime/subsystems/{sync/sync-field-diff.protocol.ts → integration/integration-field-diff.protocol.ts} +2 -2
- package/runtime/subsystems/{sync/sync-loopback.protocol.ts → integration/integration-loopback.protocol.ts} +2 -2
- package/runtime/subsystems/{sync/sync-middleware.protocol.ts → integration/integration-middleware.protocol.ts} +6 -6
- package/runtime/subsystems/{sync/sync-run-recorder.drizzle-backend.ts → integration/integration-run-recorder.drizzle-backend.ts} +39 -39
- package/runtime/subsystems/{sync/sync-run-recorder.memory-backend.ts → integration/integration-run-recorder.memory-backend.ts} +23 -23
- package/runtime/subsystems/{sync/sync-run-recorder.protocol.ts → integration/integration-run-recorder.protocol.ts} +25 -25
- package/runtime/subsystems/{sync/sync-sink.protocol.ts → integration/integration-sink.protocol.ts} +4 -4
- package/runtime/subsystems/{sync/sync.module.ts → integration/integration.module.ts} +48 -48
- package/runtime/subsystems/integration/integration.tokens.ts +49 -0
- package/runtime/subsystems/{sync → integration}/loopback.middleware.ts +5 -5
- package/runtime/subsystems/{sync → integration}/poll-change-source.ts +7 -7
- package/runtime/subsystems/{sync → integration}/webhook-change-source.ts +7 -7
- package/runtime/subsystems/jobs/bullmq.config.ts +23 -3
- package/runtime/subsystems/jobs/index.ts +13 -8
- package/runtime/subsystems/jobs/job-worker.bullmq-backend.ts +5 -2
- package/runtime/subsystems/jobs/job-worker.module.ts +27 -7
- package/runtime/subsystems/jobs/jobs-domain.module.ts +27 -2
- package/runtime/subsystems/observability/index.ts +1 -1
- package/runtime/subsystems/observability/observability.module.ts +2 -2
- package/runtime/subsystems/observability/observability.protocol.ts +11 -11
- package/runtime/subsystems/observability/observability.service.ts +13 -13
- package/runtime/subsystems/observability/observability.tokens.ts +1 -1
- package/src/patterns/library/index.ts +4 -4
- package/src/patterns/library/{synced.pattern.ts → integrated.pattern.ts} +12 -12
- package/src/patterns/library/junction.pattern.ts +1 -1
- package/src/patterns/pattern-definition.ts +3 -3
- package/templates/entity/new/backend/modules/core/{sync-source.ejs.t → integration-source.ejs.t} +6 -6
- package/templates/entity/new/backend/modules/core/{sync-source.providers.ejs.t → integration-source.providers.ejs.t} +2 -2
- package/templates/entity/new/clean-lite-ps/entity.ejs.t +1 -1
- package/templates/entity/new/clean-lite-ps/module.ejs.t +1 -1
- package/templates/entity/new/clean-lite-ps/prompt-extension.js +33 -33
- package/templates/entity/new/clean-lite-ps/repository.ejs.t +27 -27
- package/templates/entity/new/frontend/collections/collection.ejs.t +26 -1
- package/templates/entity/new/frontend/collections/collections-base.ejs.t +11 -0
- package/templates/entity/new/frontend/entity/combined.ejs.t +31 -1
- package/templates/entity/new/prompt.js +27 -15
- package/templates/junction/new/entity.ejs.t +1 -1
- package/templates/junction/new/prompt.js +24 -24
- package/templates/junction/new/repository.ejs.t +19 -19
- package/templates/subsystem/auth/auth-oauth-state.schema.ejs.t +2 -2
- package/templates/subsystem/auth-config/prompt.js +1 -1
- package/templates/subsystem/auth-integrations/app-module-hook.ejs.t +5 -5
- package/templates/subsystem/bridge/prompt.js +1 -1
- package/templates/subsystem/events/domain-events.schema.ejs.t +43 -2
- package/templates/subsystem/integration/integration-audit.schema.ejs.t +192 -0
- package/templates/subsystem/{sync → integration}/prompt.js +12 -12
- package/templates/subsystem/{sync-config/codegen-config-sync-block.ejs.t → integration-config/codegen-config-integration-block.ejs.t} +7 -7
- package/templates/subsystem/integration-config/prompt.js +22 -0
- package/templates/subsystem/jobs/worker.ejs.t +2 -2
- package/templates/subsystem/observability/main-hook.ejs.t +1 -1
- package/templates/subsystem/observability/prompt.js +1 -1
- package/templates/subsystem/openapi-config/prompt.js +1 -1
- package/dist/runtime/base-classes/junction-sync-repository.js.map +0 -1
- package/dist/runtime/base-classes/sync-upsert-config.js +0 -1
- package/dist/runtime/base-classes/synced-entity-repository.js.map +0 -1
- package/dist/runtime/base-classes/synced-entity-service.js.map +0 -1
- package/dist/runtime/subsystems/auth/protocols/integration-store.js +0 -1
- package/dist/runtime/subsystems/auth/runtime/integration-broken.error.js +0 -19
- package/dist/runtime/subsystems/auth/runtime/integration-broken.error.js.map +0 -1
- package/dist/runtime/subsystems/sync/build-change-source.js.map +0 -1
- package/dist/runtime/subsystems/sync/deep-equal.differ.js.map +0 -1
- package/dist/runtime/subsystems/sync/detection-config.schema.js.map +0 -1
- package/dist/runtime/subsystems/sync/execute-sync.use-case.js.map +0 -1
- package/dist/runtime/subsystems/sync/index.d.ts +0 -28
- package/dist/runtime/subsystems/sync/index.js.map +0 -1
- package/dist/runtime/subsystems/sync/loopback.middleware.js.map +0 -1
- package/dist/runtime/subsystems/sync/poll-change-source.js.map +0 -1
- package/dist/runtime/subsystems/sync/sync-audit.schema.js.map +0 -1
- package/dist/runtime/subsystems/sync/sync-change-source.protocol.js +0 -1
- package/dist/runtime/subsystems/sync/sync-cursor-store.drizzle-backend.js.map +0 -1
- package/dist/runtime/subsystems/sync/sync-cursor-store.memory-backend.js.map +0 -1
- package/dist/runtime/subsystems/sync/sync-cursor-store.protocol.js +0 -1
- package/dist/runtime/subsystems/sync/sync-errors.js.map +0 -1
- package/dist/runtime/subsystems/sync/sync-field-diff.protocol.js.map +0 -1
- package/dist/runtime/subsystems/sync/sync-loopback.protocol.js +0 -1
- package/dist/runtime/subsystems/sync/sync-middleware.protocol.js +0 -1
- package/dist/runtime/subsystems/sync/sync-run-recorder.drizzle-backend.js.map +0 -1
- package/dist/runtime/subsystems/sync/sync-run-recorder.memory-backend.js.map +0 -1
- package/dist/runtime/subsystems/sync/sync-run-recorder.protocol.js +0 -1
- package/dist/runtime/subsystems/sync/sync-sink.protocol.js +0 -1
- package/dist/runtime/subsystems/sync/sync.module.js.map +0 -1
- package/dist/runtime/subsystems/sync/sync.tokens.d.ts +0 -47
- package/dist/runtime/subsystems/sync/sync.tokens.js +0 -18
- package/dist/runtime/subsystems/sync/sync.tokens.js.map +0 -1
- package/dist/runtime/subsystems/sync/webhook-change-source.js.map +0 -1
- package/examples/auth-integrations/runtime/integrations/integrations-auth.module.ts +0 -81
- package/examples/auth-integrations/runtime/integrations/oauth/use-cases/list-user-integrations.use-case.ts +0 -21
- package/examples/auth-integrations/runtime/integrations/oauth/use-cases/mark-integration-requires-reauth.use-case.ts +0 -21
- package/runtime/subsystems/sync/sync.tokens.ts +0 -49
- package/templates/subsystem/sync/sync-audit.schema.ejs.t +0 -192
- package/templates/subsystem/sync-config/prompt.js +0 -22
- /package/dist/runtime/base-classes/{sync-upsert-config.js.map → integration-upsert-config.js.map} +0 -0
- /package/dist/runtime/subsystems/auth/protocols/{integration-store.js.map → connection-store.js.map} +0 -0
- /package/dist/runtime/subsystems/{sync/sync-change-source.protocol.js.map → integration/integration-change-source.protocol.js.map} +0 -0
- /package/dist/runtime/subsystems/{sync/sync-cursor-store.protocol.js.map → integration/integration-cursor-store.protocol.js.map} +0 -0
- /package/dist/runtime/subsystems/{sync/sync-loopback.protocol.js.map → integration/integration-loopback.protocol.js.map} +0 -0
- /package/dist/runtime/subsystems/{sync/sync-middleware.protocol.js.map → integration/integration-middleware.protocol.js.map} +0 -0
- /package/dist/runtime/subsystems/{sync/sync-run-recorder.protocol.js.map → integration/integration-run-recorder.protocol.js.map} +0 -0
- /package/dist/runtime/subsystems/{sync/sync-sink.protocol.js.map → integration/integration-sink.protocol.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../runtime/base-classes/junction-integration-repository.ts","../../../runtime/base-classes/base-repository.ts","../../../runtime/base-classes/tenant-context.ts"],"sourcesContent":["/**\n * JunctionIntegrationRepository<TEntity, TIntegrationWrite, TIntegrationProjection>\n *\n * Base for junction repos that participate in inbound integration (#374). A junction's\n * integration identity is the tuple `(leftId, rightId[, role])` — there is no native\n * `external_id`/`provider` column, so the integration seam's externalId is a COMPOSITE\n * string `<leftExternalId>::<rightExternalId>[::<role>]` (see the static\n * build/parse helpers below).\n *\n * Both parent FKs are resolved STRICTLY in the write path (a missing parent\n * throws → the orchestrator records a failed item and continues). As of #372\n * role-bearing junctions carry a unique constraint over `(left, right, role)`,\n * so the upsert uses `onConflictDoUpdate` (not the legacy select-then-write).\n * Role-less junctions conflict on `(left, right)`.\n */\nimport { and, eq } from 'drizzle-orm';\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nimport type { PgTableWithColumns } from 'drizzle-orm/pg-core';\nimport type { DrizzleTx } from '../types/drizzle';\nimport { BaseRepository } from './base-repository';\n\nexport interface JunctionIntegrationConfig {\n /** Left endpoint: local FK column (camel) + strict parent table. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n left: { column: string; refTable: PgTableWithColumns<any> };\n /** Right endpoint: local FK column (camel) + strict parent table. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n right: { column: string; refTable: PgTableWithColumns<any> };\n /** Role column (camel), or null for a role-less (2-part composite) junction. */\n roleColumn: string | null;\n}\n\nexport abstract class JunctionIntegrationRepository<\n TEntity,\n TIntegrationWrite,\n TIntegrationProjection,\n> extends BaseRepository<TEntity> {\n /**\n * Declarative junction integration surface. Concrete repos declare this — the\n * template emits it with live parent-table handles.\n */\n protected abstract readonly integrationConfig: JunctionIntegrationConfig;\n\n /**\n * Upsert ONE junction row by its composite identity, in a single transaction:\n * 1. resolve the REQUIRED left FK (provider-scoped) — STRICT: missing → throws;\n * 2. resolve the REQUIRED right FK (provider-scoped) — STRICT: missing → throws;\n * 3. insert-or-update on the `(left, right[, role])` conflict target.\n *\n * Idempotent. Returns the composite externalId as the projection `id`.\n *\n * @param write parent external ids (`<left>ExternalId`/`<right>ExternalId`)\n * + optional `role` + `userId`\n * @param provider adapter/provider label used to scope the parent lookups\n * @param tx optional outer transaction; when omitted we open our own\n */\n async integrationUpsertOne(\n write: TIntegrationWrite,\n provider: string,\n tx?: DrizzleTx,\n ): Promise<TIntegrationProjection> {\n const cfg = this.integrationConfig;\n const w = write as Record<string, unknown>;\n const leftWriteKey = `${cfg.left.column.replace(/Id$/, '')}ExternalId`;\n const rightWriteKey = `${cfg.right.column.replace(/Id$/, '')}ExternalId`;\n\n const run = async (db: DrizzleTx): Promise<TIntegrationProjection> => {\n const leftId = await this.resolveStrict(\n db, cfg.left.refTable, w[leftWriteKey] as string, provider, cfg.left.column,\n );\n const rightId = await this.resolveStrict(\n db, cfg.right.refTable, w[rightWriteKey] as string, provider, cfg.right.column,\n );\n\n const now = new Date();\n const role = cfg.roleColumn ? (w['role'] as unknown) : undefined;\n const values: Record<string, unknown> = {\n [cfg.left.column]: leftId,\n [cfg.right.column]: rightId,\n ...(cfg.roleColumn ? { [cfg.roleColumn]: role } : {}),\n ...(this.behaviors.timestamps ? { updatedAt: now } : {}),\n };\n const target = cfg.roleColumn\n ? [this.table[cfg.left.column], this.table[cfg.right.column], this.table[cfg.roleColumn]]\n : [this.table[cfg.left.column], this.table[cfg.right.column]];\n\n const rows = await db\n .insert(this.table)\n .values(values as never)\n .onConflictDoUpdate({\n target,\n set: { ...(this.behaviors.timestamps ? { updatedAt: now } : {}) } as never,\n })\n .returning();\n\n const saved = rows[0] as Record<string, unknown>;\n return this.toProjection(saved as TEntity, w, provider);\n };\n\n return tx ? run(tx) : this.db.transaction((t) => run(t));\n }\n\n /**\n * Canonical-projected lookup by the COMPOSITE externalId, differ-ready. Parses\n * the composite, resolves BOTH parents NON-throwing (→ null), then selects by\n * the identity tuple. Returns `null` on malformed composite / unresolved\n * parent / no row (a missing \"before\" side is a create from the differ's view).\n */\n async findByExternalIdProjected(\n externalId: string,\n provider: string,\n ): Promise<TIntegrationProjection | null> {\n const cfg = this.integrationConfig;\n const parsed = parseCompositeExternalId(externalId, cfg.roleColumn !== null);\n if (!parsed) return null;\n\n const leftId = await this.resolveLoose(this.db, cfg.left.refTable, parsed.left, provider);\n if (leftId === null) return null;\n const rightId = await this.resolveLoose(this.db, cfg.right.refTable, parsed.right, provider);\n if (rightId === null) return null;\n\n const rows = await this.db\n .select()\n .from(this.table)\n .where(this.identityWhere(leftId, rightId, parsed.role))\n .limit(1);\n const row = rows[0] as TEntity | undefined;\n if (!row) return null;\n\n const w: Record<string, unknown> = {\n [`${cfg.left.column.replace(/Id$/, '')}ExternalId`]: parsed.left,\n [`${cfg.right.column.replace(/Id$/, '')}ExternalId`]: parsed.right,\n ...(cfg.roleColumn ? { role: parsed.role } : {}),\n userId: '',\n };\n return this.toProjection(row, w, provider);\n }\n\n /**\n * Hard-delete the junction by composite externalId. Junctions have no\n * `deleted_at` and no external-linkage columns to clear, so a integration \"delete\"\n * removes the row. Resolves both parents NON-throwing, then deletes by the\n * identity tuple. Returns the composite id, or `null` when nothing matched.\n */\n async softDeleteByExternalId(\n externalId: string,\n provider: string,\n tx?: DrizzleTx,\n ): Promise<{ id: string } | null> {\n const cfg = this.integrationConfig;\n const parsed = parseCompositeExternalId(externalId, cfg.roleColumn !== null);\n if (!parsed) return null;\n const db = this.runner(tx);\n\n const leftId = await this.resolveLoose(db, cfg.left.refTable, parsed.left, provider);\n if (leftId === null) return null;\n const rightId = await this.resolveLoose(db, cfg.right.refTable, parsed.right, provider);\n if (rightId === null) return null;\n\n const rows = await db\n .delete(this.table)\n .where(this.identityWhere(leftId, rightId, parsed.role))\n .returning({ id: this.table[cfg.left.column] });\n return rows[0] ? { id: externalId } : null;\n }\n\n /**\n * Project a raw junction row to the differ shape: the COMPOSITE externalId\n * `id` (junctions have no surrogate id) plus the local FK columns, role (when\n * role-bearing) and timestamps — matching the emitted\n * `<Junction>IntegrationProjection` interface (id + leftId + rightId + role? +\n * createdAt + updatedAt). Junction projections are purely structural, so this\n * is fully generic over `integrationConfig` — no per-junction override is emitted.\n */\n protected toProjection(\n row: TEntity,\n write: Record<string, unknown>,\n _provider: string,\n ): TIntegrationProjection {\n const cfg = this.integrationConfig;\n const r = row as Record<string, unknown>;\n const leftExt = write[`${cfg.left.column.replace(/Id$/, '')}ExternalId`] as string;\n const rightExt = write[`${cfg.right.column.replace(/Id$/, '')}ExternalId`] as string;\n const role = cfg.roleColumn ? (r[cfg.roleColumn] as string | undefined) : undefined;\n const out: Record<string, unknown> = {\n id: buildCompositeExternalId(leftExt, rightExt, role),\n [cfg.left.column]: r[cfg.left.column],\n [cfg.right.column]: r[cfg.right.column],\n };\n if (cfg.roleColumn) out[cfg.roleColumn] = r[cfg.roleColumn];\n out['createdAt'] = r['createdAt'];\n out['updatedAt'] = r['updatedAt'];\n return out as TIntegrationProjection;\n }\n\n /** Build the identity WHERE clause `(left, right[, role])`. */\n private identityWhere(leftId: string, rightId: string, role: string | undefined) {\n const cfg = this.integrationConfig;\n const conds = [\n eq(this.table[cfg.left.column], leftId),\n eq(this.table[cfg.right.column], rightId),\n ];\n if (cfg.roleColumn && role !== undefined) {\n conds.push(eq(this.table[cfg.roleColumn], role));\n }\n return and(...conds);\n }\n\n /** Resolve a parent id (provider-scoped), throwing when unresolved. */\n private async resolveStrict(\n db: DrizzleTx,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n refTable: PgTableWithColumns<any>,\n parentExternalId: string,\n provider: string,\n column: string,\n ): Promise<string> {\n const id = await this.resolveLoose(db, refTable, parentExternalId, provider);\n if (!id) {\n throw new Error(\n `${this.constructor.name}.integrationUpsertOne: unresolved parent ` +\n `'${parentExternalId}' (provider '${provider}') for '${column}' — ` +\n `parent not integrated yet`,\n );\n }\n return id;\n }\n\n /** Resolve a parent id (provider-scoped), returning null when unresolved. */\n private async resolveLoose(\n db: DrizzleTx,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n refTable: PgTableWithColumns<any>,\n parentExternalId: string | null | undefined,\n provider: string,\n ): Promise<string | null> {\n if (!parentExternalId) return null;\n const rows = await db\n .select({ id: refTable['id'] })\n .from(refTable)\n .where(\n and(\n eq(refTable['provider'], provider),\n eq(refTable['externalId'], parentExternalId),\n ),\n )\n .limit(1);\n return (rows[0]?.id as string | undefined) ?? null;\n }\n}\n\n// ============================================================================\n// Composite externalId — the junction integration seam's deterministic identity.\n//\n// Format: `<leftExternalId>::<rightExternalId>[::<role>]`\n// e.g. `hubspot:42::hubspot:99::employee` (role-bearing)\n// `hubspot:42::hubspot:99` (role-less)\n//\n// Vendor-prefixed ids use a SINGLE colon, so `::` is an unambiguous delimiter.\n// Kept static in the base (replacing the per-repo free functions) so every\n// junction's lookups + its ChangeSource share one definition.\n// ============================================================================\n\n/**\n * Build the composite externalId from the two parent external ids (+ role when\n * the junction is role-bearing).\n */\nexport function buildCompositeExternalId(\n leftExternalId: string,\n rightExternalId: string,\n role?: string,\n): string {\n return role !== undefined\n ? `${leftExternalId}::${rightExternalId}::${role}`\n : `${leftExternalId}::${rightExternalId}`;\n}\n\n/**\n * Parse a composite externalId. `withRole` selects the expected part count\n * (3 when role-bearing, else 2). Returns `null` when the shape doesn't match\n * or any part is empty.\n */\nexport function parseCompositeExternalId(\n externalId: string,\n withRole: boolean,\n): { left: string; right: string; role: string | undefined } | null {\n const parts = externalId.split('::');\n const expected = withRole ? 3 : 2;\n if (parts.length !== expected || parts.some((p) => p.length === 0)) return null;\n return {\n left: parts[0] as string,\n right: parts[1] as string,\n role: withRole ? (parts[2] as string) : undefined,\n };\n}\n","/**\n * BaseRepository<TEntity>\n *\n * Abstract base class providing standard CRUD operations via Drizzle ORM.\n * Every generated repository extends this class.\n *\n * Family-specific bases (CrmEntityRepository, etc.) extend this in v0.1\n * without any changes to BaseRepository.\n *\n * NOT @Injectable — concrete repositories are @Injectable and inject DRIZZLE.\n */\nimport { and, eq, inArray, isNull, sql } from 'drizzle-orm';\nimport type { PgTableWithColumns, PgColumn } from 'drizzle-orm/pg-core';\nimport type { SQL } from 'drizzle-orm';\nimport type { DrizzleClient, DrizzleTx } from '../types/drizzle';\nimport {\n requireRequester,\n tryGetRequester,\n type RequesterScope,\n} from './tenant-context';\n\n// ============================================================================\n// Interfaces\n// ============================================================================\n\n/**\n * Behavior flags for the repository. Controls automatic timestamp injection\n * and soft-delete filtering.\n */\nexport interface BehaviorConfig {\n timestamps: boolean;\n softDelete: boolean;\n userTracking: boolean;\n}\n\n/**\n * Options for the list() method.\n */\nexport interface ListOptions {\n where?: SQL;\n limit?: number;\n offset?: number;\n orderBy?: PgColumn | SQL;\n}\n\n// ============================================================================\n// BaseRepository\n// ============================================================================\n\nexport abstract class BaseRepository<TEntity> {\n /**\n * The Drizzle table schema for this entity.\n * Concrete repositories declare this as a class property.\n */\n protected abstract readonly table: PgTableWithColumns<any>; // eslint-disable-line @typescript-eslint/no-explicit-any\n\n /**\n * Behavior flags controlling automatic behavior injection.\n * Override in concrete repositories to enable behaviors.\n */\n protected readonly behaviors: BehaviorConfig = {\n timestamps: false,\n softDelete: false,\n userTracking: false,\n };\n\n /**\n * Ambient tenant-scope enforcement for `userTracking` repos (see\n * `scopePredicate`). Only has effect when `behaviors.userTracking === true`.\n *\n * - `'lenient'` (default): when no ambient requester context is active,\n * reads/writes are NOT scoped — preserves pre-scoping behavior, so adopting\n * ambient scoping is additive. Scoping kicks in automatically once a\n * boundary installs `withRequester(...)`.\n * - `'strict'`: a missing ambient context throws (`requireRequester`),\n * making a forgotten boundary fail loud instead of silently returning\n * cross-tenant rows. Recommended for new multi-tenant consumers — override\n * in a concrete repo or a family base class.\n */\n protected readonly scopeEnforcement: 'lenient' | 'strict' = 'lenient';\n\n protected readonly db: DrizzleClient;\n\n constructor(db: DrizzleClient) {\n this.db = db;\n }\n\n /**\n * Pick the runner for a write: the caller-supplied transaction handle\n * if present, otherwise the repository's own client. Keeps the `tx`\n * parameter purely additive — callers without a transaction call as\n * before. Used by the write methods below + consumer overrides (e.g.\n * the generated `upsertCurrentValues` on EAV value tables).\n */\n protected runner(tx?: DrizzleTx): DrizzleClient {\n return tx ?? this.db;\n }\n\n // ============================================================================\n // Read Operations\n // ============================================================================\n\n /**\n * Find a single entity by its primary key.\n * Returns null if not found (or soft-deleted when softDelete=true).\n */\n async findById(id: string): Promise<TEntity | null> {\n const rows = await this.baseQuery(eq(this.table['id'], id)).limit(1);\n return (rows[0] as TEntity) ?? null;\n }\n\n /**\n * Find multiple entities by their primary keys.\n * Returns empty array immediately for empty input (avoids DB errors).\n */\n async findByIds(ids: string[]): Promise<TEntity[]> {\n if (ids.length === 0) return [];\n const rows = await this.baseQuery(inArray(this.table['id'], ids));\n return rows as TEntity[];\n }\n\n /**\n * List entities with optional filtering, pagination, and ordering.\n */\n async list(options?: ListOptions): Promise<TEntity[]> {\n let query = this.baseQuery(options?.where);\n\n if (options?.orderBy) {\n query = query.orderBy(options.orderBy as SQL) as typeof query;\n }\n if (options?.limit !== undefined) {\n query = query.limit(options.limit) as typeof query;\n }\n if (options?.offset !== undefined) {\n query = query.offset(options.offset) as typeof query;\n }\n\n const rows = await query;\n return rows as TEntity[];\n }\n\n /**\n * Count entities matching an optional WHERE clause.\n * Soft-deleted rows are always excluded when softDelete=true.\n */\n async count(where?: SQL): Promise<number> {\n let query = this.db\n .select({ count: sql<number>`cast(count(*) as integer)` })\n .from(this.table);\n\n const conditions: SQL[] = [];\n if (this.behaviors.softDelete) {\n conditions.push(isNull(this.table['deletedAt']));\n }\n const scope = this.scopePredicate();\n if (scope) {\n conditions.push(scope);\n }\n if (where) {\n conditions.push(where);\n }\n\n if (conditions.length === 1) {\n query = query.where(conditions[0]) as typeof query;\n } else if (conditions.length > 1) {\n query = query.where(and(...conditions)) as typeof query;\n }\n\n const rows = await query;\n return rows[0]?.count ?? 0;\n }\n\n /**\n * Check whether an entity with the given id exists.\n */\n async exists(id: string): Promise<boolean> {\n const result = await this.findById(id);\n return result !== null;\n }\n\n // ============================================================================\n // Write Operations\n // ============================================================================\n\n /**\n * Insert a new entity. Timestamps are auto-injected when timestamps=true.\n */\n async create(input: Partial<TEntity>, tx?: DrizzleTx): Promise<TEntity> {\n const data = this.withTimestamps(input as Record<string, unknown>, 'create');\n const rows = await this.runner(tx)\n .insert(this.table)\n .values(data as any) // eslint-disable-line @typescript-eslint/no-explicit-any\n .returning();\n return rows[0] as TEntity;\n }\n\n /**\n * Update an existing entity by id. updatedAt is auto-injected when timestamps=true.\n * Returns the updated entity.\n */\n async update(id: string, input: Partial<TEntity>, tx?: DrizzleTx): Promise<TEntity> {\n const data = this.withTimestamps(input as Record<string, unknown>, 'update');\n const rows = await this.runner(tx)\n .update(this.table)\n .set(data as any) // eslint-disable-line @typescript-eslint/no-explicit-any\n .where(this.scopeAnd(eq(this.table['id'], id)))\n .returning();\n return rows[0] as TEntity;\n }\n\n /**\n * Delete an entity by id.\n * - softDelete=true: sets deletedAt to current timestamp\n * - softDelete=false: hard-deletes the row\n */\n async delete(id: string, tx?: DrizzleTx): Promise<void> {\n const runner = this.runner(tx);\n if (this.behaviors.softDelete) {\n await runner\n .update(this.table)\n .set({ deletedAt: new Date() } as any) // eslint-disable-line @typescript-eslint/no-explicit-any\n .where(this.scopeAnd(eq(this.table['id'], id)));\n } else {\n await runner\n .delete(this.table)\n .where(this.scopeAnd(eq(this.table['id'], id)));\n }\n }\n\n /**\n * Insert or update multiple entities.\n * Default naive implementation — family repositories override with\n * proper conflict-target upsert (e.g., CrmEntityRepository).\n */\n async upsertMany(inputs: Array<Partial<TEntity>>, tx?: DrizzleTx): Promise<TEntity[]> {\n return Promise.all(inputs.map((input) => this.create(input, tx)));\n }\n\n // ============================================================================\n // Protected Helpers\n // ============================================================================\n\n /**\n * Base SELECT query that automatically applies the ambient guards —\n * soft-delete exclusion (when `softDelete`) and tenant scope (when\n * `userTracking` + an active requester context) — combined with an optional\n * caller `extra` predicate into a SINGLE `WHERE`.\n *\n * Pass the leaf predicate as `extra` rather than chaining a second\n * `.where(...)`: Drizzle's `.where()` OVERRIDES (does not AND) a prior\n * `.where()` on a `$dynamic()` query, so a chained call would silently drop\n * the soft-delete and scope guards. `baseQuery(extra)` is the safe form.\n */\n protected baseQuery(extra?: SQL) {\n const query = this.db.select().from(this.table).$dynamic();\n const where = this.scopeAnd(extra, { softDelete: this.behaviors.softDelete });\n return where ? query.where(where) : query;\n }\n\n /**\n * Build the ambient tenant-scope predicate for this repo's table.\n *\n * Returns `undefined` (no scoping) when:\n * - `behaviors.userTracking` is false (repo is not user-owned), or\n * - no ambient requester context is active AND `scopeEnforcement` is\n * `'lenient'` (the default — preserves pre-scoping behavior).\n *\n * When a requester context is active, scopes by `user_id` per the ambient\n * scope: `'user'` → `user_id = ctx.userId`; `'org'` → `user_id IN\n * ctx.orgUserIds` (empty list matches nothing — fail-closed); `'superuser'`\n * → no filter. See `tenant-context.ts` for the boundary-install contract.\n */\n protected scopePredicate(): SQL | undefined {\n if (!this.behaviors.userTracking) return undefined;\n const ctx =\n this.scopeEnforcement === 'strict'\n ? requireRequester()\n : tryGetRequester();\n if (!ctx) return undefined;\n const scope: RequesterScope = ctx.scope ?? 'user';\n switch (scope) {\n case 'superuser':\n return undefined;\n case 'org':\n return ctx.orgUserIds && ctx.orgUserIds.length > 0\n ? inArray(this.table['userId'], ctx.orgUserIds as string[])\n : sql`false`;\n case 'user':\n default:\n return eq(this.table['userId'], ctx.userId);\n }\n }\n\n /**\n * Combine the ambient scope predicate (and, optionally, the soft-delete\n * guard) with a caller `extra` predicate into one `SQL`. Returns `undefined`\n * when nothing applies. Used by read + by-id write paths so a single\n * `.where(...)` carries every guard.\n */\n protected scopeAnd(\n extra?: SQL,\n opts?: { softDelete?: boolean },\n ): SQL | undefined {\n const conditions: SQL[] = [];\n if (opts?.softDelete) conditions.push(isNull(this.table['deletedAt']));\n const scope = this.scopePredicate();\n if (scope) conditions.push(scope);\n if (extra) conditions.push(extra);\n if (conditions.length === 0) return undefined;\n if (conditions.length === 1) return conditions[0];\n return and(...conditions);\n }\n\n /**\n * Merge timestamp fields into an input object.\n * - mode='create': adds createdAt and updatedAt\n * - mode='update': adds updatedAt only\n *\n * No-op when timestamps behavior is disabled.\n */\n protected withTimestamps(\n input: Record<string, unknown>,\n mode: 'create' | 'update',\n ): Record<string, unknown> {\n if (!this.behaviors.timestamps) return input;\n const now = new Date();\n if (mode === 'create') {\n return { ...input, createdAt: now, updatedAt: now };\n }\n return { ...input, updatedAt: now };\n }\n\n /**\n * Build a WHERE clause fragment that restricts results to rows whose\n * parent (identified by a belongs_to FK) is not soft-deleted.\n *\n * Use this in custom repository methods when you need \"rows reachable\n * from an active parent\". The default findAll / findById behavior is\n * NOT changed by this helper — opt in explicitly where needed.\n *\n * ADR-021 — Soft-delete cascade: Option A (filter at query time).\n * `on_delete` FK rules do not fire for soft-deletes; use this helper\n * instead of expecting cascade semantics on the DB level.\n *\n * Example:\n * async listActiveMessages(): Promise<Message[]> {\n * return this.list({\n * where: this.activeParentFilter(conversations, this.table['conversationId']),\n * });\n * }\n *\n * @param parentTable The Drizzle table object for the parent entity.\n * @param parentFkColumn The FK column on this (child) table that references parent.id.\n */\n protected activeParentFilter(\n parentTable: PgTableWithColumns<any>, // eslint-disable-line @typescript-eslint/no-explicit-any\n parentFkColumn: PgColumn,\n ): SQL {\n return sql`EXISTS (\n SELECT 1 FROM ${parentTable} p\n WHERE p.id = ${parentFkColumn}\n AND p.deleted_at IS NULL\n )`;\n }\n}\n","/**\n * Ambient requester context — AsyncLocalStorage-backed tenant scope.\n *\n * The alternative to threading `userId`/`organizationId` through every\n * repository/service signature. Set ONCE at each boundary the generated app\n * owns, read implicitly inside `BaseRepository` (see `scopePredicate`).\n *\n * ## Where to set it (boundaries)\n *\n * - HTTP / tRPC handlers — from the authenticated `ctx.user`\n * - OAuth callback controllers — from the authenticated session\n * - Queue/worker `process()` — from the job's owning user after the\n * job's record is loaded\n *\n * Each boundary wraps the rest of the request in `withRequester({ userId,\n * organizationId }, () => ...)`. The context propagates through every `await`\n * to all downstream repo/service calls without being passed explicitly.\n *\n * ## Where to read it\n *\n * - `BaseRepository.scopePredicate()` reads it (via `tryGetRequester` in\n * lenient mode, `requireRequester` in strict mode) and filters every read\n * by the ambient scope when the repo declares `userTracking: true`.\n *\n * ## Why AsyncLocalStorage over an explicit parameter\n *\n * Threading `userId` (and later `organizationId`) through dozens of method\n * signatures is pure parameter pollution. Ambient context also lets a repo\n * make the \"I forgot to scope\" mistake impossible at runtime: in strict mode\n * `requireRequester()` throws when no context is active, surfacing a missing\n * boundary call loudly rather than silently leaking cross-tenant data.\n *\n * ## Not-found semantics\n *\n * When a row exists but belongs to a different requester, scoped reads return\n * `null`/`[]` — identical to \"truly doesn't exist\". No existence oracle;\n * callers throw NotFound uniformly. Standard security practice.\n *\n * ## Testing\n *\n * Tests that exercise scoped repos must wrap the call in `withRequester(...)`.\n * In strict mode an unwrapped call hitting `requireRequester()` throws — by\n * design. In lenient mode (the default) an unwrapped call is simply unscoped.\n */\nimport { AsyncLocalStorage } from 'node:async_hooks';\n\n/**\n * Data-visibility scope. The auth layer decides which scope a request is\n * allowed to claim; the repo trusts whatever the ambient context says.\n *\n * - `'user'`: filter every read by `user_id = ctx.userId`. Default.\n * - `'org'`: filter every read by membership in the requester's org, resolved\n * via `user_id IN (ctx.orgUserIds)` rather than via a per-entity\n * `organization_id` column. Works for every user-owned table and keeps repos\n * single-table — the org member list is pre-resolved at the boundary.\n * - `'superuser'`: no scope filter. Engineering / internal-tools only.\n *\n * AUTHORIZATION (who is allowed to claim each scope) lives in boundary\n * middleware, not in the repo. The repo trusts the ambient context — same\n * trust model as a threaded `userId`.\n */\nexport type RequesterScope = 'user' | 'org' | 'superuser';\n\nexport interface RequesterContext {\n /**\n * The user making the request. Always present — even in `'org'` and\n * `'superuser'` scopes it is the audit-trail \"who actually did this\".\n */\n readonly userId: string;\n /**\n * The organization the requester belongs to. Required when\n * `scope === 'org'`; may be null for `'user'` (users with no org) and for\n * `'superuser'` (cross-org reads).\n */\n readonly organizationId: string | null;\n /**\n * Data-visibility scope. Defaults to `'user'` when omitted.\n */\n readonly scope?: RequesterScope;\n /**\n * For `scope === 'org'`: the list of user IDs in the requester's org,\n * pre-resolved by the boundary middleware that established the `'org'`\n * scope (one `SELECT users.id WHERE organization_id = X` at the trust\n * boundary). Repos use this as a literal `IN (...)` filter — they never\n * JOIN to `users` themselves. Required when `scope === 'org'`.\n */\n readonly orgUserIds?: readonly string[];\n}\n\nconst als = new AsyncLocalStorage<RequesterContext>();\n\n/**\n * Set the ambient requester context for the duration of `fn`. The context\n * propagates through `await` boundaries to all downstream calls. Nesting is\n * fine — an inner `withRequester` overrides the outer for its callback.\n */\nexport function withRequester<T>(\n ctx: RequesterContext,\n fn: () => Promise<T>,\n): Promise<T> {\n return als.run(ctx, fn);\n}\n\n/**\n * Read the ambient requester context. Throws if no context is active — by\n * design. Used by repos in strict scope-enforcement mode; an unwrapped call\n * site is a missing boundary.\n */\nexport function requireRequester(): RequesterContext {\n const ctx = als.getStore();\n if (!ctx) {\n throw new Error(\n 'No requester context active. Wrap the entry point in ' +\n 'withRequester({ userId, organizationId }, fn). See tenant-context.ts.',\n );\n }\n return ctx;\n}\n\n/**\n * Read the ambient requester context without throwing. Returns `undefined`\n * when no context is active. Used by repos in lenient scope-enforcement mode\n * (the default) and by code paths that legitimately run outside a request.\n */\nexport function tryGetRequester(): RequesterContext | undefined {\n return als.getStore();\n}\n\n/**\n * Resolve the effective scope for the ambient context, defaulting to `'user'`.\n */\nexport function requireRequesterScope(): RequesterScope {\n return requireRequester().scope ?? 'user';\n}\n\n/**\n * Convenience helpers for setting scope explicitly. All three preserve\n * `userId` in the context (audit trail) regardless of scope.\n *\n * - `withUserScope`: regular end-user requests. Most call sites.\n * - `withOrgScope`: admin / org-shared resource access. The caller MUST verify\n * the requester's role permits `'org'` before calling — the helper does not\n * enforce authorization. `orgUserIds` is pre-resolved at the boundary.\n * - `withSuperuserScope`: engineering scripts / internal tools. `organizationId`\n * is null (cross-org is the point). Same authorization caveat applies.\n */\nexport function withUserScope<T>(\n userId: string,\n organizationId: string | null,\n fn: () => Promise<T>,\n): Promise<T> {\n return withRequester({ userId, organizationId, scope: 'user' }, fn);\n}\n\nexport function withOrgScope<T>(\n userId: string,\n organizationId: string,\n orgUserIds: readonly string[],\n fn: () => Promise<T>,\n): Promise<T> {\n return withRequester(\n { userId, organizationId, scope: 'org', orgUserIds },\n fn,\n );\n}\n\nexport function withSuperuserScope<T>(\n userId: string,\n fn: () => Promise<T>,\n): Promise<T> {\n return withRequester(\n { userId, organizationId: null, scope: 'superuser' },\n fn,\n );\n}\n"],"mappings":"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","names":["and","eq","eq","and"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../runtime/subsystems/auth/auth-oauth-state.schema.ts"],"sourcesContent":["/**\n * Drizzle schema for the `auth_oauth_state` table — backs the\n * `DrizzleOAuthStateStore` (`state-store.drizzle-backend.ts`).\n *\n * One row per outstanding /connect → /callback dance. Single-use; rows are\n * deleted on consume. A periodic sweep (or a `WHERE expires_at < now()`\n * filter on read) clears abandoned rows.\n *\n * Columns:\n * - `state` — opaque random token, primary key.\n * - `user_id` — text (matches the consumer-defined user-id shape;\n * the auth subsystem doesn't constrain this to UUID\n * because some apps key users by external id).\n * - `redirect` — optional post-callback redirect path.\n * - `expires_at` — TTL boundary; entries past this are treated as absent.\n *\n * Convention: schema files live at the root of the subsystem dir\n * (mirrors `cache.schema.ts`, `
|
|
1
|
+
{"version":3,"sources":["../../../../runtime/subsystems/auth/auth-oauth-state.schema.ts"],"sourcesContent":["/**\n * Drizzle schema for the `auth_oauth_state` table — backs the\n * `DrizzleOAuthStateStore` (`state-store.drizzle-backend.ts`).\n *\n * One row per outstanding /connect → /callback dance. Single-use; rows are\n * deleted on consume. A periodic sweep (or a `WHERE expires_at < now()`\n * filter on read) clears abandoned rows.\n *\n * Columns:\n * - `state` — opaque random token, primary key.\n * - `user_id` — text (matches the consumer-defined user-id shape;\n * the auth subsystem doesn't constrain this to UUID\n * because some apps key users by external id).\n * - `redirect` — optional post-callback redirect path.\n * - `expires_at` — TTL boundary; entries past this are treated as absent.\n *\n * Convention: schema files live at the root of the subsystem dir\n * (mirrors `cache.schema.ts`, `integration-audit.schema.ts`, `domain-events.schema.ts`).\n */\nimport { pgTable, text, timestamp } from 'drizzle-orm/pg-core';\nimport type { InferSelectModel } from 'drizzle-orm';\n\nexport const authOAuthState = pgTable('auth_oauth_state', {\n state: text('state').primaryKey(),\n userId: text('user_id').notNull(),\n redirect: text('redirect'),\n expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),\n});\n\nexport type AuthOAuthState = InferSelectModel<typeof authOAuthState>;\n"],"mappings":";AAmBA,SAAS,SAAS,MAAM,iBAAiB;AAGlC,IAAM,iBAAiB,QAAQ,oBAAoB;AAAA,EACxD,OAAO,KAAK,OAAO,EAAE,WAAW;AAAA,EAChC,QAAQ,KAAK,SAAS,EAAE,QAAQ;AAAA,EAChC,UAAU,KAAK,UAAU;AAAA,EACzB,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,EAAE,QAAQ;AACrE,CAAC;","names":[]}
|
|
@@ -11,17 +11,17 @@ import { Provider, DynamicModule } from '@nestjs/common';
|
|
|
11
11
|
* - `AUTH_OPTIONS` → resolved options bag (used by AuthController
|
|
12
12
|
* for `redirectUriBase`).
|
|
13
13
|
*
|
|
14
|
-
* The
|
|
15
|
-
* `
|
|
14
|
+
* The connection-store ports (`AUTH_CONNECTION_READER`,
|
|
15
|
+
* `AUTH_CONNECTION_TOKEN_WRITER`, `AUTH_CONNECTION_GRANT_SINK`),
|
|
16
16
|
* `AUTH_USER_CONTEXT`, and `STRATEGY_REGISTRY` are deliberately **not**
|
|
17
17
|
* wired here — they are always consumer-specific:
|
|
18
|
-
* -
|
|
18
|
+
* - connection-store ports adapt the consumer's `connections` storage;
|
|
19
19
|
* - `IUserContext` adapts the app's session/JWT scheme;
|
|
20
20
|
* - `STRATEGY_REGISTRY` is populated from the per-provider strategy
|
|
21
21
|
* classes the consumer maintains.
|
|
22
22
|
*
|
|
23
23
|
* Consumers provide them in their app module (or by importing the
|
|
24
|
-
* `auth-integrations` starter, which binds the three
|
|
24
|
+
* `auth-integrations` starter, which binds the three connection-store
|
|
25
25
|
* ports off a single canonical entity).
|
|
26
26
|
*
|
|
27
27
|
* Usage in AppModule:
|
|
@@ -16,7 +16,7 @@ import { Module } from "@nestjs/common";
|
|
|
16
16
|
// runtime/subsystems/auth/auth.tokens.ts
|
|
17
17
|
var ENCRYPTION_KEY = /* @__PURE__ */ Symbol("ENCRYPTION_KEY");
|
|
18
18
|
var OAUTH_STATE_STORE = /* @__PURE__ */ Symbol("OAUTH_STATE_STORE");
|
|
19
|
-
var
|
|
19
|
+
var AUTH_CONNECTION_GRANT_SINK = /* @__PURE__ */ Symbol("AUTH_CONNECTION_GRANT_SINK");
|
|
20
20
|
var AUTH_USER_CONTEXT = /* @__PURE__ */ Symbol("AUTH_USER_CONTEXT");
|
|
21
21
|
var STRATEGY_REGISTRY = /* @__PURE__ */ Symbol("STRATEGY_REGISTRY");
|
|
22
22
|
var AUTH_OPTIONS = /* @__PURE__ */ Symbol("AUTH_OPTIONS");
|
|
@@ -240,7 +240,7 @@ var AuthController = class {
|
|
|
240
240
|
});
|
|
241
241
|
return res.redirect(
|
|
242
242
|
HttpStatus.FOUND,
|
|
243
|
-
redirect ?? `/settings/
|
|
243
|
+
redirect ?? `/settings/connections?connected=${encodeURIComponent(slug)}`
|
|
244
244
|
);
|
|
245
245
|
}
|
|
246
246
|
requireStrategy(slug) {
|
|
@@ -283,7 +283,7 @@ AuthController = __decorateClass([
|
|
|
283
283
|
__decorateParam(0, Inject(STRATEGY_REGISTRY)),
|
|
284
284
|
__decorateParam(1, Inject(AUTH_USER_CONTEXT)),
|
|
285
285
|
__decorateParam(2, Inject(OAUTH_STATE_STORE)),
|
|
286
|
-
__decorateParam(3, Inject(
|
|
286
|
+
__decorateParam(3, Inject(AUTH_CONNECTION_GRANT_SINK)),
|
|
287
287
|
__decorateParam(4, Inject(AUTH_OPTIONS))
|
|
288
288
|
], AuthController);
|
|
289
289
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../runtime/subsystems/auth/auth.module.ts","../../../../runtime/subsystems/auth/auth.tokens.ts","../../../../runtime/subsystems/auth/backends/encryption-key/env.ts","../../../../runtime/subsystems/auth/backends/state-store.memory-backend.ts","../../../../runtime/subsystems/auth/protocols/oauth-state-store.ts","../../../../runtime/subsystems/auth/backends/state-store.drizzle-backend.ts","../../../../runtime/subsystems/auth/auth-oauth-state.schema.ts","../../../../runtime/subsystems/auth/controllers/auth.controller.ts","../../../../runtime/constants/tokens.ts"],"sourcesContent":["/**\n * AuthModule — DynamicModule factory for the auth subsystem.\n *\n * Wires the pluggable backends the subsystem ships with:\n * - `ENCRYPTION_KEY` → `EnvEncryptionKey` (AES-256-GCM from env)\n * - `OAUTH_STATE_STORE` → `MemoryOAuthStateStore` (dev/tests) or\n * `DrizzleOAuthStateStore` (prod, requires\n * DRIZZLE provider).\n * - `AUTH_OPTIONS` → resolved options bag (used by AuthController\n * for `redirectUriBase`).\n *\n * The integration-store ports (`AUTH_INTEGRATION_READER`,\n * `AUTH_INTEGRATION_TOKEN_WRITER`, `AUTH_INTEGRATION_GRANT_SINK`),\n * `AUTH_USER_CONTEXT`, and `STRATEGY_REGISTRY` are deliberately **not**\n * wired here — they are always consumer-specific:\n * - integration-store ports adapt the consumer's `integrations` storage;\n * - `IUserContext` adapts the app's session/JWT scheme;\n * - `STRATEGY_REGISTRY` is populated from the per-provider strategy\n * classes the consumer maintains.\n *\n * Consumers provide them in their app module (or by importing the\n * `auth-integrations` starter, which binds the three integration-store\n * ports off a single canonical entity).\n *\n * Usage in AppModule:\n * ```typescript\n * AuthModule.forRoot({\n * encryptionKey: 'env',\n * oauthStateStore: 'memory', // or 'drizzle'\n * enableController: true,\n * redirectUriBase: 'http://localhost:3000',\n * });\n * ```\n *\n * `global: true` means other modules don't need to re-import AuthModule to\n * inject the auth tokens.\n */\nimport { Module, type DynamicModule, type Provider } from '@nestjs/common';\nimport {\n AUTH_OPTIONS,\n ENCRYPTION_KEY,\n OAUTH_STATE_STORE,\n} from './auth.tokens';\nimport { EnvEncryptionKey } from './backends/encryption-key/env';\nimport { MemoryOAuthStateStore } from './backends/state-store.memory-backend';\nimport { DrizzleOAuthStateStore } from './backends/state-store.drizzle-backend';\nimport { AuthController } from './controllers/auth.controller';\nimport { DRIZZLE } from '../../constants/tokens';\nimport type { DrizzleClient } from '../../types/drizzle';\n\ntype EncryptionKeyChoice =\n | 'env'\n | Omit<Provider, 'provide'>;\n\ntype OAuthStateStoreChoice =\n | 'memory'\n | 'drizzle'\n | Omit<Provider, 'provide'>;\n\nexport interface AuthModuleOptions {\n /** `'env'` (default) or a full provider definition (e.g. `{ useClass: MyKmsEncryptionKey }`). */\n encryptionKey?: EncryptionKeyChoice;\n /**\n * `'memory'` (default — tests/dev) or `'drizzle'` (prod, requires DRIZZLE\n * provider) or a full provider definition for a custom impl.\n */\n oauthStateStore?: OAuthStateStoreChoice;\n /**\n * Mount `AuthController` (`/auth/:provider/connect` + `/callback`).\n * Default `false` — apps that hand-roll connect/callback (rare) or that\n * use the subsystem only for the refresh path can opt out.\n */\n enableController?: boolean;\n /**\n * Public base URL of the API server. Used to construct per-provider\n * callback URIs as `${redirectUriBase}/auth/:provider/callback`.\n * Required when `enableController: true`.\n */\n redirectUriBase?: string;\n}\n\nfunction resolveEncryptionKeyProvider(choice: EncryptionKeyChoice): Provider {\n if (choice === 'env') {\n return { provide: ENCRYPTION_KEY, useClass: EnvEncryptionKey };\n }\n return { provide: ENCRYPTION_KEY, ...choice } as Provider;\n}\n\nfunction resolveOAuthStateStoreProvider(\n choice: OAuthStateStoreChoice,\n): Provider {\n if (choice === 'memory') {\n return { provide: OAUTH_STATE_STORE, useClass: MemoryOAuthStateStore };\n }\n if (choice === 'drizzle') {\n return {\n provide: OAUTH_STATE_STORE,\n useFactory: (db: DrizzleClient | null) => {\n if (!db) {\n throw new Error(\n \"AuthModule.forRoot: oauthStateStore: 'drizzle' selected but DRIZZLE provider is not available. \" +\n 'Ensure DatabaseModule (or another provider exposing DRIZZLE) is imported before AuthModule.forRoot.',\n );\n }\n return new DrizzleOAuthStateStore(db);\n },\n inject: [{ token: DRIZZLE, optional: true }],\n };\n }\n return { provide: OAUTH_STATE_STORE, ...choice } as Provider;\n}\n\n@Module({})\nexport class AuthModule {\n static forRoot(options: AuthModuleOptions = {}): DynamicModule {\n const resolved: AuthModuleOptions = {\n encryptionKey: options.encryptionKey ?? 'env',\n oauthStateStore: options.oauthStateStore ?? 'memory',\n enableController: options.enableController ?? false,\n redirectUriBase: options.redirectUriBase,\n };\n\n if (resolved.enableController && !resolved.redirectUriBase) {\n throw new Error(\n 'AuthModule.forRoot: redirectUriBase is required when enableController: true',\n );\n }\n\n const encryptionKeyProvider = resolveEncryptionKeyProvider(\n resolved.encryptionKey!,\n );\n const oauthStateStoreProvider = resolveOAuthStateStoreProvider(\n resolved.oauthStateStore!,\n );\n const optionsProvider: Provider = {\n provide: AUTH_OPTIONS,\n useValue: resolved,\n };\n\n return {\n module: AuthModule,\n global: true,\n providers: [encryptionKeyProvider, oauthStateStoreProvider, optionsProvider],\n controllers: resolved.enableController ? [AuthController] : [],\n exports: [ENCRYPTION_KEY, OAUTH_STATE_STORE, AUTH_OPTIONS],\n };\n }\n}\n","/**\n * Auth subsystem — injection tokens.\n *\n * Following ADR-008 guidance: `Symbol()` tokens for type safety and collision\n * avoidance. Consumers inject these via `@Inject(...)` against the matching\n * protocol interface.\n *\n * Usage:\n * ```typescript\n * constructor(\n * @Inject(ENCRYPTION_KEY) private readonly key: IEncryptionKey,\n * @Inject(OAUTH_STATE_STORE) private readonly states: IOAuthStateStore,\n * @Inject(AUTH_INTEGRATION_READER) private readonly reader: IIntegrationReader,\n * @Inject(AUTH_INTEGRATION_TOKEN_WRITER) private readonly writer: IIntegrationTokenWriter,\n * @Inject(AUTH_INTEGRATION_GRANT_SINK) private readonly grants: IIntegrationGrantSink,\n * @Inject(AUTH_USER_CONTEXT) private readonly userCtx: IUserContext,\n * @Inject(STRATEGY_REGISTRY) private readonly registry: ProviderStrategyRegistry,\n * ) {}\n * ```\n *\n * `IAuthStrategy` implementations are provider-specific and registered under\n * provider-specific tokens (e.g. `SALESFORCE_AUTH_STRATEGY`,\n * `HUBSPOT_AUTH_STRATEGY`) by each integration module — this subsystem does\n * not mandate a single `AUTH_STRATEGY` token because an app typically has\n * many concurrent strategies, one per provider. They are dispatched through\n * `STRATEGY_REGISTRY` (a `ReadonlyMap<slug, IProviderStrategy>`), populated\n * by per-provider modules via a `useFactory` provider.\n */\nexport const ENCRYPTION_KEY = Symbol('ENCRYPTION_KEY');\nexport const OAUTH_STATE_STORE = Symbol('OAUTH_STATE_STORE');\nexport const AUTH_INTEGRATION_READER = Symbol('AUTH_INTEGRATION_READER');\nexport const AUTH_INTEGRATION_TOKEN_WRITER = Symbol('AUTH_INTEGRATION_TOKEN_WRITER');\nexport const AUTH_INTEGRATION_GRANT_SINK = Symbol('AUTH_INTEGRATION_GRANT_SINK');\nexport const AUTH_USER_CONTEXT = Symbol('AUTH_USER_CONTEXT');\nexport const STRATEGY_REGISTRY = Symbol('STRATEGY_REGISTRY');\n/**\n * Holds the resolved `AuthModuleOptions` (used by `AuthController` to read\n * `redirectUriBase` for building per-provider callback URIs).\n */\nexport const AUTH_OPTIONS = Symbol('AUTH_OPTIONS');\n","/**\n * Env-backed AES-256-GCM encryption.\n *\n * Framing: `base64( nonce(12B) || ciphertext || authTag(16B) )`. Random nonce\n * per call means two encryptions of the same plaintext produce different\n * ciphertexts — prevents replay-style inference. Auth tag enforces integrity;\n * any tampering throws on decrypt.\n *\n * Key source: `INTEGRATION_TOKEN_ENCRYPTION_KEY` env var, 32 bytes base64-encoded.\n * Generate via `openssl rand -base64 32`.\n *\n * Future backend: `kms.ts` (AWS/GCP KMS) for production deployments that\n * need key rotation + audit trails.\n */\nimport { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';\nimport type { IEncryptionKey } from '../../protocols/encryption-key';\n\nexport interface EnvEncryptionKeyOptions {\n /** Defaults to `process.env`. Tests inject a fixture. */\n env?: NodeJS.ProcessEnv;\n /** Defaults to `'INTEGRATION_TOKEN_ENCRYPTION_KEY'`. */\n envVar?: string;\n}\n\nconst ALGO = 'aes-256-gcm';\nconst NONCE_BYTES = 12;\nconst TAG_BYTES = 16;\nconst KEY_BYTES = 32;\n\nexport class EnvEncryptionKey implements IEncryptionKey {\n private readonly key: Buffer;\n\n constructor(opts: EnvEncryptionKeyOptions = {}) {\n const env = opts.env ?? process.env;\n const envVar = opts.envVar ?? 'INTEGRATION_TOKEN_ENCRYPTION_KEY';\n const raw = env[envVar];\n if (!raw) {\n throw new Error(\n `EnvEncryptionKey: ${envVar} is not set. Generate with: openssl rand -base64 32`,\n );\n }\n const decoded = Buffer.from(raw, 'base64');\n if (decoded.length !== KEY_BYTES) {\n throw new Error(\n `EnvEncryptionKey: ${envVar} must decode to ${KEY_BYTES} bytes (got ${decoded.length}). Use: openssl rand -base64 32`,\n );\n }\n this.key = decoded;\n }\n\n async encrypt(plaintext: string): Promise<string> {\n const nonce = randomBytes(NONCE_BYTES);\n const cipher = createCipheriv(ALGO, this.key, nonce);\n const ciphertext = Buffer.concat([\n cipher.update(plaintext, 'utf8'),\n cipher.final(),\n ]);\n const authTag = cipher.getAuthTag();\n return Buffer.concat([nonce, ciphertext, authTag]).toString('base64');\n }\n\n async decrypt(ciphertext: string): Promise<string> {\n const buf = Buffer.from(ciphertext, 'base64');\n if (buf.length < NONCE_BYTES + TAG_BYTES) {\n throw new Error('EnvEncryptionKey: ciphertext too short');\n }\n const nonce = buf.subarray(0, NONCE_BYTES);\n const authTag = buf.subarray(buf.length - TAG_BYTES);\n const body = buf.subarray(NONCE_BYTES, buf.length - TAG_BYTES);\n\n const decipher = createDecipheriv(ALGO, this.key, nonce);\n decipher.setAuthTag(authTag);\n const plain = Buffer.concat([decipher.update(body), decipher.final()]);\n return plain.toString('utf8');\n }\n}\n","/**\n * In-memory `IOAuthStateStore` backend.\n *\n * Single-process store — Map<state, { record, expiresAt }>. Suitable for\n * tests and single-worker dev. Production deployments select the drizzle\n * backend so state survives restarts and is shared across workers.\n *\n * Single-use semantics:\n * - `generate(record)` mints a 256-bit random token (base64url, opaque).\n * - `consume(state)` deletes the entry on read. A second call with the\n * same state throws `OAuthStateError('replay')`.\n * - Expired entries also throw (`'expired'`); the entry is deleted as a\n * side effect so a later replay still surfaces correctly.\n *\n * TTL defaults to 10 minutes — long enough for a user to complete the\n * provider's consent screen, short enough that abandoned states age out.\n */\nimport { randomBytes } from 'node:crypto';\nimport {\n type IOAuthStateStore,\n type OAuthStateRecord,\n OAuthStateError,\n} from '../protocols/oauth-state-store';\n\nexport interface MemoryOAuthStateStoreOptions {\n /** TTL in ms. Default 10 minutes. */\n ttlMs?: number;\n /** Injectable clock for tests. Default `Date.now`. */\n now?: () => number;\n /** Injectable token generator for tests. Default 32-byte base64url. */\n generateToken?: () => string;\n}\n\ninterface Slot {\n record: OAuthStateRecord;\n expiresAt: number;\n}\n\nexport class MemoryOAuthStateStore implements IOAuthStateStore {\n private readonly store = new Map<string, Slot>();\n private readonly ttlMs: number;\n private readonly now: () => number;\n private readonly generateToken: () => string;\n\n constructor(opts: MemoryOAuthStateStoreOptions = {}) {\n this.ttlMs = opts.ttlMs ?? 10 * 60 * 1000;\n this.now = opts.now ?? (() => Date.now());\n this.generateToken =\n opts.generateToken ?? (() => randomBytes(32).toString('base64url'));\n }\n\n async generate(record: OAuthStateRecord): Promise<string> {\n const state = this.generateToken();\n this.store.set(state, {\n record: { ...record },\n expiresAt: this.now() + this.ttlMs,\n });\n return state;\n }\n\n async consume(state: string): Promise<OAuthStateRecord> {\n const slot = this.store.get(state);\n if (!slot) {\n throw new OAuthStateError(\n `OAuth state token unknown or already consumed`,\n 'missing',\n );\n }\n // Delete first so a concurrent consume can't replay.\n this.store.delete(state);\n if (slot.expiresAt <= this.now()) {\n throw new OAuthStateError(`OAuth state token expired`, 'expired');\n }\n return slot.record;\n }\n}\n","/**\n * Auth subsystem — `IOAuthStateStore` port.\n *\n * CSRF protection for the OAuth2 authorize-code callback. Generic across\n * providers. The store mints opaque state tokens at /connect time and\n * single-use consumes them at /callback time, returning the original\n * record (userId + optional post-callback redirect path).\n *\n * Concrete backends live under `../backends/`:\n * - `state-store.memory-backend.ts` — in-process Map (tests/dev).\n * - `state-store.drizzle-backend.ts` — Postgres (prod).\n *\n * Semantics:\n * - `generate(record)` → returns an opaque state token; record is stored\n * under that token until consumed or until TTL expires.\n * - `consume(state)` → atomically deletes the entry and returns the\n * record. Throws on missing, expired, or replayed state. Never returns\n * null — a missing/expired state is a CSRF signal.\n */\nexport interface OAuthStateRecord {\n userId: string;\n /** Optional post-callback redirect path (relative URL). */\n redirect?: string;\n}\n\nexport interface IOAuthStateStore {\n /** Mint an opaque state token bound to `record`. Single-use. */\n generate(record: OAuthStateRecord): Promise<string>;\n /**\n * Atomically consume `state`, returning the bound record. Throws on\n * missing / expired / replayed state.\n */\n consume(state: string): Promise<OAuthStateRecord>;\n}\n\n/**\n * Thrown by `IOAuthStateStore.consume` when the state token is unknown,\n * expired, or has already been consumed (replay attempt).\n */\nexport class OAuthStateError extends Error {\n constructor(\n message: string,\n public readonly reason: 'missing' | 'expired',\n ) {\n super(message);\n this.name = 'OAuthStateError';\n }\n}\n","/**\n * Drizzle-backed `IOAuthStateStore`.\n *\n * Uses the `auth_oauth_state` table (see `auth-oauth-state.schema.ts`).\n * Single-use semantics enforced via `DELETE ... RETURNING`: the consume\n * path atomically deletes and returns the row, so a concurrent /callback\n * with the same state cannot replay.\n *\n * Behaviour:\n * - `generate(record)` mints a 256-bit base64url token, INSERTs the row\n * with `expires_at = now() + ttlMs`.\n * - `consume(state)` runs `DELETE ... WHERE state = $1 RETURNING ...`\n * once. Throws `OAuthStateError('missing')` if no row was deleted\n * (unknown or already consumed) and `OAuthStateError('expired')` if\n * the deleted row was past its `expires_at`.\n */\nimport { randomBytes } from 'node:crypto';\nimport { eq } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../../types/drizzle';\nimport { authOAuthState } from '../auth-oauth-state.schema';\nimport {\n type IOAuthStateStore,\n type OAuthStateRecord,\n OAuthStateError,\n} from '../protocols/oauth-state-store';\n\nexport interface DrizzleOAuthStateStoreOptions {\n /** TTL in ms. Default 10 minutes. */\n ttlMs?: number;\n /** Injectable clock for tests. Default `Date.now`. */\n now?: () => number;\n /** Injectable token generator for tests. Default 32-byte base64url. */\n generateToken?: () => string;\n}\n\nexport class DrizzleOAuthStateStore implements IOAuthStateStore {\n private readonly ttlMs: number;\n private readonly now: () => number;\n private readonly generateToken: () => string;\n\n constructor(\n private readonly db: DrizzleClient,\n opts: DrizzleOAuthStateStoreOptions = {},\n ) {\n this.ttlMs = opts.ttlMs ?? 10 * 60 * 1000;\n this.now = opts.now ?? (() => Date.now());\n this.generateToken =\n opts.generateToken ?? (() => randomBytes(32).toString('base64url'));\n }\n\n async generate(record: OAuthStateRecord): Promise<string> {\n const state = this.generateToken();\n const expiresAt = new Date(this.now() + this.ttlMs);\n await this.db.insert(authOAuthState).values({\n state,\n userId: record.userId,\n redirect: record.redirect ?? null,\n expiresAt,\n });\n return state;\n }\n\n async consume(state: string): Promise<OAuthStateRecord> {\n const rows = await this.db\n .delete(authOAuthState)\n .where(eq(authOAuthState.state, state))\n .returning();\n const row = rows[0];\n if (!row) {\n throw new OAuthStateError(\n `OAuth state token unknown or already consumed`,\n 'missing',\n );\n }\n if (row.expiresAt.getTime() <= this.now()) {\n throw new OAuthStateError(`OAuth state token expired`, 'expired');\n }\n return {\n userId: row.userId,\n redirect: row.redirect ?? undefined,\n };\n }\n}\n","/**\n * Drizzle schema for the `auth_oauth_state` table — backs the\n * `DrizzleOAuthStateStore` (`state-store.drizzle-backend.ts`).\n *\n * One row per outstanding /connect → /callback dance. Single-use; rows are\n * deleted on consume. A periodic sweep (or a `WHERE expires_at < now()`\n * filter on read) clears abandoned rows.\n *\n * Columns:\n * - `state` — opaque random token, primary key.\n * - `user_id` — text (matches the consumer-defined user-id shape;\n * the auth subsystem doesn't constrain this to UUID\n * because some apps key users by external id).\n * - `redirect` — optional post-callback redirect path.\n * - `expires_at` — TTL boundary; entries past this are treated as absent.\n *\n * Convention: schema files live at the root of the subsystem dir\n * (mirrors `cache.schema.ts`, `sync-audit.schema.ts`, `domain-events.schema.ts`).\n */\nimport { pgTable, text, timestamp } from 'drizzle-orm/pg-core';\nimport type { InferSelectModel } from 'drizzle-orm';\n\nexport const authOAuthState = pgTable('auth_oauth_state', {\n state: text('state').primaryKey(),\n userId: text('user_id').notNull(),\n redirect: text('redirect'),\n expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),\n});\n\nexport type AuthOAuthState = InferSelectModel<typeof authOAuthState>;\n","/**\n * AuthController — provider-agnostic OAuth2 connect/callback dance.\n *\n * Mounts two routes:\n * - `GET /auth/:provider/connect?redirect=...` — generates state, builds\n * the provider's authorize-url, 302-redirects the browser there.\n * - `GET /auth/:provider/callback?code=...&state=...` — consumes state,\n * exchanges the code for tokens, hands them to the grant sink, then\n * 302-redirects to the post-connect path.\n *\n * Hexagonal seams:\n * - `STRATEGY_REGISTRY` (ReadonlyMap<slug, IProviderStrategy>) — dispatch.\n * Concrete per-provider strategies live consumer-side and contribute\n * entries via a `useFactory` in the consumer's app module.\n * - `AUTH_USER_CONTEXT` (IUserContext) — resolves \"who is this request\"\n * from the consumer's session/JWT/etc.\n * - `OAUTH_STATE_STORE` (IOAuthStateStore) — CSRF state minting/consume.\n * - `AUTH_INTEGRATION_GRANT_SINK` (IIntegrationGrantSink) — persists the\n * freshly-minted grant. Adapter lives consumer-side (e.g. the\n * auth-integrations starter from #285).\n *\n * The controller never imports `IntegrationsService` or any other concrete\n * consumer type — it goes through ports only.\n */\nimport {\n Controller,\n Get,\n Inject,\n Param,\n Query,\n Req,\n Res,\n HttpException,\n HttpStatus,\n} from '@nestjs/common';\nimport {\n AUTH_INTEGRATION_GRANT_SINK,\n AUTH_OPTIONS,\n AUTH_USER_CONTEXT,\n OAUTH_STATE_STORE,\n STRATEGY_REGISTRY,\n} from '../auth.tokens';\nimport type { AuthModuleOptions } from '../auth.module';\nimport type { IOAuthStateStore } from '../protocols/oauth-state-store';\nimport type { IUserContext } from '../protocols/user-context';\nimport type {\n IProviderStrategy,\n ProviderStrategyRegistry,\n} from '../protocols/provider-strategy';\nimport type { IIntegrationGrantSink } from '../protocols/integration-store';\n\n/**\n * Minimal response surface used by the controller — typed loosely so we\n * don't pull a hard dep on `express` or `fastify`. Both popular HTTP\n * adapters expose `redirect(status, url)`.\n */\ninterface RedirectingResponse {\n redirect(statusCode: number, url: string): unknown;\n}\n\n@Controller('auth')\nexport class AuthController {\n constructor(\n @Inject(STRATEGY_REGISTRY)\n private readonly registry: ProviderStrategyRegistry,\n @Inject(AUTH_USER_CONTEXT)\n private readonly userContext: IUserContext,\n @Inject(OAUTH_STATE_STORE)\n private readonly stateStore: IOAuthStateStore,\n @Inject(AUTH_INTEGRATION_GRANT_SINK)\n private readonly grantSink: IIntegrationGrantSink,\n @Inject(AUTH_OPTIONS)\n private readonly options: AuthModuleOptions,\n ) {}\n\n @Get(':provider/connect')\n async connect(\n @Param('provider') slug: string,\n @Query('redirect') redirect: string | undefined,\n @Req() req: unknown,\n @Res() res: RedirectingResponse,\n ): Promise<unknown> {\n const strategy = this.requireStrategy(slug);\n const userId = await this.userContext.getCurrentUserId(req);\n const state = await this.stateStore.generate({ userId, redirect });\n const url = strategy.buildAuthorizeUrl({\n state,\n redirectUri: this.redirectUriFor(slug),\n });\n return res.redirect(HttpStatus.FOUND, url);\n }\n\n @Get(':provider/callback')\n async callback(\n @Param('provider') slug: string,\n @Query('code') code: string | undefined,\n @Query('state') state: string | undefined,\n @Res() res: RedirectingResponse,\n ): Promise<unknown> {\n const strategy = this.requireStrategy(slug);\n if (!code) {\n throw new HttpException(\n `Missing 'code' query param`,\n HttpStatus.BAD_REQUEST,\n );\n }\n if (!state) {\n throw new HttpException(\n `Missing 'state' query param`,\n HttpStatus.BAD_REQUEST,\n );\n }\n const { userId, redirect } = await this.stateStore.consume(state);\n const tokens = await strategy.exchangeCodeForTokens({\n code,\n redirectUri: this.redirectUriFor(slug),\n });\n await this.grantSink.createOrUpdateFromOAuthGrant({\n userId,\n provider: slug,\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n expiresAt: tokens.expiresAt,\n scope: tokens.scope,\n externalAccountId: tokens.externalAccountId,\n providerMetadata: tokens.providerMetadata,\n });\n return res.redirect(\n HttpStatus.FOUND,\n redirect ?? `/settings/integrations?connected=${encodeURIComponent(slug)}`,\n );\n }\n\n private requireStrategy(slug: string): IProviderStrategy {\n const strategy = this.registry.get(slug);\n if (!strategy) {\n throw new HttpException(\n `Unknown provider '${slug}'`,\n HttpStatus.NOT_FOUND,\n );\n }\n return strategy;\n }\n\n private redirectUriFor(slug: string): string {\n const base = this.options.redirectUriBase;\n if (!base) {\n throw new Error(\n `AuthModule.forRoot: redirectUriBase is required when AuthController is enabled`,\n );\n }\n const trimmed = base.replace(/\\/+$/, '');\n return `${trimmed}/auth/${encodeURIComponent(slug)}/callback`;\n }\n}\n","/**\n * NestJS injection tokens\n *\n * Used with @Inject() decorator in concrete repository constructors.\n */\n\n/**\n * Injection token for the Drizzle ORM database client.\n *\n * Usage in concrete repositories:\n * ```typescript\n * constructor(@Inject(DRIZZLE) db: DrizzleClient) { super(db); }\n * ```\n */\nexport const DRIZZLE = 'DRIZZLE' as const;\n\n/**\n * Injection token for the event bus (IEventBus).\n *\n * Optional — only resolved when EventsModule.forRoot() is registered.\n * BaseService uses this with @Optional() to emit lifecycle events\n * without requiring the events subsystem to be installed.\n *\n * Usage in services/use cases:\n * ```typescript\n * @Optional() @Inject(EVENT_BUS) eventBus?: IEventBus\n * ```\n */\nexport const EVENT_BUS = 'EVENT_BUS' as const;\n"],"mappings":"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","names":["randomBytes","randomBytes","randomBytes","randomBytes"]}
|
|
1
|
+
{"version":3,"sources":["../../../../runtime/subsystems/auth/auth.module.ts","../../../../runtime/subsystems/auth/auth.tokens.ts","../../../../runtime/subsystems/auth/backends/encryption-key/env.ts","../../../../runtime/subsystems/auth/backends/state-store.memory-backend.ts","../../../../runtime/subsystems/auth/protocols/oauth-state-store.ts","../../../../runtime/subsystems/auth/backends/state-store.drizzle-backend.ts","../../../../runtime/subsystems/auth/auth-oauth-state.schema.ts","../../../../runtime/subsystems/auth/controllers/auth.controller.ts","../../../../runtime/constants/tokens.ts"],"sourcesContent":["/**\n * AuthModule — DynamicModule factory for the auth subsystem.\n *\n * Wires the pluggable backends the subsystem ships with:\n * - `ENCRYPTION_KEY` → `EnvEncryptionKey` (AES-256-GCM from env)\n * - `OAUTH_STATE_STORE` → `MemoryOAuthStateStore` (dev/tests) or\n * `DrizzleOAuthStateStore` (prod, requires\n * DRIZZLE provider).\n * - `AUTH_OPTIONS` → resolved options bag (used by AuthController\n * for `redirectUriBase`).\n *\n * The connection-store ports (`AUTH_CONNECTION_READER`,\n * `AUTH_CONNECTION_TOKEN_WRITER`, `AUTH_CONNECTION_GRANT_SINK`),\n * `AUTH_USER_CONTEXT`, and `STRATEGY_REGISTRY` are deliberately **not**\n * wired here — they are always consumer-specific:\n * - connection-store ports adapt the consumer's `connections` storage;\n * - `IUserContext` adapts the app's session/JWT scheme;\n * - `STRATEGY_REGISTRY` is populated from the per-provider strategy\n * classes the consumer maintains.\n *\n * Consumers provide them in their app module (or by importing the\n * `auth-integrations` starter, which binds the three connection-store\n * ports off a single canonical entity).\n *\n * Usage in AppModule:\n * ```typescript\n * AuthModule.forRoot({\n * encryptionKey: 'env',\n * oauthStateStore: 'memory', // or 'drizzle'\n * enableController: true,\n * redirectUriBase: 'http://localhost:3000',\n * });\n * ```\n *\n * `global: true` means other modules don't need to re-import AuthModule to\n * inject the auth tokens.\n */\nimport { Module, type DynamicModule, type Provider } from '@nestjs/common';\nimport {\n AUTH_OPTIONS,\n ENCRYPTION_KEY,\n OAUTH_STATE_STORE,\n} from './auth.tokens';\nimport { EnvEncryptionKey } from './backends/encryption-key/env';\nimport { MemoryOAuthStateStore } from './backends/state-store.memory-backend';\nimport { DrizzleOAuthStateStore } from './backends/state-store.drizzle-backend';\nimport { AuthController } from './controllers/auth.controller';\nimport { DRIZZLE } from '../../constants/tokens';\nimport type { DrizzleClient } from '../../types/drizzle';\n\ntype EncryptionKeyChoice =\n | 'env'\n | Omit<Provider, 'provide'>;\n\ntype OAuthStateStoreChoice =\n | 'memory'\n | 'drizzle'\n | Omit<Provider, 'provide'>;\n\nexport interface AuthModuleOptions {\n /** `'env'` (default) or a full provider definition (e.g. `{ useClass: MyKmsEncryptionKey }`). */\n encryptionKey?: EncryptionKeyChoice;\n /**\n * `'memory'` (default — tests/dev) or `'drizzle'` (prod, requires DRIZZLE\n * provider) or a full provider definition for a custom impl.\n */\n oauthStateStore?: OAuthStateStoreChoice;\n /**\n * Mount `AuthController` (`/auth/:provider/connect` + `/callback`).\n * Default `false` — apps that hand-roll connect/callback (rare) or that\n * use the subsystem only for the refresh path can opt out.\n */\n enableController?: boolean;\n /**\n * Public base URL of the API server. Used to construct per-provider\n * callback URIs as `${redirectUriBase}/auth/:provider/callback`.\n * Required when `enableController: true`.\n */\n redirectUriBase?: string;\n}\n\nfunction resolveEncryptionKeyProvider(choice: EncryptionKeyChoice): Provider {\n if (choice === 'env') {\n return { provide: ENCRYPTION_KEY, useClass: EnvEncryptionKey };\n }\n return { provide: ENCRYPTION_KEY, ...choice } as Provider;\n}\n\nfunction resolveOAuthStateStoreProvider(\n choice: OAuthStateStoreChoice,\n): Provider {\n if (choice === 'memory') {\n return { provide: OAUTH_STATE_STORE, useClass: MemoryOAuthStateStore };\n }\n if (choice === 'drizzle') {\n return {\n provide: OAUTH_STATE_STORE,\n useFactory: (db: DrizzleClient | null) => {\n if (!db) {\n throw new Error(\n \"AuthModule.forRoot: oauthStateStore: 'drizzle' selected but DRIZZLE provider is not available. \" +\n 'Ensure DatabaseModule (or another provider exposing DRIZZLE) is imported before AuthModule.forRoot.',\n );\n }\n return new DrizzleOAuthStateStore(db);\n },\n inject: [{ token: DRIZZLE, optional: true }],\n };\n }\n return { provide: OAUTH_STATE_STORE, ...choice } as Provider;\n}\n\n@Module({})\nexport class AuthModule {\n static forRoot(options: AuthModuleOptions = {}): DynamicModule {\n const resolved: AuthModuleOptions = {\n encryptionKey: options.encryptionKey ?? 'env',\n oauthStateStore: options.oauthStateStore ?? 'memory',\n enableController: options.enableController ?? false,\n redirectUriBase: options.redirectUriBase,\n };\n\n if (resolved.enableController && !resolved.redirectUriBase) {\n throw new Error(\n 'AuthModule.forRoot: redirectUriBase is required when enableController: true',\n );\n }\n\n const encryptionKeyProvider = resolveEncryptionKeyProvider(\n resolved.encryptionKey!,\n );\n const oauthStateStoreProvider = resolveOAuthStateStoreProvider(\n resolved.oauthStateStore!,\n );\n const optionsProvider: Provider = {\n provide: AUTH_OPTIONS,\n useValue: resolved,\n };\n\n return {\n module: AuthModule,\n global: true,\n providers: [encryptionKeyProvider, oauthStateStoreProvider, optionsProvider],\n controllers: resolved.enableController ? [AuthController] : [],\n exports: [ENCRYPTION_KEY, OAUTH_STATE_STORE, AUTH_OPTIONS],\n };\n }\n}\n","/**\n * Auth subsystem — injection tokens.\n *\n * Following ADR-008 guidance: `Symbol()` tokens for type safety and collision\n * avoidance. Consumers inject these via `@Inject(...)` against the matching\n * protocol interface.\n *\n * Usage:\n * ```typescript\n * constructor(\n * @Inject(ENCRYPTION_KEY) private readonly key: IEncryptionKey,\n * @Inject(OAUTH_STATE_STORE) private readonly states: IOAuthStateStore,\n * @Inject(AUTH_CONNECTION_READER) private readonly reader: IConnectionReader,\n * @Inject(AUTH_CONNECTION_TOKEN_WRITER) private readonly writer: IConnectionTokenWriter,\n * @Inject(AUTH_CONNECTION_GRANT_SINK) private readonly grants: IConnectionGrantSink,\n * @Inject(AUTH_USER_CONTEXT) private readonly userCtx: IUserContext,\n * @Inject(STRATEGY_REGISTRY) private readonly registry: ProviderStrategyRegistry,\n * ) {}\n * ```\n *\n * `IAuthStrategy` implementations are provider-specific and registered under\n * provider-specific tokens (e.g. `SALESFORCE_AUTH_STRATEGY`,\n * `HUBSPOT_AUTH_STRATEGY`) by each connection module — this subsystem does\n * not mandate a single `AUTH_STRATEGY` token because an app typically has\n * many concurrent strategies, one per provider. They are dispatched through\n * `STRATEGY_REGISTRY` (a `ReadonlyMap<slug, IProviderStrategy>`), populated\n * by per-provider modules via a `useFactory` provider.\n */\nexport const ENCRYPTION_KEY = Symbol('ENCRYPTION_KEY');\nexport const OAUTH_STATE_STORE = Symbol('OAUTH_STATE_STORE');\nexport const AUTH_CONNECTION_READER = Symbol('AUTH_CONNECTION_READER');\nexport const AUTH_CONNECTION_TOKEN_WRITER = Symbol('AUTH_CONNECTION_TOKEN_WRITER');\nexport const AUTH_CONNECTION_GRANT_SINK = Symbol('AUTH_CONNECTION_GRANT_SINK');\nexport const AUTH_USER_CONTEXT = Symbol('AUTH_USER_CONTEXT');\nexport const STRATEGY_REGISTRY = Symbol('STRATEGY_REGISTRY');\n/**\n * Holds the resolved `AuthModuleOptions` (used by `AuthController` to read\n * `redirectUriBase` for building per-provider callback URIs).\n */\nexport const AUTH_OPTIONS = Symbol('AUTH_OPTIONS');\n","/**\n * Env-backed AES-256-GCM encryption.\n *\n * Framing: `base64( nonce(12B) || ciphertext || authTag(16B) )`. Random nonce\n * per call means two encryptions of the same plaintext produce different\n * ciphertexts — prevents replay-style inference. Auth tag enforces integrity;\n * any tampering throws on decrypt.\n *\n * Key source: `INTEGRATION_TOKEN_ENCRYPTION_KEY` env var, 32 bytes base64-encoded.\n * Generate via `openssl rand -base64 32`.\n *\n * Future backend: `kms.ts` (AWS/GCP KMS) for production deployments that\n * need key rotation + audit trails.\n */\nimport { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';\nimport type { IEncryptionKey } from '../../protocols/encryption-key';\n\nexport interface EnvEncryptionKeyOptions {\n /** Defaults to `process.env`. Tests inject a fixture. */\n env?: NodeJS.ProcessEnv;\n /** Defaults to `'INTEGRATION_TOKEN_ENCRYPTION_KEY'`. */\n envVar?: string;\n}\n\nconst ALGO = 'aes-256-gcm';\nconst NONCE_BYTES = 12;\nconst TAG_BYTES = 16;\nconst KEY_BYTES = 32;\n\nexport class EnvEncryptionKey implements IEncryptionKey {\n private readonly key: Buffer;\n\n constructor(opts: EnvEncryptionKeyOptions = {}) {\n const env = opts.env ?? process.env;\n const envVar = opts.envVar ?? 'INTEGRATION_TOKEN_ENCRYPTION_KEY';\n const raw = env[envVar];\n if (!raw) {\n throw new Error(\n `EnvEncryptionKey: ${envVar} is not set. Generate with: openssl rand -base64 32`,\n );\n }\n const decoded = Buffer.from(raw, 'base64');\n if (decoded.length !== KEY_BYTES) {\n throw new Error(\n `EnvEncryptionKey: ${envVar} must decode to ${KEY_BYTES} bytes (got ${decoded.length}). Use: openssl rand -base64 32`,\n );\n }\n this.key = decoded;\n }\n\n async encrypt(plaintext: string): Promise<string> {\n const nonce = randomBytes(NONCE_BYTES);\n const cipher = createCipheriv(ALGO, this.key, nonce);\n const ciphertext = Buffer.concat([\n cipher.update(plaintext, 'utf8'),\n cipher.final(),\n ]);\n const authTag = cipher.getAuthTag();\n return Buffer.concat([nonce, ciphertext, authTag]).toString('base64');\n }\n\n async decrypt(ciphertext: string): Promise<string> {\n const buf = Buffer.from(ciphertext, 'base64');\n if (buf.length < NONCE_BYTES + TAG_BYTES) {\n throw new Error('EnvEncryptionKey: ciphertext too short');\n }\n const nonce = buf.subarray(0, NONCE_BYTES);\n const authTag = buf.subarray(buf.length - TAG_BYTES);\n const body = buf.subarray(NONCE_BYTES, buf.length - TAG_BYTES);\n\n const decipher = createDecipheriv(ALGO, this.key, nonce);\n decipher.setAuthTag(authTag);\n const plain = Buffer.concat([decipher.update(body), decipher.final()]);\n return plain.toString('utf8');\n }\n}\n","/**\n * In-memory `IOAuthStateStore` backend.\n *\n * Single-process store — Map<state, { record, expiresAt }>. Suitable for\n * tests and single-worker dev. Production deployments select the drizzle\n * backend so state survives restarts and is shared across workers.\n *\n * Single-use semantics:\n * - `generate(record)` mints a 256-bit random token (base64url, opaque).\n * - `consume(state)` deletes the entry on read. A second call with the\n * same state throws `OAuthStateError('replay')`.\n * - Expired entries also throw (`'expired'`); the entry is deleted as a\n * side effect so a later replay still surfaces correctly.\n *\n * TTL defaults to 10 minutes — long enough for a user to complete the\n * provider's consent screen, short enough that abandoned states age out.\n */\nimport { randomBytes } from 'node:crypto';\nimport {\n type IOAuthStateStore,\n type OAuthStateRecord,\n OAuthStateError,\n} from '../protocols/oauth-state-store';\n\nexport interface MemoryOAuthStateStoreOptions {\n /** TTL in ms. Default 10 minutes. */\n ttlMs?: number;\n /** Injectable clock for tests. Default `Date.now`. */\n now?: () => number;\n /** Injectable token generator for tests. Default 32-byte base64url. */\n generateToken?: () => string;\n}\n\ninterface Slot {\n record: OAuthStateRecord;\n expiresAt: number;\n}\n\nexport class MemoryOAuthStateStore implements IOAuthStateStore {\n private readonly store = new Map<string, Slot>();\n private readonly ttlMs: number;\n private readonly now: () => number;\n private readonly generateToken: () => string;\n\n constructor(opts: MemoryOAuthStateStoreOptions = {}) {\n this.ttlMs = opts.ttlMs ?? 10 * 60 * 1000;\n this.now = opts.now ?? (() => Date.now());\n this.generateToken =\n opts.generateToken ?? (() => randomBytes(32).toString('base64url'));\n }\n\n async generate(record: OAuthStateRecord): Promise<string> {\n const state = this.generateToken();\n this.store.set(state, {\n record: { ...record },\n expiresAt: this.now() + this.ttlMs,\n });\n return state;\n }\n\n async consume(state: string): Promise<OAuthStateRecord> {\n const slot = this.store.get(state);\n if (!slot) {\n throw new OAuthStateError(\n `OAuth state token unknown or already consumed`,\n 'missing',\n );\n }\n // Delete first so a concurrent consume can't replay.\n this.store.delete(state);\n if (slot.expiresAt <= this.now()) {\n throw new OAuthStateError(`OAuth state token expired`, 'expired');\n }\n return slot.record;\n }\n}\n","/**\n * Auth subsystem — `IOAuthStateStore` port.\n *\n * CSRF protection for the OAuth2 authorize-code callback. Generic across\n * providers. The store mints opaque state tokens at /connect time and\n * single-use consumes them at /callback time, returning the original\n * record (userId + optional post-callback redirect path).\n *\n * Concrete backends live under `../backends/`:\n * - `state-store.memory-backend.ts` — in-process Map (tests/dev).\n * - `state-store.drizzle-backend.ts` — Postgres (prod).\n *\n * Semantics:\n * - `generate(record)` → returns an opaque state token; record is stored\n * under that token until consumed or until TTL expires.\n * - `consume(state)` → atomically deletes the entry and returns the\n * record. Throws on missing, expired, or replayed state. Never returns\n * null — a missing/expired state is a CSRF signal.\n */\nexport interface OAuthStateRecord {\n userId: string;\n /** Optional post-callback redirect path (relative URL). */\n redirect?: string;\n}\n\nexport interface IOAuthStateStore {\n /** Mint an opaque state token bound to `record`. Single-use. */\n generate(record: OAuthStateRecord): Promise<string>;\n /**\n * Atomically consume `state`, returning the bound record. Throws on\n * missing / expired / replayed state.\n */\n consume(state: string): Promise<OAuthStateRecord>;\n}\n\n/**\n * Thrown by `IOAuthStateStore.consume` when the state token is unknown,\n * expired, or has already been consumed (replay attempt).\n */\nexport class OAuthStateError extends Error {\n constructor(\n message: string,\n public readonly reason: 'missing' | 'expired',\n ) {\n super(message);\n this.name = 'OAuthStateError';\n }\n}\n","/**\n * Drizzle-backed `IOAuthStateStore`.\n *\n * Uses the `auth_oauth_state` table (see `auth-oauth-state.schema.ts`).\n * Single-use semantics enforced via `DELETE ... RETURNING`: the consume\n * path atomically deletes and returns the row, so a concurrent /callback\n * with the same state cannot replay.\n *\n * Behaviour:\n * - `generate(record)` mints a 256-bit base64url token, INSERTs the row\n * with `expires_at = now() + ttlMs`.\n * - `consume(state)` runs `DELETE ... WHERE state = $1 RETURNING ...`\n * once. Throws `OAuthStateError('missing')` if no row was deleted\n * (unknown or already consumed) and `OAuthStateError('expired')` if\n * the deleted row was past its `expires_at`.\n */\nimport { randomBytes } from 'node:crypto';\nimport { eq } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../../types/drizzle';\nimport { authOAuthState } from '../auth-oauth-state.schema';\nimport {\n type IOAuthStateStore,\n type OAuthStateRecord,\n OAuthStateError,\n} from '../protocols/oauth-state-store';\n\nexport interface DrizzleOAuthStateStoreOptions {\n /** TTL in ms. Default 10 minutes. */\n ttlMs?: number;\n /** Injectable clock for tests. Default `Date.now`. */\n now?: () => number;\n /** Injectable token generator for tests. Default 32-byte base64url. */\n generateToken?: () => string;\n}\n\nexport class DrizzleOAuthStateStore implements IOAuthStateStore {\n private readonly ttlMs: number;\n private readonly now: () => number;\n private readonly generateToken: () => string;\n\n constructor(\n private readonly db: DrizzleClient,\n opts: DrizzleOAuthStateStoreOptions = {},\n ) {\n this.ttlMs = opts.ttlMs ?? 10 * 60 * 1000;\n this.now = opts.now ?? (() => Date.now());\n this.generateToken =\n opts.generateToken ?? (() => randomBytes(32).toString('base64url'));\n }\n\n async generate(record: OAuthStateRecord): Promise<string> {\n const state = this.generateToken();\n const expiresAt = new Date(this.now() + this.ttlMs);\n await this.db.insert(authOAuthState).values({\n state,\n userId: record.userId,\n redirect: record.redirect ?? null,\n expiresAt,\n });\n return state;\n }\n\n async consume(state: string): Promise<OAuthStateRecord> {\n const rows = await this.db\n .delete(authOAuthState)\n .where(eq(authOAuthState.state, state))\n .returning();\n const row = rows[0];\n if (!row) {\n throw new OAuthStateError(\n `OAuth state token unknown or already consumed`,\n 'missing',\n );\n }\n if (row.expiresAt.getTime() <= this.now()) {\n throw new OAuthStateError(`OAuth state token expired`, 'expired');\n }\n return {\n userId: row.userId,\n redirect: row.redirect ?? undefined,\n };\n }\n}\n","/**\n * Drizzle schema for the `auth_oauth_state` table — backs the\n * `DrizzleOAuthStateStore` (`state-store.drizzle-backend.ts`).\n *\n * One row per outstanding /connect → /callback dance. Single-use; rows are\n * deleted on consume. A periodic sweep (or a `WHERE expires_at < now()`\n * filter on read) clears abandoned rows.\n *\n * Columns:\n * - `state` — opaque random token, primary key.\n * - `user_id` — text (matches the consumer-defined user-id shape;\n * the auth subsystem doesn't constrain this to UUID\n * because some apps key users by external id).\n * - `redirect` — optional post-callback redirect path.\n * - `expires_at` — TTL boundary; entries past this are treated as absent.\n *\n * Convention: schema files live at the root of the subsystem dir\n * (mirrors `cache.schema.ts`, `integration-audit.schema.ts`, `domain-events.schema.ts`).\n */\nimport { pgTable, text, timestamp } from 'drizzle-orm/pg-core';\nimport type { InferSelectModel } from 'drizzle-orm';\n\nexport const authOAuthState = pgTable('auth_oauth_state', {\n state: text('state').primaryKey(),\n userId: text('user_id').notNull(),\n redirect: text('redirect'),\n expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),\n});\n\nexport type AuthOAuthState = InferSelectModel<typeof authOAuthState>;\n","/**\n * AuthController — provider-agnostic OAuth2 connect/callback dance.\n *\n * Mounts two routes:\n * - `GET /auth/:provider/connect?redirect=...` — generates state, builds\n * the provider's authorize-url, 302-redirects the browser there.\n * - `GET /auth/:provider/callback?code=...&state=...` — consumes state,\n * exchanges the code for tokens, hands them to the grant sink, then\n * 302-redirects to the post-connect path.\n *\n * Hexagonal seams:\n * - `STRATEGY_REGISTRY` (ReadonlyMap<slug, IProviderStrategy>) — dispatch.\n * Concrete per-provider strategies live consumer-side and contribute\n * entries via a `useFactory` in the consumer's app module.\n * - `AUTH_USER_CONTEXT` (IUserContext) — resolves \"who is this request\"\n * from the consumer's session/JWT/etc.\n * - `OAUTH_STATE_STORE` (IOAuthStateStore) — CSRF state minting/consume.\n * - `AUTH_CONNECTION_GRANT_SINK` (IConnectionGrantSink) — persists the\n * freshly-minted grant. Adapter lives consumer-side (e.g. the\n * auth-integrations starter from #285).\n *\n * The controller never imports `ConnectionsService` or any other concrete\n * consumer type — it goes through ports only.\n */\nimport {\n Controller,\n Get,\n Inject,\n Param,\n Query,\n Req,\n Res,\n HttpException,\n HttpStatus,\n} from '@nestjs/common';\nimport {\n AUTH_CONNECTION_GRANT_SINK,\n AUTH_OPTIONS,\n AUTH_USER_CONTEXT,\n OAUTH_STATE_STORE,\n STRATEGY_REGISTRY,\n} from '../auth.tokens';\nimport type { AuthModuleOptions } from '../auth.module';\nimport type { IOAuthStateStore } from '../protocols/oauth-state-store';\nimport type { IUserContext } from '../protocols/user-context';\nimport type {\n IProviderStrategy,\n ProviderStrategyRegistry,\n} from '../protocols/provider-strategy';\nimport type { IConnectionGrantSink } from '../protocols/connection-store';\n\n/**\n * Minimal response surface used by the controller — typed loosely so we\n * don't pull a hard dep on `express` or `fastify`. Both popular HTTP\n * adapters expose `redirect(status, url)`.\n */\ninterface RedirectingResponse {\n redirect(statusCode: number, url: string): unknown;\n}\n\n@Controller('auth')\nexport class AuthController {\n constructor(\n @Inject(STRATEGY_REGISTRY)\n private readonly registry: ProviderStrategyRegistry,\n @Inject(AUTH_USER_CONTEXT)\n private readonly userContext: IUserContext,\n @Inject(OAUTH_STATE_STORE)\n private readonly stateStore: IOAuthStateStore,\n @Inject(AUTH_CONNECTION_GRANT_SINK)\n private readonly grantSink: IConnectionGrantSink,\n @Inject(AUTH_OPTIONS)\n private readonly options: AuthModuleOptions,\n ) {}\n\n @Get(':provider/connect')\n async connect(\n @Param('provider') slug: string,\n @Query('redirect') redirect: string | undefined,\n @Req() req: unknown,\n @Res() res: RedirectingResponse,\n ): Promise<unknown> {\n const strategy = this.requireStrategy(slug);\n const userId = await this.userContext.getCurrentUserId(req);\n const state = await this.stateStore.generate({ userId, redirect });\n const url = strategy.buildAuthorizeUrl({\n state,\n redirectUri: this.redirectUriFor(slug),\n });\n return res.redirect(HttpStatus.FOUND, url);\n }\n\n @Get(':provider/callback')\n async callback(\n @Param('provider') slug: string,\n @Query('code') code: string | undefined,\n @Query('state') state: string | undefined,\n @Res() res: RedirectingResponse,\n ): Promise<unknown> {\n const strategy = this.requireStrategy(slug);\n if (!code) {\n throw new HttpException(\n `Missing 'code' query param`,\n HttpStatus.BAD_REQUEST,\n );\n }\n if (!state) {\n throw new HttpException(\n `Missing 'state' query param`,\n HttpStatus.BAD_REQUEST,\n );\n }\n const { userId, redirect } = await this.stateStore.consume(state);\n const tokens = await strategy.exchangeCodeForTokens({\n code,\n redirectUri: this.redirectUriFor(slug),\n });\n await this.grantSink.createOrUpdateFromOAuthGrant({\n userId,\n provider: slug,\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n expiresAt: tokens.expiresAt,\n scope: tokens.scope,\n externalAccountId: tokens.externalAccountId,\n providerMetadata: tokens.providerMetadata,\n });\n return res.redirect(\n HttpStatus.FOUND,\n redirect ?? `/settings/connections?connected=${encodeURIComponent(slug)}`,\n );\n }\n\n private requireStrategy(slug: string): IProviderStrategy {\n const strategy = this.registry.get(slug);\n if (!strategy) {\n throw new HttpException(\n `Unknown provider '${slug}'`,\n HttpStatus.NOT_FOUND,\n );\n }\n return strategy;\n }\n\n private redirectUriFor(slug: string): string {\n const base = this.options.redirectUriBase;\n if (!base) {\n throw new Error(\n `AuthModule.forRoot: redirectUriBase is required when AuthController is enabled`,\n );\n }\n const trimmed = base.replace(/\\/+$/, '');\n return `${trimmed}/auth/${encodeURIComponent(slug)}/callback`;\n }\n}\n","/**\n * NestJS injection tokens\n *\n * Used with @Inject() decorator in concrete repository constructors.\n */\n\n/**\n * Injection token for the Drizzle ORM database client.\n *\n * Usage in concrete repositories:\n * ```typescript\n * constructor(@Inject(DRIZZLE) db: DrizzleClient) { super(db); }\n * ```\n */\nexport const DRIZZLE = 'DRIZZLE' as const;\n\n/**\n * Injection token for the event bus (IEventBus).\n *\n * Optional — only resolved when EventsModule.forRoot() is registered.\n * BaseService uses this with @Optional() to emit lifecycle events\n * without requiring the events subsystem to be installed.\n *\n * Usage in services/use cases:\n * ```typescript\n * @Optional() @Inject(EVENT_BUS) eventBus?: IEventBus\n * ```\n */\nexport const EVENT_BUS = 'EVENT_BUS' as const;\n"],"mappings":"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","names":["randomBytes","randomBytes","randomBytes","randomBytes"]}
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* constructor(
|
|
11
11
|
* @Inject(ENCRYPTION_KEY) private readonly key: IEncryptionKey,
|
|
12
12
|
* @Inject(OAUTH_STATE_STORE) private readonly states: IOAuthStateStore,
|
|
13
|
-
* @Inject(
|
|
14
|
-
* @Inject(
|
|
15
|
-
* @Inject(
|
|
13
|
+
* @Inject(AUTH_CONNECTION_READER) private readonly reader: IConnectionReader,
|
|
14
|
+
* @Inject(AUTH_CONNECTION_TOKEN_WRITER) private readonly writer: IConnectionTokenWriter,
|
|
15
|
+
* @Inject(AUTH_CONNECTION_GRANT_SINK) private readonly grants: IConnectionGrantSink,
|
|
16
16
|
* @Inject(AUTH_USER_CONTEXT) private readonly userCtx: IUserContext,
|
|
17
17
|
* @Inject(STRATEGY_REGISTRY) private readonly registry: ProviderStrategyRegistry,
|
|
18
18
|
* ) {}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
* `IAuthStrategy` implementations are provider-specific and registered under
|
|
22
22
|
* provider-specific tokens (e.g. `SALESFORCE_AUTH_STRATEGY`,
|
|
23
|
-
* `HUBSPOT_AUTH_STRATEGY`) by each
|
|
23
|
+
* `HUBSPOT_AUTH_STRATEGY`) by each connection module — this subsystem does
|
|
24
24
|
* not mandate a single `AUTH_STRATEGY` token because an app typically has
|
|
25
25
|
* many concurrent strategies, one per provider. They are dispatched through
|
|
26
26
|
* `STRATEGY_REGISTRY` (a `ReadonlyMap<slug, IProviderStrategy>`), populated
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
*/
|
|
29
29
|
declare const ENCRYPTION_KEY: unique symbol;
|
|
30
30
|
declare const OAUTH_STATE_STORE: unique symbol;
|
|
31
|
-
declare const
|
|
32
|
-
declare const
|
|
33
|
-
declare const
|
|
31
|
+
declare const AUTH_CONNECTION_READER: unique symbol;
|
|
32
|
+
declare const AUTH_CONNECTION_TOKEN_WRITER: unique symbol;
|
|
33
|
+
declare const AUTH_CONNECTION_GRANT_SINK: unique symbol;
|
|
34
34
|
declare const AUTH_USER_CONTEXT: unique symbol;
|
|
35
35
|
declare const STRATEGY_REGISTRY: unique symbol;
|
|
36
36
|
/**
|
|
@@ -39,4 +39,4 @@ declare const STRATEGY_REGISTRY: unique symbol;
|
|
|
39
39
|
*/
|
|
40
40
|
declare const AUTH_OPTIONS: unique symbol;
|
|
41
41
|
|
|
42
|
-
export {
|
|
42
|
+
export { AUTH_CONNECTION_GRANT_SINK, AUTH_CONNECTION_READER, AUTH_CONNECTION_TOKEN_WRITER, AUTH_OPTIONS, AUTH_USER_CONTEXT, ENCRYPTION_KEY, OAUTH_STATE_STORE, STRATEGY_REGISTRY };
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
// runtime/subsystems/auth/auth.tokens.ts
|
|
2
2
|
var ENCRYPTION_KEY = /* @__PURE__ */ Symbol("ENCRYPTION_KEY");
|
|
3
3
|
var OAUTH_STATE_STORE = /* @__PURE__ */ Symbol("OAUTH_STATE_STORE");
|
|
4
|
-
var
|
|
5
|
-
var
|
|
6
|
-
var
|
|
4
|
+
var AUTH_CONNECTION_READER = /* @__PURE__ */ Symbol("AUTH_CONNECTION_READER");
|
|
5
|
+
var AUTH_CONNECTION_TOKEN_WRITER = /* @__PURE__ */ Symbol("AUTH_CONNECTION_TOKEN_WRITER");
|
|
6
|
+
var AUTH_CONNECTION_GRANT_SINK = /* @__PURE__ */ Symbol("AUTH_CONNECTION_GRANT_SINK");
|
|
7
7
|
var AUTH_USER_CONTEXT = /* @__PURE__ */ Symbol("AUTH_USER_CONTEXT");
|
|
8
8
|
var STRATEGY_REGISTRY = /* @__PURE__ */ Symbol("STRATEGY_REGISTRY");
|
|
9
9
|
var AUTH_OPTIONS = /* @__PURE__ */ Symbol("AUTH_OPTIONS");
|
|
10
10
|
export {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
AUTH_CONNECTION_GRANT_SINK,
|
|
12
|
+
AUTH_CONNECTION_READER,
|
|
13
|
+
AUTH_CONNECTION_TOKEN_WRITER,
|
|
14
14
|
AUTH_OPTIONS,
|
|
15
15
|
AUTH_USER_CONTEXT,
|
|
16
16
|
ENCRYPTION_KEY,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../runtime/subsystems/auth/auth.tokens.ts"],"sourcesContent":["/**\n * Auth subsystem — injection tokens.\n *\n * Following ADR-008 guidance: `Symbol()` tokens for type safety and collision\n * avoidance. Consumers inject these via `@Inject(...)` against the matching\n * protocol interface.\n *\n * Usage:\n * ```typescript\n * constructor(\n * @Inject(ENCRYPTION_KEY) private readonly key: IEncryptionKey,\n * @Inject(OAUTH_STATE_STORE) private readonly states: IOAuthStateStore,\n * @Inject(
|
|
1
|
+
{"version":3,"sources":["../../../../runtime/subsystems/auth/auth.tokens.ts"],"sourcesContent":["/**\n * Auth subsystem — injection tokens.\n *\n * Following ADR-008 guidance: `Symbol()` tokens for type safety and collision\n * avoidance. Consumers inject these via `@Inject(...)` against the matching\n * protocol interface.\n *\n * Usage:\n * ```typescript\n * constructor(\n * @Inject(ENCRYPTION_KEY) private readonly key: IEncryptionKey,\n * @Inject(OAUTH_STATE_STORE) private readonly states: IOAuthStateStore,\n * @Inject(AUTH_CONNECTION_READER) private readonly reader: IConnectionReader,\n * @Inject(AUTH_CONNECTION_TOKEN_WRITER) private readonly writer: IConnectionTokenWriter,\n * @Inject(AUTH_CONNECTION_GRANT_SINK) private readonly grants: IConnectionGrantSink,\n * @Inject(AUTH_USER_CONTEXT) private readonly userCtx: IUserContext,\n * @Inject(STRATEGY_REGISTRY) private readonly registry: ProviderStrategyRegistry,\n * ) {}\n * ```\n *\n * `IAuthStrategy` implementations are provider-specific and registered under\n * provider-specific tokens (e.g. `SALESFORCE_AUTH_STRATEGY`,\n * `HUBSPOT_AUTH_STRATEGY`) by each connection module — this subsystem does\n * not mandate a single `AUTH_STRATEGY` token because an app typically has\n * many concurrent strategies, one per provider. They are dispatched through\n * `STRATEGY_REGISTRY` (a `ReadonlyMap<slug, IProviderStrategy>`), populated\n * by per-provider modules via a `useFactory` provider.\n */\nexport const ENCRYPTION_KEY = Symbol('ENCRYPTION_KEY');\nexport const OAUTH_STATE_STORE = Symbol('OAUTH_STATE_STORE');\nexport const AUTH_CONNECTION_READER = Symbol('AUTH_CONNECTION_READER');\nexport const AUTH_CONNECTION_TOKEN_WRITER = Symbol('AUTH_CONNECTION_TOKEN_WRITER');\nexport const AUTH_CONNECTION_GRANT_SINK = Symbol('AUTH_CONNECTION_GRANT_SINK');\nexport const AUTH_USER_CONTEXT = Symbol('AUTH_USER_CONTEXT');\nexport const STRATEGY_REGISTRY = Symbol('STRATEGY_REGISTRY');\n/**\n * Holds the resolved `AuthModuleOptions` (used by `AuthController` to read\n * `redirectUriBase` for building per-provider callback URIs).\n */\nexport const AUTH_OPTIONS = Symbol('AUTH_OPTIONS');\n"],"mappings":";AA4BO,IAAM,iBAAiB,uBAAO,gBAAgB;AAC9C,IAAM,oBAAoB,uBAAO,mBAAmB;AACpD,IAAM,yBAAyB,uBAAO,wBAAwB;AAC9D,IAAM,+BAA+B,uBAAO,8BAA8B;AAC1E,IAAM,6BAA6B,uBAAO,4BAA4B;AACtE,IAAM,oBAAoB,uBAAO,mBAAmB;AACpD,IAAM,oBAAoB,uBAAO,mBAAmB;AAKpD,IAAM,eAAe,uBAAO,cAAc;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../runtime/subsystems/auth/backends/state-store.drizzle-backend.ts","../../../../../runtime/subsystems/auth/auth-oauth-state.schema.ts","../../../../../runtime/subsystems/auth/protocols/oauth-state-store.ts"],"sourcesContent":["/**\n * Drizzle-backed `IOAuthStateStore`.\n *\n * Uses the `auth_oauth_state` table (see `auth-oauth-state.schema.ts`).\n * Single-use semantics enforced via `DELETE ... RETURNING`: the consume\n * path atomically deletes and returns the row, so a concurrent /callback\n * with the same state cannot replay.\n *\n * Behaviour:\n * - `generate(record)` mints a 256-bit base64url token, INSERTs the row\n * with `expires_at = now() + ttlMs`.\n * - `consume(state)` runs `DELETE ... WHERE state = $1 RETURNING ...`\n * once. Throws `OAuthStateError('missing')` if no row was deleted\n * (unknown or already consumed) and `OAuthStateError('expired')` if\n * the deleted row was past its `expires_at`.\n */\nimport { randomBytes } from 'node:crypto';\nimport { eq } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../../types/drizzle';\nimport { authOAuthState } from '../auth-oauth-state.schema';\nimport {\n type IOAuthStateStore,\n type OAuthStateRecord,\n OAuthStateError,\n} from '../protocols/oauth-state-store';\n\nexport interface DrizzleOAuthStateStoreOptions {\n /** TTL in ms. Default 10 minutes. */\n ttlMs?: number;\n /** Injectable clock for tests. Default `Date.now`. */\n now?: () => number;\n /** Injectable token generator for tests. Default 32-byte base64url. */\n generateToken?: () => string;\n}\n\nexport class DrizzleOAuthStateStore implements IOAuthStateStore {\n private readonly ttlMs: number;\n private readonly now: () => number;\n private readonly generateToken: () => string;\n\n constructor(\n private readonly db: DrizzleClient,\n opts: DrizzleOAuthStateStoreOptions = {},\n ) {\n this.ttlMs = opts.ttlMs ?? 10 * 60 * 1000;\n this.now = opts.now ?? (() => Date.now());\n this.generateToken =\n opts.generateToken ?? (() => randomBytes(32).toString('base64url'));\n }\n\n async generate(record: OAuthStateRecord): Promise<string> {\n const state = this.generateToken();\n const expiresAt = new Date(this.now() + this.ttlMs);\n await this.db.insert(authOAuthState).values({\n state,\n userId: record.userId,\n redirect: record.redirect ?? null,\n expiresAt,\n });\n return state;\n }\n\n async consume(state: string): Promise<OAuthStateRecord> {\n const rows = await this.db\n .delete(authOAuthState)\n .where(eq(authOAuthState.state, state))\n .returning();\n const row = rows[0];\n if (!row) {\n throw new OAuthStateError(\n `OAuth state token unknown or already consumed`,\n 'missing',\n );\n }\n if (row.expiresAt.getTime() <= this.now()) {\n throw new OAuthStateError(`OAuth state token expired`, 'expired');\n }\n return {\n userId: row.userId,\n redirect: row.redirect ?? undefined,\n };\n }\n}\n","/**\n * Drizzle schema for the `auth_oauth_state` table — backs the\n * `DrizzleOAuthStateStore` (`state-store.drizzle-backend.ts`).\n *\n * One row per outstanding /connect → /callback dance. Single-use; rows are\n * deleted on consume. A periodic sweep (or a `WHERE expires_at < now()`\n * filter on read) clears abandoned rows.\n *\n * Columns:\n * - `state` — opaque random token, primary key.\n * - `user_id` — text (matches the consumer-defined user-id shape;\n * the auth subsystem doesn't constrain this to UUID\n * because some apps key users by external id).\n * - `redirect` — optional post-callback redirect path.\n * - `expires_at` — TTL boundary; entries past this are treated as absent.\n *\n * Convention: schema files live at the root of the subsystem dir\n * (mirrors `cache.schema.ts`, `
|
|
1
|
+
{"version":3,"sources":["../../../../../runtime/subsystems/auth/backends/state-store.drizzle-backend.ts","../../../../../runtime/subsystems/auth/auth-oauth-state.schema.ts","../../../../../runtime/subsystems/auth/protocols/oauth-state-store.ts"],"sourcesContent":["/**\n * Drizzle-backed `IOAuthStateStore`.\n *\n * Uses the `auth_oauth_state` table (see `auth-oauth-state.schema.ts`).\n * Single-use semantics enforced via `DELETE ... RETURNING`: the consume\n * path atomically deletes and returns the row, so a concurrent /callback\n * with the same state cannot replay.\n *\n * Behaviour:\n * - `generate(record)` mints a 256-bit base64url token, INSERTs the row\n * with `expires_at = now() + ttlMs`.\n * - `consume(state)` runs `DELETE ... WHERE state = $1 RETURNING ...`\n * once. Throws `OAuthStateError('missing')` if no row was deleted\n * (unknown or already consumed) and `OAuthStateError('expired')` if\n * the deleted row was past its `expires_at`.\n */\nimport { randomBytes } from 'node:crypto';\nimport { eq } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../../types/drizzle';\nimport { authOAuthState } from '../auth-oauth-state.schema';\nimport {\n type IOAuthStateStore,\n type OAuthStateRecord,\n OAuthStateError,\n} from '../protocols/oauth-state-store';\n\nexport interface DrizzleOAuthStateStoreOptions {\n /** TTL in ms. Default 10 minutes. */\n ttlMs?: number;\n /** Injectable clock for tests. Default `Date.now`. */\n now?: () => number;\n /** Injectable token generator for tests. Default 32-byte base64url. */\n generateToken?: () => string;\n}\n\nexport class DrizzleOAuthStateStore implements IOAuthStateStore {\n private readonly ttlMs: number;\n private readonly now: () => number;\n private readonly generateToken: () => string;\n\n constructor(\n private readonly db: DrizzleClient,\n opts: DrizzleOAuthStateStoreOptions = {},\n ) {\n this.ttlMs = opts.ttlMs ?? 10 * 60 * 1000;\n this.now = opts.now ?? (() => Date.now());\n this.generateToken =\n opts.generateToken ?? (() => randomBytes(32).toString('base64url'));\n }\n\n async generate(record: OAuthStateRecord): Promise<string> {\n const state = this.generateToken();\n const expiresAt = new Date(this.now() + this.ttlMs);\n await this.db.insert(authOAuthState).values({\n state,\n userId: record.userId,\n redirect: record.redirect ?? null,\n expiresAt,\n });\n return state;\n }\n\n async consume(state: string): Promise<OAuthStateRecord> {\n const rows = await this.db\n .delete(authOAuthState)\n .where(eq(authOAuthState.state, state))\n .returning();\n const row = rows[0];\n if (!row) {\n throw new OAuthStateError(\n `OAuth state token unknown or already consumed`,\n 'missing',\n );\n }\n if (row.expiresAt.getTime() <= this.now()) {\n throw new OAuthStateError(`OAuth state token expired`, 'expired');\n }\n return {\n userId: row.userId,\n redirect: row.redirect ?? undefined,\n };\n }\n}\n","/**\n * Drizzle schema for the `auth_oauth_state` table — backs the\n * `DrizzleOAuthStateStore` (`state-store.drizzle-backend.ts`).\n *\n * One row per outstanding /connect → /callback dance. Single-use; rows are\n * deleted on consume. A periodic sweep (or a `WHERE expires_at < now()`\n * filter on read) clears abandoned rows.\n *\n * Columns:\n * - `state` — opaque random token, primary key.\n * - `user_id` — text (matches the consumer-defined user-id shape;\n * the auth subsystem doesn't constrain this to UUID\n * because some apps key users by external id).\n * - `redirect` — optional post-callback redirect path.\n * - `expires_at` — TTL boundary; entries past this are treated as absent.\n *\n * Convention: schema files live at the root of the subsystem dir\n * (mirrors `cache.schema.ts`, `integration-audit.schema.ts`, `domain-events.schema.ts`).\n */\nimport { pgTable, text, timestamp } from 'drizzle-orm/pg-core';\nimport type { InferSelectModel } from 'drizzle-orm';\n\nexport const authOAuthState = pgTable('auth_oauth_state', {\n state: text('state').primaryKey(),\n userId: text('user_id').notNull(),\n redirect: text('redirect'),\n expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),\n});\n\nexport type AuthOAuthState = InferSelectModel<typeof authOAuthState>;\n","/**\n * Auth subsystem — `IOAuthStateStore` port.\n *\n * CSRF protection for the OAuth2 authorize-code callback. Generic across\n * providers. The store mints opaque state tokens at /connect time and\n * single-use consumes them at /callback time, returning the original\n * record (userId + optional post-callback redirect path).\n *\n * Concrete backends live under `../backends/`:\n * - `state-store.memory-backend.ts` — in-process Map (tests/dev).\n * - `state-store.drizzle-backend.ts` — Postgres (prod).\n *\n * Semantics:\n * - `generate(record)` → returns an opaque state token; record is stored\n * under that token until consumed or until TTL expires.\n * - `consume(state)` → atomically deletes the entry and returns the\n * record. Throws on missing, expired, or replayed state. Never returns\n * null — a missing/expired state is a CSRF signal.\n */\nexport interface OAuthStateRecord {\n userId: string;\n /** Optional post-callback redirect path (relative URL). */\n redirect?: string;\n}\n\nexport interface IOAuthStateStore {\n /** Mint an opaque state token bound to `record`. Single-use. */\n generate(record: OAuthStateRecord): Promise<string>;\n /**\n * Atomically consume `state`, returning the bound record. Throws on\n * missing / expired / replayed state.\n */\n consume(state: string): Promise<OAuthStateRecord>;\n}\n\n/**\n * Thrown by `IOAuthStateStore.consume` when the state token is unknown,\n * expired, or has already been consumed (replay attempt).\n */\nexport class OAuthStateError extends Error {\n constructor(\n message: string,\n public readonly reason: 'missing' | 'expired',\n ) {\n super(message);\n this.name = 'OAuthStateError';\n }\n}\n"],"mappings":";AAgBA,SAAS,mBAAmB;AAC5B,SAAS,UAAU;;;ACEnB,SAAS,SAAS,MAAM,iBAAiB;AAGlC,IAAM,iBAAiB,QAAQ,oBAAoB;AAAA,EACxD,OAAO,KAAK,OAAO,EAAE,WAAW;AAAA,EAChC,QAAQ,KAAK,SAAS,EAAE,QAAQ;AAAA,EAChC,UAAU,KAAK,UAAU;AAAA,EACzB,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,EAAE,QAAQ;AACrE,CAAC;;;ACYM,IAAM,kBAAN,cAA8B,MAAM;AAAA,EACzC,YACE,SACgB,QAChB;AACA,UAAM,OAAO;AAFG;AAGhB,SAAK,OAAO;AAAA,EACd;AAAA,EAJkB;AAKpB;;;AFZO,IAAM,yBAAN,MAAyD;AAAA,EAK9D,YACmB,IACjB,OAAsC,CAAC,GACvC;AAFiB;AAGjB,SAAK,QAAQ,KAAK,SAAS,KAAK,KAAK;AACrC,SAAK,MAAM,KAAK,QAAQ,MAAM,KAAK,IAAI;AACvC,SAAK,gBACH,KAAK,kBAAkB,MAAM,YAAY,EAAE,EAAE,SAAS,WAAW;AAAA,EACrE;AAAA,EAPmB;AAAA,EALF;AAAA,EACA;AAAA,EACA;AAAA,EAYjB,MAAM,SAAS,QAA2C;AACxD,UAAM,QAAQ,KAAK,cAAc;AACjC,UAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK;AAClD,UAAM,KAAK,GAAG,OAAO,cAAc,EAAE,OAAO;AAAA,MAC1C;AAAA,MACA,QAAQ,OAAO;AAAA,MACf,UAAU,OAAO,YAAY;AAAA,MAC7B;AAAA,IACF,CAAC;AACD,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,QAAQ,OAA0C;AACtD,UAAM,OAAO,MAAM,KAAK,GACrB,OAAO,cAAc,EACrB,MAAM,GAAG,eAAe,OAAO,KAAK,CAAC,EACrC,UAAU;AACb,UAAM,MAAM,KAAK,CAAC;AAClB,QAAI,CAAC,KAAK;AACR,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,MACF;AAAA,IACF;AACA,QAAI,IAAI,UAAU,QAAQ,KAAK,KAAK,IAAI,GAAG;AACzC,YAAM,IAAI,gBAAgB,6BAA6B,SAAS;AAAA,IAClE;AACA,WAAO;AAAA,MACL,QAAQ,IAAI;AAAA,MACZ,UAAU,IAAI,YAAY;AAAA,IAC5B;AAAA,EACF;AACF;","names":[]}
|
|
@@ -2,7 +2,7 @@ import { AuthModuleOptions } from '../auth.module.js';
|
|
|
2
2
|
import { IOAuthStateStore } from '../protocols/oauth-state-store.js';
|
|
3
3
|
import { IUserContext } from '../protocols/user-context.js';
|
|
4
4
|
import { ProviderStrategyRegistry } from '../protocols/provider-strategy.js';
|
|
5
|
-
import {
|
|
5
|
+
import { IConnectionGrantSink } from '../protocols/connection-store.js';
|
|
6
6
|
import '@nestjs/common';
|
|
7
7
|
import '../../../base-classes/tenant-context.js';
|
|
8
8
|
import '../runtime/oauth2-refresh.strategy.js';
|
|
@@ -22,7 +22,7 @@ declare class AuthController {
|
|
|
22
22
|
private readonly stateStore;
|
|
23
23
|
private readonly grantSink;
|
|
24
24
|
private readonly options;
|
|
25
|
-
constructor(registry: ProviderStrategyRegistry, userContext: IUserContext, stateStore: IOAuthStateStore, grantSink:
|
|
25
|
+
constructor(registry: ProviderStrategyRegistry, userContext: IUserContext, stateStore: IOAuthStateStore, grantSink: IConnectionGrantSink, options: AuthModuleOptions);
|
|
26
26
|
connect(slug: string, redirect: string | undefined, req: unknown, res: RedirectingResponse): Promise<unknown>;
|
|
27
27
|
callback(slug: string, code: string | undefined, state: string | undefined, res: RedirectingResponse): Promise<unknown>;
|
|
28
28
|
private requireStrategy;
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
|
|
26
26
|
// runtime/subsystems/auth/auth.tokens.ts
|
|
27
27
|
var OAUTH_STATE_STORE = /* @__PURE__ */ Symbol("OAUTH_STATE_STORE");
|
|
28
|
-
var
|
|
28
|
+
var AUTH_CONNECTION_GRANT_SINK = /* @__PURE__ */ Symbol("AUTH_CONNECTION_GRANT_SINK");
|
|
29
29
|
var AUTH_USER_CONTEXT = /* @__PURE__ */ Symbol("AUTH_USER_CONTEXT");
|
|
30
30
|
var STRATEGY_REGISTRY = /* @__PURE__ */ Symbol("STRATEGY_REGISTRY");
|
|
31
31
|
var AUTH_OPTIONS = /* @__PURE__ */ Symbol("AUTH_OPTIONS");
|
|
@@ -85,7 +85,7 @@ var AuthController = class {
|
|
|
85
85
|
});
|
|
86
86
|
return res.redirect(
|
|
87
87
|
HttpStatus.FOUND,
|
|
88
|
-
redirect ?? `/settings/
|
|
88
|
+
redirect ?? `/settings/connections?connected=${encodeURIComponent(slug)}`
|
|
89
89
|
);
|
|
90
90
|
}
|
|
91
91
|
requireStrategy(slug) {
|
|
@@ -128,7 +128,7 @@ AuthController = __decorateClass([
|
|
|
128
128
|
__decorateParam(0, Inject(STRATEGY_REGISTRY)),
|
|
129
129
|
__decorateParam(1, Inject(AUTH_USER_CONTEXT)),
|
|
130
130
|
__decorateParam(2, Inject(OAUTH_STATE_STORE)),
|
|
131
|
-
__decorateParam(3, Inject(
|
|
131
|
+
__decorateParam(3, Inject(AUTH_CONNECTION_GRANT_SINK)),
|
|
132
132
|
__decorateParam(4, Inject(AUTH_OPTIONS))
|
|
133
133
|
], AuthController);
|
|
134
134
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../runtime/subsystems/auth/controllers/auth.controller.ts","../../../../../runtime/subsystems/auth/auth.tokens.ts"],"sourcesContent":["/**\n * AuthController — provider-agnostic OAuth2 connect/callback dance.\n *\n * Mounts two routes:\n * - `GET /auth/:provider/connect?redirect=...` — generates state, builds\n * the provider's authorize-url, 302-redirects the browser there.\n * - `GET /auth/:provider/callback?code=...&state=...` — consumes state,\n * exchanges the code for tokens, hands them to the grant sink, then\n * 302-redirects to the post-connect path.\n *\n * Hexagonal seams:\n * - `STRATEGY_REGISTRY` (ReadonlyMap<slug, IProviderStrategy>) — dispatch.\n * Concrete per-provider strategies live consumer-side and contribute\n * entries via a `useFactory` in the consumer's app module.\n * - `AUTH_USER_CONTEXT` (IUserContext) — resolves \"who is this request\"\n * from the consumer's session/JWT/etc.\n * - `OAUTH_STATE_STORE` (IOAuthStateStore) — CSRF state minting/consume.\n * - `
|
|
1
|
+
{"version":3,"sources":["../../../../../runtime/subsystems/auth/controllers/auth.controller.ts","../../../../../runtime/subsystems/auth/auth.tokens.ts"],"sourcesContent":["/**\n * AuthController — provider-agnostic OAuth2 connect/callback dance.\n *\n * Mounts two routes:\n * - `GET /auth/:provider/connect?redirect=...` — generates state, builds\n * the provider's authorize-url, 302-redirects the browser there.\n * - `GET /auth/:provider/callback?code=...&state=...` — consumes state,\n * exchanges the code for tokens, hands them to the grant sink, then\n * 302-redirects to the post-connect path.\n *\n * Hexagonal seams:\n * - `STRATEGY_REGISTRY` (ReadonlyMap<slug, IProviderStrategy>) — dispatch.\n * Concrete per-provider strategies live consumer-side and contribute\n * entries via a `useFactory` in the consumer's app module.\n * - `AUTH_USER_CONTEXT` (IUserContext) — resolves \"who is this request\"\n * from the consumer's session/JWT/etc.\n * - `OAUTH_STATE_STORE` (IOAuthStateStore) — CSRF state minting/consume.\n * - `AUTH_CONNECTION_GRANT_SINK` (IConnectionGrantSink) — persists the\n * freshly-minted grant. Adapter lives consumer-side (e.g. the\n * auth-integrations starter from #285).\n *\n * The controller never imports `ConnectionsService` or any other concrete\n * consumer type — it goes through ports only.\n */\nimport {\n Controller,\n Get,\n Inject,\n Param,\n Query,\n Req,\n Res,\n HttpException,\n HttpStatus,\n} from '@nestjs/common';\nimport {\n AUTH_CONNECTION_GRANT_SINK,\n AUTH_OPTIONS,\n AUTH_USER_CONTEXT,\n OAUTH_STATE_STORE,\n STRATEGY_REGISTRY,\n} from '../auth.tokens';\nimport type { AuthModuleOptions } from '../auth.module';\nimport type { IOAuthStateStore } from '../protocols/oauth-state-store';\nimport type { IUserContext } from '../protocols/user-context';\nimport type {\n IProviderStrategy,\n ProviderStrategyRegistry,\n} from '../protocols/provider-strategy';\nimport type { IConnectionGrantSink } from '../protocols/connection-store';\n\n/**\n * Minimal response surface used by the controller — typed loosely so we\n * don't pull a hard dep on `express` or `fastify`. Both popular HTTP\n * adapters expose `redirect(status, url)`.\n */\ninterface RedirectingResponse {\n redirect(statusCode: number, url: string): unknown;\n}\n\n@Controller('auth')\nexport class AuthController {\n constructor(\n @Inject(STRATEGY_REGISTRY)\n private readonly registry: ProviderStrategyRegistry,\n @Inject(AUTH_USER_CONTEXT)\n private readonly userContext: IUserContext,\n @Inject(OAUTH_STATE_STORE)\n private readonly stateStore: IOAuthStateStore,\n @Inject(AUTH_CONNECTION_GRANT_SINK)\n private readonly grantSink: IConnectionGrantSink,\n @Inject(AUTH_OPTIONS)\n private readonly options: AuthModuleOptions,\n ) {}\n\n @Get(':provider/connect')\n async connect(\n @Param('provider') slug: string,\n @Query('redirect') redirect: string | undefined,\n @Req() req: unknown,\n @Res() res: RedirectingResponse,\n ): Promise<unknown> {\n const strategy = this.requireStrategy(slug);\n const userId = await this.userContext.getCurrentUserId(req);\n const state = await this.stateStore.generate({ userId, redirect });\n const url = strategy.buildAuthorizeUrl({\n state,\n redirectUri: this.redirectUriFor(slug),\n });\n return res.redirect(HttpStatus.FOUND, url);\n }\n\n @Get(':provider/callback')\n async callback(\n @Param('provider') slug: string,\n @Query('code') code: string | undefined,\n @Query('state') state: string | undefined,\n @Res() res: RedirectingResponse,\n ): Promise<unknown> {\n const strategy = this.requireStrategy(slug);\n if (!code) {\n throw new HttpException(\n `Missing 'code' query param`,\n HttpStatus.BAD_REQUEST,\n );\n }\n if (!state) {\n throw new HttpException(\n `Missing 'state' query param`,\n HttpStatus.BAD_REQUEST,\n );\n }\n const { userId, redirect } = await this.stateStore.consume(state);\n const tokens = await strategy.exchangeCodeForTokens({\n code,\n redirectUri: this.redirectUriFor(slug),\n });\n await this.grantSink.createOrUpdateFromOAuthGrant({\n userId,\n provider: slug,\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n expiresAt: tokens.expiresAt,\n scope: tokens.scope,\n externalAccountId: tokens.externalAccountId,\n providerMetadata: tokens.providerMetadata,\n });\n return res.redirect(\n HttpStatus.FOUND,\n redirect ?? `/settings/connections?connected=${encodeURIComponent(slug)}`,\n );\n }\n\n private requireStrategy(slug: string): IProviderStrategy {\n const strategy = this.registry.get(slug);\n if (!strategy) {\n throw new HttpException(\n `Unknown provider '${slug}'`,\n HttpStatus.NOT_FOUND,\n );\n }\n return strategy;\n }\n\n private redirectUriFor(slug: string): string {\n const base = this.options.redirectUriBase;\n if (!base) {\n throw new Error(\n `AuthModule.forRoot: redirectUriBase is required when AuthController is enabled`,\n );\n }\n const trimmed = base.replace(/\\/+$/, '');\n return `${trimmed}/auth/${encodeURIComponent(slug)}/callback`;\n }\n}\n","/**\n * Auth subsystem — injection tokens.\n *\n * Following ADR-008 guidance: `Symbol()` tokens for type safety and collision\n * avoidance. Consumers inject these via `@Inject(...)` against the matching\n * protocol interface.\n *\n * Usage:\n * ```typescript\n * constructor(\n * @Inject(ENCRYPTION_KEY) private readonly key: IEncryptionKey,\n * @Inject(OAUTH_STATE_STORE) private readonly states: IOAuthStateStore,\n * @Inject(AUTH_CONNECTION_READER) private readonly reader: IConnectionReader,\n * @Inject(AUTH_CONNECTION_TOKEN_WRITER) private readonly writer: IConnectionTokenWriter,\n * @Inject(AUTH_CONNECTION_GRANT_SINK) private readonly grants: IConnectionGrantSink,\n * @Inject(AUTH_USER_CONTEXT) private readonly userCtx: IUserContext,\n * @Inject(STRATEGY_REGISTRY) private readonly registry: ProviderStrategyRegistry,\n * ) {}\n * ```\n *\n * `IAuthStrategy` implementations are provider-specific and registered under\n * provider-specific tokens (e.g. `SALESFORCE_AUTH_STRATEGY`,\n * `HUBSPOT_AUTH_STRATEGY`) by each connection module — this subsystem does\n * not mandate a single `AUTH_STRATEGY` token because an app typically has\n * many concurrent strategies, one per provider. They are dispatched through\n * `STRATEGY_REGISTRY` (a `ReadonlyMap<slug, IProviderStrategy>`), populated\n * by per-provider modules via a `useFactory` provider.\n */\nexport const ENCRYPTION_KEY = Symbol('ENCRYPTION_KEY');\nexport const OAUTH_STATE_STORE = Symbol('OAUTH_STATE_STORE');\nexport const AUTH_CONNECTION_READER = Symbol('AUTH_CONNECTION_READER');\nexport const AUTH_CONNECTION_TOKEN_WRITER = Symbol('AUTH_CONNECTION_TOKEN_WRITER');\nexport const AUTH_CONNECTION_GRANT_SINK = Symbol('AUTH_CONNECTION_GRANT_SINK');\nexport const AUTH_USER_CONTEXT = Symbol('AUTH_USER_CONTEXT');\nexport const STRATEGY_REGISTRY = Symbol('STRATEGY_REGISTRY');\n/**\n * Holds the resolved `AuthModuleOptions` (used by `AuthController` to read\n * `redirectUriBase` for building per-provider callback URIs).\n */\nexport const AUTH_OPTIONS = Symbol('AUTH_OPTIONS');\n"],"mappings":"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","names":[]}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
export { AuthCredentials, AuthResolveOptions, IAuthStrategy } from './protocols/auth-strategy.js';
|
|
2
2
|
export { IEncryptionKey } from './protocols/encryption-key.js';
|
|
3
3
|
export { IOAuthStateStore, OAuthStateError, OAuthStateRecord } from './protocols/oauth-state-store.js';
|
|
4
|
-
export {
|
|
4
|
+
export { ConnectionGrantInput, ConnectionTokenUpdate, DecryptedConnection, IConnectionGrantSink, IConnectionReader, IConnectionTokenWriter } from './protocols/connection-store.js';
|
|
5
5
|
export { IUserContext } from './protocols/user-context.js';
|
|
6
6
|
export { ExchangedTokens, IProviderStrategy, ProviderStrategyRegistry } from './protocols/provider-strategy.js';
|
|
7
|
-
export {
|
|
7
|
+
export { AUTH_CONNECTION_GRANT_SINK, AUTH_CONNECTION_READER, AUTH_CONNECTION_TOKEN_WRITER, AUTH_OPTIONS, AUTH_USER_CONTEXT, ENCRYPTION_KEY, OAUTH_STATE_STORE, STRATEGY_REGISTRY } from './auth.tokens.js';
|
|
8
8
|
export { FetchLike, OAuth2RefreshStrategy, OAuth2RefreshStrategyOptions, ParsedRefreshResponse } from './runtime/oauth2-refresh.strategy.js';
|
|
9
9
|
export { WithAuthRetryOptions, withAuthRetry } from './runtime/with-auth-retry.js';
|
|
10
|
-
export {
|
|
10
|
+
export { ConnectionBrokenError } from './runtime/connection-broken.error.js';
|
|
11
11
|
export { SessionExpiredError, isSessionExpiredError } from './runtime/session-expired.error.js';
|
|
12
12
|
export { AuthOAuthState, authOAuthState } from './auth-oauth-state.schema.js';
|
|
13
13
|
export { EnvEncryptionKey, EnvEncryptionKeyOptions } from './backends/encryption-key/env.js';
|