@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
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Roles, permissions & authorization
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import { authorization, definePermissions } from "@digimuza/saas-core/authorization";
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
> Orientation and recipes: [`src/authorization/README.md`](../src/authorization/README.md).
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
Authorization always requires workspace context. A user's effective permissions are
|
|
11
|
+
the **union** of the permissions carried by every role they hold in that workspace.
|
|
12
|
+
|
|
13
|
+
Permissions are application-defined; the core stores the vocabulary you register
|
|
14
|
+
rather than shipping its own.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// lib/permissions.ts
|
|
22
|
+
import { definePermissions } from "@digimuza/saas-core/authorization";
|
|
23
|
+
|
|
24
|
+
export const PERMISSIONS = definePermissions([
|
|
25
|
+
"projects.read",
|
|
26
|
+
"projects.create",
|
|
27
|
+
"projects.delete",
|
|
28
|
+
"members.invite",
|
|
29
|
+
"billing.read",
|
|
30
|
+
] as const);
|
|
31
|
+
|
|
32
|
+
export type AppPermission = (typeof PERMISSIONS)[number];
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// once at startup, or from a migration step
|
|
37
|
+
await authorization.registerPermissions(PERMISSIONS.map((key) => ({ key })));
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### `definePermissions(keys)`
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
function definePermissions<const T extends readonly string[]>(keys: T): T
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Purely a typing helper — no I/O. The `const` type parameter preserves the literal
|
|
47
|
+
keys so you get a union type and autocompletion.
|
|
48
|
+
|
|
49
|
+
### `registerPermissions(definitions)`
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
function registerPermissions(definitions: readonly { key: string; description?: string }[]): Promise<void>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Idempotent, so it is safe on every boot. Permissions must exist before a role can
|
|
56
|
+
be granted them.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Checking
|
|
61
|
+
|
|
62
|
+
### `authorization.can(context, permission)`
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
function can(context: WorkspaceContext, permission: string): Promise<boolean>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
if (await authorization.can(context, "projects.delete")) {
|
|
70
|
+
// show the delete button
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### `authorization.require(context, permission)`
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
function require(context: WorkspaceContext, permission: string): Promise<void>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Throws `ForbiddenError` with `.permission` set to the missing key.
|
|
81
|
+
|
|
82
|
+
### `canAll` / `canAny`
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
function canAll(context: WorkspaceContext, required: readonly string[]): Promise<boolean>
|
|
86
|
+
function canAny(context: WorkspaceContext, required: readonly string[]): Promise<boolean>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Empty lists are handled explicitly and differently: `canAll([])` is `true`
|
|
90
|
+
(vacuously satisfied), `canAny([])` is `false` (nothing to satisfy). Both are
|
|
91
|
+
tested so neither is accidental.
|
|
92
|
+
|
|
93
|
+
### `authorization.getEffectivePermissions(context)`
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
function getEffectivePermissions(context: WorkspaceContext): Promise<Set<string>>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The union across all the user's roles, resolved in **one** query — a `SELECT
|
|
100
|
+
DISTINCT` over member grants joined through roles to permissions. Holding more
|
|
101
|
+
roles simply widens the set.
|
|
102
|
+
|
|
103
|
+
Both halves of the join to `roles` are constrained by workspace, so a stale grant
|
|
104
|
+
could not contribute another tenant's permissions.
|
|
105
|
+
|
|
106
|
+
### `authorization.hasRole(context, roleKey)`
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
function hasRole(context: WorkspaceContext, roleKey: string): Promise<boolean>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Prefer permissions.** Role checks couple application logic to the role structure,
|
|
113
|
+
so restructuring roles silently changes behaviour. This exists for the rare check
|
|
114
|
+
that genuinely is about role identity.
|
|
115
|
+
|
|
116
|
+
Matches on `key`, the stable machine identifier, never on `name`, which is
|
|
117
|
+
user-editable and localisable — renaming a role must not change who can do what.
|
|
118
|
+
|
|
119
|
+
### `authorization.getRoles(context)`
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
function getRoles(context: WorkspaceContext): Promise<Role[]>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Role administration
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
authorization.roles.list(context): Promise<Role[]>
|
|
131
|
+
|
|
132
|
+
authorization.roles.create(context, {
|
|
133
|
+
key: string;
|
|
134
|
+
name: string;
|
|
135
|
+
description?: string;
|
|
136
|
+
isDefault?: boolean;
|
|
137
|
+
permissions?: readonly string[];
|
|
138
|
+
}): Promise<Role>
|
|
139
|
+
|
|
140
|
+
authorization.roles.setPermissions(context, roleId, keys): Promise<void>
|
|
141
|
+
authorization.roles.delete(context, roleId): Promise<void>
|
|
142
|
+
authorization.roles.assign(context, { userId, roleId }): Promise<void>
|
|
143
|
+
authorization.roles.unassign(context, { userId, roleId }): Promise<void>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- `setPermissions` **replaces** the set; pass `[]` to clear it.
|
|
147
|
+
- `delete` throws `ForbiddenError` for system roles. Deleting `admin` would leave
|
|
148
|
+
nobody able to administer the workspace.
|
|
149
|
+
- `assign` is idempotent and emits nothing when the role was already held —
|
|
150
|
+
re-announcing an unchanged grant would make event consumers act twice.
|
|
151
|
+
- `unassign` throws `NotFoundError` if the user does not hold the role.
|
|
152
|
+
- Every one of these verifies the role belongs to the caller's workspace first, so
|
|
153
|
+
a role id from another tenant cannot be probed or modified.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Cross-workspace isolation is a database constraint
|
|
158
|
+
|
|
159
|
+
`workspace_member_roles` stores `workspace_id` beside `user_id` and `role_id` so
|
|
160
|
+
that two composite foreign keys can share it:
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
(workspace_id, user_id) → workspace_members(workspace_id, user_id)
|
|
164
|
+
(role_id, workspace_id) → roles(id, workspace_id)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Both read the **same** `workspace_id` column, so granting a member of workspace A
|
|
168
|
+
a role belonging to workspace B is rejected by PostgreSQL — not by application code
|
|
169
|
+
that could be forgotten or bypassed. Switching `workspace_id` to satisfy one key
|
|
170
|
+
breaks the other. There are tests for both directions, plus the bypass attempt.
|
|
171
|
+
|
|
172
|
+
`workspace_invitations` carries the same guard on its optional `role_id`.
|
|
173
|
+
|
|
174
|
+
When the constraint fires, `roles.assign` translates it to `NotFoundError` — a
|
|
175
|
+
foreign-key violation is the database enforcing tenancy, not an unexpected fault.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Roles table
|
|
180
|
+
|
|
181
|
+
| Column | Notes |
|
|
182
|
+
| --- | --- |
|
|
183
|
+
| `key` | Stable machine identifier. Unique per workspace. |
|
|
184
|
+
| `name` | Human label. Editable; never used in authorization logic. |
|
|
185
|
+
| `is_system` | Created with the workspace; protected from deletion. |
|
|
186
|
+
| `is_default` | Granted to new members. At most one per workspace. |
|
|
187
|
+
|
|
188
|
+
`key` and the two flags are additions beyond the original spec's column list.
|
|
189
|
+
`key` exists so authorization never depends on a renameable string; the flags
|
|
190
|
+
implement the spec's "system roles, default roles, custom roles" requirement.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## `findRolesWithPermission(db, workspaceId, permission)`
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
function findRolesWithPermission(
|
|
198
|
+
db: CoreDatabase, workspaceId: string, permission: string,
|
|
199
|
+
): Promise<Role[]>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
"Which roles here can do X?" — for building a permissions matrix UI.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Authorization vs entitlements
|
|
207
|
+
|
|
208
|
+
They answer different questions and **both** must pass:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
await authorization.require(context, "projects.create"); // may this user?
|
|
212
|
+
await entitlements.require(context, "ai_generation"); // has this workspace paid?
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
See [entitlements.md](./entitlements.md).
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Custom construction
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import { createAuthorizationService } from "@digimuza/saas-core/authorization";
|
|
223
|
+
|
|
224
|
+
const authorization = createAuthorizationService({ db, events }); // events optional
|
|
225
|
+
```
|
package/docs/billing.md
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# Billing
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import { billing, InvalidWebhookSignatureError } from "@digimuza/saas-core/billing";
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
> Orientation and recipes: [`src/billing/README.md`](../src/billing/README.md).
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
Everything above the `BillingProvider` interface is provider-independent, so
|
|
11
|
+
adopting a second payment provider means writing an adapter — a test enforces that
|
|
12
|
+
nothing outside the adapter mentions Revolut.
|
|
13
|
+
|
|
14
|
+
The core does not implement payment processing.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Plans
|
|
19
|
+
|
|
20
|
+
### `billing.plans.upsert(input)`
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
function upsert(input: {
|
|
24
|
+
key: string; // stable internal identifier
|
|
25
|
+
name: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
providerPriceId?: string; // for providers with a price catalogue
|
|
28
|
+
amountCents?: number; // minor units, to avoid floating-point money
|
|
29
|
+
currency?: string; // ISO 4217, uppercased for you
|
|
30
|
+
interval?: "month" | "year" | "one_time";
|
|
31
|
+
isActive?: boolean;
|
|
32
|
+
sortOrder?: number;
|
|
33
|
+
entitlements?: readonly EntitlementDefinition[];
|
|
34
|
+
}): Promise<Plan>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Idempotent — run it on every boot to sync the catalogue from code.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
await billing.plans.upsert({
|
|
41
|
+
key: "pro",
|
|
42
|
+
name: "Pro",
|
|
43
|
+
amountCents: 2900,
|
|
44
|
+
entitlements: [
|
|
45
|
+
{ key: "ai_generation", kind: "feature" },
|
|
46
|
+
{ key: "ai_generations", kind: "limit", limitValue: 100 },
|
|
47
|
+
{ key: "team_members", kind: "limit", limitValue: null }, // unlimited
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Omit `entitlements` and the existing set is left alone; pass it and the set is
|
|
53
|
+
**replaced**. A `limitValue` on a `feature` is discarded — storing one would make
|
|
54
|
+
`getLimit()` on a feature look meaningful.
|
|
55
|
+
|
|
56
|
+
Throws `InvalidInputError` for duplicate keys or a negative limit.
|
|
57
|
+
|
|
58
|
+
### `billing.plans.list` / `getByKey` / `setEntitlements`
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
function list(options?: { includeInactive?: boolean }): Promise<Plan[]>
|
|
62
|
+
function getByKey(key: string): Promise<Plan | undefined>
|
|
63
|
+
function setEntitlements(planKey: string, definitions: readonly EntitlementDefinition[]): Promise<void>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`list()` hides inactive plans by default: `isActive: false` retires a plan from the
|
|
67
|
+
pricing page while still honouring it for existing customers.
|
|
68
|
+
|
|
69
|
+
Plans use `ON DELETE RESTRICT` from `subscriptions`. Deleting a plan customers are
|
|
70
|
+
paying for would silently strip their entitlements — retire it instead. There is a
|
|
71
|
+
test asserting the delete fails.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Customers
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
function getCustomer(context: WorkspaceContext): Promise<BillingCustomer | undefined>
|
|
79
|
+
function ensureCustomer(context: WorkspaceContext, options?: { email?: string }): Promise<BillingCustomer>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Billing is workspace-scoped: the tenant pays, and members come and go.
|
|
83
|
+
|
|
84
|
+
`ensureCustomer` is safe to call concurrently — a race on the workspace constraint
|
|
85
|
+
adopts the winner's row. It matches on
|
|
86
|
+
`billing_customers_workspace_id_unique` specifically; a violation of
|
|
87
|
+
`billing_customers_provider_customer_unique` means the provider handed back an id
|
|
88
|
+
already bound to a *different* workspace, which is a genuine fault that must
|
|
89
|
+
surface.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Checkout
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
function createCheckout(context: WorkspaceContext, options: {
|
|
97
|
+
planKey: string;
|
|
98
|
+
successUrl: string;
|
|
99
|
+
cancelUrl: string;
|
|
100
|
+
quantity?: number;
|
|
101
|
+
metadata?: Record<string, string>;
|
|
102
|
+
}): Promise<{ url: string; providerCheckoutId?: string }>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const { url } = await billing.createCheckout(context, {
|
|
107
|
+
planKey: "pro",
|
|
108
|
+
successUrl: "https://app.example.com/billing/done",
|
|
109
|
+
cancelUrl: "https://app.example.com/billing",
|
|
110
|
+
});
|
|
111
|
+
redirect(url);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Creates the provider customer lazily if needed. Throws `NotFoundError` for an
|
|
115
|
+
unknown plan, `ConflictError` for a retired one.
|
|
116
|
+
|
|
117
|
+
**Do not grant access when the user returns to `successUrl`.** That redirect is
|
|
118
|
+
attacker-controllable and proves nothing about payment. Access follows from the
|
|
119
|
+
webhook.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Webhooks are the only source of truth
|
|
124
|
+
|
|
125
|
+
### `billing.webhooks.handle(request)`
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
function handle(request: {
|
|
129
|
+
rawBody: string; // exactly as received, before any parsing
|
|
130
|
+
signature: string;
|
|
131
|
+
timestamp?: string;
|
|
132
|
+
}): Promise<WebhookOutcome>
|
|
133
|
+
|
|
134
|
+
type WebhookOutcome =
|
|
135
|
+
| { status: "processed"; eventId: string; type: string }
|
|
136
|
+
| { status: "duplicate"; type: string }
|
|
137
|
+
| { status: "ignored"; eventId: string; type: string };
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
// app/api/billing/webhook/route.ts
|
|
142
|
+
export async function POST(request: Request) {
|
|
143
|
+
const rawBody = await request.text(); // text, not json
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const outcome = await billing.webhooks.handle({
|
|
147
|
+
rawBody,
|
|
148
|
+
signature: request.headers.get("revolut-signature") ?? "",
|
|
149
|
+
timestamp: request.headers.get("revolut-request-timestamp") ?? undefined,
|
|
150
|
+
});
|
|
151
|
+
return Response.json(outcome);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error instanceof InvalidWebhookSignatureError) {
|
|
154
|
+
return new Response("invalid signature", { status: 401 });
|
|
155
|
+
}
|
|
156
|
+
throw error; // 5xx makes the provider retry
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Read the body as text.** Parsing and re-serialising JSON changes key order and
|
|
162
|
+
whitespace, which invalidates the signature.
|
|
163
|
+
|
|
164
|
+
Verification happens before the parser, and before anything touches the database.
|
|
165
|
+
`InvalidWebhookSignatureError`'s message is deliberately uninformative — a caller
|
|
166
|
+
who cannot produce a valid signature gets no hint about which part failed.
|
|
167
|
+
|
|
168
|
+
### Idempotency is a database constraint
|
|
169
|
+
|
|
170
|
+
`(provider, provider_event_id)` is unique and **the insert is the claim**. So
|
|
171
|
+
at-least-once delivery and concurrent redeliveries are handled by PostgreSQL, not
|
|
172
|
+
by a check-then-act two requests could pass together. There is a test firing three
|
|
173
|
+
identical deliveries concurrently: one processes, two dedupe, one subscription row
|
|
174
|
+
results.
|
|
175
|
+
|
|
176
|
+
The claim is a conditional upsert with `setWhere: status = 'failed'`, so a retry
|
|
177
|
+
re-claims an event whose previous attempt **failed** but never one already
|
|
178
|
+
`processed` or currently `pending`. Genuine failures are replayable without
|
|
179
|
+
risking a double application. The raw payload is retained so replay needs no help
|
|
180
|
+
from the provider.
|
|
181
|
+
|
|
182
|
+
### `processed` vs `ignored` vs `duplicate`
|
|
183
|
+
|
|
184
|
+
`ignored` means verified and stored but not actionable — an unknown customer or an
|
|
185
|
+
unresolvable plan. Routine when several environments share one provider account;
|
|
186
|
+
conflating it with `failed` would bury real failures.
|
|
187
|
+
|
|
188
|
+
### Tenancy comes from our own records
|
|
189
|
+
|
|
190
|
+
The workspace is resolved from our `billing_customers` row, matched on the provider
|
|
191
|
+
customer id — **never** from the webhook payload. So even a leaked signing secret
|
|
192
|
+
could not redirect a subscription to another tenant.
|
|
193
|
+
|
|
194
|
+
### Upgrades retire the previous subscription
|
|
195
|
+
|
|
196
|
+
`subscriptions_one_live_per_workspace_idx` allows exactly one live subscription per
|
|
197
|
+
workspace, which keeps "what plan is this workspace on?" unambiguous rather than
|
|
198
|
+
row-order dependent. An upgrade arrives as a *new* subscription, so applying it
|
|
199
|
+
cancels the old one.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Subscription lifecycle
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
function getSubscription(context: WorkspaceContext): Promise<{ subscription: Subscription; plan: Plan } | undefined>
|
|
207
|
+
function cancelSubscription(context: WorkspaceContext, options?: { atPeriodEnd?: boolean }): Promise<Subscription>
|
|
208
|
+
function getPortal(context: WorkspaceContext, options: { returnUrl: string }): Promise<{ url: string | undefined }>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`cancelSubscription` defaults to `atPeriodEnd: true`, which keeps the subscription
|
|
212
|
+
live — and therefore entitling — until the paid period expires. `false` ends
|
|
213
|
+
entitlements immediately.
|
|
214
|
+
|
|
215
|
+
**`getPortal` can return `undefined`.** Handle it; do not assume a URL.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Revolut's capability boundaries
|
|
220
|
+
|
|
221
|
+
Revolut Merchant is an order-and-payment API, not a subscription platform. It has
|
|
222
|
+
no hosted customer portal and no server-side subscription object. So
|
|
223
|
+
`cancelSubscription` and `getPortalUrl` are **optional** on `BillingProvider` and
|
|
224
|
+
absent from the Revolut adapter rather than stubbed to silently do nothing:
|
|
225
|
+
|
|
226
|
+
- `getPortal()` returns `{ url: undefined }` — build your own billing screen.
|
|
227
|
+
- `cancelSubscription()` records the local state change, and calls the provider
|
|
228
|
+
only when the adapter supports it.
|
|
229
|
+
|
|
230
|
+
Recurring charges on Revolut are merchant-initiated: the core owns subscription
|
|
231
|
+
state and drives the charges.
|
|
232
|
+
|
|
233
|
+
### Verification status
|
|
234
|
+
|
|
235
|
+
Signature verification implements Revolut's documented scheme and is unit-tested:
|
|
236
|
+
HMAC-SHA256 over `v1.{timestamp}.{body}`, a freshness window for replay protection
|
|
237
|
+
(rejecting both stale timestamps and clocks skewed forward), constant-time
|
|
238
|
+
comparison, and the comma-separated signatures sent during secret rotation.
|
|
239
|
+
|
|
240
|
+
**The HTTP calls — `createCustomer` and `createCheckout` — have not been run
|
|
241
|
+
against live Revolut credentials.** Treat them as unverified.
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import { verifyRevolutSignature } from "@digimuza/saas-core/billing";
|
|
245
|
+
|
|
246
|
+
// Exported separately, so a route handler can verify without constructing
|
|
247
|
+
// API credentials it does not need.
|
|
248
|
+
verifyRevolutSignature({ rawBody, signature, timestamp, secret });
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Writing another provider
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
interface BillingProvider {
|
|
257
|
+
readonly name: string;
|
|
258
|
+
createCustomer(input): Promise<{ providerCustomerId: string }>;
|
|
259
|
+
createCheckout(input): Promise<{ url: string; providerCheckoutId?: string }>;
|
|
260
|
+
cancelSubscription?(input): Promise<void>; // optional
|
|
261
|
+
getPortalUrl?(input): Promise<string>; // optional
|
|
262
|
+
verifyWebhook(input): boolean;
|
|
263
|
+
parseWebhookEvent(rawBody: string): ProviderWebhookEvent;
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Two obligations the types cannot express:
|
|
268
|
+
|
|
269
|
+
1. **Map the provider's statuses onto `SubscriptionStatus`.** Fail closed — an
|
|
270
|
+
unrecognised state must not become `active`, or you grant entitlements the
|
|
271
|
+
customer may not have.
|
|
272
|
+
2. **Convert dates to `Date` instances.** Providers send ISO strings; passing one
|
|
273
|
+
through fails deep inside the database driver with an error naming neither the
|
|
274
|
+
field nor the adapter.
|
|
275
|
+
|
|
276
|
+
Resolve the plan by `providerPriceId` if the provider has a price catalogue, or by
|
|
277
|
+
`planKey` if the plan travels in a reference you set at checkout.
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
import { createBillingService } from "@digimuza/saas-core/billing";
|
|
281
|
+
|
|
282
|
+
const billing = createBillingService({ db, provider: myProvider, events });
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Diagnostics
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
import { countFailedBillingEvents } from "@digimuza/saas-core/billing";
|
|
291
|
+
|
|
292
|
+
const backlog = await countFailedBillingEvents(db); // awaiting replay
|
|
293
|
+
```
|