@digimuza/saas-core 0.0.2
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/CLAUDE.md +1165 -0
- package/LICENSE +21 -0
- package/README.md +234 -0
- package/dist/api-keys/index.d.ts +9 -0
- package/dist/api-keys/index.d.ts.map +1 -0
- package/dist/api-keys/index.js +37 -0
- package/dist/api-keys/index.js.map +1 -0
- package/dist/api-keys/keys.d.ts +55 -0
- package/dist/api-keys/keys.d.ts.map +1 -0
- package/dist/api-keys/keys.js +92 -0
- package/dist/api-keys/keys.js.map +1 -0
- package/dist/api-keys/service.d.ts +111 -0
- package/dist/api-keys/service.d.ts.map +1 -0
- package/dist/api-keys/service.js +210 -0
- package/dist/api-keys/service.js.map +1 -0
- package/dist/audit/index.d.ts +5 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +44 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/audit/service.d.ts +88 -0
- package/dist/audit/service.d.ts.map +1 -0
- package/dist/audit/service.js +128 -0
- package/dist/audit/service.js.map +1 -0
- package/dist/auth/client.d.ts +5 -0
- package/dist/auth/client.d.ts.map +1 -0
- package/dist/auth/client.js +26 -0
- package/dist/auth/client.js.map +1 -0
- package/dist/auth/cookies.d.ts +51 -0
- package/dist/auth/cookies.d.ts.map +1 -0
- package/dist/auth/cookies.js +58 -0
- package/dist/auth/cookies.js.map +1 -0
- package/dist/auth/index.d.ts +14 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +46 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/service.d.ts +67 -0
- package/dist/auth/service.d.ts.map +1 -0
- package/dist/auth/service.js +275 -0
- package/dist/auth/service.js.map +1 -0
- package/dist/auth/types.d.ts +56 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +2 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/authorization/index.d.ts +4 -0
- package/dist/authorization/index.d.ts.map +1 -0
- package/dist/authorization/index.js +24 -0
- package/dist/authorization/index.js.map +1 -0
- package/dist/authorization/service.d.ts +92 -0
- package/dist/authorization/service.d.ts.map +1 -0
- package/dist/authorization/service.js +254 -0
- package/dist/authorization/service.js.map +1 -0
- package/dist/billing/index.d.ts +16 -0
- package/dist/billing/index.d.ts.map +1 -0
- package/dist/billing/index.js +47 -0
- package/dist/billing/index.js.map +1 -0
- package/dist/billing/provider.d.ts +114 -0
- package/dist/billing/provider.d.ts.map +1 -0
- package/dist/billing/provider.js +2 -0
- package/dist/billing/provider.js.map +1 -0
- package/dist/billing/revolut.d.ts +68 -0
- package/dist/billing/revolut.d.ts.map +1 -0
- package/dist/billing/revolut.js +181 -0
- package/dist/billing/revolut.js.map +1 -0
- package/dist/billing/service.d.ts +124 -0
- package/dist/billing/service.d.ts.map +1 -0
- package/dist/billing/service.js +470 -0
- package/dist/billing/service.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +43 -0
- package/dist/cli/commands/dev.d.ts.map +1 -0
- package/dist/cli/commands/dev.js +212 -0
- package/dist/cli/commands/dev.js.map +1 -0
- package/dist/cli/commands/project.d.ts +12 -0
- package/dist/cli/commands/project.d.ts.map +1 -0
- package/dist/cli/commands/project.js +49 -0
- package/dist/cli/commands/project.js.map +1 -0
- package/dist/cli/docker.d.ts +50 -0
- package/dist/cli/docker.d.ts.map +1 -0
- package/dist/cli/docker.js +84 -0
- package/dist/cli/docker.js.map +1 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +198 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/output.d.ts +30 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +66 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/templates.d.ts +52 -0
- package/dist/cli/templates.d.ts.map +1 -0
- package/dist/cli/templates.js +225 -0
- package/dist/cli/templates.js.map +1 -0
- package/dist/config/client.d.ts +60 -0
- package/dist/config/client.d.ts.map +1 -0
- package/dist/config/client.js +94 -0
- package/dist/config/client.js.map +1 -0
- package/dist/config/config-loader.d.ts +2 -0
- package/dist/config/config-loader.d.ts.map +1 -0
- package/dist/config/config-loader.js +2 -0
- package/dist/config/config-loader.js.map +1 -0
- package/dist/config/env.d.ts +24 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +40 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/errors.d.ts +17 -0
- package/dist/config/errors.d.ts.map +1 -0
- package/dist/config/errors.js +26 -0
- package/dist/config/errors.js.map +1 -0
- package/dist/config/index.d.ts +72 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +106 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/runtime.d.ts +15 -0
- package/dist/config/runtime.d.ts.map +1 -0
- package/dist/config/runtime.js +22 -0
- package/dist/config/runtime.js.map +1 -0
- package/dist/config/sections.d.ts +226 -0
- package/dist/config/sections.d.ts.map +1 -0
- package/dist/config/sections.js +141 -0
- package/dist/config/sections.js.map +1 -0
- package/dist/db/client.d.ts +43 -0
- package/dist/db/client.d.ts.map +1 -0
- package/dist/db/client.js +77 -0
- package/dist/db/client.js.map +1 -0
- package/dist/db/errors.d.ts +39 -0
- package/dist/db/errors.d.ts.map +1 -0
- package/dist/db/errors.js +67 -0
- package/dist/db/errors.js.map +1 -0
- package/dist/db/index.d.ts +10 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +10 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/migrate.d.ts +21 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/migrate.js +28 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/schema/_shared.d.ts +30 -0
- package/dist/db/schema/_shared.d.ts.map +1 -0
- package/dist/db/schema/_shared.js +36 -0
- package/dist/db/schema/_shared.js.map +1 -0
- package/dist/db/schema/apikeys.d.ts +263 -0
- package/dist/db/schema/apikeys.d.ts.map +1 -0
- package/dist/db/schema/apikeys.js +70 -0
- package/dist/db/schema/apikeys.js.map +1 -0
- package/dist/db/schema/authorization.d.ts +465 -0
- package/dist/db/schema/authorization.d.ts.map +1 -0
- package/dist/db/schema/authorization.js +134 -0
- package/dist/db/schema/authorization.js.map +1 -0
- package/dist/db/schema/billing.d.ts +1100 -0
- package/dist/db/schema/billing.d.ts.map +1 -0
- package/dist/db/schema/billing.js +223 -0
- package/dist/db/schema/billing.js.map +1 -0
- package/dist/db/schema/events.d.ts +161 -0
- package/dist/db/schema/events.d.ts.map +1 -0
- package/dist/db/schema/events.js +66 -0
- package/dist/db/schema/events.js.map +1 -0
- package/dist/db/schema/features.d.ts +243 -0
- package/dist/db/schema/features.d.ts.map +1 -0
- package/dist/db/schema/features.js +87 -0
- package/dist/db/schema/features.js.map +1 -0
- package/dist/db/schema/files.d.ts +253 -0
- package/dist/db/schema/files.d.ts.map +1 -0
- package/dist/db/schema/files.js +74 -0
- package/dist/db/schema/files.js.map +1 -0
- package/dist/db/schema/index.d.ts +38 -0
- package/dist/db/schema/index.d.ts.map +1 -0
- package/dist/db/schema/index.js +38 -0
- package/dist/db/schema/index.js.map +1 -0
- package/dist/db/schema/invitations.d.ts +214 -0
- package/dist/db/schema/invitations.d.ts.map +1 -0
- package/dist/db/schema/invitations.js +80 -0
- package/dist/db/schema/invitations.js.map +1 -0
- package/dist/db/schema/messaging.d.ts +442 -0
- package/dist/db/schema/messaging.d.ts.map +1 -0
- package/dist/db/schema/messaging.js +120 -0
- package/dist/db/schema/messaging.js.map +1 -0
- package/dist/db/schema/users.d.ts +246 -0
- package/dist/db/schema/users.d.ts.map +1 -0
- package/dist/db/schema/users.js +60 -0
- package/dist/db/schema/users.js.map +1 -0
- package/dist/db/schema/webhooks.d.ts +693 -0
- package/dist/db/schema/webhooks.d.ts.map +1 -0
- package/dist/db/schema/webhooks.js +147 -0
- package/dist/db/schema/webhooks.js.map +1 -0
- package/dist/db/schema/workspaces.d.ts +196 -0
- package/dist/db/schema/workspaces.d.ts.map +1 -0
- package/dist/db/schema/workspaces.js +57 -0
- package/dist/db/schema/workspaces.js.map +1 -0
- package/dist/db/types.d.ts +18 -0
- package/dist/db/types.d.ts.map +1 -0
- package/dist/db/types.js +2 -0
- package/dist/db/types.js.map +1 -0
- package/dist/email/adapters.d.ts +29 -0
- package/dist/email/adapters.d.ts.map +1 -0
- package/dist/email/adapters.js +60 -0
- package/dist/email/adapters.js.map +1 -0
- package/dist/email/index.d.ts +20 -0
- package/dist/email/index.d.ts.map +1 -0
- package/dist/email/index.js +79 -0
- package/dist/email/index.js.map +1 -0
- package/dist/email/provider.d.ts +47 -0
- package/dist/email/provider.d.ts.map +1 -0
- package/dist/email/provider.js +7 -0
- package/dist/email/provider.js.map +1 -0
- package/dist/email/resend.d.ts +22 -0
- package/dist/email/resend.d.ts.map +1 -0
- package/dist/email/resend.js +51 -0
- package/dist/email/resend.js.map +1 -0
- package/dist/email/service.d.ts +55 -0
- package/dist/email/service.d.ts.map +1 -0
- package/dist/email/service.js +87 -0
- package/dist/email/service.js.map +1 -0
- package/dist/email/templates.d.ts +81 -0
- package/dist/email/templates.d.ts.map +1 -0
- package/dist/email/templates.js +84 -0
- package/dist/email/templates.js.map +1 -0
- package/dist/entitlements/index.d.ts +10 -0
- package/dist/entitlements/index.d.ts.map +1 -0
- package/dist/entitlements/index.js +28 -0
- package/dist/entitlements/index.js.map +1 -0
- package/dist/entitlements/service.d.ts +83 -0
- package/dist/entitlements/service.d.ts.map +1 -0
- package/dist/entitlements/service.js +158 -0
- package/dist/entitlements/service.js.map +1 -0
- package/dist/errors.d.ts +85 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +95 -0
- package/dist/errors.js.map +1 -0
- package/dist/events/core-events.d.ts +120 -0
- package/dist/events/core-events.d.ts.map +1 -0
- package/dist/events/core-events.js +109 -0
- package/dist/events/core-events.js.map +1 -0
- package/dist/events/index.d.ts +15 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +58 -0
- package/dist/events/index.js.map +1 -0
- package/dist/events/publisher.d.ts +46 -0
- package/dist/events/publisher.d.ts.map +1 -0
- package/dist/events/publisher.js +54 -0
- package/dist/events/publisher.js.map +1 -0
- package/dist/events/registry.d.ts +87 -0
- package/dist/events/registry.d.ts.map +1 -0
- package/dist/events/registry.js +34 -0
- package/dist/events/registry.js.map +1 -0
- package/dist/events/service.d.ts +78 -0
- package/dist/events/service.d.ts.map +1 -0
- package/dist/events/service.js +182 -0
- package/dist/events/service.js.map +1 -0
- package/dist/features/bucket.d.ts +22 -0
- package/dist/features/bucket.d.ts.map +1 -0
- package/dist/features/bucket.js +42 -0
- package/dist/features/bucket.js.map +1 -0
- package/dist/features/index.d.ts +15 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +78 -0
- package/dist/features/index.js.map +1 -0
- package/dist/features/registry.d.ts +51 -0
- package/dist/features/registry.d.ts.map +1 -0
- package/dist/features/registry.js +52 -0
- package/dist/features/registry.js.map +1 -0
- package/dist/features/service.d.ts +78 -0
- package/dist/features/service.d.ts.map +1 -0
- package/dist/features/service.js +207 -0
- package/dist/features/service.js.map +1 -0
- package/dist/files/index.d.ts +6 -0
- package/dist/files/index.d.ts.map +1 -0
- package/dist/files/index.js +64 -0
- package/dist/files/index.js.map +1 -0
- package/dist/files/service.d.ts +92 -0
- package/dist/files/service.d.ts.map +1 -0
- package/dist/files/service.js +303 -0
- package/dist/files/service.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/jobs/backend.d.ts +23 -0
- package/dist/jobs/backend.d.ts.map +1 -0
- package/dist/jobs/backend.js +23 -0
- package/dist/jobs/backend.js.map +1 -0
- package/dist/jobs/index.d.ts +29 -0
- package/dist/jobs/index.d.ts.map +1 -0
- package/dist/jobs/index.js +114 -0
- package/dist/jobs/index.js.map +1 -0
- package/dist/jobs/service.d.ts +67 -0
- package/dist/jobs/service.d.ts.map +1 -0
- package/dist/jobs/service.js +139 -0
- package/dist/jobs/service.js.map +1 -0
- package/dist/jobs/types.d.ts +78 -0
- package/dist/jobs/types.d.ts.map +1 -0
- package/dist/jobs/types.js +4 -0
- package/dist/jobs/types.js.map +1 -0
- package/dist/logger/default.d.ts +7 -0
- package/dist/logger/default.d.ts.map +1 -0
- package/dist/logger/default.js +28 -0
- package/dist/logger/default.js.map +1 -0
- package/dist/logger/index.d.ts +24 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +90 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/redact.d.ts +40 -0
- package/dist/logger/redact.d.ts.map +1 -0
- package/dist/logger/redact.js +178 -0
- package/dist/logger/redact.js.map +1 -0
- package/dist/logger/service.d.ts +14 -0
- package/dist/logger/service.d.ts.map +1 -0
- package/dist/logger/service.js +56 -0
- package/dist/logger/service.js.map +1 -0
- package/dist/logger/sinks.d.ts +37 -0
- package/dist/logger/sinks.d.ts.map +1 -0
- package/dist/logger/sinks.js +135 -0
- package/dist/logger/sinks.js.map +1 -0
- package/dist/logger/types.d.ts +47 -0
- package/dist/logger/types.d.ts.map +1 -0
- package/dist/logger/types.js +8 -0
- package/dist/logger/types.js.map +1 -0
- package/dist/notifications/index.d.ts +8 -0
- package/dist/notifications/index.d.ts.map +1 -0
- package/dist/notifications/index.js +72 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/notifications/service.d.ts +79 -0
- package/dist/notifications/service.d.ts.map +1 -0
- package/dist/notifications/service.js +164 -0
- package/dist/notifications/service.js.map +1 -0
- package/dist/realtime/channels.d.ts +37 -0
- package/dist/realtime/channels.d.ts.map +1 -0
- package/dist/realtime/channels.js +58 -0
- package/dist/realtime/channels.js.map +1 -0
- package/dist/realtime/index.d.ts +19 -0
- package/dist/realtime/index.d.ts.map +1 -0
- package/dist/realtime/index.js +118 -0
- package/dist/realtime/index.js.map +1 -0
- package/dist/realtime/postgres.d.ts +48 -0
- package/dist/realtime/postgres.d.ts.map +1 -0
- package/dist/realtime/postgres.js +151 -0
- package/dist/realtime/postgres.js.map +1 -0
- package/dist/realtime/service.d.ts +75 -0
- package/dist/realtime/service.d.ts.map +1 -0
- package/dist/realtime/service.js +103 -0
- package/dist/realtime/service.js.map +1 -0
- package/dist/realtime/sse.d.ts +55 -0
- package/dist/realtime/sse.d.ts.map +1 -0
- package/dist/realtime/sse.js +123 -0
- package/dist/realtime/sse.js.map +1 -0
- package/dist/realtime/transport.d.ts +52 -0
- package/dist/realtime/transport.d.ts.map +1 -0
- package/dist/realtime/transport.js +2 -0
- package/dist/realtime/transport.js.map +1 -0
- package/dist/storage/index.d.ts +18 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +89 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/keys.d.ts +50 -0
- package/dist/storage/keys.d.ts.map +1 -0
- package/dist/storage/keys.js +120 -0
- package/dist/storage/keys.js.map +1 -0
- package/dist/storage/memory.d.ts +32 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +84 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/storage/provider.d.ts +93 -0
- package/dist/storage/provider.d.ts.map +1 -0
- package/dist/storage/provider.js +2 -0
- package/dist/storage/provider.js.map +1 -0
- package/dist/storage/s3.d.ts +34 -0
- package/dist/storage/s3.d.ts.map +1 -0
- package/dist/storage/s3.js +175 -0
- package/dist/storage/s3.js.map +1 -0
- package/dist/storage/service.d.ts +86 -0
- package/dist/storage/service.d.ts.map +1 -0
- package/dist/storage/service.js +107 -0
- package/dist/storage/service.js.map +1 -0
- package/dist/storage/sigv4.d.ts +58 -0
- package/dist/storage/sigv4.d.ts.map +1 -0
- package/dist/storage/sigv4.js +114 -0
- package/dist/storage/sigv4.js.map +1 -0
- package/dist/streams/client.d.ts +91 -0
- package/dist/streams/client.d.ts.map +1 -0
- package/dist/streams/client.js +128 -0
- package/dist/streams/client.js.map +1 -0
- package/dist/streams/events.d.ts +68 -0
- package/dist/streams/events.d.ts.map +1 -0
- package/dist/streams/events.js +81 -0
- package/dist/streams/events.js.map +1 -0
- package/dist/streams/index.d.ts +47 -0
- package/dist/streams/index.d.ts.map +1 -0
- package/dist/streams/index.js +47 -0
- package/dist/streams/index.js.map +1 -0
- package/dist/streams/realtime.d.ts +27 -0
- package/dist/streams/realtime.d.ts.map +1 -0
- package/dist/streams/realtime.js +50 -0
- package/dist/streams/realtime.js.map +1 -0
- package/dist/streams/state.d.ts +26 -0
- package/dist/streams/state.d.ts.map +1 -0
- package/dist/streams/state.js +34 -0
- package/dist/streams/state.js.map +1 -0
- package/dist/users/index.d.ts +9 -0
- package/dist/users/index.d.ts.map +1 -0
- package/dist/users/index.js +23 -0
- package/dist/users/index.js.map +1 -0
- package/dist/users/service.d.ts +43 -0
- package/dist/users/service.d.ts.map +1 -0
- package/dist/users/service.js +113 -0
- package/dist/users/service.js.map +1 -0
- package/dist/webhooks/inbound.d.ts +70 -0
- package/dist/webhooks/inbound.d.ts.map +1 -0
- package/dist/webhooks/inbound.js +146 -0
- package/dist/webhooks/inbound.js.map +1 -0
- package/dist/webhooks/index.d.ts +12 -0
- package/dist/webhooks/index.d.ts.map +1 -0
- package/dist/webhooks/index.js +65 -0
- package/dist/webhooks/index.js.map +1 -0
- package/dist/webhooks/outgoing.d.ts +89 -0
- package/dist/webhooks/outgoing.d.ts.map +1 -0
- package/dist/webhooks/outgoing.js +375 -0
- package/dist/webhooks/outgoing.js.map +1 -0
- package/dist/webhooks/signature.d.ts +40 -0
- package/dist/webhooks/signature.d.ts.map +1 -0
- package/dist/webhooks/signature.js +77 -0
- package/dist/webhooks/signature.js.map +1 -0
- package/dist/workspaces/context.d.ts +30 -0
- package/dist/workspaces/context.d.ts.map +1 -0
- package/dist/workspaces/context.js +12 -0
- package/dist/workspaces/context.js.map +1 -0
- package/dist/workspaces/index.d.ts +5 -0
- package/dist/workspaces/index.d.ts.map +1 -0
- package/dist/workspaces/index.js +21 -0
- package/dist/workspaces/index.js.map +1 -0
- package/dist/workspaces/service.d.ts +126 -0
- package/dist/workspaces/service.d.ts.map +1 -0
- package/dist/workspaces/service.js +495 -0
- package/dist/workspaces/service.js.map +1 -0
- package/docs/README.md +71 -0
- package/docs/api-keys.md +184 -0
- package/docs/audit.md +198 -0
- package/docs/auth.md +196 -0
- package/docs/authorization.md +225 -0
- package/docs/billing.md +293 -0
- package/docs/cli.md +238 -0
- package/docs/config.md +163 -0
- package/docs/db.md +219 -0
- package/docs/email.md +222 -0
- package/docs/entitlements.md +194 -0
- package/docs/errors.md +158 -0
- package/docs/events.md +279 -0
- package/docs/feature-flags.md +237 -0
- package/docs/files.md +229 -0
- package/docs/jobs.md +288 -0
- package/docs/logger.md +269 -0
- package/docs/notifications.md +179 -0
- package/docs/publishing.md +128 -0
- package/docs/realtime.md +360 -0
- package/docs/storage.md +279 -0
- package/docs/streams.md +296 -0
- package/docs/users.md +117 -0
- package/docs/webhooks.md +272 -0
- package/docs/workspaces.md +269 -0
- package/drizzle/0000_init_users.sql +21 -0
- package/drizzle/0001_workspaces_roles_permissions.sql +86 -0
- package/drizzle/0002_billing_entitlements.sql +97 -0
- package/drizzle/0003_events.sql +17 -0
- package/drizzle/0004_notifications_audit.sql +64 -0
- package/drizzle/0005_api_keys_webhooks.sql +77 -0
- package/drizzle/0006_feature_flags.sql +22 -0
- package/drizzle/0007_files.sql +23 -0
- package/drizzle/meta/0000_snapshot.json +160 -0
- package/drizzle/meta/0001_snapshot.json +837 -0
- package/drizzle/meta/0002_snapshot.json +1580 -0
- package/drizzle/meta/0003_snapshot.json +1751 -0
- package/drizzle/meta/0004_snapshot.json +2109 -0
- package/drizzle/meta/0005_snapshot.json +2719 -0
- package/drizzle/meta/0006_snapshot.json +2909 -0
- package/drizzle/meta/0007_snapshot.json +3113 -0
- package/drizzle/meta/_journal.json +62 -0
- package/package.json +180 -0
package/docs/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# @digimuza/saas-core — module reference
|
|
2
|
+
|
|
3
|
+
Per-module guides. Each covers what the module is for, every exported function
|
|
4
|
+
with its signature, and the reasoning behind the design decisions that are not
|
|
5
|
+
obvious from the types.
|
|
6
|
+
|
|
7
|
+
The [root README](../README.md) is the architectural overview; start there if you
|
|
8
|
+
have not read it. Each module also carries a shorter, task-oriented README beside
|
|
9
|
+
its code — see [`src/README.md`](../src/README.md) for that index. The split is
|
|
10
|
+
deliberate: the colocated README orients you, these guides are the reference, and
|
|
11
|
+
neither repeats the other.
|
|
12
|
+
|
|
13
|
+
## Modules
|
|
14
|
+
|
|
15
|
+
| Module | Import from | Guide |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| Configuration | `@digimuza/saas-core/config` | [config.md](./config.md) |
|
|
18
|
+
| Database & schema | `@digimuza/saas-core/db`, `/db/schema` | [db.md](./db.md) |
|
|
19
|
+
| Authentication | `@digimuza/saas-core/auth` | [auth.md](./auth.md) |
|
|
20
|
+
| Users & profiles | `@digimuza/saas-core/users` | [users.md](./users.md) |
|
|
21
|
+
| Workspaces & members | `@digimuza/saas-core/workspaces` | [workspaces.md](./workspaces.md) |
|
|
22
|
+
| Roles & permissions | `@digimuza/saas-core/authorization` | [authorization.md](./authorization.md) |
|
|
23
|
+
| Billing | `@digimuza/saas-core/billing` | [billing.md](./billing.md) |
|
|
24
|
+
| Entitlements | `@digimuza/saas-core/entitlements` | [entitlements.md](./entitlements.md) |
|
|
25
|
+
| Events | `@digimuza/saas-core/events` | [events.md](./events.md) |
|
|
26
|
+
| Background jobs | `@digimuza/saas-core/jobs` | [jobs.md](./jobs.md) |
|
|
27
|
+
| Realtime | `@digimuza/saas-core/realtime` | [realtime.md](./realtime.md) |
|
|
28
|
+
| Streams (RxJS) | `@digimuza/saas-core/streams` | [streams.md](./streams.md) |
|
|
29
|
+
| Email | `@digimuza/saas-core/email` | [email.md](./email.md) |
|
|
30
|
+
| Notifications | `@digimuza/saas-core/notifications` | [notifications.md](./notifications.md) |
|
|
31
|
+
| Audit | `@digimuza/saas-core/audit` | [audit.md](./audit.md) |
|
|
32
|
+
| API keys | `@digimuza/saas-core/api-keys` | [api-keys.md](./api-keys.md) |
|
|
33
|
+
| Webhooks | `@digimuza/saas-core/webhooks` | [webhooks.md](./webhooks.md) |
|
|
34
|
+
| Feature flags | `@digimuza/saas-core/feature-flags` | [feature-flags.md](./feature-flags.md) |
|
|
35
|
+
| Storage | `@digimuza/saas-core/storage` | [storage.md](./storage.md) |
|
|
36
|
+
| Files | `@digimuza/saas-core/files` | [files.md](./files.md) |
|
|
37
|
+
| Logger | `@digimuza/saas-core/logger` | [logger.md](./logger.md) |
|
|
38
|
+
| CLI | the `saas-core` binary | [cli.md](./cli.md) |
|
|
39
|
+
| Publishing | release workflow | [publishing.md](./publishing.md) |
|
|
40
|
+
| Errors | `@digimuza/saas-core` or `/errors` | [errors.md](./errors.md) |
|
|
41
|
+
|
|
42
|
+
## Two conventions that apply everywhere
|
|
43
|
+
|
|
44
|
+
**Every module exports both a factory and a default instance.**
|
|
45
|
+
`createWorkspacesService({ db, events })` builds one explicitly — use it in tests
|
|
46
|
+
and when you need different dependencies. The default (`workspaces`) is wired to
|
|
47
|
+
the configuration-driven database handle and the core event registry, and its
|
|
48
|
+
dependencies resolve on *first use*, not at import. So importing a module never
|
|
49
|
+
requires configuration you have not set up.
|
|
50
|
+
|
|
51
|
+
**Tenant-scoped operations take a `WorkspaceContext`, never a raw id.**
|
|
52
|
+
A context can only come from a verified membership lookup
|
|
53
|
+
(`workspaces.requireContext`). Passing a `workspaceId` from a request body where a
|
|
54
|
+
context is expected will not type-check. See
|
|
55
|
+
[workspaces.md](./workspaces.md#workspacecontext).
|
|
56
|
+
|
|
57
|
+
## Reading order for a new application
|
|
58
|
+
|
|
59
|
+
1. [config.md](./config.md) — get configuration flowing from Infisical
|
|
60
|
+
2. [db.md](./db.md) — run the core migrations, reference core tables from yours
|
|
61
|
+
3. [auth.md](./auth.md) — sign-in, callback, session
|
|
62
|
+
4. [workspaces.md](./workspaces.md) — tenancy, and how a request becomes a context
|
|
63
|
+
5. [authorization.md](./authorization.md) — register permissions, gate actions
|
|
64
|
+
6. [billing.md](./billing.md) + [entitlements.md](./entitlements.md) — charge, then gate on what was paid for
|
|
65
|
+
7. [events.md](./events.md) — declare your own events
|
|
66
|
+
8. [jobs.md](./jobs.md) — move slow work off the request
|
|
67
|
+
9. [realtime.md](./realtime.md) — push what happened to the browser
|
|
68
|
+
10. [streams.md](./streams.md) — react to it, in the server or the browser
|
|
69
|
+
11. [notifications.md](./notifications.md) + [email.md](./email.md) — tell the user
|
|
70
|
+
12. [audit.md](./audit.md) — keep an immutable record of what mattered
|
|
71
|
+
13. [api-keys.md](./api-keys.md) + [webhooks.md](./webhooks.md) — let other systems in and out
|
package/docs/api-keys.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# API keys
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import { apiKeys, generateApiKey, parseApiKey } from "@digimuza/saas-core/api-keys";
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
> Orientation and recipes: [`src/api-keys/README.md`](../src/api-keys/README.md).
|
|
8
|
+
|
|
9
|
+
Workspace-scoped machine credentials. A key is **not a user**: it holds *scopes*,
|
|
10
|
+
not roles, so authorization for a key is a scope check rather than a permission
|
|
11
|
+
check.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## The key format
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
dgk_<32 hex>_<64 hex>
|
|
19
|
+
│ │ └── secret — 256 bits, SHA-256 hashed, never stored
|
|
20
|
+
│ └─────────── keyId — 128 bits, stored plaintext and indexed
|
|
21
|
+
└──────────────────── prefix — identifies the key as ours
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Why the split.** Hashing the whole key would mean either scanning every row's
|
|
25
|
+
hash on every request, or storing a searchable copy — the first is O(n) per API
|
|
26
|
+
call, the second defeats the point of hashing. A plaintext, indexed `keyId` makes
|
|
27
|
+
validation one lookup.
|
|
28
|
+
|
|
29
|
+
**Why hex and not base64url.** base64url includes `_`, which collides with the `_`
|
|
30
|
+
delimiter and makes the token ambiguous to split. That fails only for the fraction
|
|
31
|
+
of keys whose random bytes happen to encode one — the worst kind of bug. A test
|
|
32
|
+
generates 200 keys and asserts three parts every time.
|
|
33
|
+
|
|
34
|
+
**Why SHA-256 and not bcrypt.** The secret is 256 bits of CSPRNG output, so there
|
|
35
|
+
is no dictionary to attack. bcrypt and Argon2 exist to make *low-entropy human
|
|
36
|
+
passwords* expensive to guess; applying one here would add tens of milliseconds to
|
|
37
|
+
every authenticated request and buy nothing.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## `apiKeys.create(context, input)`
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
function create(context: WorkspaceContext, input: {
|
|
45
|
+
name: string; // max 120 chars
|
|
46
|
+
scopes?: readonly string[]; // deduplicated, max 64
|
|
47
|
+
expiresAt?: Date | null; // null never expires
|
|
48
|
+
prefix?: string; // 2-16 lowercase alphanumerics
|
|
49
|
+
}): Promise<{ key: ApiKeySummary; token: string }>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const { key, token } = await apiKeys.create(context, {
|
|
54
|
+
name: "CI deploy",
|
|
55
|
+
scopes: ["projects.read", "projects.deploy"],
|
|
56
|
+
});
|
|
57
|
+
showOnce(token);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**`token` exists exactly once.** Only the hash of its secret half is stored, so
|
|
61
|
+
there is no way to recover it. Losing it means revoking and issuing a new one.
|
|
62
|
+
|
|
63
|
+
`ApiKeySummary` never contains a secret or a hash. `masked` is `dgk_a1b2c3…` —
|
|
64
|
+
enough to match a log line to a row.
|
|
65
|
+
|
|
66
|
+
## `apiKeys.validate(token)` / `tryValidate(token)`
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
function validate(token: string): Promise<ApiKeyPrincipal>
|
|
70
|
+
function tryValidate(token: string): Promise<ApiKeyPrincipal | undefined>
|
|
71
|
+
|
|
72
|
+
interface ApiKeyPrincipal {
|
|
73
|
+
apiKeyId: string;
|
|
74
|
+
keyId: string;
|
|
75
|
+
workspaceId: string;
|
|
76
|
+
name: string;
|
|
77
|
+
scopes: readonly string[];
|
|
78
|
+
createdByUserId: string | null;
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Note what is absent: there is no user. The workspace comes from the key's own row,
|
|
83
|
+
never from the request.
|
|
84
|
+
|
|
85
|
+
**Every failure produces the same error.** Malformed, unknown, revoked and expired
|
|
86
|
+
all raise `UnauthorizedError("Invalid API key.")`, so a caller cannot probe. There
|
|
87
|
+
is a test asserting all three yield one distinct message.
|
|
88
|
+
|
|
89
|
+
Comparison is constant-time — a `===` on hex digests leaks, through timing, how
|
|
90
|
+
many leading characters matched.
|
|
91
|
+
|
|
92
|
+
### `last_used_at`
|
|
93
|
+
|
|
94
|
+
Recorded after the key is known good, and only when the stored value is at least a
|
|
95
|
+
minute stale. Updating on every request would turn validation into a write path;
|
|
96
|
+
the update is also guarded so two concurrent requests do not both write.
|
|
97
|
+
|
|
98
|
+
Configure with `lastUsedThrottleMs`.
|
|
99
|
+
|
|
100
|
+
## `apiKeys.resolveContext(token)`
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
function resolveContext(token: string): Promise<{
|
|
104
|
+
context: WorkspaceContext;
|
|
105
|
+
principal: ApiKeyPrincipal;
|
|
106
|
+
}>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Validates, then confirms the key's creator is **still a member** of the workspace,
|
|
110
|
+
and returns a context usable with the rest of the core.
|
|
111
|
+
|
|
112
|
+
The membership re-check is the point. Without it a key would keep working after the
|
|
113
|
+
person who created it lost access — the classic way a machine credential outlives
|
|
114
|
+
the authority it was granted under.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// After the creator is removed from the workspace:
|
|
118
|
+
await apiKeys.validate(token); // still fine — scopes still apply
|
|
119
|
+
await apiKeys.resolveContext(token); // UnauthorizedError
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
So a scope-only integration keeps working, and only the `WorkspaceContext` is
|
|
123
|
+
refused. Re-issue the key under a current member.
|
|
124
|
+
|
|
125
|
+
For a machine-to-machine integration with genuinely no user behind it, there is no
|
|
126
|
+
membership to verify — `unsafeContextFromPrincipal(principal)` is the deliberate
|
|
127
|
+
escape hatch, named so it cannot be used by accident.
|
|
128
|
+
|
|
129
|
+
## `apiKeys.list(context, options?)`
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
function list(context: WorkspaceContext, options?: { includeRevoked?: boolean }):
|
|
133
|
+
Promise<ApiKeySummary[]>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Revoked keys are hidden by default.
|
|
137
|
+
|
|
138
|
+
## `apiKeys.revoke(context, apiKeyId)`
|
|
139
|
+
|
|
140
|
+
Immediate and idempotent. Scoped to the caller's workspace, so another tenant's key
|
|
141
|
+
id raises `NotFoundError` rather than being revocable.
|
|
142
|
+
|
|
143
|
+
## Scopes
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
function requireScope(principal: ApiKeyPrincipal, scope: string): void // throws ForbiddenError
|
|
147
|
+
function hasScopes(principal: ApiKeyPrincipal, scopes: readonly string[]): boolean
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`ForbiddenError.permission` carries the missing scope.
|
|
151
|
+
|
|
152
|
+
**Scopes are not permissions.** Permissions belong to users through roles; scopes
|
|
153
|
+
belong to keys. See [authorization.md](./authorization.md) for the user side — and
|
|
154
|
+
note that both may apply to a request, depending on how you authenticate it.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Low-level helpers
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
generateApiKey(prefix?): GeneratedApiKey // { token, prefix, keyId, secret, keyHash }
|
|
162
|
+
parseApiKey(token): ParsedApiKey | undefined // undefined, never an explanation
|
|
163
|
+
hashApiKeySecret(secret): string
|
|
164
|
+
secretMatchesHash(secret, keyHash): boolean // constant-time
|
|
165
|
+
maskApiKey(prefix, keyId): string
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`parseApiKey` returns `undefined` rather than throwing, and never says why —
|
|
169
|
+
distinguishing "wrong shape" from "unknown key" from "revoked" would let an
|
|
170
|
+
attacker probe. It also bounds the input length before doing any work, so an
|
|
171
|
+
oversized string costs nothing.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Testing
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
createApiKeysService({
|
|
179
|
+
db,
|
|
180
|
+
workspaces, // omit and resolveContext throws an explanation
|
|
181
|
+
now: () => fixedClock,
|
|
182
|
+
lastUsedThrottleMs: 60_000,
|
|
183
|
+
});
|
|
184
|
+
```
|
package/docs/audit.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Audit
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import { audit, auditFromEvent, createAuditService } from "@digimuza/saas-core/audit";
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
> Orientation and recipes: [`src/audit/README.md`](../src/audit/README.md).
|
|
8
|
+
|
|
9
|
+
A durable, immutable record of consequential actions — who did what, to what, when.
|
|
10
|
+
For a human reading it after the fact.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Audit is not events
|
|
15
|
+
|
|
16
|
+
| | [events](./events.md) | audit |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| Audience | the application | a person, later |
|
|
19
|
+
| Volume | everything | consequential actions only |
|
|
20
|
+
| Immutability | by convention | by **database trigger** |
|
|
21
|
+
| Delivery | may fan out to handlers | synchronous write, never queued |
|
|
22
|
+
|
|
23
|
+
Recording every event would bury what matters among the routine.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## `audit.log(input, tx?)`
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
function log(input: {
|
|
31
|
+
action: string; // "member.role.updated" — past tense, dotted
|
|
32
|
+
actorId?: string | null; // null for the system
|
|
33
|
+
workspaceId?: string | null; // null for platform-level
|
|
34
|
+
targetType?: string;
|
|
35
|
+
targetId?: string;
|
|
36
|
+
metadata?: Record<string, unknown>;
|
|
37
|
+
request?: { requestId?: string; ipAddress?: string; userAgent?: string };
|
|
38
|
+
occurredAt?: Date;
|
|
39
|
+
}, tx?: CoreDatabase): Promise<AuditLogEntry>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Durable the moment it returns — written straight to PostgreSQL, never through
|
|
43
|
+
realtime, an in-process handler, or a queue.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
await audit.log({
|
|
47
|
+
action: "member.role.updated",
|
|
48
|
+
actorId: user.id,
|
|
49
|
+
workspaceId: context.workspaceId,
|
|
50
|
+
targetType: "user",
|
|
51
|
+
targetId: member.id,
|
|
52
|
+
metadata: { from: "member", to: "admin" },
|
|
53
|
+
request: { requestId, ipAddress, userAgent },
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Pass `tx` to make the entry and the change it describes commit together:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
await db.transaction(async (tx) => {
|
|
61
|
+
await assignRole(tx, member.id, roleId);
|
|
62
|
+
await audit.log({ action: "member.role.updated", /* ... */ }, tx);
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Both directions are tested: the entry commits with the change, and rolls back with
|
|
67
|
+
a failure.
|
|
68
|
+
|
|
69
|
+
`action` is capped at 120 characters so one oversized string cannot make a page of
|
|
70
|
+
the trail unreadable.
|
|
71
|
+
|
|
72
|
+
## `audit.logMany(inputs, tx?)`
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
function logMany(inputs: readonly AuditEntryInput[], tx?: CoreDatabase): Promise<AuditLogEntry[]>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
One statement for several entries.
|
|
79
|
+
|
|
80
|
+
## `audit.list(context, options?)`
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
function list(context: WorkspaceContext, options?: {
|
|
84
|
+
actions?: readonly string[];
|
|
85
|
+
actorId?: string;
|
|
86
|
+
targetType?: string;
|
|
87
|
+
targetId?: string;
|
|
88
|
+
since?: Date;
|
|
89
|
+
before?: Date; // keyset pagination
|
|
90
|
+
limit?: number; // default 50, capped at 500
|
|
91
|
+
}): Promise<AuditLogEntry[]>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Requires a verified `WorkspaceContext` — a trail is tenant data. Newest first,
|
|
95
|
+
tie-broken on id so keyset pagination cannot loop on equal timestamps.
|
|
96
|
+
|
|
97
|
+
## `audit.listPlatform(options?)`
|
|
98
|
+
|
|
99
|
+
Entries with no workspace: account-level actions, and anything belonging to a
|
|
100
|
+
workspace that has since been deleted.
|
|
101
|
+
|
|
102
|
+
**Not tenant-scoped.** Gate it on an operator-level permission of your own — the
|
|
103
|
+
core cannot know who is allowed to read it.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Immutability, enforced by PostgreSQL
|
|
108
|
+
|
|
109
|
+
There is no update or delete API, and a trigger rejects both operations:
|
|
110
|
+
|
|
111
|
+
```sql
|
|
112
|
+
CREATE TRIGGER audit_logs_no_update
|
|
113
|
+
BEFORE UPDATE ON "audit_logs"
|
|
114
|
+
FOR EACH ROW EXECUTE FUNCTION saas_core_audit_logs_immutable();
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
So a migration, a data repair, or a stray `psql` session cannot quietly rewrite
|
|
118
|
+
history either. Amending an entry means deliberately dropping the trigger — which
|
|
119
|
+
is itself an auditable act.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
await expectDatabaseError(
|
|
123
|
+
db.update(auditLogs).set({ action: "nothing.happened" }).where(eq(auditLogs.id, id)),
|
|
124
|
+
/append-only/,
|
|
125
|
+
);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Both operations are tested against real PostgreSQL.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Why `audit_logs` has no foreign keys
|
|
133
|
+
|
|
134
|
+
`actor_user_id` and `workspace_id` are plain `uuid` columns. Two reasons, and they
|
|
135
|
+
reinforce each other:
|
|
136
|
+
|
|
137
|
+
1. **An entry must outlive what it describes.** `ON DELETE CASCADE` would erase the
|
|
138
|
+
trail exactly when it matters most. `SET NULL` would keep the row and destroy
|
|
139
|
+
its value.
|
|
140
|
+
2. **`SET NULL` is an `UPDATE`,** which the immutability trigger rejects — so a
|
|
141
|
+
referencing audit row would make its own subject *undeletable*. That is a real
|
|
142
|
+
conflict, found by a test: deleting a user with audit entries failed outright.
|
|
143
|
+
|
|
144
|
+
They are historical identifiers, not live references. Join explicitly when
|
|
145
|
+
displaying a trail, and expect misses:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
const rows = await db
|
|
149
|
+
.select({ entry: auditLogs, actor: userProfiles })
|
|
150
|
+
.from(auditLogs)
|
|
151
|
+
.leftJoin(userProfiles, eq(userProfiles.userId, auditLogs.actorUserId))
|
|
152
|
+
.where(eq(auditLogs.workspaceId, context.workspaceId));
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
There is deliberately no Drizzle `relations()` for `audit_logs` — declaring one
|
|
156
|
+
would imply the actor is always resolvable.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## `auditFromEvent(event, mapping)`
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
function auditFromEvent(
|
|
164
|
+
event: SaasEvent,
|
|
165
|
+
mapping: Record<string, (event: never) => Omit<AuditEntryInput, "actorId" | "workspaceId">>,
|
|
166
|
+
): AuditEntryInput | undefined
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Translate selected events into audit entries. Returns `undefined` for anything
|
|
170
|
+
unmapped, so the mapping is explicit about what is worth keeping.
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
events.onAny(async (event) => {
|
|
174
|
+
const entry = auditFromEvent(event, {
|
|
175
|
+
"workspace.member.removed": (e) => ({
|
|
176
|
+
action: "member.removed",
|
|
177
|
+
targetType: "user",
|
|
178
|
+
targetId: e.data.userId,
|
|
179
|
+
}),
|
|
180
|
+
});
|
|
181
|
+
if (entry !== undefined) await audit.log(entry);
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`actorId`, `workspaceId` and `occurredAt` come from the event.
|
|
186
|
+
|
|
187
|
+
**But note the shape.** This writes audit from a *handler*, and a handler is not
|
|
188
|
+
durable — a crash between the commit and the handler loses the entry. Convenient
|
|
189
|
+
for the routine cases; for anything that must never be missed, call `audit.log` in
|
|
190
|
+
the same transaction as the change.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Custom construction
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
createAuditService({ db, now: () => fixedClock });
|
|
198
|
+
```
|
package/docs/auth.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Authentication
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import {
|
|
5
|
+
createConfiguredAuthService,
|
|
6
|
+
createCookieStore,
|
|
7
|
+
} from "@digimuza/saas-core/auth";
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
> Orientation and recipes: [`src/auth/README.md`](../src/auth/README.md).
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
WorkOS owns identity — OAuth, SAML, OIDC, passkeys, MFA and session sealing. The
|
|
14
|
+
core keeps its details behind a small API so application code never imports the
|
|
15
|
+
WorkOS SDK; a test enforces that.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Setup
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// lib/auth.ts
|
|
23
|
+
import { cookies } from "next/headers";
|
|
24
|
+
import { createConfiguredAuthService, createCookieStore } from "@digimuza/saas-core/auth";
|
|
25
|
+
|
|
26
|
+
export const auth = createConfiguredAuthService(createCookieStore(cookies));
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Pass the accessor `cookies`, **not** `cookies()`. Next resolves the jar per
|
|
30
|
+
request, so calling it once at module scope would bind every request to the first
|
|
31
|
+
one.
|
|
32
|
+
|
|
33
|
+
### Why you have to wire this yourself
|
|
34
|
+
|
|
35
|
+
The core does not import `next/headers`. Next has no `exports` map, so that
|
|
36
|
+
specifier only resolves through a bundler — importing it would make the package
|
|
37
|
+
unloadable in plain Node, where migration scripts, Trigger.dev tasks and Vitest
|
|
38
|
+
run. Three lines in your app keeps the core framework-agnostic.
|
|
39
|
+
|
|
40
|
+
For any other runtime, implement `CookieStore` (three methods) or use
|
|
41
|
+
`createMemoryCookieStore()`.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## `auth.getAuthorizationUrl(options?)`
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
function getAuthorizationUrl(options?: {
|
|
49
|
+
redirectUri?: string;
|
|
50
|
+
state?: string;
|
|
51
|
+
screenHint?: "sign-up" | "sign-in";
|
|
52
|
+
loginHint?: string;
|
|
53
|
+
organizationId?: string;
|
|
54
|
+
connectionId?: string;
|
|
55
|
+
invitationToken?: string;
|
|
56
|
+
}): Promise<{ url: string; state: string }>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Builds the WorkOS AuthKit URL **and stores a single-use CSRF state cookie** —
|
|
60
|
+
which is why it is async.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// app/login/route.ts
|
|
64
|
+
const { url } = await auth.getAuthorizationUrl({ screenHint: "sign-in" });
|
|
65
|
+
redirect(url);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`provider: "authkit"` is sent only when neither `organizationId` nor
|
|
69
|
+
`connectionId` is given: WorkOS expects exactly one selector, and sending both
|
|
70
|
+
conflicts with the more specific choice.
|
|
71
|
+
|
|
72
|
+
## `auth.handleCallback(input)`
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
function handleCallback(input: { code: string; state?: string }): Promise<CallbackResult>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// app/callback/route.ts
|
|
80
|
+
const { session } = await auth.handleCallback({
|
|
81
|
+
code: searchParams.get("code")!,
|
|
82
|
+
state: searchParams.get("state")!,
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
In order: verifies `state` against the cookie in constant time, consumes the
|
|
87
|
+
cookie, exchanges the code, seals the session into an httpOnly cookie, and syncs
|
|
88
|
+
the identity into `users` / `user_profiles`.
|
|
89
|
+
|
|
90
|
+
**The state check is not optional ceremony.** Without it an attacker can feed a
|
|
91
|
+
victim's browser a callback URL bearing the attacker's `code` and sign them into
|
|
92
|
+
the attacker's account. The code is not exchanged unless the state matches —
|
|
93
|
+
tested.
|
|
94
|
+
|
|
95
|
+
Throws `UnauthorizedError` for a missing, unknown or mismatched state.
|
|
96
|
+
|
|
97
|
+
## `auth.getSession()`
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
function getSession(): Promise<AuthSession | undefined>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
interface AuthSession {
|
|
105
|
+
userId: string; // local id — key off this
|
|
106
|
+
workosUserId: string; // identity-provider id
|
|
107
|
+
sessionId: string;
|
|
108
|
+
email: string;
|
|
109
|
+
organizationId?: string;
|
|
110
|
+
impersonatorEmail?: string;
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Reads the cookie, unseals, and validates. An expired access token is refreshed
|
|
115
|
+
transparently — that is the normal case, not an error, since the sealed session
|
|
116
|
+
carries a refresh token. Any other failure reason (tampered or absent cookie) is
|
|
117
|
+
treated as unauthenticated, and no refresh is attempted.
|
|
118
|
+
|
|
119
|
+
Also provisions the local user if this database has not seen the WorkOS identity
|
|
120
|
+
before — true after a restore, or on the first sign-in following a deploy.
|
|
121
|
+
|
|
122
|
+
### Refresh and Server Components
|
|
123
|
+
|
|
124
|
+
Next.js Server Components cannot write cookies, and attempting it throws. The
|
|
125
|
+
refresh already succeeded at WorkOS, so the request *is* authenticated — the write
|
|
126
|
+
failure is tolerated and the session returned. If you want the rotated token to
|
|
127
|
+
persist, refresh in middleware, where writing cookies is allowed.
|
|
128
|
+
|
|
129
|
+
## `auth.requireSession()` / `auth.requireUser()`
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
function requireSession(): Promise<AuthSession>
|
|
133
|
+
function requireUser(): Promise<UserWithProfile>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Throw `UnauthorizedError` when unauthenticated. Use these in route handlers and
|
|
137
|
+
Server Actions; use the `get*` variants when unauthenticated is a valid state.
|
|
138
|
+
|
|
139
|
+
## `auth.getCurrentUser()`
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
function getCurrentUser(): Promise<UserWithProfile | undefined>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The session's user, with their profile. See [users.md](./users.md).
|
|
146
|
+
|
|
147
|
+
## `auth.signOut(options?)`
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
function signOut(options?: { returnTo?: string }): Promise<{ logoutUrl: string | undefined }>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const { logoutUrl } = await auth.signOut();
|
|
155
|
+
redirect(logoutUrl ?? "/");
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Clears the cookie **before** contacting WorkOS, so a provider outage cannot leave
|
|
159
|
+
a user signed in locally. You must redirect to `logoutUrl` — clearing the cookie
|
|
160
|
+
alone leaves the WorkOS session alive, so the next sign-in would silently succeed
|
|
161
|
+
without re-authenticating.
|
|
162
|
+
|
|
163
|
+
Returns `undefined` when there was no session, or when the sealed session could
|
|
164
|
+
not be read (nothing left to log out).
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Cookie attributes
|
|
169
|
+
|
|
170
|
+
`httpOnly`, `Secure` outside development, `SameSite=Lax`, `path=/`, 30-day
|
|
171
|
+
lifetime.
|
|
172
|
+
|
|
173
|
+
`Lax` and not `Strict`: the identity provider returns the user via a top-level GET
|
|
174
|
+
navigation, and `Strict` would withhold the cookie on that request and break every
|
|
175
|
+
sign-in. `Secure` follows `APP_ENV` — requiring it in local development over plain
|
|
176
|
+
HTTP would also break sign-in.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Custom construction
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
import { createAuthService } from "@digimuza/saas-core/auth";
|
|
184
|
+
|
|
185
|
+
const auth = createAuthService({
|
|
186
|
+
workos, cookies, users,
|
|
187
|
+
clientId, cookiePassword, redirectUri,
|
|
188
|
+
secureCookies: true,
|
|
189
|
+
sessionCookieName: "my_session", // default "dg_session"
|
|
190
|
+
stateCookieName: "my_state", // default "dg_oauth_state"
|
|
191
|
+
sessionMaxAgeSeconds: 60 * 60 * 24 * 7,
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`getWorkos()` returns the configured, `globalThis`-cached WorkOS client if you need
|
|
196
|
+
a capability the core does not wrap.
|