@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.
- package/README.md +3 -5
- package/dist/bin.js +6488 -1571
- package/dist/browser/index.js +10 -7
- package/dist/browser/locks.js +3 -5
- package/dist/browser/navigation.js +7 -10
- package/dist/browser/runtime.js +35 -33
- package/dist/client/core/types.js +17 -0
- package/dist/client/factors/device.js +26 -19
- package/dist/client/index.js +151 -163
- package/dist/client/runtime/proxy.js +6 -6
- package/dist/client/services/adapters.js +3 -7
- package/dist/client/services/http.js +2 -5
- package/dist/client/services/resolve.js +5 -11
- package/dist/client/services/runtime.js +2 -5
- package/dist/component/_generated/component.d.ts +46 -0
- package/dist/component/index.d.ts +3 -3
- package/dist/component/model.d.ts +25 -25
- package/dist/component/public/identity/sessions.js +38 -1
- package/dist/component/public/identity/tokens.js +81 -3
- package/dist/component/public/identity/verifiers.js +9 -3
- package/dist/component/public.js +3 -3
- package/dist/component/schema.d.ts +320 -320
- package/dist/core/index.d.ts +380 -0
- package/dist/core/index.js +83 -0
- package/dist/otel.d.ts +13 -17
- package/dist/otel.js +39 -49
- package/dist/providers/email.d.ts +2 -2
- package/dist/providers/password.js +8 -16
- package/dist/providers/phone.js +2 -9
- package/dist/server/auth-context.d.ts +204 -0
- package/dist/server/auth-context.js +76 -0
- package/dist/server/auth.d.ts +25 -187
- package/dist/server/auth.js +5 -96
- package/dist/server/componentContext.d.ts +12 -0
- package/dist/server/componentContext.js +1 -0
- package/dist/server/config.js +1 -12
- package/dist/server/constants.js +6 -0
- package/dist/server/contract.d.ts +1 -1
- package/dist/server/core.js +5 -14
- package/dist/server/crypto.js +26 -18
- package/dist/server/db.js +6 -1
- package/dist/server/device.js +88 -78
- package/dist/server/http.d.ts +4 -3
- package/dist/server/http.js +74 -86
- package/dist/server/index.d.ts +2 -1
- package/dist/server/limits.js +22 -15
- package/dist/server/mounts.d.ts +103 -103
- package/dist/server/mutations/account.js +6 -4
- package/dist/server/mutations/invalidate.js +3 -6
- package/dist/server/mutations/oauth.js +86 -88
- package/dist/server/mutations/refresh.js +45 -87
- package/dist/server/mutations/register.js +19 -19
- package/dist/server/mutations/retrieve.js +17 -15
- package/dist/server/mutations/signature.js +9 -13
- package/dist/server/mutations/signin.js +7 -3
- package/dist/server/mutations/signout.js +10 -15
- package/dist/server/mutations/store.js +22 -12
- package/dist/server/mutations/verifier.js +11 -6
- package/dist/server/mutations/verify.js +55 -46
- package/dist/server/oauth/runtime.js +27 -25
- package/dist/server/passkey.js +299 -250
- package/dist/server/prefetch.js +283 -281
- package/dist/server/refresh.js +7 -60
- package/dist/server/runtime.d.ts +82 -206
- package/dist/server/runtime.js +63 -56
- package/dist/server/services/config.js +5 -3
- package/dist/server/services/logger.js +2 -4
- package/dist/server/services/providers.js +2 -4
- package/dist/server/services/refresh.js +2 -4
- package/dist/server/services/resolve.js +15 -14
- package/dist/server/services/signin.js +2 -4
- package/dist/server/sessions.js +32 -33
- package/dist/server/signin.js +177 -142
- package/dist/server/sso/domain.d.ts +20 -68
- package/dist/server/sso/domain.js +444 -413
- package/dist/server/sso/http.js +53 -59
- package/dist/server/sso/oidc.js +94 -80
- package/dist/server/tokens.js +13 -3
- package/dist/server/totp.js +153 -116
- package/dist/server/types.d.ts +2 -2
- package/dist/server/users.js +18 -23
- package/dist/server/utils/cache.js +51 -0
- package/dist/server/utils/dispatch.js +36 -0
- package/dist/server/utils/retry.js +24 -0
- package/dist/server/utils/span.js +32 -0
- package/dist/shared/errors.js +9 -3
- package/dist/shared/log.js +20 -22
- package/package.json +41 -33
package/dist/server/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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) =>
|
|
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 { };
|
package/dist/server/config.js
CHANGED
|
@@ -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
|
|
59
|
+
export { configDefaults, listAvailableProviders };
|
|
71
60
|
//# sourceMappingURL=config.js.map
|
package/dist/server/core.js
CHANGED
|
@@ -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 "./
|
|
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,
|
|
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
|
-
|
|
234
|
-
|
|
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) };
|
package/dist/server/crypto.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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, {
|
|
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,
|
package/dist/server/device.js
CHANGED
|
@@ -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) =>
|
|
29
|
-
try
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 };
|
package/dist/server/http.d.ts
CHANGED
|
@@ -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 &
|
|
13
|
+
type HttpContextCtx = HttpIdentityCtx & ComponentReadCtx;
|
|
13
14
|
/**
|
|
14
15
|
* Auth context returned by `auth.http.context(ctx, request)`.
|
|
15
16
|
*
|