@robelest/convex-auth 0.0.4-preview.27 → 0.0.4-preview.28

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.
Files changed (88) hide show
  1. package/README.md +3 -5
  2. package/dist/bin.js +6488 -1571
  3. package/dist/browser/index.js +10 -7
  4. package/dist/browser/locks.js +3 -5
  5. package/dist/browser/navigation.js +7 -10
  6. package/dist/browser/runtime.js +35 -33
  7. package/dist/client/core/types.js +17 -0
  8. package/dist/client/factors/device.js +26 -19
  9. package/dist/client/index.js +151 -163
  10. package/dist/client/runtime/proxy.js +6 -6
  11. package/dist/client/services/adapters.js +3 -7
  12. package/dist/client/services/http.js +2 -5
  13. package/dist/client/services/resolve.js +5 -11
  14. package/dist/client/services/runtime.js +2 -5
  15. package/dist/component/_generated/component.d.ts +46 -0
  16. package/dist/component/index.d.ts +3 -3
  17. package/dist/component/model.d.ts +25 -25
  18. package/dist/component/public/identity/sessions.js +38 -1
  19. package/dist/component/public/identity/tokens.js +81 -3
  20. package/dist/component/public/identity/verifiers.js +9 -3
  21. package/dist/component/public.js +3 -3
  22. package/dist/component/schema.d.ts +320 -320
  23. package/dist/core/index.d.ts +380 -0
  24. package/dist/core/index.js +83 -0
  25. package/dist/otel.d.ts +13 -17
  26. package/dist/otel.js +39 -49
  27. package/dist/providers/email.d.ts +2 -2
  28. package/dist/providers/password.js +8 -16
  29. package/dist/providers/phone.js +2 -9
  30. package/dist/server/auth-context.d.ts +204 -0
  31. package/dist/server/auth-context.js +76 -0
  32. package/dist/server/auth.d.ts +25 -187
  33. package/dist/server/auth.js +5 -96
  34. package/dist/server/componentContext.d.ts +12 -0
  35. package/dist/server/componentContext.js +1 -0
  36. package/dist/server/config.js +1 -12
  37. package/dist/server/constants.js +6 -0
  38. package/dist/server/contract.d.ts +1 -1
  39. package/dist/server/core.js +5 -14
  40. package/dist/server/crypto.js +26 -18
  41. package/dist/server/db.js +6 -1
  42. package/dist/server/device.js +88 -78
  43. package/dist/server/http.d.ts +4 -3
  44. package/dist/server/http.js +74 -86
  45. package/dist/server/index.d.ts +2 -1
  46. package/dist/server/limits.js +22 -15
  47. package/dist/server/mounts.d.ts +103 -103
  48. package/dist/server/mutations/account.js +6 -4
  49. package/dist/server/mutations/invalidate.js +3 -6
  50. package/dist/server/mutations/oauth.js +86 -88
  51. package/dist/server/mutations/refresh.js +45 -87
  52. package/dist/server/mutations/register.js +19 -19
  53. package/dist/server/mutations/retrieve.js +17 -15
  54. package/dist/server/mutations/signature.js +9 -13
  55. package/dist/server/mutations/signin.js +7 -3
  56. package/dist/server/mutations/signout.js +10 -15
  57. package/dist/server/mutations/store.js +22 -12
  58. package/dist/server/mutations/verifier.js +11 -6
  59. package/dist/server/mutations/verify.js +55 -46
  60. package/dist/server/oauth/runtime.js +27 -25
  61. package/dist/server/passkey.js +299 -250
  62. package/dist/server/prefetch.js +283 -281
  63. package/dist/server/refresh.js +7 -60
  64. package/dist/server/runtime.d.ts +82 -206
  65. package/dist/server/runtime.js +63 -56
  66. package/dist/server/services/config.js +5 -3
  67. package/dist/server/services/logger.js +2 -4
  68. package/dist/server/services/providers.js +2 -4
  69. package/dist/server/services/refresh.js +2 -4
  70. package/dist/server/services/resolve.js +15 -14
  71. package/dist/server/services/signin.js +2 -4
  72. package/dist/server/sessions.js +32 -33
  73. package/dist/server/signin.js +177 -142
  74. package/dist/server/sso/domain.d.ts +20 -68
  75. package/dist/server/sso/domain.js +444 -413
  76. package/dist/server/sso/http.js +53 -59
  77. package/dist/server/sso/oidc.js +94 -80
  78. package/dist/server/tokens.js +13 -3
  79. package/dist/server/totp.js +153 -116
  80. package/dist/server/types.d.ts +2 -2
  81. package/dist/server/users.js +18 -23
  82. package/dist/server/utils/cache.js +51 -0
  83. package/dist/server/utils/dispatch.js +36 -0
  84. package/dist/server/utils/retry.js +24 -0
  85. package/dist/server/utils/span.js +32 -0
  86. package/dist/shared/errors.js +9 -3
  87. package/dist/shared/log.js +20 -22
  88. package/package.json +41 -33
@@ -1,4 +1,4 @@
1
- import { createUnauthenticatedAuthContext, getAuthContext } from "./context.js";
1
+ import { assertAuthResolverContext, createAuthContextCustomization, createPublicAuthContext } from "./auth-context.js";
2
2
  import { Auth } from "./runtime.js";
3
3
  import { ConvexError } from "convex/values";
4
4
 
@@ -29,29 +29,6 @@ import { ConvexError } from "convex/values";
29
29
  *
30
30
  * @see {@link AuthContextConfig}
31
31
  */
32
- async function resolveConfiguredAuthContext(auth, ctx, config) {
33
- const fallback = () => getAuthContext(auth, ctx);
34
- const authOverride = config?.authResolve ? await config.authResolve(ctx, fallback) : void 0;
35
- return authOverride === void 0 ? await fallback() : authOverride;
36
- }
37
- function createNotSignedInError() {
38
- return new ConvexError({
39
- code: "NOT_SIGNED_IN",
40
- message: "Authentication required."
41
- });
42
- }
43
- async function createPublicAuthContext(auth, ctx, config) {
44
- const resolved = await resolveConfiguredAuthContext(auth, ctx, config);
45
- if (resolved === null) {
46
- if (config?.optional !== true) throw createNotSignedInError();
47
- return createUnauthenticatedAuthContext();
48
- }
49
- const extra = config?.resolve ? await config.resolve(ctx, resolved.user, resolved) : {};
50
- return {
51
- ...resolved,
52
- ...extra
53
- };
54
- }
55
32
  function createAuth(component, config) {
56
33
  const authResult = Auth({
57
34
  ...config,
@@ -168,81 +145,13 @@ function createAuth(component, config) {
168
145
  invite: authResult.auth.invite,
169
146
  key: authResult.auth.key,
170
147
  http: authResult.auth.http,
171
- context: ((ctx, config$1) => createPublicAuthContext(authResult.auth, ctx, config$1)),
148
+ context: ((ctx, config$1) => {
149
+ assertAuthResolverContext(ctx);
150
+ return createPublicAuthContext(authResult.auth, ctx, config$1);
151
+ }),
172
152
  ctx: ((config$1) => createAuthContextCustomization(authResult.auth, config$1))
173
153
  };
174
154
  }
175
- /**
176
- * Create a context enrichment for `customQuery` / `customMutation` — optional auth.
177
- *
178
- * When `optional: true` is set, unauthenticated requests are allowed.
179
- * The enriched `ctx.auth` will have `userId: null`, `user: null`,
180
- * `groupId: null`, `role: null`, and `grants: []` for unauthenticated callers.
181
- *
182
- * @param config - Configuration with `optional: true` and an optional
183
- * `resolve` callback for attaching extra fields to the auth context.
184
- * @returns An object with `args` and `input` compatible with Convex
185
- * custom function builders.
186
- *
187
- * @example
188
- * ```ts
189
- * const authCtx = auth.ctx({
190
- * optional: true,
191
- * resolve: async (_ctx, user) => ({ plan: user.extend?.plan ?? null }),
192
- * });
193
- * ```
194
- *
195
- * @see {@link createAuth}
196
- */
197
- /**
198
- * Create a context enrichment for `customQuery` / `customMutation` — required auth (default).
199
- *
200
- * When `optional` is omitted or `false`, unauthenticated requests throw a
201
- * structured `ConvexError` before your handler runs.
202
- *
203
- * @param config - Optional configuration with a `resolve` callback
204
- * for attaching extra fields to the auth context.
205
- * @returns An object with `args` and `input` compatible with Convex
206
- * custom function builders.
207
- *
208
- * @example
209
- * ```ts
210
- * const authCtx = auth.ctx({
211
- * resolve: async (_ctx, user) => ({ email: user.email }),
212
- * });
213
- * ```
214
- *
215
- * @see {@link createAuth}
216
- */
217
- function createAuthContextCustomization(auth, config) {
218
- return {
219
- args: {},
220
- input: async (ctx, _args, _extra) => {
221
- const nativeAuth = ctx.auth;
222
- const getUserIdentity = nativeAuth.getUserIdentity.bind(nativeAuth);
223
- const resolved = await resolveConfiguredAuthContext(auth, ctx, config);
224
- if (resolved === null) {
225
- if (config?.optional !== true) throw createNotSignedInError();
226
- return {
227
- ctx: { auth: {
228
- getUserIdentity,
229
- ...createUnauthenticatedAuthContext()
230
- } },
231
- args: {}
232
- };
233
- }
234
- const extra = config?.resolve ? await config.resolve(ctx, resolved.user, resolved) : {};
235
- return {
236
- ctx: { auth: {
237
- getUserIdentity,
238
- ...resolved,
239
- ...extra
240
- } },
241
- args: {}
242
- };
243
- }
244
- };
245
- }
246
155
 
247
156
  //#endregion
248
157
  export { createAuth };
@@ -0,0 +1,12 @@
1
+ import { GenericDataModel, GenericMutationCtx, GenericQueryCtx } from "convex/server";
2
+
3
+ //#region src/server/componentContext.d.ts
4
+ type ComponentReadCtx = {
5
+ runQuery: GenericQueryCtx<GenericDataModel>["runQuery"];
6
+ };
7
+ type ComponentCtx = ComponentReadCtx & {
8
+ runMutation: GenericMutationCtx<GenericDataModel>["runMutation"];
9
+ };
10
+ //#endregion
11
+ export { ComponentCtx, ComponentReadCtx };
12
+ //# sourceMappingURL=componentContext.d.ts.map
@@ -0,0 +1 @@
1
+ export { };
@@ -12,17 +12,6 @@ function configDefaults(config_) {
12
12
  };
13
13
  }
14
14
  /**
15
- * Materialize a single provider config into its runtime form.
16
- */
17
- function materializeProvider(provider) {
18
- const config = {
19
- providers: [provider],
20
- component: {}
21
- };
22
- materializeAndDefaultProviders(config);
23
- return config.providers[0];
24
- }
25
- /**
26
15
  * List available provider IDs for error messages.
27
16
  */
28
17
  function listAvailableProviders(config, allowExtraProviders) {
@@ -67,5 +56,5 @@ function normalizeAuthorizationConfig(authorization) {
67
56
  }
68
57
 
69
58
  //#endregion
70
- export { configDefaults, listAvailableProviders, materializeProvider };
59
+ export { configDefaults, listAvailableProviders };
71
60
  //# sourceMappingURL=config.js.map
@@ -0,0 +1,6 @@
1
+ //#region src/server/constants.ts
2
+ const TOKEN_SUB_CLAIM_DIVIDER = "|";
3
+
4
+ //#endregion
5
+ export { TOKEN_SUB_CLAIM_DIVIDER };
6
+ //# sourceMappingURL=constants.js.map
@@ -1,5 +1,5 @@
1
+ import "./componentContext.js";
1
2
  import "./types.js";
2
- import { GenericActionCtx, GenericDataModel } from "convex/server";
3
3
 
4
4
  //#region src/server/contract.d.ts
5
5
  type GroupConnectionRecord = {
@@ -1,9 +1,7 @@
1
1
  import { getSessionUserId } from "./context.js";
2
- import { materializeProvider } from "./config.js";
3
2
  import { generateRandomString, sha256 } from "./random.js";
4
3
  import { buildScopeChecker, checkKeyRateLimit, generateApiKey, hashApiKey } from "./keys.js";
5
- import { TOKEN_SUB_CLAIM_DIVIDER } from "./tokens.js";
6
- import { signInImpl } from "./signin.js";
4
+ import { TOKEN_SUB_CLAIM_DIVIDER } from "./constants.js";
7
5
  import { ConvexError } from "convex/values";
8
6
 
9
7
  //#region src/server/core.ts
@@ -21,7 +19,7 @@ import { ConvexError } from "convex/values";
21
19
  * @returns The core domain namespaces consumed by the auth factory.
22
20
  */
23
21
  function createCoreDomains(deps) {
24
- const { config, callInvalidateSessions, callCreateAccountFromCredentials, callRetrieveAccountWithCredentials, callModifyAccount, getEnrichCtx, inviteTokenAlphabet, inviteTokenLength } = deps;
22
+ const { config, callInvalidateSessions, callCreateAccountFromCredentials, callRetrieveAccountWithCredentials, callModifyAccount, inviteTokenAlphabet, inviteTokenLength } = deps;
25
23
  const roleDefinitions = config.authorization.roles;
26
24
  const getRoleDefinition = (roleId) => {
27
25
  return roleDefinitions[roleId] ?? null;
@@ -229,16 +227,9 @@ function createCoreDomains(deps) {
229
227
  return { totpId };
230
228
  }
231
229
  };
232
- const provider = { signIn: async (ctx, providerConfig, args) => {
233
- const result = await signInImpl(getEnrichCtx()(ctx), materializeProvider(providerConfig), args, {
234
- generateTokens: false,
235
- allowExtraProviders: true
236
- });
237
- return result.kind === "signedIn" ? result.signedIn !== null ? {
238
- userId: result.signedIn.userId,
239
- sessionId: result.signedIn.sessionId
240
- } : null : null;
241
- } };
230
+ const provider = { signIn: deps.signInForProvider ? async (ctx, providerConfig, args) => {
231
+ return deps.signInForProvider(ctx, providerConfig, args);
232
+ } : void 0 };
242
233
  const group = {
243
234
  create: async (ctx, data) => {
244
235
  return { groupId: await ctx.runMutation(config.component.public.groupCreate, data) };
@@ -1,5 +1,4 @@
1
1
  import { ConvexError } from "convex/values";
2
- import { Effect, Match, Option, pipe } from "effect";
3
2
 
4
3
  //#region src/server/crypto.ts
5
4
  function errorMessage(error) {
@@ -9,29 +8,38 @@ const credentialsError = (code, message) => new ConvexError({
9
8
  code,
10
9
  message
11
10
  });
12
- const asCredentialsProvider = (provider) => Match.value(provider).pipe(Match.when({ type: "credentials" }, (provider$1) => Effect.succeed(provider$1)), Match.orElse((provider$1) => Effect.fail(credentialsError("INVALID_CREDENTIALS_PROVIDER", `Provider ${provider$1.id} is not a credentials provider`))));
11
+ function asCredentialsProvider(provider) {
12
+ if (provider.type !== "credentials") throw credentialsError("INVALID_CREDENTIALS_PROVIDER", `Provider ${provider.id} is not a credentials provider`);
13
+ return provider;
14
+ }
13
15
  /**
14
16
  * Hash a secret using the provider's `crypto.hashSecret` function.
17
+ * @internal
15
18
  */
16
- /** @internal */
17
- const hash = (provider, secret) => Effect.flatMap(asCredentialsProvider(provider), (provider$1) => pipe(Option.fromNullishOr(provider$1.crypto?.hashSecret), Option.match({
18
- onNone: () => Effect.fail(credentialsError("MISSING_CRYPTO_FUNCTION", `Provider ${provider$1.id} does not have a \`crypto.hashSecret\` function`)),
19
- onSome: (hashSecret) => Effect.tryPromise({
20
- try: () => hashSecret(secret),
21
- catch: (error) => credentialsError("INTERNAL_ERROR", `Hash failed: ${errorMessage(error)}`)
22
- })
23
- })));
19
+ async function hash(provider, secret) {
20
+ const credProvider = asCredentialsProvider(provider);
21
+ const hashSecret = credProvider.crypto?.hashSecret;
22
+ if (!hashSecret) throw credentialsError("MISSING_CRYPTO_FUNCTION", `Provider ${credProvider.id} does not have a \`crypto.hashSecret\` function`);
23
+ try {
24
+ return await hashSecret(secret);
25
+ } catch (error) {
26
+ throw credentialsError("INTERNAL_ERROR", `Hash failed: ${errorMessage(error)}`);
27
+ }
28
+ }
24
29
  /**
25
30
  * Verify a secret against a hash using the provider's `crypto.verifySecret` function.
31
+ * @internal
26
32
  */
27
- /** @internal */
28
- const verify = (provider, secret, hashValue) => Effect.flatMap(asCredentialsProvider(provider), (provider$1) => pipe(Option.fromNullishOr(provider$1.crypto?.verifySecret), Option.match({
29
- onNone: () => Effect.fail(credentialsError("MISSING_CRYPTO_FUNCTION", `Provider ${provider$1.id} does not have a \`crypto.verifySecret\` function`)),
30
- onSome: (verifySecret) => Effect.tryPromise({
31
- try: () => verifySecret(secret, hashValue),
32
- catch: (error) => credentialsError("INTERNAL_ERROR", `Verify failed: ${errorMessage(error)}`)
33
- })
34
- })));
33
+ async function verify(provider, secret, hashValue) {
34
+ const credProvider = asCredentialsProvider(provider);
35
+ const verifySecret = credProvider.crypto?.verifySecret;
36
+ if (!verifySecret) throw credentialsError("MISSING_CRYPTO_FUNCTION", `Provider ${credProvider.id} does not have a \`crypto.verifySecret\` function`);
37
+ try {
38
+ return await verifySecret(secret, hashValue);
39
+ } catch (error) {
40
+ throw credentialsError("INTERNAL_ERROR", `Verify failed: ${errorMessage(error)}`);
41
+ }
42
+ }
35
43
 
36
44
  //#endregion
37
45
  export { hash, verify };
package/dist/server/db.js CHANGED
@@ -35,12 +35,16 @@ function authDb(ctx, config) {
35
35
  userId,
36
36
  expirationTime
37
37
  }),
38
+ issue: (args) => ctx.runMutation(component.public.sessionIssue, args),
38
39
  getById: (sessionId) => ctx.runQuery(component.public.sessionGetById, { sessionId }),
39
40
  delete: (sessionId) => ctx.runMutation(component.public.sessionDelete, { sessionId }),
40
41
  listByUser: (userId) => ctx.runQuery(component.public.sessionListByUser, { userId })
41
42
  },
42
43
  verifiers: {
43
- create: (sessionId) => ctx.runMutation(component.public.verifierCreate, { sessionId }),
44
+ create: (sessionId, signature) => ctx.runMutation(component.public.verifierCreate, {
45
+ sessionId,
46
+ signature
47
+ }),
44
48
  getById: (verifierId) => ctx.runQuery(component.public.verifierGetById, { verifierId }),
45
49
  getBySignature: (signature) => ctx.runQuery(component.public.verifierGetBySignature, { signature }),
46
50
  patch: (verifierId, data) => ctx.runMutation(component.public.verifierPatch, {
@@ -57,6 +61,7 @@ function authDb(ctx, config) {
57
61
  },
58
62
  refreshTokens: {
59
63
  create: (args) => ctx.runMutation(component.public.refreshTokenCreate, args),
64
+ exchange: (args) => ctx.runMutation(component.public.refreshTokenExchange, args),
60
65
  getById: (refreshTokenId) => ctx.runQuery(component.public.refreshTokenGetById, { refreshTokenId }),
61
66
  patch: (refreshTokenId, data) => ctx.runMutation(component.public.refreshTokenPatch, {
62
67
  refreshTokenId,
@@ -1,12 +1,11 @@
1
1
  import { userIdFromIdentitySubject } from "./identity.js";
2
- import { requireEnv } from "./env.js";
3
2
  import { generateRandomString, sha256 } from "./random.js";
3
+ import { requireEnv } from "./env.js";
4
+ import { callSignIn } from "./mutations/signin.js";
4
5
  import { AuthFlowError, authFlowError } from "../shared/errors.js";
5
6
  import { toConvexError } from "./errors.js";
6
- import { callSignIn } from "./mutations/signin.js";
7
7
  import { mutateDeviceAuthorize, mutateDeviceDelete, mutateDeviceInsert, mutateDeviceUpdateLastPolled, queryDeviceByCodeHash, queryDeviceByUserCode } from "./types.js";
8
8
  import { ConvexError } from "convex/values";
9
- import { Effect, Match } from "effect";
10
9
 
11
10
  //#region src/server/device.ts
12
11
  /**
@@ -24,84 +23,95 @@ const assertFlow = (flow) => {
24
23
  if (DEVICE_FLOWS.includes(flow)) return flow;
25
24
  throw deviceError("DEVICE_MISSING_FLOW", "Missing `flow` parameter. Expected one of: create, poll, verify");
26
25
  };
26
+ async function handleCreate(ctx, provider) {
27
+ const deviceCode = generateRandomString(DEVICE_CODE_LENGTH, DEVICE_CODE_ALPHABET);
28
+ const deviceCodeHash = await sha256(deviceCode);
29
+ const rawUserCode = generateRandomString(provider.userCodeLength, provider.charset);
30
+ const mid = Math.floor(rawUserCode.length / 2);
31
+ const userCode = rawUserCode.slice(0, mid) + "-" + rawUserCode.slice(mid);
32
+ await mutateDeviceInsert(ctx, {
33
+ deviceCodeHash,
34
+ userCode,
35
+ expiresAt: Date.now() + provider.expiresIn * 1e3,
36
+ interval: provider.interval,
37
+ status: "pending"
38
+ });
39
+ const verificationUri = provider.verificationUri ?? `${process.env.SITE_URL ?? requireEnv("SITE_URL")}/device`;
40
+ return {
41
+ kind: "deviceCode",
42
+ deviceCode,
43
+ userCode,
44
+ verificationUri,
45
+ verificationUriComplete: `${verificationUri}?code=${encodeURIComponent(userCode)}`,
46
+ expiresIn: provider.expiresIn,
47
+ interval: provider.interval
48
+ };
49
+ }
50
+ async function handlePoll(ctx, params) {
51
+ if (typeof params.deviceCode !== "string") throw deviceError("DEVICE_MISSING_FLOW", "Missing `deviceCode` parameter for poll flow.");
52
+ const doc = await queryDeviceByCodeHash(ctx, await sha256(params.deviceCode));
53
+ if (doc === null) throw deviceError("DEVICE_CODE_EXPIRED", "The device code has expired. Please start a new authorization request.");
54
+ if (Date.now() > doc.expiresAt) {
55
+ await mutateDeviceDelete(ctx, doc._id);
56
+ throw deviceError("DEVICE_CODE_EXPIRED", "The device code has expired. Please start a new authorization request.");
57
+ }
58
+ if (doc.lastPolledAt !== void 0 && (Date.now() - doc.lastPolledAt) / 1e3 < doc.interval) throw deviceError("DEVICE_SLOW_DOWN", "Polling too frequently. Increase the interval between requests.");
59
+ await mutateDeviceUpdateLastPolled(ctx, doc._id, Date.now());
60
+ if (doc.status === "pending") throw deviceError("DEVICE_AUTHORIZATION_PENDING", "The user has not yet authorized this device.");
61
+ if (doc.status === "denied") {
62
+ await mutateDeviceDelete(ctx, doc._id);
63
+ throw deviceError("DEVICE_CODE_DENIED", "The authorization request was denied.");
64
+ }
65
+ if (!doc.userId || !doc.sessionId) throw deviceError("INTERNAL_ERROR", "Authorized device code missing userId or sessionId");
66
+ await mutateDeviceDelete(ctx, doc._id);
67
+ return {
68
+ kind: "signedIn",
69
+ signedIn: await callSignIn(ctx, {
70
+ userId: doc.userId,
71
+ sessionId: doc.sessionId,
72
+ generateTokens: true
73
+ })
74
+ };
75
+ }
76
+ async function handleDeviceVerify(ctx, params) {
77
+ if (typeof params.userCode !== "string") throw deviceError("DEVICE_INVALID_USER_CODE", "Missing `userCode` parameter for verify flow.");
78
+ const identity = await ctx.auth.getUserIdentity();
79
+ if (identity === null) throw deviceError("NOT_SIGNED_IN", "You must be signed in to authorize a device.");
80
+ const userId = userIdFromIdentitySubject(identity.subject);
81
+ const doc = await queryDeviceByUserCode(ctx, params.userCode);
82
+ if (doc === null) throw deviceError("DEVICE_INVALID_USER_CODE", "Invalid or expired user code.");
83
+ if (Date.now() > doc.expiresAt) {
84
+ await mutateDeviceDelete(ctx, doc._id);
85
+ throw deviceError("DEVICE_CODE_EXPIRED", "The device code has expired. Please start a new authorization request.");
86
+ }
87
+ if (doc.status !== "pending") throw deviceError("DEVICE_ALREADY_AUTHORIZED", "This device code has already been authorized.");
88
+ const signInResult = await callSignIn(ctx, {
89
+ userId,
90
+ generateTokens: false
91
+ });
92
+ await mutateDeviceAuthorize(ctx, doc._id, signInResult.userId, signInResult.sessionId);
93
+ return {
94
+ kind: "signedIn",
95
+ signedIn: null
96
+ };
97
+ }
27
98
  /** @internal */
28
- const handleDevice = (ctx, provider, args) => Effect.tryPromise({
29
- try: async () => {
99
+ const handleDevice = async (ctx, provider, args) => {
100
+ try {
30
101
  const params = args.params ?? {};
31
102
  const flow = assertFlow(typeof params.flow === "string" ? params.flow : "create");
32
- return await Match.value(flow).pipe(Match.when("create", async () => {
33
- const deviceCode = generateRandomString(DEVICE_CODE_LENGTH, DEVICE_CODE_ALPHABET);
34
- const deviceCodeHash = await sha256(deviceCode);
35
- const rawUserCode = generateRandomString(provider.userCodeLength, provider.charset);
36
- const mid = Math.floor(rawUserCode.length / 2);
37
- const userCode = rawUserCode.slice(0, mid) + "-" + rawUserCode.slice(mid);
38
- await mutateDeviceInsert(ctx, {
39
- deviceCodeHash,
40
- userCode,
41
- expiresAt: Date.now() + provider.expiresIn * 1e3,
42
- interval: provider.interval,
43
- status: "pending"
44
- });
45
- const verificationUri = provider.verificationUri ?? `${process.env.SITE_URL ?? requireEnv("SITE_URL")}/device`;
46
- return {
47
- kind: "deviceCode",
48
- deviceCode,
49
- userCode,
50
- verificationUri,
51
- verificationUriComplete: `${verificationUri}?code=${encodeURIComponent(userCode)}`,
52
- expiresIn: provider.expiresIn,
53
- interval: provider.interval
54
- };
55
- }), Match.when("poll", async () => {
56
- if (typeof params.deviceCode !== "string") throw deviceError("DEVICE_MISSING_FLOW", "Missing `deviceCode` parameter for poll flow.");
57
- const doc = await queryDeviceByCodeHash(ctx, await sha256(params.deviceCode));
58
- if (doc === null) throw deviceError("DEVICE_CODE_EXPIRED", "The device code has expired. Please start a new authorization request.");
59
- if (Date.now() > doc.expiresAt) {
60
- await mutateDeviceDelete(ctx, doc._id);
61
- throw deviceError("DEVICE_CODE_EXPIRED", "The device code has expired. Please start a new authorization request.");
62
- }
63
- if (doc.lastPolledAt !== void 0 && (Date.now() - doc.lastPolledAt) / 1e3 < doc.interval) throw deviceError("DEVICE_SLOW_DOWN", "Polling too frequently. Increase the interval between requests.");
64
- await mutateDeviceUpdateLastPolled(ctx, doc._id, Date.now());
65
- if (doc.status === "pending") throw deviceError("DEVICE_AUTHORIZATION_PENDING", "The user has not yet authorized this device.");
66
- if (doc.status === "denied") {
67
- await mutateDeviceDelete(ctx, doc._id);
68
- throw deviceError("DEVICE_CODE_DENIED", "The authorization request was denied.");
69
- }
70
- if (!doc.userId || !doc.sessionId) throw deviceError("INTERNAL_ERROR", "Authorized device code missing userId or sessionId");
71
- await mutateDeviceDelete(ctx, doc._id);
72
- return {
73
- kind: "signedIn",
74
- signedIn: await callSignIn(ctx, {
75
- userId: doc.userId,
76
- sessionId: doc.sessionId,
77
- generateTokens: true
78
- })
79
- };
80
- }), Match.when("verify", async () => {
81
- if (typeof params.userCode !== "string") throw deviceError("DEVICE_INVALID_USER_CODE", "Missing `userCode` parameter for verify flow.");
82
- const identity = await ctx.auth.getUserIdentity();
83
- if (identity === null) throw deviceError("NOT_SIGNED_IN", "You must be signed in to authorize a device.");
84
- const userId = userIdFromIdentitySubject(identity.subject);
85
- const doc = await queryDeviceByUserCode(ctx, params.userCode);
86
- if (doc === null) throw deviceError("DEVICE_INVALID_USER_CODE", "Invalid or expired user code.");
87
- if (Date.now() > doc.expiresAt) {
88
- await mutateDeviceDelete(ctx, doc._id);
89
- throw deviceError("DEVICE_CODE_EXPIRED", "The device code has expired. Please start a new authorization request.");
90
- }
91
- if (doc.status !== "pending") throw deviceError("DEVICE_ALREADY_AUTHORIZED", "This device code has already been authorized.");
92
- const signInResult = await callSignIn(ctx, {
93
- userId,
94
- generateTokens: false
95
- });
96
- await mutateDeviceAuthorize(ctx, doc._id, signInResult.userId, signInResult.sessionId);
97
- return {
98
- kind: "signedIn",
99
- signedIn: null
100
- };
101
- }), Match.exhaustive);
102
- },
103
- catch: (error) => error instanceof ConvexError ? error : error instanceof AuthFlowError ? toConvexError(error) : error instanceof Error ? toConvexError(authFlowError("INTERNAL_ERROR", `Device flow failed: ${error.message}`)) : toConvexError(error)
104
- });
103
+ return await new Map([
104
+ ["create", () => handleCreate(ctx, provider)],
105
+ ["poll", () => handlePoll(ctx, params)],
106
+ ["verify", () => handleDeviceVerify(ctx, params)]
107
+ ]).get(flow)();
108
+ } catch (error) {
109
+ if (error instanceof ConvexError) throw error;
110
+ if (error instanceof AuthFlowError) throw toConvexError(error);
111
+ if (error instanceof Error) throw toConvexError(authFlowError("INTERNAL_ERROR", `Device flow failed: ${error.message}`));
112
+ throw toConvexError(error);
113
+ }
114
+ };
105
115
 
106
116
  //#endregion
107
117
  export { handleDevice };
@@ -1,15 +1,16 @@
1
+ import { ComponentReadCtx } from "./componentContext.js";
1
2
  import { HttpKeyContext } from "./types.js";
2
- import { AuthContext, OptionalAuthContext, UserDoc } from "./auth.js";
3
+ import { AuthContext, OptionalAuthContext, UserDoc } from "./auth-context.js";
4
+ import "./auth.js";
3
5
  import { GenericActionCtx, GenericDataModel, HttpRouter, UserIdentity } from "convex/server";
4
6
 
5
7
  //#region src/server/http.d.ts
6
- type HttpQueryCtx = Pick<GenericActionCtx<GenericDataModel>, "runQuery">;
7
8
  type HttpIdentityCtx = {
8
9
  auth: {
9
10
  getUserIdentity: () => Promise<UserIdentity | null>;
10
11
  };
11
12
  };
12
- type HttpContextCtx = HttpIdentityCtx & HttpQueryCtx;
13
+ type HttpContextCtx = HttpIdentityCtx & ComponentReadCtx;
13
14
  /**
14
15
  * Auth context returned by `auth.http.context(ctx, request)`.
15
16
  *