@estifanos-sh/convex-auth 0.0.2 → 0.0.4
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/README.md +29 -6
- package/dist/bin.js +2 -34
- package/dist/browser/index.d.ts +2 -2
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +15 -9
- package/dist/browser/index.js.map +1 -1
- package/dist/client/core/types.d.ts +68 -97
- package/dist/client/core/types.d.ts.map +1 -1
- package/dist/client/core/types.js.map +1 -1
- package/dist/client/errors.d.ts +15 -0
- package/dist/client/errors.d.ts.map +1 -0
- package/dist/client/errors.js +8 -1
- package/dist/client/errors.js.map +1 -1
- package/dist/client/factors/device.js +4 -4
- package/dist/client/factors/device.js.map +1 -1
- package/dist/client/factors/totp.js +20 -12
- package/dist/client/factors/totp.js.map +1 -1
- package/dist/client/factors/webauthn.js +6 -2
- package/dist/client/factors/webauthn.js.map +1 -1
- package/dist/client/index.d.ts +6 -3
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +13 -10
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +13 -3
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +365 -74
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/dataModel.d.ts +2 -2
- package/dist/component/_generated/dataModel.d.ts.map +1 -1
- package/dist/component/_generated/server.d.ts +13 -2
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/_generated/server.js +2 -1
- package/dist/component/_generated/server.js.map +1 -1
- package/dist/component/account.d.ts +52 -38
- package/dist/component/account.d.ts.map +1 -1
- package/dist/component/account.js +42 -23
- package/dist/component/account.js.map +1 -1
- package/dist/component/batch.d.ts +3 -0
- package/dist/component/batch.d.ts.map +1 -0
- package/dist/component/batch.js +17 -0
- package/dist/component/batch.js.map +1 -0
- package/dist/component/connection/audit.d.ts +30 -27
- package/dist/component/connection/audit.d.ts.map +1 -1
- package/dist/component/connection/audit.js +16 -23
- package/dist/component/connection/audit.js.map +1 -1
- package/dist/component/connection/cache.d.ts +6 -6
- package/dist/component/connection/cache.js +0 -1
- package/dist/component/connection/cache.js.map +1 -1
- package/dist/component/connection/domain/verification.d.ts +19 -19
- package/dist/component/connection/domain/verification.js +1 -2
- package/dist/component/connection/domain/verification.js.map +1 -1
- package/dist/component/connection/domain.d.ts +20 -20
- package/dist/component/connection/domain.js +1 -2
- package/dist/component/connection/domain.js.map +1 -1
- package/dist/component/connection/saml/assertion.d.ts +4 -4
- package/dist/component/connection/saml/assertion.js +0 -1
- package/dist/component/connection/saml/assertion.js.map +1 -1
- package/dist/component/connection/saml/request.d.ts +8 -9
- package/dist/component/connection/saml/request.d.ts.map +1 -1
- package/dist/component/connection/saml/request.js +0 -2
- package/dist/component/connection/saml/request.js.map +1 -1
- package/dist/component/connection/scim/config.d.ts +13 -13
- package/dist/component/connection/scim/config.d.ts.map +1 -1
- package/dist/component/connection/scim/config.js +2 -2
- package/dist/component/connection/scim/config.js.map +1 -1
- package/dist/component/connection/scim/identity.d.ts +122 -61
- package/dist/component/connection/scim/identity.d.ts.map +1 -1
- package/dist/component/connection/scim/identity.js +408 -60
- package/dist/component/connection/scim/identity.js.map +1 -1
- package/dist/component/connection/secret.d.ts +15 -15
- package/dist/component/connection/secret.d.ts.map +1 -1
- package/dist/component/connection/secret.js +2 -2
- package/dist/component/connection/secret.js.map +1 -1
- package/dist/component/connection/webhook/delivery.d.ts +69 -53
- package/dist/component/connection/webhook/delivery.d.ts.map +1 -1
- package/dist/component/connection/webhook/delivery.js +129 -172
- package/dist/component/connection/webhook/delivery.js.map +1 -1
- package/dist/component/connection/webhook/endpoint.d.ts +21 -21
- package/dist/component/connection/webhook/endpoint.d.ts.map +1 -1
- package/dist/component/connection/webhook/endpoint.js +2 -2
- package/dist/component/connection/webhook/endpoint.js.map +1 -1
- package/dist/component/connection/webhook/events.d.ts +26 -0
- package/dist/component/connection/webhook/events.d.ts.map +1 -0
- package/dist/component/connection/webhook/events.js +39 -0
- package/dist/component/connection/webhook/events.js.map +1 -0
- package/dist/component/connection.d.ts +39 -39
- package/dist/component/connection.d.ts.map +1 -1
- package/dist/component/connection.js +9 -9
- package/dist/component/connection.js.map +1 -1
- package/dist/component/context.d.ts +115 -0
- package/dist/component/context.d.ts.map +1 -0
- package/dist/component/context.js +49 -0
- package/dist/component/context.js.map +1 -0
- package/dist/component/convex.config.d.ts +2 -2
- package/dist/component/crons.d.ts +2 -2
- package/dist/component/documents.d.ts +1244 -0
- package/dist/component/documents.d.ts.map +1 -0
- package/dist/component/documents.js +44 -0
- package/dist/component/documents.js.map +1 -0
- package/dist/component/event.d.ts +142 -141
- package/dist/component/event.d.ts.map +1 -1
- package/dist/component/event.js +90 -119
- package/dist/component/event.js.map +1 -1
- package/dist/component/eventstream.d.ts +30 -0
- package/dist/component/eventstream.d.ts.map +1 -0
- package/dist/component/eventstream.js +60 -0
- package/dist/component/eventstream.js.map +1 -0
- package/dist/component/factor/device.d.ts +25 -24
- package/dist/component/factor/device.d.ts.map +1 -1
- package/dist/component/factor/device.js +7 -4
- package/dist/component/factor/device.js.map +1 -1
- package/dist/component/factor/passkey.d.ts +173 -85
- package/dist/component/factor/passkey.d.ts.map +1 -1
- package/dist/component/factor/passkey.js +138 -27
- package/dist/component/factor/passkey.js.map +1 -1
- package/dist/component/factor/totp.d.ts +56 -50
- package/dist/component/factor/totp.d.ts.map +1 -1
- package/dist/component/factor/totp.js +6 -5
- package/dist/component/factor/totp.js.map +1 -1
- package/dist/component/group/active.d.ts +326 -22
- package/dist/component/group/active.d.ts.map +1 -1
- package/dist/component/group/active.js +22 -13
- package/dist/component/group/active.js.map +1 -1
- package/dist/component/group/invite.d.ts +30 -30
- package/dist/component/group/invite.d.ts.map +1 -1
- package/dist/component/group/invite.js +2 -2
- package/dist/component/group/invite.js.map +1 -1
- package/dist/component/group/member.d.ts +36 -36
- package/dist/component/group/member.d.ts.map +1 -1
- package/dist/component/group/member.js +5 -3
- package/dist/component/group/member.js.map +1 -1
- package/dist/component/group.d.ts +49 -64
- package/dist/component/group.d.ts.map +1 -1
- package/dist/component/group.js +5 -3
- package/dist/component/group.js.map +1 -1
- package/dist/component/http.d.ts +2 -2
- package/dist/component/index.d.ts +0 -10
- package/dist/component/index.js +1 -3
- package/dist/component/limits.d.ts +5 -4
- package/dist/component/limits.d.ts.map +1 -1
- package/dist/component/limits.js +7 -4
- package/dist/component/limits.js.map +1 -1
- package/dist/component/maintenance.d.ts +6 -3
- package/dist/component/maintenance.d.ts.map +1 -1
- package/dist/component/maintenance.js +45 -6
- package/dist/component/maintenance.js.map +1 -1
- package/dist/component/model.d.ts +847 -1723
- package/dist/component/model.d.ts.map +1 -1
- package/dist/component/model.js +28 -317
- package/dist/component/model.js.map +1 -1
- package/dist/component/modules.d.ts +11 -5
- package/dist/component/modules.d.ts.map +1 -1
- package/dist/component/modules.js +4 -1
- package/dist/component/modules.js.map +1 -1
- package/dist/component/oauth/client.d.ts +20 -20
- package/dist/component/oauth/client.d.ts.map +1 -1
- package/dist/component/oauth/client.js +2 -2
- package/dist/component/oauth/client.js.map +1 -1
- package/dist/component/oauth/code.d.ts +14 -14
- package/dist/component/oauth/code.js +1 -2
- package/dist/component/oauth/code.js.map +1 -1
- package/dist/component/oauth/refresh.d.ts +20 -20
- package/dist/component/oauth/refresh.js +1 -2
- package/dist/component/oauth/refresh.js.map +1 -1
- package/dist/component/schema.d.ts +1148 -607
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +43 -15
- package/dist/component/schema.js.map +1 -1
- package/dist/component/session.d.ts +68 -32
- package/dist/component/session.d.ts.map +1 -1
- package/dist/component/session.js +118 -34
- package/dist/component/session.js.map +1 -1
- package/dist/component/token/continuation.d.ts +47 -18
- package/dist/component/token/continuation.d.ts.map +1 -1
- package/dist/component/token/continuation.js +76 -18
- package/dist/component/token/continuation.js.map +1 -1
- package/dist/component/token/enrollment.d.ts +42 -0
- package/dist/component/token/enrollment.d.ts.map +1 -0
- package/dist/component/token/enrollment.js +57 -0
- package/dist/component/token/enrollment.js.map +1 -0
- package/dist/component/token/pkce.d.ts +31 -24
- package/dist/component/token/pkce.d.ts.map +1 -1
- package/dist/component/token/pkce.js +15 -20
- package/dist/component/token/pkce.js.map +1 -1
- package/dist/component/token/refresh.d.ts +42 -35
- package/dist/component/token/refresh.d.ts.map +1 -1
- package/dist/component/token/refresh.js +16 -8
- package/dist/component/token/refresh.js.map +1 -1
- package/dist/component/token/verification.d.ts +34 -30
- package/dist/component/token/verification.d.ts.map +1 -1
- package/dist/component/token/verification.js +7 -6
- package/dist/component/token/verification.js.map +1 -1
- package/dist/component/user/email.d.ts +21 -21
- package/dist/component/user/email.d.ts.map +1 -1
- package/dist/component/user/email.js +2 -2
- package/dist/component/user/email.js.map +1 -1
- package/dist/component/user/key.d.ts +36 -36
- package/dist/component/user/key.d.ts.map +1 -1
- package/dist/component/user/key.js +2 -2
- package/dist/component/user/key.js.map +1 -1
- package/dist/component/user.d.ts +52 -48
- package/dist/component/user.d.ts.map +1 -1
- package/dist/component/user.js +157 -36
- package/dist/component/user.js.map +1 -1
- package/dist/expo/index.d.ts +4 -4
- package/dist/expo/index.d.ts.map +1 -1
- package/dist/expo/index.js +20 -13
- package/dist/expo/index.js.map +1 -1
- package/dist/node_modules/.pnpm/@opentelemetry_sdk-logs@0.214.0_@opentelemetry_api@1.9.1/node_modules/@opentelemetry/sdk-logs/build/src/export/ReadableLogRecord.d.ts +2 -2
- package/dist/node_modules/.pnpm/@opentelemetry_sdk-logs@0.214.0_@opentelemetry_api@1.9.1/node_modules/@opentelemetry/sdk-logs/build/src/export/SdkLogRecord.d.ts +1 -1
- package/dist/node_modules/.pnpm/@opentelemetry_sdk-metrics@2.6.1_@opentelemetry_api@1.9.1/node_modules/@opentelemetry/sdk-metrics/build/src/MeterProvider.d.ts +2 -2
- package/dist/node_modules/.pnpm/@opentelemetry_sdk-metrics@2.6.1_@opentelemetry_api@1.9.1/node_modules/@opentelemetry/sdk-metrics/build/src/export/MetricData.d.ts +1 -1
- package/dist/node_modules/.pnpm/@opentelemetry_sdk-trace-base@2.6.1_@opentelemetry_api@1.9.1/node_modules/@opentelemetry/sdk-trace-base/build/src/Span.d.ts +1 -1
- package/dist/node_modules/.pnpm/@opentelemetry_sdk-trace-base@2.6.1_@opentelemetry_api@1.9.1/node_modules/@opentelemetry/sdk-trace-base/build/src/export/ReadableSpan.d.ts +1 -1
- package/dist/node_modules/.pnpm/@opentelemetry_sdk-trace-base@2.6.1_@opentelemetry_api@1.9.1/node_modules/@opentelemetry/sdk-trace-base/build/src/types.d.ts +1 -1
- package/dist/otel.d.ts +1 -1
- package/dist/providers/anonymous.d.ts +11 -5
- package/dist/providers/anonymous.d.ts.map +1 -1
- package/dist/providers/anonymous.js +4 -1
- package/dist/providers/anonymous.js.map +1 -1
- package/dist/providers/apple.d.ts +1 -1
- package/dist/providers/apple.js +19 -4
- package/dist/providers/apple.js.map +1 -1
- package/dist/providers/credentials.d.ts +22 -14
- package/dist/providers/credentials.d.ts.map +1 -1
- package/dist/providers/credentials.js +2 -0
- package/dist/providers/credentials.js.map +1 -1
- package/dist/providers/custom.d.ts +3 -3
- package/dist/providers/custom.d.ts.map +1 -1
- package/dist/providers/custom.js.map +1 -1
- package/dist/providers/email.d.ts +3 -3
- package/dist/providers/email.d.ts.map +1 -1
- package/dist/providers/email.js.map +1 -1
- package/dist/providers/github.d.ts +1 -1
- package/dist/providers/github.d.ts.map +1 -1
- package/dist/providers/github.js +9 -4
- package/dist/providers/github.js.map +1 -1
- package/dist/providers/google.d.ts +1 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +9 -4
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/microsoft.d.ts +1 -1
- package/dist/providers/microsoft.d.ts.map +1 -1
- package/dist/providers/microsoft.js +12 -4
- package/dist/providers/microsoft.js.map +1 -1
- package/dist/providers/password.d.ts +117 -15
- package/dist/providers/password.d.ts.map +1 -1
- package/dist/providers/password.js +71 -26
- package/dist/providers/password.js.map +1 -1
- package/dist/providers/phone.d.ts +4 -3
- package/dist/providers/phone.d.ts.map +1 -1
- package/dist/providers/phone.js.map +1 -1
- package/dist/providers/webauthn.d.ts.map +1 -1
- package/dist/providers/webauthn.js +6 -0
- package/dist/providers/webauthn.js.map +1 -1
- package/dist/react/index.d.ts +40 -41
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +27 -52
- package/dist/react/index.js.map +1 -1
- package/dist/server/auth.d.ts +224 -126
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +44 -25
- package/dist/server/auth.js.map +1 -1
- package/dist/server/component/factor/db.js +13 -6
- package/dist/server/component/factor/db.js.map +1 -1
- package/dist/server/config.d.ts +2 -1
- package/dist/server/connection/domain.d.ts +4 -65
- package/dist/server/connection/domain.d.ts.map +1 -1
- package/dist/server/connection/domain.js +29 -5
- package/dist/server/connection/domain.js.map +1 -1
- package/dist/server/connection/group/service.js +7 -5
- package/dist/server/connection/group/service.js.map +1 -1
- package/dist/server/connection/http.js +136 -291
- package/dist/server/connection/http.js.map +1 -1
- package/dist/server/connection/oidc.js +8 -17
- package/dist/server/connection/oidc.js.map +1 -1
- package/dist/server/connection/policy/domain.js +4 -1
- package/dist/server/connection/policy/domain.js.map +1 -1
- package/dist/server/connection/policy.js +14 -13
- package/dist/server/connection/policy.js.map +1 -1
- package/dist/server/connection/provision.js +5 -19
- package/dist/server/connection/provision.js.map +1 -1
- package/dist/server/connection/saml/encoding.js +2 -6
- package/dist/server/connection/saml/encoding.js.map +1 -1
- package/dist/server/connection/saml/signature.js +2 -2
- package/dist/server/connection/saml/signature.js.map +1 -1
- package/dist/server/connection/saml/xmldsig.js +1 -1
- package/dist/server/connection/saml/xmldsig.js.map +1 -1
- package/dist/server/connection/webhook.js +4 -36
- package/dist/server/connection/webhook.js.map +1 -1
- package/dist/server/context.d.ts +2 -2
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/context.js +35 -24
- package/dist/server/context.js.map +1 -1
- package/dist/server/contract.d.ts.map +1 -1
- package/dist/server/contract.js +6 -14
- package/dist/server/contract.js.map +1 -1
- package/dist/server/core.js +46 -1
- package/dist/server/core.js.map +1 -1
- package/dist/server/cors.d.ts +2 -2
- package/dist/server/cors.d.ts.map +1 -1
- package/dist/server/crypto.js.map +1 -1
- package/dist/server/db.js +6 -2
- package/dist/server/db.js.map +1 -1
- package/dist/server/device.js +1 -1
- package/dist/server/domains/account.d.ts +9 -1
- package/dist/server/domains/account.d.ts.map +1 -1
- package/dist/server/domains/account.js +21 -9
- package/dist/server/domains/account.js.map +1 -1
- package/dist/server/domains/context.js +34 -0
- package/dist/server/domains/context.js.map +1 -0
- package/dist/server/domains/factor.d.ts +3 -2
- package/dist/server/domains/factor.d.ts.map +1 -1
- package/dist/server/domains/factor.js.map +1 -1
- package/dist/server/domains/group.js.map +1 -1
- package/dist/server/domains/invite.js.map +1 -1
- package/dist/server/domains/key.js.map +1 -1
- package/dist/server/domains/member.js.map +1 -1
- package/dist/server/domains/session.js.map +1 -1
- package/dist/server/domains/user.js.map +1 -1
- package/dist/server/env.js +2 -22
- package/dist/server/env.js.map +1 -1
- package/dist/server/events.d.ts +3 -1
- package/dist/server/events.d.ts.map +1 -1
- package/dist/server/events.js +6 -6
- package/dist/server/events.js.map +1 -1
- package/dist/server/facade.d.ts +1 -33
- package/dist/server/facade.d.ts.map +1 -1
- package/dist/server/facade.js +1 -2
- package/dist/server/facade.js.map +1 -1
- package/dist/server/http.d.ts +1 -0
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +4 -4
- package/dist/server/http.js.map +1 -1
- package/dist/server/identity/claims.js +1 -1
- package/dist/server/identity/convex.d.ts +2 -0
- package/dist/server/identity/convex.d.ts.map +1 -1
- package/dist/server/index.d.ts +6 -9
- package/dist/server/index.js +2 -4
- package/dist/server/limits.js +23 -2
- package/dist/server/limits.js.map +1 -1
- package/dist/server/mutations/calls.js +3 -2
- package/dist/server/mutations/credentials/signin.js +7 -3
- package/dist/server/mutations/credentials/signin.js.map +1 -1
- package/dist/server/mutations/enrollment.js +87 -0
- package/dist/server/mutations/enrollment.js.map +1 -0
- package/dist/server/mutations/invalidate.js +5 -10
- package/dist/server/mutations/invalidate.js.map +1 -1
- package/dist/server/mutations/oauth.js +3 -3
- package/dist/server/mutations/oauth.js.map +1 -1
- package/dist/server/mutations/refresh.js +1 -0
- package/dist/server/mutations/refresh.js.map +1 -1
- package/dist/server/mutations/register.js +52 -21
- package/dist/server/mutations/register.js.map +1 -1
- package/dist/server/mutations/retrieve.js +4 -1
- package/dist/server/mutations/retrieve.js.map +1 -1
- package/dist/server/mutations/signin.js +2 -2
- package/dist/server/mutations/signin.js.map +1 -1
- package/dist/server/mutations/store.js +6 -1
- package/dist/server/mutations/store.js.map +1 -1
- package/dist/server/mutations/verify.js +6 -4
- package/dist/server/mutations/verify.js.map +1 -1
- package/dist/server/oauth/client.d.ts +1 -1
- package/dist/server/oauth/client.js.map +1 -1
- package/dist/server/oauth/code.d.ts +1 -1
- package/dist/server/oauth/code.js.map +1 -1
- package/dist/server/oauth/factory.js +89 -13
- package/dist/server/oauth/factory.js.map +1 -1
- package/dist/server/oauth/handlers.js +2 -2
- package/dist/server/oauth/runtime.js +24 -9
- package/dist/server/oauth/runtime.js.map +1 -1
- package/dist/server/preload.d.ts.map +1 -1
- package/dist/server/preload.js +14 -11
- package/dist/server/preload.js.map +1 -1
- package/dist/server/runtime.d.ts +236 -148
- package/dist/server/runtime.d.ts.map +1 -1
- package/dist/server/runtime.js +92 -44
- package/dist/server/runtime.js.map +1 -1
- package/dist/server/session/lifecycle.d.ts +2 -1
- package/dist/server/session/lifecycle.d.ts.map +1 -1
- package/dist/server/session/lifecycle.js +29 -10
- package/dist/server/session/lifecycle.js.map +1 -1
- package/dist/server/signin/flow.js +2 -2
- package/dist/server/signin/flow.js.map +1 -1
- package/dist/server/telemetry.js +1 -1
- package/dist/server/tokens.js +6 -3
- package/dist/server/tokens.js.map +1 -1
- package/dist/server/totp.js +4 -3
- package/dist/server/totp.js.map +1 -1
- package/dist/server/types.d.ts +131 -45
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/user/account.js +0 -1
- package/dist/server/user/account.js.map +1 -1
- package/dist/server/validators.d.ts +930 -944
- package/dist/server/validators.d.ts.map +1 -1
- package/dist/server/validators.js +66 -39
- package/dist/server/validators.js.map +1 -1
- package/dist/server/webauthn.js +60 -29
- package/dist/server/webauthn.js.map +1 -1
- package/dist/shared/brand.d.ts +3 -7
- package/dist/shared/brand.d.ts.map +1 -1
- package/dist/shared/params.d.ts +78 -0
- package/dist/shared/params.d.ts.map +1 -0
- package/dist/shared/results.d.ts +13 -1
- package/dist/shared/results.d.ts.map +1 -1
- package/dist/svelte/context.svelte.d.ts +23 -12
- package/dist/svelte/context.svelte.d.ts.map +1 -1
- package/dist/svelte/context.svelte.js +20 -13
- package/dist/svelte/index.d.ts +1 -4
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +1 -4
- package/dist/test.d.ts +1204 -611
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +50 -1
- package/dist/test.js.map +1 -1
- package/package.json +5 -14
- package/dist/_generated/component.d.ts +0 -3074
- package/dist/_generated/component.d.ts.map +0 -1
- package/dist/component/functions.d.ts +0 -9
- package/dist/component/functions.d.ts.map +0 -1
- package/dist/component/functions.js +0 -3
- package/dist/core/index.d.ts +0 -533
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js +0 -86
- package/dist/core/index.js.map +0 -1
- package/dist/model.js.map +0 -1
- package/dist/server/env.d.ts +0 -28
- package/dist/server/env.d.ts.map +0 -1
- package/dist/svelte/AuthLoading.svelte +0 -13
- package/dist/svelte/AuthLoading.svelte.d.ts +0 -8
- package/dist/svelte/AuthLoading.svelte.d.ts.map +0 -1
- package/dist/svelte/SignedIn.svelte +0 -14
- package/dist/svelte/SignedIn.svelte.d.ts +0 -8
- package/dist/svelte/SignedIn.svelte.d.ts.map +0 -1
- package/dist/svelte/SignedOut.svelte +0 -13
- package/dist/svelte/SignedOut.svelte.d.ts +0 -8
- package/dist/svelte/SignedOut.svelte.d.ts.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account.js","names":[],"sources":["../../../src/server/domains/account.ts"],"sourcesContent":["import { Auth, GenericActionCtx, GenericDataModel } from \"convex/server\";\nimport { ConvexError } from \"convex/values\";\n\nimport { ErrorCode } from \"../../shared/codes\";\nimport type { ComponentCtx, ComponentReadCtx } from \"../component/context\";\nimport { configDefaults } from \"../config\";\nimport { getSessionUserId } from \"../context\";\nimport { emitAuthEvent } from \"../events\";\nimport type { AuthProfile } from \"../payloads\";\nimport type { Doc } from \"../types\";\n\ntype AccountCredentials = { id: string; secret?: string };\ntype CreateAccountArgs = {\n provider: string;\n account: AccountCredentials;\n profile: AuthProfile;\n shouldLinkViaEmail?: boolean;\n shouldLinkViaPhone?: boolean;\n};\ntype RetrieveAccountArgs = { provider: string; account: AccountCredentials };\ntype UpdateAccountCredentialsArgs = {\n provider: string;\n account: { id: string; secret: string };\n};\ntype CredentialsAccountResult = {\n account: { _id: string; userId: string; secret?: string | null };\n user: Record<string, unknown>;\n};\n\nexport type AccountDeps = {\n config: ReturnType<typeof configDefaults>;\n callCreateAccountFromCredentials: <DataModel extends GenericDataModel>(\n ctx: GenericActionCtx<DataModel>,\n args: CreateAccountArgs,\n ) => Promise<CredentialsAccountResult>;\n callRetrieveAccountWithCredentials: <DataModel extends GenericDataModel>(\n ctx: GenericActionCtx<DataModel>,\n args: RetrieveAccountArgs,\n ) => Promise<\n CredentialsAccountResult | \"InvalidAccountId\" | \"InvalidSecret\" | \"TooManyFailedAttempts\"\n >;\n callModifyAccount: <DataModel extends GenericDataModel>(\n ctx: GenericActionCtx<DataModel>,\n args: UpdateAccountCredentialsArgs,\n ) => Promise<void>;\n};\n\nexport function createAccountDomain(deps: AccountDeps) {\n const {\n callCreateAccountFromCredentials,\n callRetrieveAccountWithCredentials,\n callModifyAccount,\n } = deps;\n\n return {\n /**\n * Create a new auth account linked to a user.\n *\n * Creates a credentials-based account record for a given provider. If\n * the user does not yet exist, one is created from the supplied\n * `profile`. If `shouldLinkViaEmail` or `shouldLinkViaPhone` is set,\n * the account may be linked to an existing user whose email or phone\n * matches the profile.\n *\n * The `account.secret` (e.g. a hashed password) is optional and\n * depends on the provider type.\n *\n * @param ctx - Convex action context.\n * @param args.provider - The provider ID (e.g. `\"password\"`, `\"credentials\"`).\n * @param args.account.id - Provider-specific account identifier (e.g. email address).\n * @param args.account.secret - Optional credential secret (e.g. hashed password).\n * @param args.profile - Profile data used to create or update the user document.\n * @param args.shouldLinkViaEmail - If `true`, link to an existing user by email match.\n * @param args.shouldLinkViaPhone - If `true`, link to an existing user by phone match.\n * @returns The created account and user information.\n *\n * @example\n * ```ts\n * const result = await auth.account.create(ctx, {\n * provider: \"password\",\n * account: { id: \"alice@example.com\", secret: hashedPassword },\n * profile: { email: \"alice@example.com\", name: \"Alice\" },\n * });\n * ```\n */\n create: async <DataModel extends GenericDataModel>(\n ctx: GenericActionCtx<DataModel>,\n args: CreateAccountArgs,\n ) => {\n const created = await callCreateAccountFromCredentials(ctx, args);\n return { ...created };\n },\n /**\n * Retrieve an auth account by provider and credentials.\n *\n * Looks up an account matching the given provider and account ID,\n * optionally verifying the secret (e.g. password). If the account\n * exists and the credentials are valid, the full account document is\n * returned. Returns `null` if no matching account is found or if the\n * credential verification fails (indicated by a string error from the\n * underlying RPC).\n *\n * @param ctx - Convex action context.\n * @param args.provider - The provider ID (e.g. `\"password\"`).\n * @param args.account.id - Provider-specific account identifier.\n * @param args.account.secret - Optional credential secret to verify.\n * @returns The account document, or `null` if not found or verification failed.\n *\n * @example\n * ```ts\n * const acct = await auth.account.get(ctx, {\n * provider: \"password\",\n * account: { id: \"alice@example.com\", secret: plainTextPassword },\n * });\n * if (!acct) throw new Error(\"Invalid credentials\");\n * ```\n */\n get: async <DataModel extends GenericDataModel>(\n ctx: GenericActionCtx<DataModel>,\n args: RetrieveAccountArgs,\n ) => {\n const result = await callRetrieveAccountWithCredentials(ctx, args);\n if (typeof result === \"string\") {\n return null;\n }\n return result;\n },\n /**\n * Update the credentials (secret) for an existing auth account.\n *\n * Replaces the stored secret for the account identified by `provider`\n * and `account.id`. This is the standard path for password changes\n * and password resets — the new secret is typically a freshly hashed\n * password.\n *\n * @param ctx - Convex action context.\n * @param args.provider - The provider ID (e.g. `\"password\"`).\n * @param args.account.id - Provider-specific account identifier.\n * @param args.account.secret - The new credential secret to store.\n * @returns `{ accountId }` confirming the update.\n *\n * @example Password reset\n * ```ts\n * await auth.account.update(ctx, {\n * provider: \"password\",\n * account: { id: \"alice@example.com\", secret: newHashedPassword },\n * });\n * ```\n */\n update: async <DataModel extends GenericDataModel>(\n ctx: GenericActionCtx<DataModel>,\n args: UpdateAccountCredentialsArgs,\n ) => {\n await callModifyAccount(ctx, args);\n return { accountId: args.account.id };\n },\n };\n}\n\nexport type AccountSummary = {\n id: string;\n provider: string;\n createdAt: number;\n emailVerified: boolean;\n phoneVerified: boolean;\n};\n\nconst FACTOR_ACCOUNT_PROVIDERS = new Set([\"passkey\"]);\n\nfunction isFactorAccount(account: Pick<Doc<\"Account\">, \"provider\">): boolean {\n return FACTOR_ACCOUNT_PROVIDERS.has(account.provider);\n}\n\n/** Build the safe, current-user account-management surface. */\nexport function createAccountManagementDomain({ config }: Pick<AccountDeps, \"config\">) {\n const currentUserId = async (ctx: ComponentReadCtx & { auth: Auth }) => {\n const userId = await getSessionUserId(ctx);\n if (userId === null) {\n throw new ConvexError({\n code: ErrorCode.NOT_SIGNED_IN,\n message: \"Authentication required.\",\n });\n }\n return userId;\n };\n\n return {\n /** List the current user's linked accounts without credential secrets. */\n list: async (ctx: ComponentReadCtx & { auth: Auth }): Promise<AccountSummary[]> => {\n const userId = await currentUserId(ctx);\n const accounts = (await ctx.runQuery(config.component.account.list, {\n userId,\n })) as Doc<\"Account\">[];\n return accounts\n .filter((account) => !isFactorAccount(account))\n .map((account) => ({\n id: account._id,\n provider: account.provider,\n createdAt: account._creationTime,\n emailVerified: account.emailVerified !== undefined,\n phoneVerified: account.phoneVerified !== undefined,\n }));\n },\n\n /** Remove one of the current user's linked accounts, preserving a sign-in path. */\n remove: async (\n ctx: ComponentCtx & { auth: Auth },\n args: { id: string },\n ): Promise<{ id: string }> => {\n const userId = await currentUserId(ctx);\n const account = (await ctx.runQuery(config.component.account.get, {\n id: args.id,\n })) as Doc<\"Account\"> | null;\n if (account === null || account.userId !== userId) {\n throw new ConvexError({\n code: ErrorCode.ACCOUNT_NOT_FOUND,\n message: \"Account not found.\",\n });\n }\n if (isFactorAccount(account)) {\n throw new ConvexError({\n code: ErrorCode.INVALID_PARAMETERS,\n message: \"Manage WebAuthn credentials with auth.factor.remove().\",\n });\n }\n await ctx.runMutation(config.component.account.remove, {\n id: args.id,\n userId,\n requireOtherAccount: true,\n });\n await emitAuthEvent(ctx, config, {\n kind: \"account.unlinked\",\n actor: { type: \"user\", id: userId },\n subject: { type: \"account\", id: args.id },\n targets: [{ kind: \"user\", id: userId }],\n outcome: \"success\",\n data: { accountId: args.id, provider: account.provider },\n });\n return { id: args.id };\n },\n };\n}\n"],"mappings":";;;;;;AA+CA,SAAgB,oBAAoB,MAAmB;CACrD,MAAM,EACJ,kCACA,oCACA,sBACE;AAEJ,QAAO;EA+BL,QAAQ,OACN,KACA,SACG;AAEH,UAAO,EAAE,GADO,MAAM,iCAAiC,KAAK,KAAK,EAC5C;;EA2BvB,KAAK,OACH,KACA,SACG;GACH,MAAM,SAAS,MAAM,mCAAmC,KAAK,KAAK;AAClE,OAAI,OAAO,WAAW,SACpB,QAAO;AAET,UAAO;;EAwBT,QAAQ,OACN,KACA,SACG;AACH,SAAM,kBAAkB,KAAK,KAAK;AAClC,UAAO,EAAE,WAAW,KAAK,QAAQ,IAAI;;EAExC;;AAWH,MAAM,2BAA2B,IAAI,IAAI,CAAC,UAAU,CAAC;AAErD,SAAS,gBAAgB,SAAoD;AAC3E,QAAO,yBAAyB,IAAI,QAAQ,SAAS;;;AAIvD,SAAgB,8BAA8B,EAAE,UAAuC;CACrF,MAAM,gBAAgB,OAAO,QAA2C;EACtE,MAAM,SAAS,MAAM,iBAAiB,IAAI;AAC1C,MAAI,WAAW,KACb,OAAM,IAAI,YAAY;GACpB,MAAM,UAAU;GAChB,SAAS;GACV,CAAC;AAEJ,SAAO;;AAGT,QAAO;EAEL,MAAM,OAAO,QAAsE;GACjF,MAAM,SAAS,MAAM,cAAc,IAAI;AAIvC,WAHkB,MAAM,IAAI,SAAS,OAAO,UAAU,QAAQ,MAAM,EAClE,QACD,CAAC,EAEC,QAAQ,YAAY,CAAC,gBAAgB,QAAQ,CAAC,CAC9C,KAAK,aAAa;IACjB,IAAI,QAAQ;IACZ,UAAU,QAAQ;IAClB,WAAW,QAAQ;IACnB,eAAe,QAAQ,kBAAkB;IACzC,eAAe,QAAQ,kBAAkB;IAC1C,EAAE;;EAIP,QAAQ,OACN,KACA,SAC4B;GAC5B,MAAM,SAAS,MAAM,cAAc,IAAI;GACvC,MAAM,UAAW,MAAM,IAAI,SAAS,OAAO,UAAU,QAAQ,KAAK,EAChE,IAAI,KAAK,IACV,CAAC;AACF,OAAI,YAAY,QAAQ,QAAQ,WAAW,OACzC,OAAM,IAAI,YAAY;IACpB,MAAM,UAAU;IAChB,SAAS;IACV,CAAC;AAEJ,OAAI,gBAAgB,QAAQ,CAC1B,OAAM,IAAI,YAAY;IACpB,MAAM,UAAU;IAChB,SAAS;IACV,CAAC;AAEJ,SAAM,IAAI,YAAY,OAAO,UAAU,QAAQ,QAAQ;IACrD,IAAI,KAAK;IACT;IACA,qBAAqB;IACtB,CAAC;AACF,SAAM,cAAc,KAAK,QAAQ;IAC/B,MAAM;IACN,OAAO;KAAE,MAAM;KAAQ,IAAI;KAAQ;IACnC,SAAS;KAAE,MAAM;KAAW,IAAI,KAAK;KAAI;IACzC,SAAS,CAAC;KAAE,MAAM;KAAQ,IAAI;KAAQ,CAAC;IACvC,SAAS;IACT,MAAM;KAAE,WAAW,KAAK;KAAI,UAAU,QAAQ;KAAU;IACzD,CAAC;AACF,UAAO,EAAE,IAAI,KAAK,IAAI;;EAEzB"}
|
|
1
|
+
{"version":3,"file":"account.js","names":[],"sources":["../../../src/server/domains/account.ts"],"sourcesContent":["import { Auth, GenericActionCtx, GenericDataModel } from \"convex/server\";\nimport { ConvexError, type GenericId } from \"convex/values\";\n\nimport { ErrorCode } from \"../../shared/codes\";\nimport type { ComponentCtx, ComponentReadCtx } from \"../component/context\";\nimport { configDefaults } from \"../config\";\nimport { getSessionUserId } from \"../context\";\nimport { emitAuthEvent } from \"../events\";\nimport type { AuthProfile } from \"../payloads\";\nimport type { Doc } from \"../types\";\n\ntype AccountCredentials = { id: string; secret?: string };\ntype CreateAccountArgs = {\n provider: string;\n account: AccountCredentials;\n profile: AuthProfile;\n shouldLinkViaEmail?: boolean;\n shouldLinkViaPhone?: boolean;\n};\ntype RetrieveAccountArgs = { provider: string; account: AccountCredentials };\ntype UpdateAccountCredentialsArgs = {\n provider: string;\n account: { id: string; secret: string };\n};\ntype CredentialsAccountResult = {\n account: { _id: string; userId: string; secret?: string | null };\n user: Record<string, unknown>;\n};\n\nexport type AccountDeps = {\n config: ReturnType<typeof configDefaults>;\n callCreateAccountFromCredentials: <DataModel extends GenericDataModel>(\n ctx: GenericActionCtx<DataModel>,\n args: CreateAccountArgs,\n ) => Promise<CredentialsAccountResult>;\n callRetrieveAccountWithCredentials: <DataModel extends GenericDataModel>(\n ctx: GenericActionCtx<DataModel>,\n args: RetrieveAccountArgs,\n ) => Promise<\n CredentialsAccountResult | \"InvalidAccountId\" | \"InvalidSecret\" | \"TooManyFailedAttempts\"\n >;\n callModifyAccount: <DataModel extends GenericDataModel>(\n ctx: GenericActionCtx<DataModel>,\n args: UpdateAccountCredentialsArgs,\n ) => Promise<void>;\n};\n\nexport function createAccountDomain(deps: AccountDeps) {\n const {\n callCreateAccountFromCredentials,\n callRetrieveAccountWithCredentials,\n callModifyAccount,\n } = deps;\n\n return {\n /**\n * Create a new auth account linked to a user.\n *\n * Creates a credentials-based account record for a given provider. If\n * the user does not yet exist, one is created from the supplied\n * `profile`. If `shouldLinkViaEmail` or `shouldLinkViaPhone` is set,\n * the account may be linked to an existing user whose email or phone\n * matches the profile.\n *\n * The `account.secret` (e.g. a hashed password) is optional and\n * depends on the provider type.\n *\n * @param ctx - Convex action context.\n * @param args.provider - The provider ID (e.g. `\"password\"`, `\"credentials\"`).\n * @param args.account.id - Provider-specific account identifier (e.g. email address).\n * @param args.account.secret - Optional credential secret (e.g. hashed password).\n * @param args.profile - Profile data used to create or update the user document.\n * @param args.shouldLinkViaEmail - If `true`, link to an existing user by email match.\n * @param args.shouldLinkViaPhone - If `true`, link to an existing user by phone match.\n * @returns The created account and user information.\n *\n * @example\n * ```ts\n * const result = await auth.account.create(ctx, {\n * provider: \"password\",\n * account: { id: \"alice@example.com\", secret: hashedPassword },\n * profile: { email: \"alice@example.com\", name: \"Alice\" },\n * });\n * ```\n */\n create: async <DataModel extends GenericDataModel>(\n ctx: GenericActionCtx<DataModel>,\n args: CreateAccountArgs,\n ) => {\n const created = await callCreateAccountFromCredentials(ctx, args);\n return { ...created };\n },\n /**\n * Retrieve an auth account by provider and credentials.\n *\n * Looks up an account matching the given provider and account ID,\n * optionally verifying the secret (e.g. password). If the account\n * exists and the credentials are valid, the full account document is\n * returned. Returns `null` if no matching account is found or if the\n * credential verification fails (indicated by a string error from the\n * underlying RPC).\n *\n * @param ctx - Convex action context.\n * @param args.provider - The provider ID (e.g. `\"password\"`).\n * @param args.account.id - Provider-specific account identifier.\n * @param args.account.secret - Optional credential secret to verify.\n * @returns The account document, or `null` if not found or verification failed.\n *\n * @example\n * ```ts\n * const acct = await auth.account.get(ctx, {\n * provider: \"password\",\n * account: { id: \"alice@example.com\", secret: plainTextPassword },\n * });\n * if (!acct) throw new Error(\"Invalid credentials\");\n * ```\n */\n get: async <DataModel extends GenericDataModel>(\n ctx: GenericActionCtx<DataModel>,\n args: RetrieveAccountArgs,\n ) => {\n const result = await callRetrieveAccountWithCredentials(ctx, args);\n if (typeof result === \"string\") {\n return null;\n }\n return result;\n },\n /**\n * Update the credentials (secret) for an existing auth account.\n *\n * Replaces the stored secret for the account identified by `provider`\n * and `account.id`. This is the standard path for password changes\n * and password resets — the new secret is typically a freshly hashed\n * password.\n *\n * @param ctx - Convex action context.\n * @param args.provider - The provider ID (e.g. `\"password\"`).\n * @param args.account.id - Provider-specific account identifier.\n * @param args.account.secret - The new credential secret to store.\n * @returns `{ accountId }` confirming the update.\n *\n * @example Password reset\n * ```ts\n * await auth.account.update(ctx, {\n * provider: \"password\",\n * account: { id: \"alice@example.com\", secret: newHashedPassword },\n * });\n * ```\n */\n update: async <DataModel extends GenericDataModel>(\n ctx: GenericActionCtx<DataModel>,\n args: UpdateAccountCredentialsArgs,\n ) => {\n await callModifyAccount(ctx, args);\n return { accountId: args.account.id };\n },\n };\n}\n\n/**\n * A safe, non-secret capability summary for one linked provider account.\n *\n * This deliberately excludes `providerAccountId`, `secret`, and `extend` so\n * app-facing account reads cannot leak credential or provider-private data.\n */\nexport type AccountSummary = {\n id: GenericId<\"Account\">;\n userId: GenericId<\"User\">;\n provider: string;\n createdAt: number;\n emailVerified: boolean;\n phoneVerified: boolean;\n};\n\nconst FACTOR_ACCOUNT_PROVIDERS = new Set([\"passkey\"]);\n\nfunction isFactorAccount(account: Pick<Doc<\"Account\">, \"provider\">): boolean {\n return FACTOR_ACCOUNT_PROVIDERS.has(account.provider);\n}\n\nfunction toAccountSummary(account: Doc<\"Account\">): AccountSummary {\n return {\n id: account._id,\n userId: account.userId,\n provider: account.provider,\n createdAt: account._creationTime,\n emailVerified: account.emailVerified !== undefined,\n phoneVerified: account.phoneVerified !== undefined,\n };\n}\n\n/** Build the safe, current-user account-management surface. */\nexport function createAccountManagementDomain({ config }: Pick<AccountDeps, \"config\">) {\n const currentUserId = async (ctx: ComponentReadCtx & { auth: Auth }) => {\n const userId = await getSessionUserId(ctx);\n if (userId === null) {\n throw new ConvexError({\n code: ErrorCode.NOT_SIGNED_IN,\n message: \"Authentication required.\",\n });\n }\n return userId;\n };\n\n return {\n /**\n * List safe linked-account capabilities for the current user or a bounded\n * batch of users. Batch reads are for app-authorized user lists and avoid\n * one component call per user; they never expose credential material.\n *\n * @param ctx - Convex query or mutation context. Required when reading the\n * current user's accounts; authorization for an explicit user batch is\n * owned by the calling app function.\n * @param opts - Optional explicit bounded user batch.\n * @param opts.userIds - User IDs to read in one component call. Duplicate\n * IDs are ignored and the component caps the batch at 100 users.\n * @param opts.provider - Optional provider ID (for example, `\"password\"`)\n * to read only that capability for each user.\n * @returns Safe account capability summaries, with no provider account ID,\n * credential secret, or extension data.\n *\n * @example Batch accounts for an authorized member list\n * ```ts\n * const accounts = await auth.account.list(ctx, {\n * userIds: members.map((member) => member.userId),\n * provider: \"password\",\n * });\n * ```\n */\n list: async (\n ctx: ComponentReadCtx & { auth: Auth },\n opts?: { userIds?: readonly GenericId<\"User\">[]; provider?: string },\n ): Promise<AccountSummary[]> => {\n const userIds = opts?.userIds;\n const provider = opts?.provider;\n const args =\n userIds === undefined\n ? { userId: await currentUserId(ctx), provider }\n : { userIds: Array.from(new Set(userIds)), provider };\n const accounts = (await ctx.runQuery(\n config.component.account.list,\n args,\n )) as Doc<\"Account\">[];\n return accounts.filter((account) => !isFactorAccount(account)).map(toAccountSummary);\n },\n\n /** Remove one of the current user's linked accounts, preserving a sign-in path. */\n remove: async (\n ctx: ComponentCtx & { auth: Auth },\n args: { id: GenericId<\"Account\"> },\n ): Promise<{ id: GenericId<\"Account\"> }> => {\n const userId = await currentUserId(ctx);\n const account = (await ctx.runQuery(config.component.account.get, {\n id: args.id,\n })) as Doc<\"Account\"> | null;\n if (account === null || account.userId !== userId) {\n throw new ConvexError({\n code: ErrorCode.ACCOUNT_NOT_FOUND,\n message: \"Account not found.\",\n });\n }\n if (isFactorAccount(account)) {\n throw new ConvexError({\n code: ErrorCode.INVALID_PARAMETERS,\n message: \"Manage WebAuthn credentials with auth.factor.remove().\",\n });\n }\n await ctx.runMutation(config.component.account.remove, {\n id: args.id,\n userId,\n requireOtherAccount: true,\n });\n await emitAuthEvent(ctx, config, {\n kind: \"account.unlinked\",\n actor: { type: \"user\", id: userId },\n subject: { type: \"account\", id: args.id },\n targets: [{ kind: \"user\", id: userId }],\n outcome: \"success\",\n data: { accountId: args.id, provider: account.provider },\n });\n return { id: args.id };\n },\n };\n}\n"],"mappings":";;;;;;AA+CA,SAAgB,oBAAoB,MAAmB;CACrD,MAAM,EACJ,kCACA,oCACA,sBACE;AAEJ,QAAO;EA+BL,QAAQ,OACN,KACA,SACG;AAEH,UAAO,EAAE,GADO,MAAM,iCAAiC,KAAK,KAAK,EAC5C;;EA2BvB,KAAK,OACH,KACA,SACG;GACH,MAAM,SAAS,MAAM,mCAAmC,KAAK,KAAK;AAClE,OAAI,OAAO,WAAW,SACpB,QAAO;AAET,UAAO;;EAwBT,QAAQ,OACN,KACA,SACG;AACH,SAAM,kBAAkB,KAAK,KAAK;AAClC,UAAO,EAAE,WAAW,KAAK,QAAQ,IAAI;;EAExC;;AAkBH,MAAM,2BAA2B,IAAI,IAAI,CAAC,UAAU,CAAC;AAErD,SAAS,gBAAgB,SAAoD;AAC3E,QAAO,yBAAyB,IAAI,QAAQ,SAAS;;AAGvD,SAAS,iBAAiB,SAAyC;AACjE,QAAO;EACL,IAAI,QAAQ;EACZ,QAAQ,QAAQ;EAChB,UAAU,QAAQ;EAClB,WAAW,QAAQ;EACnB,eAAe,QAAQ,kBAAkB;EACzC,eAAe,QAAQ,kBAAkB;EAC1C;;;AAIH,SAAgB,8BAA8B,EAAE,UAAuC;CACrF,MAAM,gBAAgB,OAAO,QAA2C;EACtE,MAAM,SAAS,MAAM,iBAAiB,IAAI;AAC1C,MAAI,WAAW,KACb,OAAM,IAAI,YAAY;GACpB,MAAM,UAAU;GAChB,SAAS;GACV,CAAC;AAEJ,SAAO;;AAGT,QAAO;EAyBL,MAAM,OACJ,KACA,SAC8B;GAC9B,MAAM,UAAU,MAAM;GACtB,MAAM,WAAW,MAAM;GACvB,MAAM,OACJ,YAAY,SACR;IAAE,QAAQ,MAAM,cAAc,IAAI;IAAE;IAAU,GAC9C;IAAE,SAAS,MAAM,KAAK,IAAI,IAAI,QAAQ,CAAC;IAAE;IAAU;AAKzD,WAJkB,MAAM,IAAI,SAC1B,OAAO,UAAU,QAAQ,MACzB,KACD,EACe,QAAQ,YAAY,CAAC,gBAAgB,QAAQ,CAAC,CAAC,IAAI,iBAAiB;;EAItF,QAAQ,OACN,KACA,SAC0C;GAC1C,MAAM,SAAS,MAAM,cAAc,IAAI;GACvC,MAAM,UAAW,MAAM,IAAI,SAAS,OAAO,UAAU,QAAQ,KAAK,EAChE,IAAI,KAAK,IACV,CAAC;AACF,OAAI,YAAY,QAAQ,QAAQ,WAAW,OACzC,OAAM,IAAI,YAAY;IACpB,MAAM,UAAU;IAChB,SAAS;IACV,CAAC;AAEJ,OAAI,gBAAgB,QAAQ,CAC1B,OAAM,IAAI,YAAY;IACpB,MAAM,UAAU;IAChB,SAAS;IACV,CAAC;AAEJ,SAAM,IAAI,YAAY,OAAO,UAAU,QAAQ,QAAQ;IACrD,IAAI,KAAK;IACT;IACA,qBAAqB;IACtB,CAAC;AACF,SAAM,cAAc,KAAK,QAAQ;IAC/B,MAAM;IACN,OAAO;KAAE,MAAM;KAAQ,IAAI;KAAQ;IACnC,SAAS;KAAE,MAAM;KAAW,IAAI,KAAK;KAAI;IACzC,SAAS,CAAC;KAAE,MAAM;KAAQ,IAAI;KAAQ,CAAC;IACvC,SAAS;IACT,MAAM;KAAE,WAAW,KAAK;KAAI,UAAU,QAAQ;KAAU;IACzD,CAAC;AACF,UAAO,EAAE,IAAI,KAAK,IAAI;;EAEzB"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/server/domains/context.ts
|
|
2
|
+
/**
|
|
3
|
+
* Build the internal auth-context reader.
|
|
4
|
+
*
|
|
5
|
+
* One component query returns the current user, optional session, active
|
|
6
|
+
* membership, and group. It intentionally does not memoize authorization
|
|
7
|
+
* state: a fresh component snapshot is the revocation boundary.
|
|
8
|
+
*
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
function createContextDomain(deps) {
|
|
12
|
+
const { config, resolveGrantedPermissions } = deps;
|
|
13
|
+
return { get: async (ctx, args) => {
|
|
14
|
+
const snapshot = await ctx.runQuery(config.component.context.get, args);
|
|
15
|
+
if (snapshot.active === null) return {
|
|
16
|
+
user: snapshot.user,
|
|
17
|
+
session: snapshot.session,
|
|
18
|
+
active: null
|
|
19
|
+
};
|
|
20
|
+
const roleIds = snapshot.active.membership.roleIds ?? [];
|
|
21
|
+
return {
|
|
22
|
+
...snapshot,
|
|
23
|
+
active: {
|
|
24
|
+
...snapshot.active,
|
|
25
|
+
roleIds,
|
|
26
|
+
grants: resolveGrantedPermissions(roleIds)
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
} };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
export { createContextDomain };
|
|
34
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","names":[],"sources":["../../../src/server/domains/context.ts"],"sourcesContent":["import type { ComponentReadCtx } from \"../component/context\";\nimport { configDefaults } from \"../config\";\nimport type { Doc } from \"../types\";\n\ntype ActiveSnapshot = {\n groupId: string;\n group: Doc<\"Group\"> | null;\n membership: Doc<\"GroupMember\">;\n roleIds: string[];\n grants: string[];\n};\n\ntype AuthContextSnapshot = {\n user: Doc<\"User\"> | null;\n session: Doc<\"Session\"> | null;\n active: ActiveSnapshot | null;\n};\n\nexport type ContextDeps = {\n config: ReturnType<typeof configDefaults>;\n resolveGrantedPermissions: (roleIds?: string[]) => string[];\n};\n\n/**\n * Build the internal auth-context reader.\n *\n * One component query returns the current user, optional session, active\n * membership, and group. It intentionally does not memoize authorization\n * state: a fresh component snapshot is the revocation boundary.\n *\n * @internal\n */\nexport function createContextDomain(deps: ContextDeps) {\n const { config, resolveGrantedPermissions } = deps;\n\n return {\n get: async (\n ctx: ComponentReadCtx,\n args: { userId: string; sessionId?: string },\n ): Promise<AuthContextSnapshot> => {\n const snapshot = (await ctx.runQuery(config.component.context.get, args)) as {\n user: Doc<\"User\"> | null;\n session: Doc<\"Session\"> | null;\n active: {\n groupId: string;\n group: Doc<\"Group\"> | null;\n membership: Doc<\"GroupMember\">;\n } | null;\n };\n\n if (snapshot.active === null) {\n return {\n user: snapshot.user,\n session: snapshot.session,\n active: null,\n };\n }\n const roleIds = snapshot.active.membership.roleIds ?? [];\n return {\n ...snapshot,\n active: {\n ...snapshot.active,\n roleIds,\n grants: resolveGrantedPermissions(roleIds),\n },\n };\n },\n };\n}\n"],"mappings":";;;;;;;;;;AAgCA,SAAgB,oBAAoB,MAAmB;CACrD,MAAM,EAAE,QAAQ,8BAA8B;AAE9C,QAAO,EACL,KAAK,OACH,KACA,SACiC;EACjC,MAAM,WAAY,MAAM,IAAI,SAAS,OAAO,UAAU,QAAQ,KAAK,KAAK;AAUxE,MAAI,SAAS,WAAW,KACtB,QAAO;GACL,MAAM,SAAS;GACf,SAAS,SAAS;GAClB,QAAQ;GACT;EAEH,MAAM,UAAU,SAAS,OAAO,WAAW,WAAW,EAAE;AACxD,SAAO;GACL,GAAG;GACH,QAAQ;IACN,GAAG,SAAS;IACZ;IACA,QAAQ,0BAA0B,QAAQ;IAC3C;GACF;IAEJ"}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import "../component/context.js";
|
|
2
2
|
import { Doc } from "../types.js";
|
|
3
3
|
import "../config.js";
|
|
4
|
+
import { GenericId } from "convex/values";
|
|
4
5
|
import "convex/server";
|
|
5
6
|
|
|
6
7
|
//#region src/server/domains/factor.d.ts
|
|
7
8
|
type FactorSummary = {
|
|
8
9
|
kind: "webauthn";
|
|
9
|
-
id:
|
|
10
|
+
id: GenericId<"Passkey">;
|
|
10
11
|
name: string | null;
|
|
11
12
|
createdAt: number;
|
|
12
13
|
lastUsedAt: number | null;
|
|
@@ -16,7 +17,7 @@ type FactorSummary = {
|
|
|
16
17
|
attestation: Doc<"Passkey">["attestation"] | null;
|
|
17
18
|
} | {
|
|
18
19
|
kind: "totp";
|
|
19
|
-
id:
|
|
20
|
+
id: GenericId<"TotpFactor">;
|
|
20
21
|
name: string | null;
|
|
21
22
|
createdAt: number;
|
|
22
23
|
lastUsedAt: number | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factor.d.ts","names":[],"sources":["../../../src/server/domains/factor.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"factor.d.ts","names":[],"sources":["../../../src/server/domains/factor.ts"],"mappings":";;;;;;;KAUY,aAAA;EAEN,IAAA;EACA,EAAA,EAAI,SAAA;EACJ,IAAA;EACA,SAAA;EACA,UAAA;EACA,UAAA;EACA,QAAA;EACA,UAAA;EACA,WAAA,EAAa,GAAA;AAAA;EAGb,IAAA;EACA,EAAA,EAAI,SAAA;EACJ,IAAA;EACA,SAAA;EACA,UAAA;EACA,QAAA;AAAA;AAAA,KAGM,UAAA,GAAa,aAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factor.js","names":[],"sources":["../../../src/server/domains/factor.ts"],"sourcesContent":["import type { Auth } from \"convex/server\";\nimport { ConvexError } from \"convex/values\";\n\nimport { ErrorCode } from \"../../shared/codes\";\nimport type { ComponentCtx, ComponentReadCtx } from \"../component/context\";\nimport { configDefaults } from \"../config\";\nimport { getSessionUserId } from \"../context\";\nimport { emitAuthEvent } from \"../events\";\nimport type { Doc } from \"../types\";\n\nexport type FactorSummary =\n | {\n kind: \"webauthn\";\n id:
|
|
1
|
+
{"version":3,"file":"factor.js","names":[],"sources":["../../../src/server/domains/factor.ts"],"sourcesContent":["import type { Auth } from \"convex/server\";\nimport { ConvexError, type GenericId } from \"convex/values\";\n\nimport { ErrorCode } from \"../../shared/codes\";\nimport type { ComponentCtx, ComponentReadCtx } from \"../component/context\";\nimport { configDefaults } from \"../config\";\nimport { getSessionUserId } from \"../context\";\nimport { emitAuthEvent } from \"../events\";\nimport type { Doc } from \"../types\";\n\nexport type FactorSummary =\n | {\n kind: \"webauthn\";\n id: GenericId<\"Passkey\">;\n name: string | null;\n createdAt: number;\n lastUsedAt: number | null;\n deviceType: string;\n backedUp: boolean;\n transports: string[];\n attestation: Doc<\"Passkey\">[\"attestation\"] | null;\n }\n | {\n kind: \"totp\";\n id: GenericId<\"TotpFactor\">;\n name: string | null;\n createdAt: number;\n lastUsedAt: number | null;\n verified: boolean;\n };\n\nexport type FactorKind = FactorSummary[\"kind\"];\n\ntype FactorReadCtx = ComponentReadCtx & { auth: Auth };\ntype FactorWriteCtx = ComponentCtx & { auth: Auth };\n\nexport type FactorDeps = { config: ReturnType<typeof configDefaults> };\n\nasync function currentUserId(ctx: FactorReadCtx): Promise<GenericId<\"User\">> {\n const userId = await getSessionUserId(ctx);\n if (userId === null) {\n throw new ConvexError({\n code: ErrorCode.NOT_SIGNED_IN,\n message: \"Authentication required.\",\n });\n }\n return userId as GenericId<\"User\">;\n}\n\n/** Build the safe, current-user factor-management surface. */\nexport function createFactorDomain({ config }: FactorDeps) {\n return {\n /** List bounded factor summaries without credential secrets or public keys. */\n list: async (ctx: FactorReadCtx): Promise<FactorSummary[]> => {\n const userId = await currentUserId(ctx);\n const [passkeys, totps] = (await Promise.all([\n ctx.runQuery(config.component.factor.passkey.list, { userId }),\n ctx.runQuery(config.component.factor.totp.list, { userId }),\n ])) as [Doc<\"Passkey\">[], Doc<\"TotpFactor\">[]];\n return [\n ...passkeys.map(\n (factor): FactorSummary => ({\n kind: \"webauthn\",\n id: factor._id,\n name: factor.name ?? null,\n createdAt: factor.createdAt,\n lastUsedAt: factor.lastUsedAt ?? null,\n deviceType: factor.deviceType,\n backedUp: factor.backedUp,\n transports: factor.transports ?? [],\n attestation: factor.attestation ?? null,\n }),\n ),\n ...totps.map(\n (factor): FactorSummary => ({\n kind: \"totp\",\n id: factor._id,\n name: factor.name ?? null,\n createdAt: factor.createdAt,\n lastUsedAt: factor.lastUsedAt ?? null,\n verified: factor.verified,\n }),\n ),\n ].sort((a, b) => b.createdAt - a.createdAt);\n },\n\n /** Rename a factor owned by the current user. */\n update: async (\n ctx: FactorWriteCtx,\n args: { kind: FactorKind; id: string; patch: { name: string } },\n ): Promise<{ id: string; kind: FactorKind }> => {\n const userId = await currentUserId(ctx);\n if (args.kind === \"webauthn\") {\n await ctx.runMutation(config.component.factor.passkey.update, {\n id: args.id,\n userId,\n patch: { name: args.patch.name },\n });\n } else {\n await ctx.runMutation(config.component.factor.totp.update, {\n id: args.id,\n userId,\n patch: { name: args.patch.name },\n });\n }\n return { id: args.id, kind: args.kind };\n },\n\n /** Remove a factor owned by the current user and append its audit event. */\n remove: async (\n ctx: FactorWriteCtx,\n args: { kind: FactorKind; id: string },\n ): Promise<{ id: string; kind: FactorKind }> => {\n const userId = await currentUserId(ctx);\n if (args.kind === \"webauthn\") {\n await ctx.runMutation(config.component.factor.passkey.remove, {\n id: args.id,\n userId,\n requireOtherAccount: true,\n });\n await emitAuthEvent(ctx, config, {\n kind: \"passkey.removed\",\n actor: { type: \"user\", id: userId },\n subject: { type: \"passkey\", id: args.id },\n targets: [{ kind: \"user\", id: userId }],\n outcome: \"success\",\n data: { passkeyId: args.id },\n });\n } else {\n await ctx.runMutation(config.component.factor.totp.remove, {\n id: args.id,\n userId,\n });\n await emitAuthEvent(ctx, config, {\n kind: \"totp.removed\",\n actor: { type: \"user\", id: userId },\n subject: { type: \"totp\", id: args.id },\n targets: [{ kind: \"user\", id: userId }],\n outcome: \"success\",\n data: { totpId: args.id },\n });\n }\n return { id: args.id, kind: args.kind };\n },\n };\n}\n"],"mappings":";;;;;;AAsCA,eAAe,cAAc,KAAgD;CAC3E,MAAM,SAAS,MAAM,iBAAiB,IAAI;AAC1C,KAAI,WAAW,KACb,OAAM,IAAI,YAAY;EACpB,MAAM,UAAU;EAChB,SAAS;EACV,CAAC;AAEJ,QAAO;;;AAIT,SAAgB,mBAAmB,EAAE,UAAsB;AACzD,QAAO;EAEL,MAAM,OAAO,QAAiD;GAC5D,MAAM,SAAS,MAAM,cAAc,IAAI;GACvC,MAAM,CAAC,UAAU,SAAU,MAAM,QAAQ,IAAI,CAC3C,IAAI,SAAS,OAAO,UAAU,OAAO,QAAQ,MAAM,EAAE,QAAQ,CAAC,EAC9D,IAAI,SAAS,OAAO,UAAU,OAAO,KAAK,MAAM,EAAE,QAAQ,CAAC,CAC5D,CAAC;AACF,UAAO,CACL,GAAG,SAAS,KACT,YAA2B;IAC1B,MAAM;IACN,IAAI,OAAO;IACX,MAAM,OAAO,QAAQ;IACrB,WAAW,OAAO;IAClB,YAAY,OAAO,cAAc;IACjC,YAAY,OAAO;IACnB,UAAU,OAAO;IACjB,YAAY,OAAO,cAAc,EAAE;IACnC,aAAa,OAAO,eAAe;IACpC,EACF,EACD,GAAG,MAAM,KACN,YAA2B;IAC1B,MAAM;IACN,IAAI,OAAO;IACX,MAAM,OAAO,QAAQ;IACrB,WAAW,OAAO;IAClB,YAAY,OAAO,cAAc;IACjC,UAAU,OAAO;IAClB,EACF,CACF,CAAC,MAAM,GAAG,MAAM,EAAE,YAAY,EAAE,UAAU;;EAI7C,QAAQ,OACN,KACA,SAC8C;GAC9C,MAAM,SAAS,MAAM,cAAc,IAAI;AACvC,OAAI,KAAK,SAAS,WAChB,OAAM,IAAI,YAAY,OAAO,UAAU,OAAO,QAAQ,QAAQ;IAC5D,IAAI,KAAK;IACT;IACA,OAAO,EAAE,MAAM,KAAK,MAAM,MAAM;IACjC,CAAC;OAEF,OAAM,IAAI,YAAY,OAAO,UAAU,OAAO,KAAK,QAAQ;IACzD,IAAI,KAAK;IACT;IACA,OAAO,EAAE,MAAM,KAAK,MAAM,MAAM;IACjC,CAAC;AAEJ,UAAO;IAAE,IAAI,KAAK;IAAI,MAAM,KAAK;IAAM;;EAIzC,QAAQ,OACN,KACA,SAC8C;GAC9C,MAAM,SAAS,MAAM,cAAc,IAAI;AACvC,OAAI,KAAK,SAAS,YAAY;AAC5B,UAAM,IAAI,YAAY,OAAO,UAAU,OAAO,QAAQ,QAAQ;KAC5D,IAAI,KAAK;KACT;KACA,qBAAqB;KACtB,CAAC;AACF,UAAM,cAAc,KAAK,QAAQ;KAC/B,MAAM;KACN,OAAO;MAAE,MAAM;MAAQ,IAAI;MAAQ;KACnC,SAAS;MAAE,MAAM;MAAW,IAAI,KAAK;MAAI;KACzC,SAAS,CAAC;MAAE,MAAM;MAAQ,IAAI;MAAQ,CAAC;KACvC,SAAS;KACT,MAAM,EAAE,WAAW,KAAK,IAAI;KAC7B,CAAC;UACG;AACL,UAAM,IAAI,YAAY,OAAO,UAAU,OAAO,KAAK,QAAQ;KACzD,IAAI,KAAK;KACT;KACD,CAAC;AACF,UAAM,cAAc,KAAK,QAAQ;KAC/B,MAAM;KACN,OAAO;MAAE,MAAM;MAAQ,IAAI;MAAQ;KACnC,SAAS;MAAE,MAAM;MAAQ,IAAI,KAAK;MAAI;KACtC,SAAS,CAAC;MAAE,MAAM;MAAQ,IAAI;MAAQ,CAAC;KACvC,SAAS;KACT,MAAM,EAAE,QAAQ,KAAK,IAAI;KAC1B,CAAC;;AAEJ,UAAO;IAAE,IAAI,KAAK;IAAI,MAAM,KAAK;IAAM;;EAE1C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"group.js","names":[],"sources":["../../../src/server/domains/group.ts"],"sourcesContent":["import type { ComponentCtx, ComponentReadCtx } from \"../component/context\";\nimport { configDefaults } from \"../config\";\nimport { cached, ctxCacheHas, invalidateCtxCache } from \"../cache/context\";\nimport type { Doc } from \"../types\";\n\ntype GroupDocLike = Doc<\"Group\"> | null;\n\n/** Convex-native `PaginationResult<T>` shape returned by the `*List` component queries. */\ntype Paginated<T> = {\n page: T[];\n isDone: boolean;\n continueCursor: string;\n splitCursor?: string | null;\n pageStatus?: \"SplitRecommended\" | \"SplitRequired\" | null;\n};\n\nexport type GroupDeps = {\n config: ReturnType<typeof configDefaults>;\n};\n\nexport function createGroupDomain(deps: GroupDeps) {\n const { config } = deps;\n\n function groupGet(ctx: ComponentReadCtx, opts: { id: string }): Promise<GroupDocLike>;\n function groupGet(\n ctx: ComponentReadCtx,\n opts: { ids: readonly string[] },\n ): Promise<Array<GroupDocLike>>;\n async function groupGet(\n ctx: ComponentReadCtx,\n opts: { id: string } | { ids: readonly string[] },\n ): Promise<GroupDocLike | Array<GroupDocLike>> {\n if (\"id\" in opts) {\n return (await cached(ctx, `group:${opts.id}`, () =>\n ctx.runQuery(config.component.group.get, {\n id: opts.id,\n }),\n )) as GroupDocLike;\n }\n const groupIds = opts.ids;\n if (groupIds.length === 0) return [];\n const unique = Array.from(new Set(groupIds));\n const toFetch: string[] = [];\n for (const id of unique) {\n if (!ctxCacheHas(ctx, `group:${id}`)) {\n toFetch.push(id);\n }\n }\n if (toFetch.length > 0) {\n const docs = (await ctx.runQuery(config.component.group.get, {\n ids: toFetch,\n })) as Array<GroupDocLike>;\n for (let i = 0; i < toFetch.length; i += 1) {\n const id = toFetch[i]!;\n const value = docs[i] ?? null;\n void cached(ctx, `group:${id}`, () => Promise.resolve(value));\n }\n }\n return (await Promise.all(\n groupIds.map((id) =>\n cached(ctx, `group:${id}`, () => ctx.runQuery(config.component.group.get, { id: id })),\n ),\n )) as Array<GroupDocLike>;\n }\n\n function groupGetEx(ctx: ComponentReadCtx, opts: { id: string }): Promise<GroupDocLike>;\n function groupGetEx(\n ctx: ComponentReadCtx,\n opts: { ids: readonly string[] },\n ): Promise<Array<GroupDocLike>>;\n function groupGetEx(ctx: ComponentReadCtx, selector: { slug: string }): Promise<GroupDocLike>;\n async function groupGetEx(\n ctx: ComponentReadCtx,\n opts: { id: string } | { ids: readonly string[] } | { slug: string },\n ): Promise<GroupDocLike | Array<GroupDocLike>> {\n if (\"slug\" in opts) {\n const { page } = await group.list(ctx, {\n where: { slug: opts.slug },\n paginationOpts: { numItems: 1, cursor: null },\n });\n return page[0] ?? null;\n }\n if (\"ids\" in opts) {\n return groupGet(ctx, opts);\n }\n return groupGet(ctx, { id: opts.id });\n }\n\n const group = {\n /**\n * Create a new group (organization, workspace, team, etc.).\n *\n * Groups are hierarchical — set `parentGroupId` to nest under an existing\n * group, or omit it to create a root-level group. Two denormalized fields\n * are maintained automatically:\n *\n * - `rootGroupId` — the root ancestor (self-referencing for root groups).\n * - `isRoot` — `true` when the group has no parent.\n *\n * @param ctx - Convex mutation context.\n * @param opts.data.name - Display name for the group.\n * @param opts.data.slug - URL-safe slug (optional).\n * @param opts.data.type - App-defined type string (e.g. `\"workspace\"`, `\"team\"`).\n * @param opts.data.parentGroupId - Nest under this group. Omit for a root group.\n * @param opts.data.extend - Arbitrary app-specific metadata.\n * @returns The created group ID.\n *\n * @example Root group\n * ```ts\n * const groupId = await auth.group.create(ctx, {\n * data: { name: \"Acme Corp\", type: \"workspace\" },\n * });\n * ```\n *\n * @example Nested team\n * ```ts\n * const groupId = await auth.group.create(ctx, {\n * data: { name: \"Engineering\", parentGroupId: orgId, type: \"team\" },\n * });\n * ```\n */\n create: async (\n ctx: ComponentCtx,\n opts: {\n data: {\n name: string;\n slug?: string;\n type?: string;\n parentGroupId?: string;\n extend?: Record<string, unknown>;\n };\n },\n ): Promise<string> => {\n return (await ctx.runMutation(config.component.group.create, opts.data)) as string;\n },\n /**\n * Fetch a group document by ID, or a batch of group documents by IDs.\n * See {@link userGet} for the overload pattern.\n *\n * @example Single\n * ```ts\n * const group = await auth.group.get(ctx, { id: groupId });\n * ```\n *\n * @example Batched\n * ```ts\n * const groups = await auth.group.get(ctx, { ids: membershipGroupIds });\n * ```\n *\n * @example By slug\n * ```ts\n * const group = await auth.group.get(ctx, { slug: \"acme\" });\n * ```\n *\n */\n get: groupGetEx,\n /**\n * List groups with optional filtering, pagination, and ordering.\n *\n * Supports filtering by `slug`, `type`, `parentGroupId`, `name`, and\n * `isRoot`. The `isRoot` and `parentGroupId` filters use dedicated\n * indexes for efficient queries.\n *\n * @param ctx - Convex query or mutation context.\n * @param opts.where - Filter criteria (all optional, combined with AND).\n * @param opts.where.isRoot - `true` to find root groups, `false` for nested.\n * @param opts.where.parentGroupId - List direct children of this group.\n * @param opts.paginationOpts - Convex pagination options.\n * @param opts.orderBy - Sort field: `\"_creationTime\"`, `\"name\"`, `\"slug\"`, `\"type\"`.\n * @param opts.order - Sort direction: `\"asc\"` or `\"desc\"`.\n * @returns Convex `PaginationResult` — `{ page, isDone, continueCursor }`.\n *\n * @example List root workspaces\n * ```ts\n * const { page } = await auth.group.list(ctx, {\n * where: { isRoot: true },\n * paginationOpts: { numItems: 25, cursor: null },\n * orderBy: \"name\", order: \"asc\",\n * });\n * ```\n *\n * @example List children of a group\n * ```ts\n * const { page } = await auth.group.list(ctx, {\n * where: { parentGroupId: orgId },\n * paginationOpts: { numItems: 25, cursor: null },\n * });\n * ```\n */\n list: async (\n ctx: ComponentReadCtx,\n opts?: {\n where?: {\n slug?: string;\n type?: string;\n parentGroupId?: string;\n name?: string;\n isRoot?: boolean;\n };\n paginationOpts: { numItems: number; cursor: string | null };\n orderBy?: \"_creationTime\" | \"name\" | \"slug\" | \"type\";\n order?: \"asc\" | \"desc\";\n },\n ) => {\n return (await ctx.runQuery(config.component.group.list, {\n where: opts?.where,\n paginationOpts: opts?.paginationOpts ?? { numItems: 50, cursor: null },\n orderBy: opts?.orderBy,\n order: opts?.order,\n })) as Paginated<Doc<\"Group\">>;\n },\n /**\n * Patch a group document.\n *\n * If `parentGroupId` is changed, the group's `rootGroupId` and `isRoot`\n * fields are recomputed automatically and cascaded to all descendants.\n *\n * @param ctx - Convex mutation context.\n * @param opts.id - The group's document ID.\n * @param opts.patch - Fields to merge (e.g. `name`, `slug`, `parentGroupId`).\n * @returns `null`.\n *\n * @example\n * ```ts\n * await auth.group.update(ctx, {\n * id: groupId,\n * patch: {\n * name: \"Acme Corp (renamed)\",\n * slug: \"acme-corp\",\n * },\n * });\n * ```\n */\n update: async (ctx: ComponentCtx, opts: { id: string; patch: Record<string, unknown> }) => {\n await ctx.runMutation(config.component.group.update, {\n id: opts.id,\n patch: opts.patch,\n });\n invalidateCtxCache(ctx, `group:${opts.id}`);\n return null;\n },\n /**\n * Remove a group and recursively cascade to all descendant groups,\n * their members, and invites.\n *\n * @param ctx - Convex mutation context.\n * @param opts.id - The group's document ID.\n * @returns `null`.\n *\n * @example\n * ```ts\n * await auth.group.remove(ctx, { id: groupId });\n * ```\n */\n remove: async (ctx: ComponentCtx, opts: { id: string }) => {\n await ctx.runMutation(config.component.group.remove, { id: opts.id });\n invalidateCtxCache(ctx, `group:${opts.id}`);\n invalidateCtxCache(ctx, \"member\");\n invalidateCtxCache(ctx, \"member-inspect\");\n return null;\n },\n /**\n * Walk up the group hierarchy from `groupId` and return all ancestor\n * groups in order from immediate parent to root. Detects cycles and\n * respects `maxDepth` (default 32).\n *\n * @param ctx - Convex query or mutation context.\n * @param opts.groupId - Starting group ID.\n * @param opts.maxDepth - Max levels to traverse (default 32).\n * @param opts.includeSelf - Include the starting group in the result.\n * @returns `{ ancestors, cycleDetected, maxDepthReached }`.\n *\n * @example\n * ```ts\n * const { ancestors } = await auth.group.ancestors(ctx, {\n * groupId: teamId,\n * includeSelf: true,\n * });\n * const rootOrg = ancestors[ancestors.length - 1];\n * ```\n */\n ancestors: async (\n ctx: ComponentReadCtx,\n opts: { groupId: string; maxDepth?: number; includeSelf?: boolean },\n ) => {\n const result = (await ctx.runQuery(config.component.group.ancestors, {\n id: opts.groupId,\n maxDepth: opts.maxDepth,\n includeSelf: opts.includeSelf,\n })) as {\n ancestors: Array<Exclude<GroupDocLike, null>>;\n cycleDetected: boolean;\n maxDepthReached: boolean;\n };\n for (const ancestor of result.ancestors) {\n const id = ancestor._id;\n void cached(ctx, `group:${id}`, () => Promise.resolve(ancestor));\n }\n return result;\n },\n groupGet,\n };\n\n return group;\n}\n"],"mappings":";;;AAoBA,SAAgB,kBAAkB,MAAiB;CACjD,MAAM,EAAE,WAAW;CAOnB,eAAe,SACb,KACA,MAC6C;AAC7C,MAAI,QAAQ,KACV,QAAQ,MAAM,OAAO,KAAK,SAAS,KAAK,YACtC,IAAI,SAAS,OAAO,UAAU,MAAM,KAAK,EACvC,IAAI,KAAK,IACV,CAAC,CACH;EAEH,MAAM,WAAW,KAAK;AACtB,MAAI,SAAS,WAAW,EAAG,QAAO,EAAE;EACpC,MAAM,SAAS,MAAM,KAAK,IAAI,IAAI,SAAS,CAAC;EAC5C,MAAM,UAAoB,EAAE;AAC5B,OAAK,MAAM,MAAM,OACf,KAAI,CAAC,YAAY,KAAK,SAAS,KAAK,CAClC,SAAQ,KAAK,GAAG;AAGpB,MAAI,QAAQ,SAAS,GAAG;GACtB,MAAM,OAAQ,MAAM,IAAI,SAAS,OAAO,UAAU,MAAM,KAAK,EAC3D,KAAK,SACN,CAAC;AACF,QAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK,GAAG;IAC1C,MAAM,KAAK,QAAQ;IACnB,MAAM,QAAQ,KAAK,MAAM;AACzB,IAAK,OAAO,KAAK,SAAS,YAAY,QAAQ,QAAQ,MAAM,CAAC;;;AAGjE,SAAQ,MAAM,QAAQ,IACpB,SAAS,KAAK,OACZ,OAAO,KAAK,SAAS,YAAY,IAAI,SAAS,OAAO,UAAU,MAAM,KAAK,EAAM,IAAI,CAAC,CAAC,CACvF,CACF;;CASH,eAAe,WACb,KACA,MAC6C;AAC7C,MAAI,UAAU,MAAM;GAClB,MAAM,EAAE,SAAS,MAAM,MAAM,KAAK,KAAK;IACrC,OAAO,EAAE,MAAM,KAAK,MAAM;IAC1B,gBAAgB;KAAE,UAAU;KAAG,QAAQ;KAAM;IAC9C,CAAC;AACF,UAAO,KAAK,MAAM;;AAEpB,MAAI,SAAS,KACX,QAAO,SAAS,KAAK,KAAK;AAE5B,SAAO,SAAS,KAAK,EAAE,IAAI,KAAK,IAAI,CAAC;;CAGvC,MAAM,QAAQ;EAiCZ,QAAQ,OACN,KACA,SASoB;AACpB,UAAQ,MAAM,IAAI,YAAY,OAAO,UAAU,MAAM,QAAQ,KAAK,KAAK;;EAsBzE,KAAK;EAkCL,MAAM,OACJ,KACA,SAYG;AACH,UAAQ,MAAM,IAAI,SAAS,OAAO,UAAU,MAAM,MAAM;IACtD,OAAO,MAAM;IACb,gBAAgB,MAAM,kBAAkB;KAAE,UAAU;KAAI,QAAQ;KAAM;IACtE,SAAS,MAAM;IACf,OAAO,MAAM;IACd,CAAC;;EAwBJ,QAAQ,OAAO,KAAmB,SAAyD;AACzF,SAAM,IAAI,YAAY,OAAO,UAAU,MAAM,QAAQ;IACnD,IAAI,KAAK;IACT,OAAO,KAAK;IACb,CAAC;AACF,sBAAmB,KAAK,SAAS,KAAK,KAAK;AAC3C,UAAO;;EAeT,QAAQ,OAAO,KAAmB,SAAyB;AACzD,SAAM,IAAI,YAAY,OAAO,UAAU,MAAM,QAAQ,EAAE,IAAI,KAAK,IAAI,CAAC;AACrE,sBAAmB,KAAK,SAAS,KAAK,KAAK;AAC3C,sBAAmB,KAAK,SAAS;AACjC,sBAAmB,KAAK,iBAAiB;AACzC,UAAO;;EAsBT,WAAW,OACT,KACA,SACG;GACH,MAAM,SAAU,MAAM,IAAI,SAAS,OAAO,UAAU,MAAM,WAAW;IACnE,IAAI,KAAK;IACT,UAAU,KAAK;IACf,aAAa,KAAK;IACnB,CAAC;AAKF,QAAK,MAAM,YAAY,OAAO,WAAW;IACvC,MAAM,KAAK,SAAS;AACpB,IAAK,OAAO,KAAK,SAAS,YAAY,QAAQ,QAAQ,SAAS,CAAC;;AAElE,UAAO;;EAET;EACD;AAED,QAAO"}
|
|
1
|
+
{"version":3,"file":"group.js","names":[],"sources":["../../../src/server/domains/group.ts"],"sourcesContent":["import type { GenericId } from \"convex/values\";\n\nimport type { ComponentCtx, ComponentReadCtx } from \"../component/context\";\nimport { configDefaults } from \"../config\";\nimport { cached, ctxCacheHas, invalidateCtxCache } from \"../cache/context\";\nimport type { Doc } from \"../types\";\n\ntype GroupDocLike = Doc<\"Group\"> | null;\n\n/** Convex-native `PaginationResult<T>` shape returned by the `*List` component queries. */\ntype Paginated<T> = {\n page: T[];\n isDone: boolean;\n continueCursor: string;\n splitCursor?: string | null;\n pageStatus?: \"SplitRecommended\" | \"SplitRequired\" | null;\n};\n\nexport type GroupDeps = {\n config: ReturnType<typeof configDefaults>;\n};\n\nexport function createGroupDomain(deps: GroupDeps) {\n const { config } = deps;\n\n function groupGet(ctx: ComponentReadCtx, opts: { id: GenericId<\"Group\"> }): Promise<GroupDocLike>;\n function groupGet(\n ctx: ComponentReadCtx,\n opts: { ids: readonly GenericId<\"Group\">[] },\n ): Promise<Array<GroupDocLike>>;\n async function groupGet(\n ctx: ComponentReadCtx,\n opts: { id: GenericId<\"Group\"> } | { ids: readonly GenericId<\"Group\">[] },\n ): Promise<GroupDocLike | Array<GroupDocLike>> {\n if (\"id\" in opts) {\n return (await cached(ctx, `group:${opts.id}`, () =>\n ctx.runQuery(config.component.group.get, {\n id: opts.id,\n }),\n )) as GroupDocLike;\n }\n const groupIds = opts.ids;\n if (groupIds.length === 0) return [];\n const unique = Array.from(new Set(groupIds));\n const toFetch: GenericId<\"Group\">[] = [];\n for (const id of unique) {\n if (!ctxCacheHas(ctx, `group:${id}`)) {\n toFetch.push(id);\n }\n }\n if (toFetch.length > 0) {\n const docs = (await ctx.runQuery(config.component.group.get, {\n ids: toFetch,\n })) as Array<GroupDocLike>;\n for (let i = 0; i < toFetch.length; i += 1) {\n const id = toFetch[i]!;\n const value = docs[i] ?? null;\n void cached(ctx, `group:${id}`, () => Promise.resolve(value));\n }\n }\n return (await Promise.all(\n groupIds.map((id) =>\n cached(ctx, `group:${id}`, () => ctx.runQuery(config.component.group.get, { id: id })),\n ),\n )) as Array<GroupDocLike>;\n }\n\n function groupGetEx(\n ctx: ComponentReadCtx,\n opts: { id: GenericId<\"Group\"> },\n ): Promise<GroupDocLike>;\n function groupGetEx(\n ctx: ComponentReadCtx,\n opts: { ids: readonly GenericId<\"Group\">[] },\n ): Promise<Array<GroupDocLike>>;\n function groupGetEx(ctx: ComponentReadCtx, selector: { slug: string }): Promise<GroupDocLike>;\n async function groupGetEx(\n ctx: ComponentReadCtx,\n opts: { id: GenericId<\"Group\"> } | { ids: readonly GenericId<\"Group\">[] } | { slug: string },\n ): Promise<GroupDocLike | Array<GroupDocLike>> {\n if (\"slug\" in opts) {\n const { page } = await group.list(ctx, {\n where: { slug: opts.slug },\n paginationOpts: { numItems: 1, cursor: null },\n });\n return page[0] ?? null;\n }\n if (\"ids\" in opts) {\n return groupGet(ctx, opts);\n }\n return groupGet(ctx, { id: opts.id });\n }\n\n const group = {\n /**\n * Create a new group (organization, workspace, team, etc.).\n *\n * Groups are hierarchical — set `parentGroupId` to nest under an existing\n * group, or omit it to create a root-level group. Two denormalized fields\n * are maintained automatically:\n *\n * - `rootGroupId` — the root ancestor (self-referencing for root groups).\n * - `isRoot` — `true` when the group has no parent.\n *\n * @param ctx - Convex mutation context.\n * @param opts.data.name - Display name for the group.\n * @param opts.data.slug - URL-safe slug (optional).\n * @param opts.data.type - App-defined type string (e.g. `\"workspace\"`, `\"team\"`).\n * @param opts.data.parentGroupId - Nest under this group. Omit for a root group.\n * @param opts.data.extend - Arbitrary app-specific metadata.\n * @returns The created group ID.\n *\n * @example Root group\n * ```ts\n * const groupId = await auth.group.create(ctx, {\n * data: { name: \"Acme Corp\", type: \"workspace\" },\n * });\n * ```\n *\n * @example Nested team\n * ```ts\n * const groupId = await auth.group.create(ctx, {\n * data: { name: \"Engineering\", parentGroupId: orgId, type: \"team\" },\n * });\n * ```\n */\n create: async (\n ctx: ComponentCtx,\n opts: {\n data: {\n name: string;\n slug?: string;\n type?: string;\n parentGroupId?: GenericId<\"Group\">;\n extend?: Record<string, unknown>;\n };\n },\n ): Promise<GenericId<\"Group\">> => {\n return (await ctx.runMutation(\n config.component.group.create,\n opts.data,\n )) as GenericId<\"Group\">;\n },\n /**\n * Fetch a group document by ID, or a batch of group documents by IDs.\n * See {@link userGet} for the overload pattern.\n *\n * @example Single\n * ```ts\n * const group = await auth.group.get(ctx, { id: groupId });\n * ```\n *\n * @example Batched\n * ```ts\n * const groups = await auth.group.get(ctx, { ids: membershipGroupIds });\n * ```\n *\n * @example By slug\n * ```ts\n * const group = await auth.group.get(ctx, { slug: \"acme\" });\n * ```\n *\n */\n get: groupGetEx,\n /**\n * List groups with optional filtering, pagination, and ordering.\n *\n * Supports filtering by `slug`, `type`, `parentGroupId`, `name`, and\n * `isRoot`. The `isRoot` and `parentGroupId` filters use dedicated\n * indexes for efficient queries.\n *\n * @param ctx - Convex query or mutation context.\n * @param opts.where - Filter criteria (all optional, combined with AND).\n * @param opts.where.isRoot - `true` to find root groups, `false` for nested.\n * @param opts.where.parentGroupId - List direct children of this group.\n * @param opts.paginationOpts - Convex pagination options.\n * @param opts.orderBy - Sort field: `\"_creationTime\"`, `\"name\"`, `\"slug\"`, `\"type\"`.\n * @param opts.order - Sort direction: `\"asc\"` or `\"desc\"`.\n * @returns Convex `PaginationResult` — `{ page, isDone, continueCursor }`.\n *\n * @example List root workspaces\n * ```ts\n * const { page } = await auth.group.list(ctx, {\n * where: { isRoot: true },\n * paginationOpts: { numItems: 25, cursor: null },\n * orderBy: \"name\", order: \"asc\",\n * });\n * ```\n *\n * @example List children of a group\n * ```ts\n * const { page } = await auth.group.list(ctx, {\n * where: { parentGroupId: orgId },\n * paginationOpts: { numItems: 25, cursor: null },\n * });\n * ```\n */\n list: async (\n ctx: ComponentReadCtx,\n opts?: {\n where?: {\n slug?: string;\n type?: string;\n parentGroupId?: GenericId<\"Group\">;\n name?: string;\n isRoot?: boolean;\n };\n paginationOpts: { numItems: number; cursor: string | null };\n orderBy?: \"_creationTime\" | \"name\" | \"slug\" | \"type\";\n order?: \"asc\" | \"desc\";\n },\n ) => {\n return (await ctx.runQuery(config.component.group.list, {\n where: opts?.where,\n paginationOpts: opts?.paginationOpts ?? { numItems: 50, cursor: null },\n orderBy: opts?.orderBy,\n order: opts?.order,\n })) as Paginated<Doc<\"Group\">>;\n },\n /**\n * Patch a group document.\n *\n * If `parentGroupId` is changed, the group's `rootGroupId` and `isRoot`\n * fields are recomputed automatically and cascaded to all descendants.\n *\n * @param ctx - Convex mutation context.\n * @param opts.id - The group's document ID.\n * @param opts.patch - Fields to merge (e.g. `name`, `slug`, `parentGroupId`).\n * @returns `null`.\n *\n * @example\n * ```ts\n * await auth.group.update(ctx, {\n * id: groupId,\n * patch: {\n * name: \"Acme Corp (renamed)\",\n * slug: \"acme-corp\",\n * },\n * });\n * ```\n */\n update: async (\n ctx: ComponentCtx,\n opts: { id: GenericId<\"Group\">; patch: Record<string, unknown> },\n ) => {\n await ctx.runMutation(config.component.group.update, {\n id: opts.id,\n patch: opts.patch,\n });\n invalidateCtxCache(ctx, `group:${opts.id}`);\n return null;\n },\n /**\n * Remove a group and recursively cascade to all descendant groups,\n * their members, and invites.\n *\n * @param ctx - Convex mutation context.\n * @param opts.id - The group's document ID.\n * @returns `null`.\n *\n * @example\n * ```ts\n * await auth.group.remove(ctx, { id: groupId });\n * ```\n */\n remove: async (ctx: ComponentCtx, opts: { id: GenericId<\"Group\"> }) => {\n await ctx.runMutation(config.component.group.remove, { id: opts.id });\n invalidateCtxCache(ctx, `group:${opts.id}`);\n invalidateCtxCache(ctx, \"member\");\n invalidateCtxCache(ctx, \"member-inspect\");\n return null;\n },\n /**\n * Walk up the group hierarchy from `groupId` and return all ancestor\n * groups in order from immediate parent to root. Detects cycles and\n * respects `maxDepth` (default 32).\n *\n * @param ctx - Convex query or mutation context.\n * @param opts.groupId - Starting group ID.\n * @param opts.maxDepth - Max levels to traverse (default 32).\n * @param opts.includeSelf - Include the starting group in the result.\n * @returns `{ ancestors, cycleDetected, maxDepthReached }`.\n *\n * @example\n * ```ts\n * const { ancestors } = await auth.group.ancestors(ctx, {\n * groupId: teamId,\n * includeSelf: true,\n * });\n * const rootOrg = ancestors[ancestors.length - 1];\n * ```\n */\n ancestors: async (\n ctx: ComponentReadCtx,\n opts: { groupId: GenericId<\"Group\">; maxDepth?: number; includeSelf?: boolean },\n ) => {\n const result = (await ctx.runQuery(config.component.group.ancestors, {\n id: opts.groupId,\n maxDepth: opts.maxDepth,\n includeSelf: opts.includeSelf,\n })) as {\n ancestors: Array<Exclude<GroupDocLike, null>>;\n cycleDetected: boolean;\n maxDepthReached: boolean;\n };\n for (const ancestor of result.ancestors) {\n const id = ancestor._id;\n void cached(ctx, `group:${id}`, () => Promise.resolve(ancestor));\n }\n return result;\n },\n groupGet,\n };\n\n return group;\n}\n"],"mappings":";;;AAsBA,SAAgB,kBAAkB,MAAiB;CACjD,MAAM,EAAE,WAAW;CAOnB,eAAe,SACb,KACA,MAC6C;AAC7C,MAAI,QAAQ,KACV,QAAQ,MAAM,OAAO,KAAK,SAAS,KAAK,YACtC,IAAI,SAAS,OAAO,UAAU,MAAM,KAAK,EACvC,IAAI,KAAK,IACV,CAAC,CACH;EAEH,MAAM,WAAW,KAAK;AACtB,MAAI,SAAS,WAAW,EAAG,QAAO,EAAE;EACpC,MAAM,SAAS,MAAM,KAAK,IAAI,IAAI,SAAS,CAAC;EAC5C,MAAM,UAAgC,EAAE;AACxC,OAAK,MAAM,MAAM,OACf,KAAI,CAAC,YAAY,KAAK,SAAS,KAAK,CAClC,SAAQ,KAAK,GAAG;AAGpB,MAAI,QAAQ,SAAS,GAAG;GACtB,MAAM,OAAQ,MAAM,IAAI,SAAS,OAAO,UAAU,MAAM,KAAK,EAC3D,KAAK,SACN,CAAC;AACF,QAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK,GAAG;IAC1C,MAAM,KAAK,QAAQ;IACnB,MAAM,QAAQ,KAAK,MAAM;AACzB,IAAK,OAAO,KAAK,SAAS,YAAY,QAAQ,QAAQ,MAAM,CAAC;;;AAGjE,SAAQ,MAAM,QAAQ,IACpB,SAAS,KAAK,OACZ,OAAO,KAAK,SAAS,YAAY,IAAI,SAAS,OAAO,UAAU,MAAM,KAAK,EAAM,IAAI,CAAC,CAAC,CACvF,CACF;;CAYH,eAAe,WACb,KACA,MAC6C;AAC7C,MAAI,UAAU,MAAM;GAClB,MAAM,EAAE,SAAS,MAAM,MAAM,KAAK,KAAK;IACrC,OAAO,EAAE,MAAM,KAAK,MAAM;IAC1B,gBAAgB;KAAE,UAAU;KAAG,QAAQ;KAAM;IAC9C,CAAC;AACF,UAAO,KAAK,MAAM;;AAEpB,MAAI,SAAS,KACX,QAAO,SAAS,KAAK,KAAK;AAE5B,SAAO,SAAS,KAAK,EAAE,IAAI,KAAK,IAAI,CAAC;;CAGvC,MAAM,QAAQ;EAiCZ,QAAQ,OACN,KACA,SASgC;AAChC,UAAQ,MAAM,IAAI,YAChB,OAAO,UAAU,MAAM,QACvB,KAAK,KACN;;EAsBH,KAAK;EAkCL,MAAM,OACJ,KACA,SAYG;AACH,UAAQ,MAAM,IAAI,SAAS,OAAO,UAAU,MAAM,MAAM;IACtD,OAAO,MAAM;IACb,gBAAgB,MAAM,kBAAkB;KAAE,UAAU;KAAI,QAAQ;KAAM;IACtE,SAAS,MAAM;IACf,OAAO,MAAM;IACd,CAAC;;EAwBJ,QAAQ,OACN,KACA,SACG;AACH,SAAM,IAAI,YAAY,OAAO,UAAU,MAAM,QAAQ;IACnD,IAAI,KAAK;IACT,OAAO,KAAK;IACb,CAAC;AACF,sBAAmB,KAAK,SAAS,KAAK,KAAK;AAC3C,UAAO;;EAeT,QAAQ,OAAO,KAAmB,SAAqC;AACrE,SAAM,IAAI,YAAY,OAAO,UAAU,MAAM,QAAQ,EAAE,IAAI,KAAK,IAAI,CAAC;AACrE,sBAAmB,KAAK,SAAS,KAAK,KAAK;AAC3C,sBAAmB,KAAK,SAAS;AACjC,sBAAmB,KAAK,iBAAiB;AACzC,UAAO;;EAsBT,WAAW,OACT,KACA,SACG;GACH,MAAM,SAAU,MAAM,IAAI,SAAS,OAAO,UAAU,MAAM,WAAW;IACnE,IAAI,KAAK;IACT,UAAU,KAAK;IACf,aAAa,KAAK;IACnB,CAAC;AAKF,QAAK,MAAM,YAAY,OAAO,WAAW;IACvC,MAAM,KAAK,SAAS;AACpB,IAAK,OAAO,KAAK,SAAS,YAAY,QAAQ,QAAQ,SAAS,CAAC;;AAElE,UAAO;;EAET;EACD;AAED,QAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"invite.js","names":[],"sources":["../../../src/server/domains/invite.ts"],"sourcesContent":["import type { Auth } from \"convex/server\";\nimport { ConvexError, type Value } from \"convex/values\";\n\nimport { ErrorCode } from \"../../shared/codes\";\nimport type { ComponentCtx, ComponentReadCtx } from \"../component/context\";\nimport { configDefaults } from \"../config\";\nimport { getSessionUserId } from \"../context\";\nimport { generateRandomString, sha256 } from \"../random\";\nimport type { Doc } from \"../types\";\n\n/** Convex-native `PaginationResult<T>` shape returned by the `*List` component queries. */\ntype Paginated<T> = {\n page: T[];\n isDone: boolean;\n continueCursor: string;\n splitCursor?: string | null;\n pageStatus?: \"SplitRecommended\" | \"SplitRequired\" | null;\n};\n\nexport type InviteDeps = {\n config: ReturnType<typeof configDefaults>;\n inviteTokenAlphabet: string;\n inviteTokenLength: number;\n normalizeRoleIds: (roleIds?: string[]) => string[];\n};\n\nexport function createInviteDomain(deps: InviteDeps) {\n const { config, inviteTokenAlphabet, inviteTokenLength, normalizeRoleIds } = deps;\n\n return {\n /**\n * Create a pending invite. Returns a one-time `token` the recipient\n * uses to accept. Optionally scoped to a group with role IDs.\n *\n * @param ctx - Convex mutation context.\n * @param opts.data.groupId - The group to invite the user to (optional).\n * @param opts.data.invitedByUserId - The user who created this invite (optional).\n * @param opts.data.email - The invitee's email address (optional).\n * @param opts.data.roleIds - Role IDs from `definePermissions()` to assign on acceptance (optional).\n * @param opts.data.expiresTime - Expiration timestamp in ms since epoch (optional).\n * @param opts.data.extend - Arbitrary app-specific metadata (optional).\n * @returns `{ id, token }`.\n * @throws `INVALID_ROLE_IDS` if any supplied role IDs are not defined.\n *\n * @example\n * ```ts\n * const { token } = await auth.invite.create(ctx, {\n * data: { groupId, email: \"alice@example.com\", roleIds: [roles.member.id] },\n * });\n * ```\n */\n create: async (\n ctx: ComponentCtx,\n opts: {\n data: {\n groupId?: string;\n invitedByUserId?: string;\n email?: string;\n roleIds?: string[];\n expiresTime?: number;\n extend?: { [key: string]: Value | undefined };\n };\n },\n ) => {\n const data = opts.data;\n const roleIds = normalizeRoleIds(data.roleIds);\n const token = generateRandomString(inviteTokenLength, inviteTokenAlphabet);\n const tokenHash = await sha256(token);\n const inviteId = (await ctx.runMutation(config.component.group.invite.create, {\n ...data,\n roleIds,\n tokenHash,\n })) as string;\n return { id: inviteId, token };\n },\n /**\n * Fetch an invite document by ID.\n *\n * Returns the full invite document including its status, email,\n * group, role IDs, and token hash. Useful for displaying invite\n * details or checking status before performing actions.\n *\n * @param ctx - Convex query or mutation context.\n * @param opts.id - The invite's document ID.\n * @returns The invite document, or `null` if not found.\n *\n * @example\n * ```ts\n * const invite = await auth.invite.get(ctx, { id: inviteId });\n * if (invite?.status === \"pending\") {\n * // show invite details\n * }\n * ```\n */\n get: async (\n ctx: ComponentReadCtx,\n opts: { id: string },\n ): Promise<Doc<\"GroupInvite\"> | null> => {\n return (await ctx.runQuery(config.component.group.invite.get, {\n id: opts.id,\n })) as Doc<\"GroupInvite\"> | null;\n },\n token: {\n /**\n * Look up an invite by its raw token string.\n *\n * Hashes the raw token and queries the database for a matching\n * invite. This is the standard path for invite-link landing pages\n * where the token is extracted from the URL.\n *\n * @param ctx - Convex query or mutation context.\n * @param opts.token - The raw invite token string from the invite link.\n * @returns The invite document, or `null` if no matching invite exists.\n *\n * @example\n * ```ts\n * const invite = await auth.invite.token.get(ctx, { token: tokenFromUrl });\n * if (!invite || invite.status !== \"pending\") {\n * throw new Error(\"Invalid or expired invite\");\n * }\n * ```\n */\n get: async (\n ctx: ComponentReadCtx,\n opts: { token: string },\n ): Promise<Doc<\"GroupInvite\"> | null> => {\n const tokenHash = await sha256(opts.token);\n return (await ctx.runQuery(config.component.group.invite.get, {\n tokenHash,\n })) as Doc<\"GroupInvite\"> | null;\n },\n /**\n * Accept an invite by token. Creates a membership and marks the invite as accepted.\n *\n * Hashes the raw token, finds the matching invite, creates a group\n * membership with the invite's role IDs, and transitions the invite\n * status to `\"accepted\"`.\n *\n * @param ctx - Convex mutation context.\n * @param args.token - The raw invite token string.\n * @returns The created membership details.\n *\n * @example\n * ```ts\n * const result = await auth.invite.token.accept(ctx, {\n * token: tokenFromUrl,\n * });\n * ```\n */\n accept: async (\n ctx: ComponentCtx & { auth: Auth },\n args: { token: string },\n ): Promise<{\n inviteId: string;\n groupId: string | null;\n memberId?: string;\n inviteStatus: string;\n membershipStatus: string;\n }> => {\n const acceptedByUserId = await getSessionUserId(ctx);\n if (acceptedByUserId === null) {\n throw new ConvexError({\n code: ErrorCode.NOT_SIGNED_IN,\n message: \"Authentication required.\",\n });\n }\n const tokenHash = await sha256(args.token);\n const result = (await ctx.runMutation(config.component.group.invite.accept, {\n tokenHash,\n acceptedByUserId,\n })) as {\n inviteId: string;\n groupId: string | null;\n memberId?: string;\n inviteStatus: string;\n membershipStatus: string;\n };\n return { ...result };\n },\n },\n /**\n * List invites with optional filtering by group, status, email, etc.\n * Results are paginated.\n *\n * @param ctx - Convex query or mutation context.\n * @param opts.where - Filter criteria (all optional).\n * @param opts.where.status - `\"pending\"`, `\"accepted\"`, `\"revoked\"`, or `\"expired\"`.\n * @param opts.paginationOpts - Convex pagination options.\n * @param opts.orderBy - Sort field: `\"_creationTime\"`, `\"status\"`, `\"email\"`,\n * `\"expiresTime\"`, or `\"acceptedTime\"`.\n * @param opts.order - Sort direction: `\"asc\"` or `\"desc\"`.\n * @returns Convex `PaginationResult` — `{ page, isDone, continueCursor }`.\n *\n * @example\n * ```ts\n * const { page } = await auth.invite.list(ctx, {\n * where: { groupId, status: \"pending\" },\n * paginationOpts: { numItems: 25, cursor: null },\n * orderBy: \"_creationTime\",\n * order: \"desc\",\n * });\n * ```\n */\n list: async (\n ctx: ComponentReadCtx,\n opts?: {\n where?: {\n tokenHash?: string;\n groupId?: string;\n status?: \"pending\" | \"accepted\" | \"revoked\" | \"expired\";\n email?: string;\n invitedByUserId?: string;\n roleId?: string;\n acceptedByUserId?: string;\n };\n paginationOpts: { numItems: number; cursor: string | null };\n orderBy?: \"_creationTime\" | \"status\" | \"email\" | \"expiresTime\" | \"acceptedTime\";\n order?: \"asc\" | \"desc\";\n },\n ) => {\n return (await ctx.runQuery(config.component.group.invite.list, {\n where: opts?.where,\n paginationOpts: opts?.paginationOpts ?? { numItems: 50, cursor: null },\n orderBy: opts?.orderBy,\n order: opts?.order,\n })) as Paginated<Doc<\"GroupInvite\">>;\n },\n /**\n * Accept an invite by ID. Optionally specify who accepted it.\n *\n * Transitions the invite's status to `\"accepted\"` and optionally\n * records the accepting user. Unlike `invite.token.accept`, this\n * method does not automatically create a group membership — use it\n * for admin-driven invite acceptance flows.\n *\n * @param ctx - Convex mutation context.\n * @param opts.id - The invite's document ID.\n * @param opts.acceptedByUserId - The user who accepted the invite (optional).\n * @returns `{ id, acceptedByUserId }`.\n *\n * @example\n * ```ts\n * await auth.invite.accept(ctx, { id: inviteId, acceptedByUserId: userId });\n * ```\n */\n accept: async (ctx: ComponentCtx, opts: { id: string; acceptedByUserId?: string }) => {\n const args = { id: opts.id };\n if (opts.acceptedByUserId !== undefined) {\n Object.assign(args, { acceptedByUserId: opts.acceptedByUserId });\n }\n await ctx.runMutation(config.component.group.invite.accept, args);\n return {\n id: opts.id,\n acceptedByUserId: opts.acceptedByUserId ?? null,\n };\n },\n /**\n * Revoke a pending invite. Sets its status to `\"revoked\"`.\n *\n * Once revoked, the invite's token can no longer be used to accept\n * the invitation. This is a permanent status change.\n *\n * @param ctx - Convex mutation context.\n * @param opts.id - The invite's document ID.\n * @returns `null`.\n *\n * @example\n * ```ts\n * await auth.invite.revoke(ctx, { id: inviteId });\n * ```\n */\n revoke: async (ctx: ComponentCtx, opts: { id: string }) => {\n await ctx.runMutation(config.component.group.invite.revoke, { id: opts.id });\n return null;\n },\n };\n}\n"],"mappings":";;;;;;AA0BA,SAAgB,mBAAmB,MAAkB;CACnD,MAAM,EAAE,QAAQ,qBAAqB,mBAAmB,qBAAqB;AAE7E,QAAO;EAsBL,QAAQ,OACN,KACA,SAUG;GACH,MAAM,OAAO,KAAK;GAClB,MAAM,UAAU,iBAAiB,KAAK,QAAQ;GAC9C,MAAM,QAAQ,qBAAqB,mBAAmB,oBAAoB;GAC1E,MAAM,YAAY,MAAM,OAAO,MAAM;AAMrC,UAAO;IAAE,IALS,MAAM,IAAI,YAAY,OAAO,UAAU,MAAM,OAAO,QAAQ;KAC5E,GAAG;KACH;KACA;KACD,CAAC;IACqB;IAAO;;EAqBhC,KAAK,OACH,KACA,SACuC;AACvC,UAAQ,MAAM,IAAI,SAAS,OAAO,UAAU,MAAM,OAAO,KAAK,EAC5D,IAAI,KAAK,IACV,CAAC;;EAEJ,OAAO;GAoBL,KAAK,OACH,KACA,SACuC;IACvC,MAAM,YAAY,MAAM,OAAO,KAAK,MAAM;AAC1C,WAAQ,MAAM,IAAI,SAAS,OAAO,UAAU,MAAM,OAAO,KAAK,EAC5D,WACD,CAAC;;GAoBJ,QAAQ,OACN,KACA,SAOI;IACJ,MAAM,mBAAmB,MAAM,iBAAiB,IAAI;AACpD,QAAI,qBAAqB,KACvB,OAAM,IAAI,YAAY;KACpB,MAAM,UAAU;KAChB,SAAS;KACV,CAAC;IAEJ,MAAM,YAAY,MAAM,OAAO,KAAK,MAAM;AAW1C,WAAO,EAAE,GAVO,MAAM,IAAI,YAAY,OAAO,UAAU,MAAM,OAAO,QAAQ;KAC1E;KACA;KACD,CAAC,EAOkB;;GAEvB;EAwBD,MAAM,OACJ,KACA,SAcG;AACH,UAAQ,MAAM,IAAI,SAAS,OAAO,UAAU,MAAM,OAAO,MAAM;IAC7D,OAAO,MAAM;IACb,gBAAgB,MAAM,kBAAkB;KAAE,UAAU;KAAI,QAAQ;KAAM;IACtE,SAAS,MAAM;IACf,OAAO,MAAM;IACd,CAAC;;EAoBJ,QAAQ,OAAO,KAAmB,SAAoD;GACpF,MAAM,OAAO,EAAE,IAAI,KAAK,IAAI;AAC5B,OAAI,KAAK,qBAAqB,OAC5B,QAAO,OAAO,MAAM,EAAE,kBAAkB,KAAK,kBAAkB,CAAC;AAElE,SAAM,IAAI,YAAY,OAAO,UAAU,MAAM,OAAO,QAAQ,KAAK;AACjE,UAAO;IACL,IAAI,KAAK;IACT,kBAAkB,KAAK,oBAAoB;IAC5C;;EAiBH,QAAQ,OAAO,KAAmB,SAAyB;AACzD,SAAM,IAAI,YAAY,OAAO,UAAU,MAAM,OAAO,QAAQ,EAAE,IAAI,KAAK,IAAI,CAAC;AAC5E,UAAO;;EAEV"}
|
|
1
|
+
{"version":3,"file":"invite.js","names":[],"sources":["../../../src/server/domains/invite.ts"],"sourcesContent":["import type { Auth } from \"convex/server\";\nimport { ConvexError, type GenericId, type Value } from \"convex/values\";\n\nimport { ErrorCode } from \"../../shared/codes\";\nimport type { ComponentCtx, ComponentReadCtx } from \"../component/context\";\nimport { configDefaults } from \"../config\";\nimport { getSessionUserId } from \"../context\";\nimport { generateRandomString, sha256 } from \"../random\";\nimport type { Doc } from \"../types\";\n\n/** Convex-native `PaginationResult<T>` shape returned by the `*List` component queries. */\ntype Paginated<T> = {\n page: T[];\n isDone: boolean;\n continueCursor: string;\n splitCursor?: string | null;\n pageStatus?: \"SplitRecommended\" | \"SplitRequired\" | null;\n};\n\nexport type InviteDeps = {\n config: ReturnType<typeof configDefaults>;\n inviteTokenAlphabet: string;\n inviteTokenLength: number;\n normalizeRoleIds: (roleIds?: string[]) => string[];\n};\n\nexport function createInviteDomain(deps: InviteDeps) {\n const { config, inviteTokenAlphabet, inviteTokenLength, normalizeRoleIds } = deps;\n\n return {\n /**\n * Create a pending invite. Returns a one-time `token` the recipient\n * uses to accept. Optionally scoped to a group with role IDs.\n *\n * @param ctx - Convex mutation context.\n * @param opts.data.groupId - The group to invite the user to (optional).\n * @param opts.data.invitedByUserId - The user who created this invite (optional).\n * @param opts.data.email - The invitee's email address (optional).\n * @param opts.data.roleIds - Role IDs from `definePermissions()` to assign on acceptance (optional).\n * @param opts.data.expiresTime - Expiration timestamp in ms since epoch (optional).\n * @param opts.data.extend - Arbitrary app-specific metadata (optional).\n * @returns `{ id, token }`.\n * @throws `INVALID_ROLE_IDS` if any supplied role IDs are not defined.\n *\n * @example\n * ```ts\n * const { token } = await auth.invite.create(ctx, {\n * data: { groupId, email: \"alice@example.com\", roleIds: [roles.member.id] },\n * });\n * ```\n */\n create: async (\n ctx: ComponentCtx,\n opts: {\n data: {\n groupId?: GenericId<\"Group\">;\n invitedByUserId?: GenericId<\"User\">;\n email?: string;\n roleIds?: string[];\n expiresTime?: number;\n extend?: { [key: string]: Value | undefined };\n };\n },\n ) => {\n const data = opts.data;\n const roleIds = normalizeRoleIds(data.roleIds);\n const token = generateRandomString(inviteTokenLength, inviteTokenAlphabet);\n const tokenHash = await sha256(token);\n const inviteId = (await ctx.runMutation(config.component.group.invite.create, {\n ...data,\n roleIds,\n tokenHash,\n })) as GenericId<\"GroupInvite\">;\n return { id: inviteId, token };\n },\n /**\n * Fetch an invite document by ID.\n *\n * Returns the full invite document including its status, email,\n * group, role IDs, and token hash. Useful for displaying invite\n * details or checking status before performing actions.\n *\n * @param ctx - Convex query or mutation context.\n * @param opts.id - The invite's document ID.\n * @returns The invite document, or `null` if not found.\n *\n * @example\n * ```ts\n * const invite = await auth.invite.get(ctx, { id: inviteId });\n * if (invite?.status === \"pending\") {\n * // show invite details\n * }\n * ```\n */\n get: async (\n ctx: ComponentReadCtx,\n opts: { id: GenericId<\"GroupInvite\"> },\n ): Promise<Doc<\"GroupInvite\"> | null> => {\n return (await ctx.runQuery(config.component.group.invite.get, {\n id: opts.id,\n })) as Doc<\"GroupInvite\"> | null;\n },\n token: {\n /**\n * Look up an invite by its raw token string.\n *\n * Hashes the raw token and queries the database for a matching\n * invite. This is the standard path for invite-link landing pages\n * where the token is extracted from the URL.\n *\n * @param ctx - Convex query or mutation context.\n * @param opts.token - The raw invite token string from the invite link.\n * @returns The invite document, or `null` if no matching invite exists.\n *\n * @example\n * ```ts\n * const invite = await auth.invite.token.get(ctx, { token: tokenFromUrl });\n * if (!invite || invite.status !== \"pending\") {\n * throw new Error(\"Invalid or expired invite\");\n * }\n * ```\n */\n get: async (\n ctx: ComponentReadCtx,\n opts: { token: string },\n ): Promise<Doc<\"GroupInvite\"> | null> => {\n const tokenHash = await sha256(opts.token);\n return (await ctx.runQuery(config.component.group.invite.get, {\n tokenHash,\n })) as Doc<\"GroupInvite\"> | null;\n },\n /**\n * Accept an invite by token. Creates a membership and marks the invite as accepted.\n *\n * Hashes the raw token, finds the matching invite, creates a group\n * membership with the invite's role IDs, and transitions the invite\n * status to `\"accepted\"`.\n *\n * @param ctx - Convex mutation context.\n * @param args.token - The raw invite token string.\n * @returns The created membership details.\n *\n * @example\n * ```ts\n * const result = await auth.invite.token.accept(ctx, {\n * token: tokenFromUrl,\n * });\n * ```\n */\n accept: async (\n ctx: ComponentCtx & { auth: Auth },\n args: { token: string },\n ): Promise<{\n inviteId: GenericId<\"GroupInvite\">;\n groupId: GenericId<\"Group\"> | null;\n memberId?: GenericId<\"GroupMember\">;\n inviteStatus: string;\n membershipStatus: string;\n }> => {\n const acceptedByUserId = await getSessionUserId(ctx);\n if (acceptedByUserId === null) {\n throw new ConvexError({\n code: ErrorCode.NOT_SIGNED_IN,\n message: \"Authentication required.\",\n });\n }\n const tokenHash = await sha256(args.token);\n const result = (await ctx.runMutation(config.component.group.invite.accept, {\n tokenHash,\n acceptedByUserId,\n })) as {\n inviteId: GenericId<\"GroupInvite\">;\n groupId: GenericId<\"Group\"> | null;\n memberId?: GenericId<\"GroupMember\">;\n inviteStatus: string;\n membershipStatus: string;\n };\n return { ...result };\n },\n },\n /**\n * List invites with optional filtering by group, status, email, etc.\n * Results are paginated.\n *\n * @param ctx - Convex query or mutation context.\n * @param opts.where - Filter criteria (all optional).\n * @param opts.where.status - `\"pending\"`, `\"accepted\"`, `\"revoked\"`, or `\"expired\"`.\n * @param opts.paginationOpts - Convex pagination options.\n * @param opts.orderBy - Sort field: `\"_creationTime\"`, `\"status\"`, `\"email\"`,\n * `\"expiresTime\"`, or `\"acceptedTime\"`.\n * @param opts.order - Sort direction: `\"asc\"` or `\"desc\"`.\n * @returns Convex `PaginationResult` — `{ page, isDone, continueCursor }`.\n *\n * @example\n * ```ts\n * const { page } = await auth.invite.list(ctx, {\n * where: { groupId, status: \"pending\" },\n * paginationOpts: { numItems: 25, cursor: null },\n * orderBy: \"_creationTime\",\n * order: \"desc\",\n * });\n * ```\n */\n list: async (\n ctx: ComponentReadCtx,\n opts?: {\n where?: {\n tokenHash?: string;\n groupId?: GenericId<\"Group\">;\n status?: \"pending\" | \"accepted\" | \"revoked\" | \"expired\";\n email?: string;\n invitedByUserId?: GenericId<\"User\">;\n roleId?: string;\n acceptedByUserId?: GenericId<\"User\">;\n };\n paginationOpts: { numItems: number; cursor: string | null };\n orderBy?: \"_creationTime\" | \"status\" | \"email\" | \"expiresTime\" | \"acceptedTime\";\n order?: \"asc\" | \"desc\";\n },\n ) => {\n return (await ctx.runQuery(config.component.group.invite.list, {\n where: opts?.where,\n paginationOpts: opts?.paginationOpts ?? { numItems: 50, cursor: null },\n orderBy: opts?.orderBy,\n order: opts?.order,\n })) as Paginated<Doc<\"GroupInvite\">>;\n },\n /**\n * Accept an invite by ID. Optionally specify who accepted it.\n *\n * Transitions the invite's status to `\"accepted\"` and optionally\n * records the accepting user. Unlike `invite.token.accept`, this\n * method does not automatically create a group membership — use it\n * for admin-driven invite acceptance flows.\n *\n * @param ctx - Convex mutation context.\n * @param opts.id - The invite's document ID.\n * @param opts.acceptedByUserId - The user who accepted the invite (optional).\n * @returns `{ id, acceptedByUserId }`.\n *\n * @example\n * ```ts\n * await auth.invite.accept(ctx, { id: inviteId, acceptedByUserId: userId });\n * ```\n */\n accept: async (\n ctx: ComponentCtx,\n opts: { id: GenericId<\"GroupInvite\">; acceptedByUserId?: GenericId<\"User\"> },\n ) => {\n const args = { id: opts.id };\n if (opts.acceptedByUserId !== undefined) {\n Object.assign(args, { acceptedByUserId: opts.acceptedByUserId });\n }\n await ctx.runMutation(config.component.group.invite.accept, args);\n return {\n id: opts.id,\n acceptedByUserId: opts.acceptedByUserId ?? null,\n };\n },\n /**\n * Revoke a pending invite. Sets its status to `\"revoked\"`.\n *\n * Once revoked, the invite's token can no longer be used to accept\n * the invitation. This is a permanent status change.\n *\n * @param ctx - Convex mutation context.\n * @param opts.id - The invite's document ID.\n * @returns `null`.\n *\n * @example\n * ```ts\n * await auth.invite.revoke(ctx, { id: inviteId });\n * ```\n */\n revoke: async (ctx: ComponentCtx, opts: { id: GenericId<\"GroupInvite\"> }) => {\n await ctx.runMutation(config.component.group.invite.revoke, { id: opts.id });\n return null;\n },\n };\n}\n"],"mappings":";;;;;;AA0BA,SAAgB,mBAAmB,MAAkB;CACnD,MAAM,EAAE,QAAQ,qBAAqB,mBAAmB,qBAAqB;AAE7E,QAAO;EAsBL,QAAQ,OACN,KACA,SAUG;GACH,MAAM,OAAO,KAAK;GAClB,MAAM,UAAU,iBAAiB,KAAK,QAAQ;GAC9C,MAAM,QAAQ,qBAAqB,mBAAmB,oBAAoB;GAC1E,MAAM,YAAY,MAAM,OAAO,MAAM;AAMrC,UAAO;IAAE,IALS,MAAM,IAAI,YAAY,OAAO,UAAU,MAAM,OAAO,QAAQ;KAC5E,GAAG;KACH;KACA;KACD,CAAC;IACqB;IAAO;;EAqBhC,KAAK,OACH,KACA,SACuC;AACvC,UAAQ,MAAM,IAAI,SAAS,OAAO,UAAU,MAAM,OAAO,KAAK,EAC5D,IAAI,KAAK,IACV,CAAC;;EAEJ,OAAO;GAoBL,KAAK,OACH,KACA,SACuC;IACvC,MAAM,YAAY,MAAM,OAAO,KAAK,MAAM;AAC1C,WAAQ,MAAM,IAAI,SAAS,OAAO,UAAU,MAAM,OAAO,KAAK,EAC5D,WACD,CAAC;;GAoBJ,QAAQ,OACN,KACA,SAOI;IACJ,MAAM,mBAAmB,MAAM,iBAAiB,IAAI;AACpD,QAAI,qBAAqB,KACvB,OAAM,IAAI,YAAY;KACpB,MAAM,UAAU;KAChB,SAAS;KACV,CAAC;IAEJ,MAAM,YAAY,MAAM,OAAO,KAAK,MAAM;AAW1C,WAAO,EAAE,GAVO,MAAM,IAAI,YAAY,OAAO,UAAU,MAAM,OAAO,QAAQ;KAC1E;KACA;KACD,CAAC,EAOkB;;GAEvB;EAwBD,MAAM,OACJ,KACA,SAcG;AACH,UAAQ,MAAM,IAAI,SAAS,OAAO,UAAU,MAAM,OAAO,MAAM;IAC7D,OAAO,MAAM;IACb,gBAAgB,MAAM,kBAAkB;KAAE,UAAU;KAAI,QAAQ;KAAM;IACtE,SAAS,MAAM;IACf,OAAO,MAAM;IACd,CAAC;;EAoBJ,QAAQ,OACN,KACA,SACG;GACH,MAAM,OAAO,EAAE,IAAI,KAAK,IAAI;AAC5B,OAAI,KAAK,qBAAqB,OAC5B,QAAO,OAAO,MAAM,EAAE,kBAAkB,KAAK,kBAAkB,CAAC;AAElE,SAAM,IAAI,YAAY,OAAO,UAAU,MAAM,OAAO,QAAQ,KAAK;AACjE,UAAO;IACL,IAAI,KAAK;IACT,kBAAkB,KAAK,oBAAoB;IAC5C;;EAiBH,QAAQ,OAAO,KAAmB,SAA2C;AAC3E,SAAM,IAAI,YAAY,OAAO,UAAU,MAAM,OAAO,QAAQ,EAAE,IAAI,KAAK,IAAI,CAAC;AAC5E,UAAO;;EAEV"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"key.js","names":[],"sources":["../../../src/server/domains/key.ts"],"sourcesContent":["import { ConvexError } from \"convex/values\";\n\nimport { ErrorCode } from \"../../shared/codes\";\nimport type { ComponentCtx, ComponentReadCtx } from \"../component/context\";\nimport { configDefaults } from \"../config\";\nimport { emitAuthEvent } from \"../events\";\nimport { createScopeChecker, generateApiKey, hashApiKey } from \"../keys\";\nimport type { KeyDoc, KeyRecord, KeyScope, ScopeChecker } from \"../types\";\n\n/** Convex-native `PaginationResult<T>` shape returned by the `*List` component queries. */\ntype Paginated<T> = {\n page: T[];\n isDone: boolean;\n continueCursor: string;\n splitCursor?: string | null;\n pageStatus?: \"SplitRecommended\" | \"SplitRequired\" | null;\n};\n\nexport type KeyDeps = {\n config: ReturnType<typeof configDefaults>;\n};\n\n/**\n * Sampling window for the `lastUsedAt` touch in {@link createKeyDomain}'s\n * `verify`. Bearer verification runs on every authenticated API-key request, so\n * writing `lastUsedAt` unconditionally turns read-only traffic into a\n * per-request write on a single, potentially very hot `ApiKey` document — a\n * classic OCC contention point when one shared key drives high QPS. `lastUsedAt`\n * only needs coarse (\"used ~this minute\") accuracy, so the touch is coarsened:\n * skipped unless the stored value is unset or older than this window.\n */\nconst LAST_USED_COARSEN_MS = 60_000;\n\n/**\n * Public projection of a stored `ApiKey` document. Strips the sensitive\n * `hashedKey` (the SHA-256 lookup hash) and the mutable `rateLimitState`\n * (internal token-bucket counters) so `auth.key.get` / `auth.key.list` never\n * leak them to callers — the returned shape matches {@link KeyRecord}.\n * `auth.key.verify` reads the full component document directly and is\n * unaffected.\n */\nfunction redactKeyRecord(doc: KeyDoc): KeyRecord {\n const { hashedKey: _hashedKey, rateLimitState: _rateLimitState, ...rest } = doc;\n return rest as KeyRecord;\n}\n\nexport function createKeyDomain(deps: KeyDeps) {\n const { config } = deps;\n\n // Creation/rotation commits before event append/handlers run. If an event\n // sink fails, the secret cannot be read again; returning an error would hide\n // a live credential from the caller. Preserve the credential response and\n // surface the audit outage operationally instead.\n const emitCommittedKeyEvent = async (\n ctx: ComponentCtx,\n event: Parameters<typeof emitAuthEvent>[2],\n ): Promise<void> => {\n try {\n await emitAuthEvent(ctx, config, event);\n } catch (err) {\n console.error(\"[auth] API key committed but audit event emission failed\", {\n kind: event.kind,\n err,\n });\n }\n };\n\n const key = {\n /**\n * Create an API key for programmatic access. The returned `secret`\n * (prefixed `sk_`) is shown only once — it is stored as a hash.\n *\n * @param ctx - Convex mutation context.\n * @param opts.data.userId - Owner of the key.\n * @param opts.data.name - Human-readable name (e.g. `\"CI Pipeline\"`).\n * @param opts.data.scopes - Array of `{ resource, actions }` permission scopes.\n * @param opts.data.rateLimit - Optional per-key rate limit `{ maxRequests, windowMs }`.\n * @param opts.data.expiresAt - Optional expiration timestamp (ms since epoch).\n * @param opts.data.extend - Arbitrary app-specific extend.\n * @returns `{ id, secret }`. Store `secret` securely — it cannot be retrieved later.\n *\n * @example\n * ```ts\n * const { secret } = await auth.key.create(ctx, {\n * data: {\n * userId,\n * name: \"CI Pipeline\",\n * scopes: [{ resource: \"data\", actions: [\"read\"] }],\n * },\n * });\n * ```\n */\n create: async (\n ctx: ComponentCtx,\n opts: {\n data: {\n userId: string;\n name: string;\n scopes: KeyScope[];\n rateLimit?: { maxRequests: number; windowMs: number };\n expiresAt?: number;\n extend?: Record<string, unknown>;\n };\n },\n ): Promise<{ id: string; secret: string }> => {\n const data = opts.data;\n const { raw, hashedKey, displayPrefix } = await generateApiKey(\"sk_\");\n const keyId = (await ctx.runMutation(config.component.user.key.create, {\n userId: data.userId,\n prefix: displayPrefix,\n hashedKey,\n name: data.name,\n scopes: data.scopes,\n rateLimit: data.rateLimit,\n expiresAt: data.expiresAt,\n extend: data.extend,\n })) as string;\n await emitCommittedKeyEvent(ctx, {\n kind: \"api_key.created\",\n actor: { type: \"user\", id: data.userId },\n subject: { type: \"user\", id: data.userId },\n targets: [\n { kind: \"user\", id: data.userId },\n { kind: \"api_key\", id: keyId },\n ],\n outcome: \"success\",\n data: { keyId, name: data.name, prefix: displayPrefix },\n });\n return { id: keyId, secret: raw };\n },\n /**\n * Verify an API key and return the owner's identity and scopes.\n *\n * Checks the key against the database, enforces expiration and rate\n * limits, and returns a `ScopeChecker` for permission evaluation.\n *\n * @param ctx - Convex mutation context (updates `lastUsedAt` and rate limit state).\n * @param opts.secret - The raw `sk_*` key string.\n * @returns `{ userId, keyId, scopes }` where `scopes.can(resource, action)` checks permissions.\n * @throws `INVALID_API_KEY` if the key is not found.\n * @throws `API_KEY_REVOKED` if the key was revoked.\n * @throws `API_KEY_EXPIRED` if the key is past its `expiresAt`.\n * @throws `API_KEY_RATE_LIMITED` if the rate limit is exceeded.\n *\n * @example\n * ```ts\n * const { userId, scopes } = await auth.key.verify(ctx, { secret: rawKey });\n * const canRead = scopes.can(\"data\", \"read\");\n * ```\n */\n verify: async (\n ctx: ComponentCtx,\n opts: { secret: string },\n ): Promise<{ userId: string; keyId: string; scopes: ScopeChecker }> => {\n const hashedKey = await hashApiKey(opts.secret);\n const doc = (await ctx.runQuery(config.component.user.key.get, {\n hashedKey,\n })) as KeyDoc | null;\n if (!doc) {\n throw new ConvexError({\n code: ErrorCode.INVALID_API_KEY,\n message: \"Invalid API key.\",\n });\n }\n // Make the successful verification decision and update usage in ONE\n // component mutation on the `ApiKey` row. The hash lookup above only\n // locates the row: `recordUse` re-reads its current state transactionally\n // and returns the current owner and scopes. This closes both the original\n // rate-limit race and the query → mutation window where deletion,\n // revocation, expiration, or a scope reduction could otherwise be missed.\n //\n // `coarsenMs` keeps the hot-path optimization: `recordUse` skips the\n // `lastUsedAt` write when it is still within the window (and no rate-limit\n // decrement forces a write), so read-only bearer traffic on a hot key does\n // not turn into a per-request write.\n const result = (await ctx.runMutation(config.component.user.key.recordUse, {\n id: doc._id,\n coarsenMs: LAST_USED_COARSEN_MS,\n })) as\n | { status: \"invalid\" | \"revoked\" | \"expired\" | \"limited\" }\n | {\n status: \"verified\";\n keyId: string;\n userId: string;\n scopes: KeyScope[];\n };\n switch (result.status) {\n case \"invalid\":\n throw new ConvexError({\n code: ErrorCode.INVALID_API_KEY,\n message: \"Invalid API key.\",\n });\n case \"revoked\":\n throw new ConvexError({\n code: ErrorCode.API_KEY_REVOKED,\n message: \"This API key has been revoked.\",\n });\n case \"expired\":\n throw new ConvexError({\n code: ErrorCode.API_KEY_EXPIRED,\n message: \"This API key has expired.\",\n });\n case \"limited\":\n throw new ConvexError({\n code: ErrorCode.API_KEY_RATE_LIMITED,\n message: \"API key rate limit exceeded. Please try again later.\",\n });\n }\n return {\n userId: result.userId,\n keyId: result.keyId,\n scopes: createScopeChecker(result.scopes),\n };\n },\n /**\n * List API keys with optional filtering by user, revocation status, name,\n * or prefix. Results are paginated. Does not expose raw key secrets.\n *\n * @param ctx - Convex query or mutation context.\n * @param opts.where - Filter criteria (all optional, combined with AND).\n * @param opts.paginationOpts - Convex pagination options.\n * @param opts.orderBy - Sort field: `\"_creationTime\"`, `\"name\"`, `\"lastUsedAt\"`, `\"expiresAt\"`, or `\"revoked\"`.\n * @param opts.order - Sort direction: `\"asc\"` or `\"desc\"`.\n * @returns Convex `PaginationResult` — `{ page, isDone, continueCursor }`.\n *\n * @example\n * ```ts\n * const { page } = await auth.key.list(ctx, {\n * where: { userId, revoked: false },\n * paginationOpts: { numItems: 25, cursor: null },\n * orderBy: \"lastUsedAt\",\n * order: \"desc\",\n * });\n * ```\n */\n list: async (\n ctx: ComponentReadCtx,\n opts?: {\n where?: {\n userId?: string;\n revoked?: boolean;\n name?: string;\n prefix?: string;\n };\n paginationOpts: { numItems: number; cursor: string | null };\n orderBy?: \"_creationTime\" | \"name\" | \"lastUsedAt\" | \"expiresAt\" | \"revoked\";\n order?: \"asc\" | \"desc\";\n },\n ) => {\n const result = (await ctx.runQuery(config.component.user.key.list, {\n where: opts?.where,\n paginationOpts: opts?.paginationOpts ?? { numItems: 50, cursor: null },\n orderBy: opts?.orderBy,\n order: opts?.order,\n })) as Paginated<KeyDoc>;\n return { ...result, page: result.page.map(redactKeyRecord) };\n },\n /**\n * Fetch an API key record by ID. Does not expose the raw key secret.\n *\n * Returns the key document including extend, scopes, rate limit\n * configuration, and revocation status. The raw secret is never\n * stored or returned — only the hashed key and display prefix.\n *\n * @param ctx - Convex query or mutation context.\n * @param opts.id - The API key's document ID.\n * @returns The key document, or `null` if not found.\n *\n * @example\n * ```ts\n * const key = await auth.key.get(ctx, { id: keyId });\n * if (!key) throw new Error(\"Key not found\");\n * console.log(key.name, key.prefix);\n * ```\n */\n get: async (ctx: ComponentReadCtx, opts: { id: string }): Promise<KeyRecord | null> => {\n const doc = (await ctx.runQuery(config.component.user.key.get, {\n id: opts.id,\n })) as KeyDoc | null;\n return doc === null ? null : redactKeyRecord(doc);\n },\n /**\n * Update a key's name, scopes, or rate limit.\n *\n * Patches the specified fields on the API key document. Only the\n * provided fields are changed — omitted fields remain unchanged.\n *\n * @param ctx - Convex mutation context.\n * @param opts.id - The API key's document ID.\n * @param opts.patch - Fields to merge into the key document.\n * @returns `null`.\n *\n * @example\n * ```ts\n * await auth.key.update(ctx, {\n * id: keyId,\n * patch: {\n * name: \"CI Pipeline (updated)\",\n * scopes: [{ resource: \"data\", actions: [\"read\", \"write\"] }],\n * },\n * });\n * ```\n */\n update: async (\n ctx: ComponentCtx,\n opts: {\n id: string;\n patch: {\n name?: string;\n scopes?: KeyScope[];\n rateLimit?: { maxRequests: number; windowMs: number };\n };\n },\n ) => {\n await ctx.runMutation(config.component.user.key.update, {\n id: opts.id,\n patch: opts.patch,\n });\n return null;\n },\n /**\n * Soft-delete: set `revoked: true`. The key can no longer be verified.\n *\n * After revocation, any subsequent calls to `auth.key.verify` with\n * this key will throw `API_KEY_REVOKED`.\n * The key record is preserved for audit purposes.\n *\n * @param ctx - Convex mutation context.\n * @param opts.id - The API key's document ID.\n * @returns `null`.\n *\n * @example\n * ```ts\n * await auth.key.revoke(ctx, { id: keyId });\n * ```\n */\n revoke: async (ctx: ComponentCtx, opts: { id: string }) => {\n const key = (await ctx.runQuery(config.component.user.key.get, {\n id: opts.id,\n })) as KeyDoc | null;\n await ctx.runMutation(config.component.user.key.update, {\n id: opts.id,\n patch: { revoked: true },\n });\n if (key !== null) {\n await emitAuthEvent(ctx, config, {\n kind: \"api_key.revoked\",\n actor: { type: \"user\", id: key.userId },\n subject: { type: \"user\", id: key.userId },\n targets: [\n { kind: \"user\", id: key.userId },\n { kind: \"api_key\", id: opts.id },\n ],\n outcome: \"success\",\n data: { keyId: opts.id },\n });\n }\n return null;\n },\n /**\n * Hard-delete: permanently remove the key record.\n *\n * Unlike `revoke`, this permanently removes the key document from\n * the database. Use this when you need to fully clean up a key\n * rather than preserving it for audit history.\n *\n * @param ctx - Convex mutation context.\n * @param opts.id - The API key's document ID.\n * @returns `null`.\n *\n * @example\n * ```ts\n * await auth.key.remove(ctx, { id: keyId });\n * ```\n */\n remove: async (ctx: ComponentCtx, opts: { id: string }) => {\n await ctx.runMutation(config.component.user.key.remove, { id: opts.id });\n return null;\n },\n /**\n * Rotate a key: revokes the old key and creates a new one with the\n * same user, scopes, and rate limit. Returns the new `id` and `secret`.\n * Throws if the key does not exist or is already revoked.\n *\n * @param ctx - Convex mutation context.\n * @param opts.id - The existing API key's document ID to rotate.\n * @param opts.name - Optional new name for the rotated key (defaults to the old name).\n * @param opts.expiresAt - Optional new expiration timestamp in ms since epoch.\n * @returns `{ id, secret }` with the new key.\n * @throws `INVALID_PARAMETERS` if the key does not exist.\n * @throws `API_KEY_REVOKED` if the key is already revoked.\n *\n * @example\n * ```ts\n * const { id, secret } = await auth.key.rotate(ctx, {\n * id: oldKeyId,\n * expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000, // 30 days\n * });\n * // Store secret securely — shown only once\n * ```\n */\n rotate: async (\n ctx: ComponentCtx,\n opts: { id: string; name?: string; expiresAt?: number },\n ): Promise<{ id: string; secret: string }> => {\n const { raw, hashedKey, displayPrefix } = await generateApiKey(\"sk_\");\n const result = (await ctx.runMutation((config.component.user.key as any).rotate, {\n id: opts.id,\n prefix: displayPrefix,\n hashedKey,\n name: opts.name,\n expiresAt: opts.expiresAt,\n })) as\n | { status: \"invalid\" }\n | { status: \"revoked\" }\n | { status: \"invalid_rate_limit\" }\n | { status: \"rotated\"; id: string; userId: string; name: string };\n if (result.status === \"invalid\") {\n throw new ConvexError({\n code: ErrorCode.INVALID_PARAMETERS,\n message: \"The provided parameters are invalid.\",\n });\n }\n if (result.status === \"revoked\") {\n throw new ConvexError({\n code: ErrorCode.API_KEY_REVOKED,\n message: \"This API key has been revoked.\",\n });\n }\n if (result.status === \"invalid_rate_limit\") {\n throw new ConvexError({\n code: ErrorCode.INVALID_PARAMETERS,\n message: \"The API key has an invalid rate limit configuration.\",\n });\n }\n await emitCommittedKeyEvent(ctx, {\n kind: \"api_key.revoked\",\n actor: { type: \"user\", id: result.userId },\n subject: { type: \"user\", id: result.userId },\n targets: [\n { kind: \"user\", id: result.userId },\n { kind: \"api_key\", id: opts.id },\n ],\n outcome: \"success\",\n data: { keyId: opts.id },\n });\n await emitCommittedKeyEvent(ctx, {\n kind: \"api_key.created\",\n actor: { type: \"user\", id: result.userId },\n subject: { type: \"user\", id: result.userId },\n targets: [\n { kind: \"user\", id: result.userId },\n { kind: \"api_key\", id: result.id },\n ],\n outcome: \"success\",\n data: { keyId: result.id, name: result.name, prefix: displayPrefix },\n });\n return { id: result.id, secret: raw };\n },\n };\n\n return key;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"key.js","names":[],"sources":["../../../src/server/domains/key.ts"],"sourcesContent":["import { ConvexError, type GenericId } from \"convex/values\";\n\nimport { ErrorCode } from \"../../shared/codes\";\nimport type { ComponentCtx, ComponentReadCtx } from \"../component/context\";\nimport { configDefaults } from \"../config\";\nimport { emitAuthEvent } from \"../events\";\nimport { createScopeChecker, generateApiKey, hashApiKey } from \"../keys\";\nimport type { KeyDoc, KeyRecord, KeyScope, ScopeChecker } from \"../types\";\n\n/** Convex-native `PaginationResult<T>` shape returned by the `*List` component queries. */\ntype Paginated<T> = {\n page: T[];\n isDone: boolean;\n continueCursor: string;\n splitCursor?: string | null;\n pageStatus?: \"SplitRecommended\" | \"SplitRequired\" | null;\n};\n\nexport type KeyDeps = {\n config: ReturnType<typeof configDefaults>;\n};\n\n/**\n * Sampling window for the `lastUsedAt` touch in {@link createKeyDomain}'s\n * `verify`. Bearer verification runs on every authenticated API-key request, so\n * writing `lastUsedAt` unconditionally turns read-only traffic into a\n * per-request write on a single, potentially very hot `ApiKey` document — a\n * classic OCC contention point when one shared key drives high QPS. `lastUsedAt`\n * only needs coarse (\"used ~this minute\") accuracy, so the touch is coarsened:\n * skipped unless the stored value is unset or older than this window.\n */\nconst LAST_USED_COARSEN_MS = 60_000;\n\n/**\n * Public projection of a stored `ApiKey` document. Strips the sensitive\n * `hashedKey` (the SHA-256 lookup hash) and the mutable `rateLimitState`\n * (internal token-bucket counters) so `auth.key.get` / `auth.key.list` never\n * leak them to callers — the returned shape matches {@link KeyRecord}.\n * `auth.key.verify` reads the full component document directly and is\n * unaffected.\n */\nfunction redactKeyRecord(doc: KeyDoc): KeyRecord {\n const { hashedKey: _hashedKey, rateLimitState: _rateLimitState, ...rest } = doc;\n return rest as KeyRecord;\n}\n\nexport function createKeyDomain(deps: KeyDeps) {\n const { config } = deps;\n\n // Creation/rotation commits before event append/handlers run. If an event\n // sink fails, the secret cannot be read again; returning an error would hide\n // a live credential from the caller. Preserve the credential response and\n // surface the audit outage operationally instead.\n const emitCommittedKeyEvent = async (\n ctx: ComponentCtx,\n event: Parameters<typeof emitAuthEvent>[2],\n ): Promise<void> => {\n try {\n await emitAuthEvent(ctx, config, event);\n } catch (err) {\n console.error(\"[auth] API key committed but audit event emission failed\", {\n kind: event.kind,\n err,\n });\n }\n };\n\n const key = {\n /**\n * Create an API key for programmatic access. The returned `secret`\n * (prefixed `sk_`) is shown only once — it is stored as a hash.\n *\n * @param ctx - Convex mutation context.\n * @param opts.data.userId - Owner of the key.\n * @param opts.data.name - Human-readable name (e.g. `\"CI Pipeline\"`).\n * @param opts.data.scopes - Array of `{ resource, actions }` permission scopes.\n * @param opts.data.rateLimit - Optional per-key rate limit `{ maxRequests, windowMs }`.\n * @param opts.data.expiresAt - Optional expiration timestamp (ms since epoch).\n * @param opts.data.extend - Arbitrary app-specific extend.\n * @returns `{ id, secret }`. Store `secret` securely — it cannot be retrieved later.\n *\n * @example\n * ```ts\n * const { secret } = await auth.key.create(ctx, {\n * data: {\n * userId,\n * name: \"CI Pipeline\",\n * scopes: [{ resource: \"data\", actions: [\"read\"] }],\n * },\n * });\n * ```\n */\n create: async (\n ctx: ComponentCtx,\n opts: {\n data: {\n userId: GenericId<\"User\">;\n name: string;\n scopes: KeyScope[];\n rateLimit?: { maxRequests: number; windowMs: number };\n expiresAt?: number;\n extend?: Record<string, unknown>;\n };\n },\n ): Promise<{ id: GenericId<\"ApiKey\">; secret: string }> => {\n const data = opts.data;\n const { raw, hashedKey, displayPrefix } = await generateApiKey(\"sk_\");\n const keyId = (await ctx.runMutation(config.component.user.key.create, {\n userId: data.userId,\n prefix: displayPrefix,\n hashedKey,\n name: data.name,\n scopes: data.scopes,\n rateLimit: data.rateLimit,\n expiresAt: data.expiresAt,\n extend: data.extend,\n })) as GenericId<\"ApiKey\">;\n await emitCommittedKeyEvent(ctx, {\n kind: \"api_key.created\",\n actor: { type: \"user\", id: data.userId },\n subject: { type: \"user\", id: data.userId },\n targets: [\n { kind: \"user\", id: data.userId },\n { kind: \"api_key\", id: keyId },\n ],\n outcome: \"success\",\n data: { keyId, name: data.name, prefix: displayPrefix },\n });\n return { id: keyId, secret: raw };\n },\n /**\n * Verify an API key and return the owner's identity and scopes.\n *\n * Checks the key against the database, enforces expiration and rate\n * limits, and returns a `ScopeChecker` for permission evaluation.\n *\n * @param ctx - Convex mutation context (updates `lastUsedAt` and rate limit state).\n * @param opts.secret - The raw `sk_*` key string.\n * @returns `{ userId, keyId, scopes }` where `scopes.can(resource, action)` checks permissions.\n * @throws `INVALID_API_KEY` if the key is not found.\n * @throws `API_KEY_REVOKED` if the key was revoked.\n * @throws `API_KEY_EXPIRED` if the key is past its `expiresAt`.\n * @throws `API_KEY_RATE_LIMITED` if the rate limit is exceeded.\n *\n * @example\n * ```ts\n * const { userId, scopes } = await auth.key.verify(ctx, { secret: rawKey });\n * const canRead = scopes.can(\"data\", \"read\");\n * ```\n */\n verify: async (\n ctx: ComponentCtx,\n opts: { secret: string },\n ): Promise<{ userId: GenericId<\"User\">; keyId: GenericId<\"ApiKey\">; scopes: ScopeChecker }> => {\n const hashedKey = await hashApiKey(opts.secret);\n const doc = (await ctx.runQuery(config.component.user.key.get, {\n hashedKey,\n })) as KeyDoc | null;\n if (!doc) {\n throw new ConvexError({\n code: ErrorCode.INVALID_API_KEY,\n message: \"Invalid API key.\",\n });\n }\n // Make the successful verification decision and update usage in ONE\n // component mutation on the `ApiKey` row. The hash lookup above only\n // locates the row: `recordUse` re-reads its current state transactionally\n // and returns the current owner and scopes. This closes both the original\n // rate-limit race and the query → mutation window where deletion,\n // revocation, expiration, or a scope reduction could otherwise be missed.\n //\n // `coarsenMs` keeps the hot-path optimization: `recordUse` skips the\n // `lastUsedAt` write when it is still within the window (and no rate-limit\n // decrement forces a write), so read-only bearer traffic on a hot key does\n // not turn into a per-request write.\n const result = (await ctx.runMutation(config.component.user.key.recordUse, {\n id: doc._id,\n coarsenMs: LAST_USED_COARSEN_MS,\n })) as\n | { status: \"invalid\" | \"revoked\" | \"expired\" | \"limited\" }\n | {\n status: \"verified\";\n keyId: GenericId<\"ApiKey\">;\n userId: GenericId<\"User\">;\n scopes: KeyScope[];\n };\n switch (result.status) {\n case \"invalid\":\n throw new ConvexError({\n code: ErrorCode.INVALID_API_KEY,\n message: \"Invalid API key.\",\n });\n case \"revoked\":\n throw new ConvexError({\n code: ErrorCode.API_KEY_REVOKED,\n message: \"This API key has been revoked.\",\n });\n case \"expired\":\n throw new ConvexError({\n code: ErrorCode.API_KEY_EXPIRED,\n message: \"This API key has expired.\",\n });\n case \"limited\":\n throw new ConvexError({\n code: ErrorCode.API_KEY_RATE_LIMITED,\n message: \"API key rate limit exceeded. Please try again later.\",\n });\n }\n return {\n userId: result.userId,\n keyId: result.keyId,\n scopes: createScopeChecker(result.scopes),\n };\n },\n /**\n * List API keys with optional filtering by user, revocation status, name,\n * or prefix. Results are paginated. Does not expose raw key secrets.\n *\n * @param ctx - Convex query or mutation context.\n * @param opts.where - Filter criteria (all optional, combined with AND).\n * @param opts.paginationOpts - Convex pagination options.\n * @param opts.orderBy - Sort field: `\"_creationTime\"`, `\"name\"`, `\"lastUsedAt\"`, `\"expiresAt\"`, or `\"revoked\"`.\n * @param opts.order - Sort direction: `\"asc\"` or `\"desc\"`.\n * @returns Convex `PaginationResult` — `{ page, isDone, continueCursor }`.\n *\n * @example\n * ```ts\n * const { page } = await auth.key.list(ctx, {\n * where: { userId, revoked: false },\n * paginationOpts: { numItems: 25, cursor: null },\n * orderBy: \"lastUsedAt\",\n * order: \"desc\",\n * });\n * ```\n */\n list: async (\n ctx: ComponentReadCtx,\n opts?: {\n where?: {\n userId?: GenericId<\"User\">;\n revoked?: boolean;\n name?: string;\n prefix?: string;\n };\n paginationOpts: { numItems: number; cursor: string | null };\n orderBy?: \"_creationTime\" | \"name\" | \"lastUsedAt\" | \"expiresAt\" | \"revoked\";\n order?: \"asc\" | \"desc\";\n },\n ) => {\n const result = (await ctx.runQuery(config.component.user.key.list, {\n where: opts?.where,\n paginationOpts: opts?.paginationOpts ?? { numItems: 50, cursor: null },\n orderBy: opts?.orderBy,\n order: opts?.order,\n })) as Paginated<KeyDoc>;\n return { ...result, page: result.page.map(redactKeyRecord) };\n },\n /**\n * Fetch an API key record by ID. Does not expose the raw key secret.\n *\n * Returns the key document including extend, scopes, rate limit\n * configuration, and revocation status. The raw secret is never\n * stored or returned — only the hashed key and display prefix.\n *\n * @param ctx - Convex query or mutation context.\n * @param opts.id - The API key's document ID.\n * @returns The key document, or `null` if not found.\n *\n * @example\n * ```ts\n * const key = await auth.key.get(ctx, { id: keyId });\n * if (!key) throw new Error(\"Key not found\");\n * console.log(key.name, key.prefix);\n * ```\n */\n get: async (\n ctx: ComponentReadCtx,\n opts: { id: GenericId<\"ApiKey\"> },\n ): Promise<KeyRecord | null> => {\n const doc = (await ctx.runQuery(config.component.user.key.get, {\n id: opts.id,\n })) as KeyDoc | null;\n return doc === null ? null : redactKeyRecord(doc);\n },\n /**\n * Update a key's name, scopes, or rate limit.\n *\n * Patches the specified fields on the API key document. Only the\n * provided fields are changed — omitted fields remain unchanged.\n *\n * @param ctx - Convex mutation context.\n * @param opts.id - The API key's document ID.\n * @param opts.patch - Fields to merge into the key document.\n * @returns `null`.\n *\n * @example\n * ```ts\n * await auth.key.update(ctx, {\n * id: keyId,\n * patch: {\n * name: \"CI Pipeline (updated)\",\n * scopes: [{ resource: \"data\", actions: [\"read\", \"write\"] }],\n * },\n * });\n * ```\n */\n update: async (\n ctx: ComponentCtx,\n opts: {\n id: GenericId<\"ApiKey\">;\n patch: {\n name?: string;\n scopes?: KeyScope[];\n rateLimit?: { maxRequests: number; windowMs: number };\n };\n },\n ) => {\n await ctx.runMutation(config.component.user.key.update, {\n id: opts.id,\n patch: opts.patch,\n });\n return null;\n },\n /**\n * Soft-delete: set `revoked: true`. The key can no longer be verified.\n *\n * After revocation, any subsequent calls to `auth.key.verify` with\n * this key will throw `API_KEY_REVOKED`.\n * The key record is preserved for audit purposes.\n *\n * @param ctx - Convex mutation context.\n * @param opts.id - The API key's document ID.\n * @returns `null`.\n *\n * @example\n * ```ts\n * await auth.key.revoke(ctx, { id: keyId });\n * ```\n */\n revoke: async (ctx: ComponentCtx, opts: { id: GenericId<\"ApiKey\"> }) => {\n const key = (await ctx.runQuery(config.component.user.key.get, {\n id: opts.id,\n })) as KeyDoc | null;\n await ctx.runMutation(config.component.user.key.update, {\n id: opts.id,\n patch: { revoked: true },\n });\n if (key !== null) {\n await emitAuthEvent(ctx, config, {\n kind: \"api_key.revoked\",\n actor: { type: \"user\", id: key.userId },\n subject: { type: \"user\", id: key.userId },\n targets: [\n { kind: \"user\", id: key.userId },\n { kind: \"api_key\", id: opts.id },\n ],\n outcome: \"success\",\n data: { keyId: opts.id },\n });\n }\n return null;\n },\n /**\n * Hard-delete: permanently remove the key record.\n *\n * Unlike `revoke`, this permanently removes the key document from\n * the database. Use this when you need to fully clean up a key\n * rather than preserving it for audit history.\n *\n * @param ctx - Convex mutation context.\n * @param opts.id - The API key's document ID.\n * @returns `null`.\n *\n * @example\n * ```ts\n * await auth.key.remove(ctx, { id: keyId });\n * ```\n */\n remove: async (ctx: ComponentCtx, opts: { id: GenericId<\"ApiKey\"> }) => {\n await ctx.runMutation(config.component.user.key.remove, { id: opts.id });\n return null;\n },\n /**\n * Rotate a key: revokes the old key and creates a new one with the\n * same user, scopes, and rate limit. Returns the new `id` and `secret`.\n * Throws if the key does not exist or is already revoked.\n *\n * @param ctx - Convex mutation context.\n * @param opts.id - The existing API key's document ID to rotate.\n * @param opts.name - Optional new name for the rotated key (defaults to the old name).\n * @param opts.expiresAt - Optional new expiration timestamp in ms since epoch.\n * @returns `{ id, secret }` with the new key.\n * @throws `INVALID_PARAMETERS` if the key does not exist.\n * @throws `API_KEY_REVOKED` if the key is already revoked.\n *\n * @example\n * ```ts\n * const { id, secret } = await auth.key.rotate(ctx, {\n * id: oldKeyId,\n * expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000, // 30 days\n * });\n * // Store secret securely — shown only once\n * ```\n */\n rotate: async (\n ctx: ComponentCtx,\n opts: { id: GenericId<\"ApiKey\">; name?: string; expiresAt?: number },\n ): Promise<{ id: GenericId<\"ApiKey\">; secret: string }> => {\n const { raw, hashedKey, displayPrefix } = await generateApiKey(\"sk_\");\n const result = await ctx.runMutation(config.component.user.key.rotate, {\n id: opts.id,\n prefix: displayPrefix,\n hashedKey,\n name: opts.name,\n expiresAt: opts.expiresAt,\n });\n if (result.status === \"invalid\") {\n throw new ConvexError({\n code: ErrorCode.INVALID_PARAMETERS,\n message: \"The provided parameters are invalid.\",\n });\n }\n if (result.status === \"revoked\") {\n throw new ConvexError({\n code: ErrorCode.API_KEY_REVOKED,\n message: \"This API key has been revoked.\",\n });\n }\n if (result.status === \"invalid_rate_limit\") {\n throw new ConvexError({\n code: ErrorCode.INVALID_PARAMETERS,\n message: \"The API key has an invalid rate limit configuration.\",\n });\n }\n await emitCommittedKeyEvent(ctx, {\n kind: \"api_key.revoked\",\n actor: { type: \"user\", id: result.userId },\n subject: { type: \"user\", id: result.userId },\n targets: [\n { kind: \"user\", id: result.userId },\n { kind: \"api_key\", id: opts.id },\n ],\n outcome: \"success\",\n data: { keyId: opts.id },\n });\n await emitCommittedKeyEvent(ctx, {\n kind: \"api_key.created\",\n actor: { type: \"user\", id: result.userId },\n subject: { type: \"user\", id: result.userId },\n targets: [\n { kind: \"user\", id: result.userId },\n { kind: \"api_key\", id: result.id },\n ],\n outcome: \"success\",\n data: { keyId: result.id, name: result.name, prefix: displayPrefix },\n });\n return { id: result.id as GenericId<\"ApiKey\">, secret: raw };\n },\n };\n\n return key;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"member.js","names":["membership","membershipRoleIds"],"sources":["../../../src/server/domains/member.ts"],"sourcesContent":["import { ConvexError } from \"convex/values\";\n\nimport { ErrorCode } from \"../../shared/codes\";\nimport type { ComponentCtx, ComponentReadCtx } from \"../component/context\";\nimport { configDefaults } from \"../config\";\nimport { cached, ctxCacheHas, invalidateCtxCache } from \"../cache/context\";\nimport type { Doc } from \"../types\";\nimport { capGrantsForCaller, resolveOAuthCaller } from \"./access\";\n\n/** Convex-native `PaginationResult<T>` shape returned by the `*List` component queries. */\ntype Paginated<T> = {\n page: T[];\n isDone: boolean;\n continueCursor: string;\n splitCursor?: string | null;\n pageStatus?: \"SplitRecommended\" | \"SplitRequired\" | null;\n};\n\ntype MemberDocLike = {\n _id: string;\n _creationTime: number;\n groupId: string;\n userId: string;\n role?: string;\n roleIds?: string[];\n status?: string;\n extend?: Record<string, unknown>;\n} | null;\n\n/** Options accepted by `member.list`. */\ntype MemberListOpts = {\n where?: { groupId?: string; userId?: string; status?: string };\n paginationOpts: { numItems: number; cursor: string | null };\n orderBy?: \"_creationTime\" | \"status\";\n order?: \"asc\" | \"desc\";\n};\n\ntype InspectResult = {\n membership: MemberDocLike;\n roleIds: string[];\n grants: string[];\n};\n\ntype ResolveResult = InspectResult & {\n matchedGroupId: string | null;\n depth: number | null;\n isDirect: boolean;\n isInherited: boolean;\n traversedGroupIds: string[];\n};\n\nexport type MemberDeps = {\n config: ReturnType<typeof configDefaults>;\n normalizeRoleIds: (roleIds?: string[]) => string[];\n resolveGrantedPermissions: (roleIds?: string[]) => string[];\n};\n\nexport function createMemberDomain(deps: MemberDeps) {\n const { config, normalizeRoleIds, resolveGrantedPermissions } = deps;\n\n function memberInspect(\n ctx: ComponentReadCtx,\n opts: {\n userId: string;\n groupId: string;\n },\n ): Promise<InspectResult>;\n function memberInspect(\n ctx: ComponentReadCtx,\n opts: { userId: string; groupIds: readonly string[] },\n ): Promise<Array<InspectResult>>;\n async function memberInspect(\n ctx: ComponentReadCtx,\n opts:\n | {\n userId: string;\n groupId: string;\n }\n | { userId: string; groupIds: readonly string[] },\n ): Promise<InspectResult | Array<InspectResult>> {\n const oauthCaller = await resolveOAuthCaller(ctx);\n if (\"groupIds\" in opts) {\n const { userId, groupIds } = opts;\n if (groupIds.length === 0) return [];\n const unique = Array.from(new Set(groupIds));\n const toFetch: string[] = [];\n for (const groupId of unique) {\n if (!ctxCacheHas(ctx, `member-inspect:${userId}:${groupId}:n`)) {\n toFetch.push(groupId);\n }\n }\n if (toFetch.length > 0) {\n const docs = (await ctx.runQuery(config.component.group.member.get, {\n userId,\n groupIds: toFetch,\n })) as Array<MemberDocLike>;\n for (let i = 0; i < toFetch.length; i += 1) {\n const groupId = toFetch[i]!;\n const value = docs[i] ?? null;\n void cached(ctx, `member-inspect:${userId}:${groupId}:n`, () => Promise.resolve(value));\n }\n }\n return await Promise.all(\n groupIds.map(async (groupId) => {\n const membership = (await cached(ctx, `member-inspect:${userId}:${groupId}:n`, () =>\n ctx.runQuery(config.component.group.member.get, {\n userId,\n groupId,\n }),\n )) as MemberDocLike;\n if (membership === null) {\n return {\n membership: null,\n roleIds: [] as string[],\n grants: [] as string[],\n };\n }\n const membershipRoleIds = membership.roleIds ?? [];\n const membershipGrants = capGrantsForCaller(\n oauthCaller,\n userId,\n resolveGrantedPermissions(membershipRoleIds),\n );\n return {\n membership,\n roleIds: membershipRoleIds,\n grants: membershipGrants,\n };\n }),\n );\n }\n\n const cacheKey = `member-inspect:${opts.userId}:${opts.groupId}:n`;\n const membership = (await cached(ctx, cacheKey, () =>\n ctx.runQuery(config.component.group.member.get, {\n userId: opts.userId,\n groupId: opts.groupId,\n }),\n )) as MemberDocLike;\n\n if (membership === null) {\n return {\n membership: null,\n roleIds: [] as string[],\n grants: [] as string[],\n };\n }\n\n const membershipRoleIds = membership.roleIds ?? [];\n const membershipGrants = capGrantsForCaller(\n oauthCaller,\n opts.userId,\n resolveGrantedPermissions(membershipRoleIds),\n );\n\n return {\n membership,\n roleIds: membershipRoleIds,\n grants: membershipGrants,\n };\n }\n\n const member = {\n /**\n * Add a user to a group with optional role IDs.\n *\n * Role IDs are validated against the roles defined in `definePermissions()` —\n * invalid IDs throw `INVALID_ROLE_IDS`.\n * Throws `DUPLICATE_MEMBERSHIP` if the user is already a member.\n *\n * @param ctx - Convex mutation context.\n * @param opts.data.groupId - The group to add the user to.\n * @param opts.data.userId - The user's document ID.\n * @param opts.data.roleIds - Role IDs from `definePermissions()` (optional).\n * @param opts.data.status - Membership status string (optional, app-defined).\n * @param opts.data.extend - Arbitrary app-specific metadata.\n * @returns The created membership ID.\n * @throws `INVALID_ROLE_IDS` if any supplied role IDs are not defined.\n *\n * @example\n * ```ts\n * const memberId = await auth.member.create(ctx, {\n * data: {\n * groupId: orgId,\n * userId,\n * roleIds: [roles.orgAdmin.id],\n * },\n * });\n * ```\n */\n create: async (\n ctx: ComponentCtx,\n opts: {\n data: {\n groupId: string;\n userId: string;\n roleIds?: string[];\n status?: string;\n extend?: Record<string, unknown>;\n };\n },\n ) => {\n const data = opts.data;\n const roleIds = normalizeRoleIds(data.roleIds);\n const memberId = (await ctx.runMutation(config.component.group.member.create, {\n ...data,\n roleIds,\n })) as string;\n invalidateCtxCache(ctx, `member-inspect:${data.userId}:${data.groupId}`);\n return memberId;\n },\n /**\n * List memberships with optional filtering and pagination.\n *\n * Supports filtering by `groupId`, `userId`, and `status`.\n * When `groupId` and `status` are both provided, a compound index\n * is used for efficient queries.\n *\n * @param ctx - Convex query or mutation context.\n * @param opts.where - Filter criteria (all optional).\n * @param opts.paginationOpts - Convex pagination options.\n * @param opts.orderBy - Sort field: `\"_creationTime\"` or `\"status\"`.\n * @param opts.order - Sort direction: `\"asc\"` or `\"desc\"`.\n * @returns Convex `PaginationResult` — `{ page, isDone, continueCursor }`.\n *\n * @example\n * ```ts\n * const { page } = await auth.member.list(ctx, {\n * where: { groupId: orgId },\n * paginationOpts: { numItems: 20, cursor: null },\n * orderBy: \"_creationTime\",\n * order: \"asc\",\n * });\n * ```\n */\n list: async (\n ctx: ComponentReadCtx,\n opts?: MemberListOpts,\n ): Promise<Paginated<Doc<\"GroupMember\">>> => {\n return (await ctx.runQuery(config.component.group.member.list, {\n where: opts?.where,\n paginationOpts: opts?.paginationOpts ?? { numItems: 50, cursor: null },\n orderBy: opts?.orderBy,\n order: opts?.order,\n })) as Paginated<Doc<\"GroupMember\">>;\n },\n /**\n * Remove a membership by its document ID.\n *\n * @param ctx - Convex mutation context.\n * @param opts.id - The membership document ID.\n * @returns `null`.\n *\n * @example\n * ```ts\n * await auth.member.remove(ctx, { id: memberId });\n * ```\n */\n remove: async (ctx: ComponentCtx, opts: { id: string }) => {\n await ctx.runMutation(config.component.group.member.remove, { id: opts.id });\n invalidateCtxCache(ctx, \"member\");\n invalidateCtxCache(ctx, \"member-inspect\");\n return null;\n },\n /**\n * Patch a membership's `roleIds`, `status`, or `extend` fields.\n * Role IDs are validated against `definePermissions()`.\n *\n * @param ctx - Convex mutation context.\n * @param opts.id - The membership document ID.\n * @param opts.patch - Fields to merge. `roleIds` are validated.\n * @returns `null`.\n * @throws `INVALID_ROLE_IDS` if any supplied role IDs are not defined.\n *\n * @example\n * ```ts\n * await auth.member.update(ctx, {\n * id: memberId,\n * patch: {\n * roleIds: [roles.orgAdmin.id],\n * status: \"active\",\n * },\n * });\n * ```\n */\n update: async (ctx: ComponentCtx, opts: { id: string; patch: Record<string, unknown> }) => {\n const nextData = { ...opts.patch };\n if (\"roleIds\" in nextData) {\n nextData.roleIds = normalizeRoleIds(\n Array.isArray(nextData.roleIds) ? (nextData.roleIds as string[]) : undefined,\n );\n }\n await ctx.runMutation(config.component.group.member.update, {\n id: opts.id,\n patch: nextData,\n });\n invalidateCtxCache(ctx, `member:${opts.id}`);\n invalidateCtxCache(ctx, \"member-inspect\");\n return null;\n },\n /**\n * Read a user's direct membership and its resolved grants.\n *\n * @param ctx - Convex query or mutation context.\n * @param opts.userId - The user's document ID.\n * @param opts.groupId - The group to check membership in.\n * @returns `{ membership, roleIds, grants }`.\n *\n * @example Direct lookup\n * ```ts\n * const result = await auth.member.get(ctx, { userId, groupId });\n * if (!result.membership) return null;\n * ```\n *\n * @example Check grants after inspection\n * ```ts\n * const result = await auth.member.get(ctx, {\n * userId, groupId,\n * });\n * const canCreate = result.grants.includes(\"issues.create\");\n * ```\n *\n * @example Batched across many groups (one RPC)\n * ```ts\n * const resolutions = await auth.member.get(ctx, {\n * userId, groupIds: rootGroupIds,\n * });\n * ```\n */\n get: memberInspect,\n /**\n * Resolve inherited membership explicitly by walking toward the root.\n *\n * Unlike {@link get}, this operation performs hierarchy traversal and\n * always reports the traversed groups and the location of the match.\n */\n resolve: async (\n ctx: ComponentReadCtx,\n opts: { userId: string; groupId: string; maxDepth?: number },\n ): Promise<ResolveResult> => {\n const maxDepth = Math.max(0, Math.floor(opts.maxDepth ?? 32));\n const result = (await ctx.runQuery(config.component.group.member.resolve, {\n userId: opts.userId,\n groupId: opts.groupId,\n maxDepth,\n })) as Omit<ResolveResult, \"roleIds\" | \"grants\">;\n const roleIds = result.membership?.roleIds ?? [];\n const caller = await resolveOAuthCaller(ctx);\n return {\n ...result,\n roleIds,\n grants: capGrantsForCaller(caller, opts.userId, resolveGrantedPermissions(roleIds)),\n };\n },\n assert: async (\n ctx: ComponentReadCtx,\n opts: {\n userId: string;\n groupId: string;\n roleIds?: string[];\n grants?: string[];\n },\n ) => {\n const validatedRoleIds = normalizeRoleIds(opts.roleIds);\n const requiredGrants = Array.from(new Set(opts.grants ?? []));\n const roleFilter = validatedRoleIds.length > 0 ? new Set(validatedRoleIds) : null;\n const result: InspectResult = await memberInspect(ctx, {\n userId: opts.userId,\n groupId: opts.groupId,\n });\n if (result.membership === null) {\n throw new ConvexError({\n code: ErrorCode.NOT_A_MEMBER,\n message: \"User is not a member of this group.\",\n groupId: opts.groupId,\n });\n }\n if (roleFilter !== null && !result.roleIds.some((roleId: string) => roleFilter.has(roleId))) {\n throw new ConvexError({\n code: ErrorCode.NOT_A_MEMBER,\n message: \"User is not a member of this group.\",\n groupId: opts.groupId,\n });\n }\n const missingGrants = requiredGrants.filter((grant) => !result.grants.includes(grant));\n if (missingGrants.length > 0) {\n throw new ConvexError({\n code: ErrorCode.MISSING_GRANTS,\n message: \"User is missing required grants.\",\n groupId: opts.groupId,\n missingGrants,\n });\n }\n return result;\n },\n };\n\n return member;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"member.js","names":["membership","membershipRoleIds"],"sources":["../../../src/server/domains/member.ts"],"sourcesContent":["import { ConvexError, type GenericId } from \"convex/values\";\n\nimport { ErrorCode } from \"../../shared/codes\";\nimport type { ComponentCtx, ComponentReadCtx } from \"../component/context\";\nimport { configDefaults } from \"../config\";\nimport { cached, ctxCacheHas, invalidateCtxCache } from \"../cache/context\";\nimport type { Doc } from \"../types\";\nimport { capGrantsForCaller, resolveOAuthCaller } from \"./access\";\n\n/** Convex-native `PaginationResult<T>` shape returned by the `*List` component queries. */\ntype Paginated<T> = {\n page: T[];\n isDone: boolean;\n continueCursor: string;\n splitCursor?: string | null;\n pageStatus?: \"SplitRecommended\" | \"SplitRequired\" | null;\n};\n\ntype MemberDocLike = Doc<\"GroupMember\"> | null;\n\n/** Options accepted by `member.list`. */\ntype MemberListOpts = {\n where?: { groupId?: GenericId<\"Group\">; userId?: GenericId<\"User\">; status?: string };\n paginationOpts: { numItems: number; cursor: string | null };\n orderBy?: \"_creationTime\" | \"status\";\n order?: \"asc\" | \"desc\";\n};\n\ntype InspectResult = {\n membership: MemberDocLike;\n roleIds: string[];\n grants: string[];\n};\n\ntype ResolveResult = InspectResult & {\n matchedGroupId: string | null;\n depth: number | null;\n isDirect: boolean;\n isInherited: boolean;\n traversedGroupIds: string[];\n};\n\nexport type MemberDeps = {\n config: ReturnType<typeof configDefaults>;\n normalizeRoleIds: (roleIds?: string[]) => string[];\n resolveGrantedPermissions: (roleIds?: string[]) => string[];\n};\n\nexport function createMemberDomain(deps: MemberDeps) {\n const { config, normalizeRoleIds, resolveGrantedPermissions } = deps;\n\n function memberInspect(\n ctx: ComponentReadCtx,\n opts: {\n userId: GenericId<\"User\">;\n groupId: GenericId<\"Group\">;\n },\n ): Promise<InspectResult>;\n function memberInspect(\n ctx: ComponentReadCtx,\n opts: { userId: GenericId<\"User\">; groupIds: readonly GenericId<\"Group\">[] },\n ): Promise<Array<InspectResult>>;\n async function memberInspect(\n ctx: ComponentReadCtx,\n opts:\n | {\n userId: GenericId<\"User\">;\n groupId: GenericId<\"Group\">;\n }\n | { userId: GenericId<\"User\">; groupIds: readonly GenericId<\"Group\">[] },\n ): Promise<InspectResult | Array<InspectResult>> {\n const oauthCaller = await resolveOAuthCaller(ctx);\n if (\"groupIds\" in opts) {\n const { userId, groupIds } = opts;\n if (groupIds.length === 0) return [];\n const unique = Array.from(new Set(groupIds));\n const toFetch: GenericId<\"Group\">[] = [];\n for (const groupId of unique) {\n if (!ctxCacheHas(ctx, `member-inspect:${userId}:${groupId}:n`)) {\n toFetch.push(groupId);\n }\n }\n if (toFetch.length > 0) {\n const docs = (await ctx.runQuery(config.component.group.member.get, {\n userId,\n groupIds: toFetch,\n })) as Array<MemberDocLike>;\n for (let i = 0; i < toFetch.length; i += 1) {\n const groupId = toFetch[i]!;\n const value = docs[i] ?? null;\n void cached(ctx, `member-inspect:${userId}:${groupId}:n`, () => Promise.resolve(value));\n }\n }\n return await Promise.all(\n groupIds.map(async (groupId) => {\n const membership = (await cached(ctx, `member-inspect:${userId}:${groupId}:n`, () =>\n ctx.runQuery(config.component.group.member.get, {\n userId,\n groupId,\n }),\n )) as MemberDocLike;\n if (membership === null) {\n return {\n membership: null,\n roleIds: [] as string[],\n grants: [] as string[],\n };\n }\n const membershipRoleIds = membership.roleIds ?? [];\n const membershipGrants = capGrantsForCaller(\n oauthCaller,\n userId,\n resolveGrantedPermissions(membershipRoleIds),\n );\n return {\n membership,\n roleIds: membershipRoleIds,\n grants: membershipGrants,\n };\n }),\n );\n }\n\n const cacheKey = `member-inspect:${opts.userId}:${opts.groupId}:n`;\n const membership = (await cached(ctx, cacheKey, () =>\n ctx.runQuery(config.component.group.member.get, {\n userId: opts.userId,\n groupId: opts.groupId,\n }),\n )) as MemberDocLike;\n\n if (membership === null) {\n return {\n membership: null,\n roleIds: [] as string[],\n grants: [] as string[],\n };\n }\n\n const membershipRoleIds = membership.roleIds ?? [];\n const membershipGrants = capGrantsForCaller(\n oauthCaller,\n opts.userId,\n resolveGrantedPermissions(membershipRoleIds),\n );\n\n return {\n membership,\n roleIds: membershipRoleIds,\n grants: membershipGrants,\n };\n }\n\n const member = {\n /**\n * Add a user to a group with optional role IDs.\n *\n * Role IDs are validated against the roles defined in `definePermissions()` —\n * invalid IDs throw `INVALID_ROLE_IDS`.\n * Throws `DUPLICATE_MEMBERSHIP` if the user is already a member.\n *\n * @param ctx - Convex mutation context.\n * @param opts.data.groupId - The group to add the user to.\n * @param opts.data.userId - The user's document ID.\n * @param opts.data.roleIds - Role IDs from `definePermissions()` (optional).\n * @param opts.data.status - Membership status string (optional, app-defined).\n * @param opts.data.extend - Arbitrary app-specific metadata.\n * @returns The created membership ID.\n * @throws `INVALID_ROLE_IDS` if any supplied role IDs are not defined.\n *\n * @example\n * ```ts\n * const memberId = await auth.member.create(ctx, {\n * data: {\n * groupId: orgId,\n * userId,\n * roleIds: [roles.orgAdmin.id],\n * },\n * });\n * ```\n */\n create: async (\n ctx: ComponentCtx,\n opts: {\n data: {\n groupId: GenericId<\"Group\">;\n userId: GenericId<\"User\">;\n roleIds?: string[];\n status?: string;\n extend?: Record<string, unknown>;\n };\n },\n ) => {\n const data = opts.data;\n const roleIds = normalizeRoleIds(data.roleIds);\n const memberId = (await ctx.runMutation(config.component.group.member.create, {\n ...data,\n roleIds,\n })) as GenericId<\"GroupMember\">;\n invalidateCtxCache(ctx, `member-inspect:${data.userId}:${data.groupId}`);\n return memberId;\n },\n /**\n * List memberships with optional filtering and pagination.\n *\n * Supports filtering by `groupId`, `userId`, and `status`.\n * When `groupId` and `status` are both provided, a compound index\n * is used for efficient queries.\n *\n * @param ctx - Convex query or mutation context.\n * @param opts.where - Filter criteria (all optional).\n * @param opts.paginationOpts - Convex pagination options.\n * @param opts.orderBy - Sort field: `\"_creationTime\"` or `\"status\"`.\n * @param opts.order - Sort direction: `\"asc\"` or `\"desc\"`.\n * @returns Convex `PaginationResult` — `{ page, isDone, continueCursor }`.\n *\n * @example\n * ```ts\n * const { page } = await auth.member.list(ctx, {\n * where: { groupId: orgId },\n * paginationOpts: { numItems: 20, cursor: null },\n * orderBy: \"_creationTime\",\n * order: \"asc\",\n * });\n * ```\n */\n list: async (\n ctx: ComponentReadCtx,\n opts?: MemberListOpts,\n ): Promise<Paginated<Doc<\"GroupMember\">>> => {\n return (await ctx.runQuery(config.component.group.member.list, {\n where: opts?.where,\n paginationOpts: opts?.paginationOpts ?? { numItems: 50, cursor: null },\n orderBy: opts?.orderBy,\n order: opts?.order,\n })) as Paginated<Doc<\"GroupMember\">>;\n },\n /**\n * Remove a membership by its document ID.\n *\n * @param ctx - Convex mutation context.\n * @param opts.id - The membership document ID.\n * @returns `null`.\n *\n * @example\n * ```ts\n * await auth.member.remove(ctx, { id: memberId });\n * ```\n */\n remove: async (ctx: ComponentCtx, opts: { id: GenericId<\"GroupMember\"> }) => {\n await ctx.runMutation(config.component.group.member.remove, { id: opts.id });\n invalidateCtxCache(ctx, \"member\");\n invalidateCtxCache(ctx, \"member-inspect\");\n return null;\n },\n /**\n * Patch a membership's `roleIds`, `status`, or `extend` fields.\n * Role IDs are validated against `definePermissions()`.\n *\n * @param ctx - Convex mutation context.\n * @param opts.id - The membership document ID.\n * @param opts.patch - Fields to merge. `roleIds` are validated.\n * @returns `null`.\n * @throws `INVALID_ROLE_IDS` if any supplied role IDs are not defined.\n *\n * @example\n * ```ts\n * await auth.member.update(ctx, {\n * id: memberId,\n * patch: {\n * roleIds: [roles.orgAdmin.id],\n * status: \"active\",\n * },\n * });\n * ```\n */\n update: async (\n ctx: ComponentCtx,\n opts: { id: GenericId<\"GroupMember\">; patch: Record<string, unknown> },\n ) => {\n const nextData = { ...opts.patch };\n if (\"roleIds\" in nextData) {\n nextData.roleIds = normalizeRoleIds(\n Array.isArray(nextData.roleIds) ? (nextData.roleIds as string[]) : undefined,\n );\n }\n await ctx.runMutation(config.component.group.member.update, {\n id: opts.id,\n patch: nextData,\n });\n invalidateCtxCache(ctx, `member:${opts.id}`);\n invalidateCtxCache(ctx, \"member-inspect\");\n return null;\n },\n /**\n * Read a user's direct membership and its resolved grants.\n *\n * @param ctx - Convex query or mutation context.\n * @param opts.userId - The user's document ID.\n * @param opts.groupId - The group to check membership in.\n * @returns `{ membership, roleIds, grants }`.\n *\n * @example Direct lookup\n * ```ts\n * const result = await auth.member.get(ctx, { userId, groupId });\n * if (!result.membership) return null;\n * ```\n *\n * @example Check grants after inspection\n * ```ts\n * const result = await auth.member.get(ctx, {\n * userId, groupId,\n * });\n * const canCreate = result.grants.includes(\"issues.create\");\n * ```\n *\n * @example Batched across many groups (one RPC)\n * ```ts\n * const resolutions = await auth.member.get(ctx, {\n * userId, groupIds: rootGroupIds,\n * });\n * ```\n */\n get: memberInspect,\n /**\n * Resolve inherited membership explicitly by walking toward the root.\n *\n * Unlike {@link get}, this operation performs hierarchy traversal and\n * always reports the traversed groups and the location of the match.\n */\n resolve: async (\n ctx: ComponentReadCtx,\n opts: { userId: GenericId<\"User\">; groupId: GenericId<\"Group\">; maxDepth?: number },\n ): Promise<ResolveResult> => {\n const maxDepth = Math.max(0, Math.floor(opts.maxDepth ?? 32));\n const result = (await ctx.runQuery(config.component.group.member.resolve, {\n userId: opts.userId,\n groupId: opts.groupId,\n maxDepth,\n })) as Omit<ResolveResult, \"roleIds\" | \"grants\">;\n const roleIds = result.membership?.roleIds ?? [];\n const caller = await resolveOAuthCaller(ctx);\n return {\n ...result,\n roleIds,\n grants: capGrantsForCaller(caller, opts.userId, resolveGrantedPermissions(roleIds)),\n };\n },\n assert: async (\n ctx: ComponentReadCtx,\n opts: {\n userId: GenericId<\"User\">;\n groupId: GenericId<\"Group\">;\n roleIds?: string[];\n grants?: string[];\n },\n ) => {\n const validatedRoleIds = normalizeRoleIds(opts.roleIds);\n const requiredGrants = Array.from(new Set(opts.grants ?? []));\n const roleFilter = validatedRoleIds.length > 0 ? new Set(validatedRoleIds) : null;\n const result: InspectResult = await memberInspect(ctx, {\n userId: opts.userId,\n groupId: opts.groupId,\n });\n if (result.membership === null) {\n throw new ConvexError({\n code: ErrorCode.NOT_A_MEMBER,\n message: \"User is not a member of this group.\",\n groupId: opts.groupId,\n });\n }\n if (roleFilter !== null && !result.roleIds.some((roleId: string) => roleFilter.has(roleId))) {\n throw new ConvexError({\n code: ErrorCode.NOT_A_MEMBER,\n message: \"User is not a member of this group.\",\n groupId: opts.groupId,\n });\n }\n const missingGrants = requiredGrants.filter((grant) => !result.grants.includes(grant));\n if (missingGrants.length > 0) {\n throw new ConvexError({\n code: ErrorCode.MISSING_GRANTS,\n message: \"User is missing required grants.\",\n groupId: opts.groupId,\n missingGrants,\n });\n }\n return result;\n },\n };\n\n return member;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","names":[],"sources":["../../../src/server/domains/session.ts"],"sourcesContent":["import { GenericActionCtx, GenericDataModel } from \"convex/server\";\nimport { GenericId } from \"convex/values\";\n\nimport type { ComponentReadCtx } from \"../component/context\";\nimport { configDefaults } from \"../config\";\nimport { cached } from \"../cache/context\";\nimport { getAuthSessionId } from \"../session/lifecycle\";\nimport type { Doc } from \"../types\";\n\ntype ComponentAuthReadCtx = ComponentReadCtx & { auth: import(\"convex/server\").Auth };\n\nexport type SessionDeps = {\n config: ReturnType<typeof configDefaults>;\n callInvalidateSessions: <DataModel extends GenericDataModel>(\n ctx: GenericActionCtx<DataModel>,\n args: { userId: GenericId<\"User\">; except?: GenericId<\"Session\">[] },\n ) => Promise<void>;\n};\n\nexport function createSessionDomain(deps: SessionDeps) {\n const { config, callInvalidateSessions } = deps;\n\n return {\n /**\n * Revoke (sign out) all sessions for a given user.\n *\n *
|
|
1
|
+
{"version":3,"file":"session.js","names":[],"sources":["../../../src/server/domains/session.ts"],"sourcesContent":["import { GenericActionCtx, GenericDataModel } from \"convex/server\";\nimport { GenericId } from \"convex/values\";\n\nimport type { ComponentReadCtx } from \"../component/context\";\nimport { configDefaults } from \"../config\";\nimport { cached } from \"../cache/context\";\nimport { getAuthSessionId } from \"../session/lifecycle\";\nimport type { Doc } from \"../types\";\n\ntype ComponentAuthReadCtx = ComponentReadCtx & { auth: import(\"convex/server\").Auth };\n\nexport type SessionDeps = {\n config: ReturnType<typeof configDefaults>;\n callInvalidateSessions: <DataModel extends GenericDataModel>(\n ctx: GenericActionCtx<DataModel>,\n args: { userId: GenericId<\"User\">; except?: GenericId<\"Session\">[] },\n ) => Promise<void>;\n};\n\nexport function createSessionDomain(deps: SessionDeps) {\n const { config, callInvalidateSessions } = deps;\n\n return {\n /**\n * Revoke (sign out) all sessions for a given user.\n *\n * Advances the user's session epoch, so every non-excluded session stops\n * authenticating and refreshing atomically. Physical cleanup is bounded\n * and never defines the revocation boundary.\n * Optionally, one or more sessions can be excluded — this is useful\n * when you want to sign out all *other* devices while keeping the\n * current session alive.\n *\n * This method delegates to the component's internal session\n * invalidation RPC.\n *\n * @param ctx - Convex action context.\n * @param args.userId - The user whose sessions should be revoked.\n * @param args.except - Optional array of session IDs to keep valid.\n * @returns `{ userId, except }` confirming the operation.\n *\n * @example Sign out everywhere except the current session\n * ```ts\n * const identity = await ctx.auth.getUserIdentity();\n * const sessionId = identity?.sid;\n * await auth.session.revoke(ctx, {\n * userId,\n * except: sessionId ? [sessionId] : [],\n * });\n * ```\n */\n revoke: async <DataModel extends GenericDataModel>(\n ctx: GenericActionCtx<DataModel>,\n args: { userId: GenericId<\"User\">; except?: GenericId<\"Session\">[] },\n ) => {\n await callInvalidateSessions(ctx, args);\n return {\n userId: args.userId,\n except: args.except ?? [],\n };\n },\n /**\n * Read a session document by ID.\n *\n * Returns the full session document from the component database, or\n * `null` if no session with the given ID exists. Useful for inspecting\n * session metadata such as creation time or associated device info.\n *\n * @param ctx - Convex query or mutation context.\n * @param opts.id - The session's document ID.\n * @returns The session document, or `null` if not found.\n *\n * @example\n * ```ts\n * const session = await auth.session.get(ctx, { id: sessionId });\n * if (!session) throw new Error(\"Session not found\");\n * ```\n */\n get: async (\n ctx: ComponentReadCtx,\n opts: { id: GenericId<\"Session\"> },\n ): Promise<Doc<\"Session\"> | null> => {\n return (await cached(ctx, `session:${opts.id}`, () =>\n ctx.runQuery(config.component.session.get, {\n id: opts.id,\n }),\n )) as Doc<\"Session\"> | null;\n },\n /**\n * The current session's id, or `null` when unauthenticated.\n *\n * Pairs with `auth.user.id(ctx)`; resolves the session id from the\n * incoming JWT without a DB read.\n *\n * @example\n * ```ts\n * const sessionId = await auth.session.id(ctx);\n * if (sessionId === null) return null;\n * ```\n */\n id: async (ctx: ComponentAuthReadCtx) => {\n return (await getAuthSessionId(ctx)) as GenericId<\"Session\"> | null;\n },\n /**\n * List a bounded set of sessions belonging to a user.\n *\n * Returns at most 16 sessions in the user's current revocation epoch. The\n * result includes `expirationTime` so a UI can compare it with the client\n * clock without making a cached Convex query depend on wall-clock time.\n *\n * @param ctx - Convex query or mutation context.\n * @param opts.userId - The user whose sessions to list.\n * @returns An array of session documents.\n *\n * @example\n * ```ts\n * const sessions = await auth.session.list(ctx, { userId });\n * console.log(`User has ${sessions.length} sessions`);\n * ```\n */\n list: async (\n ctx: ComponentReadCtx,\n opts: { userId: GenericId<\"User\"> },\n ): Promise<Doc<\"Session\">[]> => {\n return (await ctx.runQuery(config.component.session.list, {\n userId: opts.userId,\n })) as Doc<\"Session\">[];\n },\n };\n}\n"],"mappings":";;;;AAmBA,SAAgB,oBAAoB,MAAmB;CACrD,MAAM,EAAE,QAAQ,2BAA2B;AAE3C,QAAO;EA6BL,QAAQ,OACN,KACA,SACG;AACH,SAAM,uBAAuB,KAAK,KAAK;AACvC,UAAO;IACL,QAAQ,KAAK;IACb,QAAQ,KAAK,UAAU,EAAE;IAC1B;;EAmBH,KAAK,OACH,KACA,SACmC;AACnC,UAAQ,MAAM,OAAO,KAAK,WAAW,KAAK,YACxC,IAAI,SAAS,OAAO,UAAU,QAAQ,KAAK,EACzC,IAAI,KAAK,IACV,CAAC,CACH;;EAcH,IAAI,OAAO,QAA8B;AACvC,UAAQ,MAAM,iBAAiB,IAAI;;EAmBrC,MAAM,OACJ,KACA,SAC8B;AAC9B,UAAQ,MAAM,IAAI,SAAS,OAAO,UAAU,QAAQ,MAAM,EACxD,QAAQ,KAAK,QACd,CAAC;;EAEL"}
|