@lupinum/better-convex-nuxt 1.0.0-beta.4 → 1.0.0-beta.6
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/dist/module.json +1 -1
- package/dist/runtime/cli/init.js +1 -1
- package/dist/runtime/convex-auth/adapter/define-functions.d.ts +15 -0
- package/dist/runtime/convex-auth/adapter/define-functions.js +84 -0
- package/dist/runtime/convex-auth/component/_generated/component.d.ts +9 -0
- package/dist/runtime/convex-auth/component/adapter.d.ts +12 -1
- package/dist/runtime/convex-auth/component/adapter.js +1 -0
- package/dist/runtime/convex-auth/create-better-convex-auth.js +16 -1
- package/dist/runtime/convex-auth/index.d.ts +1 -0
- package/dist/runtime/convex-auth/index.js +1 -0
- package/dist/runtime/convex-auth/plugin.js +2 -1
- package/dist/runtime/convex-auth/rate-limit-storage.d.ts +20 -0
- package/dist/runtime/convex-auth/rate-limit-storage.js +46 -0
- package/dist/runtime/convex-auth/session-generation.d.ts +10 -0
- package/dist/runtime/convex-auth/session-generation.js +28 -0
- package/dist/runtime/shared/release-fingerprint.js +1 -1
- package/package.json +4 -4
- package/security/upstream-convex-better-auth.json +25 -14
package/dist/module.json
CHANGED
package/dist/runtime/cli/init.js
CHANGED
|
@@ -42,7 +42,7 @@ import schema from './schema'
|
|
|
42
42
|
import schemaMetadata from './schemaMetadata'
|
|
43
43
|
|
|
44
44
|
// This module is inside the isolated betterAuth component, not the public app API.
|
|
45
|
-
export const { assertProfile, consumeOne, count, create, deleteMany, deleteOne, findMany, findOne, incrementOne, rotateSigningKey, sessionAdmission, expireWorkforceSession, listWorkforceSessions, revokeAllWorkforceSessions, revokeWorkforceSession, touchWorkforceSession, updateMany, updateOne } = defineAuthAdapterFunctions({ metadata: schemaMetadata, schema })
|
|
45
|
+
export const { assertProfile, consumeOne, consumeRateLimit, count, create, deleteMany, deleteOne, findMany, findOne, incrementOne, rotateSigningKey, sessionAdmission, expireWorkforceSession, listWorkforceSessions, revokeAllWorkforceSessions, revokeWorkforceSession, touchWorkforceSession, updateMany, updateOne } = defineAuthAdapterFunctions({ metadata: schemaMetadata, schema })
|
|
46
46
|
`,
|
|
47
47
|
"convex/betterAuth/convex.config.ts": `import { defineComponent } from 'convex/server'
|
|
48
48
|
|
|
@@ -6,6 +6,9 @@ export interface DefineAuthAdapterFunctionsOptions<Schema extends SchemaDefiniti
|
|
|
6
6
|
metadata: AuthSchemaMetadata;
|
|
7
7
|
}
|
|
8
8
|
export declare function defineAuthAdapterFunctions<Schema extends SchemaDefinition<any, any>>({ schema, metadata, }: DefineAuthAdapterFunctionsOptions<Schema>): {
|
|
9
|
+
migrateBeta3UserGeneration: import("convex/server").RegisteredMutation<"internal", {
|
|
10
|
+
mode: "forward" | "rollback";
|
|
11
|
+
}, Promise<import("../session-generation.js").Beta3UserGenerationMigrationResult>>;
|
|
9
12
|
touchWorkforceSession: import("convex/server").RegisteredMutation<"public", {
|
|
10
13
|
actor: {
|
|
11
14
|
userId: string;
|
|
@@ -300,6 +303,18 @@ export declare function defineAuthAdapterFunctions<Schema extends SchemaDefiniti
|
|
|
300
303
|
field: string;
|
|
301
304
|
}[];
|
|
302
305
|
}, Promise<Record<string, string | number | boolean | string[] | number[] | null> | null>>;
|
|
306
|
+
consumeRateLimit: import("convex/server").RegisteredMutation<"public", {
|
|
307
|
+
window: number;
|
|
308
|
+
key: string;
|
|
309
|
+
max: number;
|
|
310
|
+
retentionWindow: number;
|
|
311
|
+
}, Promise<{
|
|
312
|
+
allowed: boolean;
|
|
313
|
+
retryAfter: null;
|
|
314
|
+
} | {
|
|
315
|
+
allowed: boolean;
|
|
316
|
+
retryAfter: number;
|
|
317
|
+
}>>;
|
|
303
318
|
incrementOne: import("convex/server").RegisteredMutation<"public", {
|
|
304
319
|
set?: any;
|
|
305
320
|
workforce?: {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
normalizeSigningKeyCandidate,
|
|
13
13
|
signingKeyCandidateValidator
|
|
14
14
|
} from "../jwks-rotation.js";
|
|
15
|
+
import { migrateBeta3UserGeneration, sessionGenerationAuthority } from "../session-generation.js";
|
|
15
16
|
import { createWorkforceAdapterPolicy } from "../workforce/adapter-policy.js";
|
|
16
17
|
import { readAuthSessionAdmission } from "../workforce/admission.js";
|
|
17
18
|
import {
|
|
@@ -78,6 +79,7 @@ const readArgs = {
|
|
|
78
79
|
offset: v.optional(v.number())
|
|
79
80
|
};
|
|
80
81
|
const expireSessionReference = makeFunctionReference("adapter:expireWorkforceSession");
|
|
82
|
+
const pruneRateLimitsReference = makeFunctionReference("adapter:deleteMany");
|
|
81
83
|
function assertRecord(value, code) {
|
|
82
84
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(code);
|
|
83
85
|
}
|
|
@@ -222,6 +224,7 @@ export function defineAuthAdapterFunctions({
|
|
|
222
224
|
}) {
|
|
223
225
|
assertAuthSchemaMatchesMetadata(schema, metadata);
|
|
224
226
|
const workforce = hasWorkforceSchema(metadata);
|
|
227
|
+
const generationAuthority = sessionGenerationAuthority(metadata);
|
|
225
228
|
const workforcePolicy = createWorkforceAdapterPolicy(workforce, metadata);
|
|
226
229
|
const relationships = createAuthRelationshipEngine({
|
|
227
230
|
schema,
|
|
@@ -233,6 +236,19 @@ export function defineAuthAdapterFunctions({
|
|
|
233
236
|
if (!workforce) throw new Error("AUTH_WORKFORCE_SCHEMA_REQUIRED");
|
|
234
237
|
}
|
|
235
238
|
return {
|
|
239
|
+
migrateBeta3UserGeneration: internalMutationGeneric({
|
|
240
|
+
args: {
|
|
241
|
+
mode: v.union(v.literal("forward"), v.literal("rollback"))
|
|
242
|
+
},
|
|
243
|
+
returns: v.object({
|
|
244
|
+
patched: v.number(),
|
|
245
|
+
scanned: v.number()
|
|
246
|
+
}),
|
|
247
|
+
handler: (ctx, args) => {
|
|
248
|
+
if (!generationAuthority) throw new Error("AUTH_SESSION_GENERATION_SCHEMA_INVALID");
|
|
249
|
+
return migrateBeta3UserGeneration(ctx, args.mode, generationAuthority);
|
|
250
|
+
}
|
|
251
|
+
}),
|
|
236
252
|
touchWorkforceSession: mutationGeneric({
|
|
237
253
|
args: { actor: workforceSessionActorValidator },
|
|
238
254
|
returns: v.object({ expiresAt: v.number() }),
|
|
@@ -547,6 +563,74 @@ export function defineAuthAdapterFunctions({
|
|
|
547
563
|
return toBetterAuthDocument(current);
|
|
548
564
|
}
|
|
549
565
|
}),
|
|
566
|
+
consumeRateLimit: mutationGeneric({
|
|
567
|
+
args: {
|
|
568
|
+
key: v.string(),
|
|
569
|
+
max: v.number(),
|
|
570
|
+
retentionWindow: v.number(),
|
|
571
|
+
window: v.number()
|
|
572
|
+
},
|
|
573
|
+
returns: v.object({
|
|
574
|
+
allowed: v.boolean(),
|
|
575
|
+
retryAfter: v.union(v.number(), v.null())
|
|
576
|
+
}),
|
|
577
|
+
handler: async (ctx, args) => {
|
|
578
|
+
const windowInMs = args.window * 1e3;
|
|
579
|
+
const retentionWindowInMs = args.retentionWindow * 1e3;
|
|
580
|
+
if (args.key.length === 0 || !Number.isSafeInteger(args.max) || args.max <= 0 || !Number.isFinite(args.window) || args.window <= 0 || !Number.isFinite(args.retentionWindow) || args.retentionWindow < args.window || !Number.isFinite(windowInMs) || !Number.isFinite(retentionWindowInMs)) {
|
|
581
|
+
throw new Error("AUTH_RATE_LIMIT_RULE_INVALID");
|
|
582
|
+
}
|
|
583
|
+
const now = Date.now();
|
|
584
|
+
const current = oneOrNull(
|
|
585
|
+
await findAuthRows(
|
|
586
|
+
ctx,
|
|
587
|
+
schema,
|
|
588
|
+
metadata,
|
|
589
|
+
{ model: "rateLimit", where: [{ field: "key", value: args.key }] },
|
|
590
|
+
2
|
|
591
|
+
),
|
|
592
|
+
"AUTH_RATE_LIMIT"
|
|
593
|
+
);
|
|
594
|
+
if (!current) {
|
|
595
|
+
await ctx.db.insert(
|
|
596
|
+
"rateLimit",
|
|
597
|
+
{ id: args.key, key: args.key, count: 1, lastRequest: now }
|
|
598
|
+
);
|
|
599
|
+
return { allowed: true, retryAfter: null };
|
|
600
|
+
}
|
|
601
|
+
if (typeof current.count !== "number" || !Number.isSafeInteger(current.count) || current.count < 0 || typeof current.lastRequest !== "number" || !Number.isFinite(current.lastRequest)) {
|
|
602
|
+
throw new Error("AUTH_RATE_LIMIT_ROW_INVALID");
|
|
603
|
+
}
|
|
604
|
+
if (now - current.lastRequest >= windowInMs) {
|
|
605
|
+
await ctx.db.patch(current._id, { count: 1, lastRequest: now });
|
|
606
|
+
await ctx.scheduler.runAfter(0, pruneRateLimitsReference, {
|
|
607
|
+
model: "rateLimit",
|
|
608
|
+
where: [
|
|
609
|
+
{
|
|
610
|
+
field: "lastRequest",
|
|
611
|
+
operator: "lt",
|
|
612
|
+
value: now - retentionWindowInMs
|
|
613
|
+
}
|
|
614
|
+
]
|
|
615
|
+
});
|
|
616
|
+
return { allowed: true, retryAfter: null };
|
|
617
|
+
}
|
|
618
|
+
if (current.count >= args.max) {
|
|
619
|
+
return {
|
|
620
|
+
allowed: false,
|
|
621
|
+
retryAfter: Math.max(1, Math.ceil((current.lastRequest + windowInMs - now) / 1e3))
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
await ctx.db.patch(
|
|
625
|
+
current._id,
|
|
626
|
+
{
|
|
627
|
+
count: current.count + 1,
|
|
628
|
+
lastRequest: now
|
|
629
|
+
}
|
|
630
|
+
);
|
|
631
|
+
return { allowed: true, retryAfter: null };
|
|
632
|
+
}
|
|
633
|
+
}),
|
|
550
634
|
incrementOne: mutationGeneric({
|
|
551
635
|
returns: v.union(authDocumentValidator, v.null()),
|
|
552
636
|
args: {
|
|
@@ -37,6 +37,15 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
37
37
|
value: string | number | boolean | Array<string> | Array<number> | null;
|
|
38
38
|
}>;
|
|
39
39
|
}, Record<string, string | number | boolean | Array<string> | Array<number> | null> | null, Name>;
|
|
40
|
+
consumeRateLimit: FunctionReference<"mutation", "internal", {
|
|
41
|
+
key: string;
|
|
42
|
+
max: number;
|
|
43
|
+
retentionWindow: number;
|
|
44
|
+
window: number;
|
|
45
|
+
}, {
|
|
46
|
+
allowed: boolean;
|
|
47
|
+
retryAfter: number | null;
|
|
48
|
+
}, Name>;
|
|
40
49
|
count: FunctionReference<"query", "internal", {
|
|
41
50
|
model: string;
|
|
42
51
|
where?: Array<{
|
|
@@ -13,7 +13,18 @@ export declare const assertProfile: import("convex/server").RegisteredQuery<"pub
|
|
|
13
13
|
value: string | number | boolean | string[] | number[] | null;
|
|
14
14
|
field: string;
|
|
15
15
|
}[];
|
|
16
|
-
}, Promise<Record<string, string | number | boolean | string[] | number[] | null> | null>>,
|
|
16
|
+
}, Promise<Record<string, string | number | boolean | string[] | number[] | null> | null>>, consumeRateLimit: import("convex/server").RegisteredMutation<"public", {
|
|
17
|
+
window: number;
|
|
18
|
+
key: string;
|
|
19
|
+
max: number;
|
|
20
|
+
retentionWindow: number;
|
|
21
|
+
}, Promise<{
|
|
22
|
+
allowed: boolean;
|
|
23
|
+
retryAfter: null;
|
|
24
|
+
} | {
|
|
25
|
+
allowed: boolean;
|
|
26
|
+
retryAfter: number;
|
|
27
|
+
}>>, count: import("convex/server").RegisteredQuery<"public", {
|
|
17
28
|
where?: {
|
|
18
29
|
mode?: "sensitive" | "insensitive" | undefined;
|
|
19
30
|
operator?: "in" | "lt" | "lte" | "gt" | "gte" | "eq" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
|
|
@@ -11,6 +11,7 @@ import { createOAuthOperator } from "./oauth-operator.js";
|
|
|
11
11
|
import { requireAuthOrigin } from "./origin.js";
|
|
12
12
|
import { convexAuth } from "./plugin.js";
|
|
13
13
|
import { getConvexAuthProvider } from "./provider.js";
|
|
14
|
+
import { createConvexAuthRateLimitStorage } from "./rate-limit-storage.js";
|
|
14
15
|
import { workforceSessionPolicy } from "./workforce/operations.js";
|
|
15
16
|
import { createWorkforceSchemaPlugins } from "./workforce/profile.js";
|
|
16
17
|
import { createWorkforceProviderHooks } from "./workforce/provider-hooks.js";
|
|
@@ -209,6 +210,15 @@ export function createBetterConvexAuthOwned(component, options = {}, extraPlugin
|
|
|
209
210
|
] : [],
|
|
210
211
|
...oauthProfile ? [createOAuthProvider(oauthProfile)] : []
|
|
211
212
|
];
|
|
213
|
+
const maximumConfiguredPluginRateLimitWindow = Math.max(
|
|
214
|
+
0,
|
|
215
|
+
...plugins.flatMap((plugin) => plugin.rateLimit?.map((rule) => rule.window) ?? [])
|
|
216
|
+
);
|
|
217
|
+
const rateLimitStorage = createConvexAuthRateLimitStorage(
|
|
218
|
+
ctx,
|
|
219
|
+
component,
|
|
220
|
+
maximumConfiguredPluginRateLimitWindow
|
|
221
|
+
);
|
|
212
222
|
const auth = betterAuth({
|
|
213
223
|
appName: options.appName,
|
|
214
224
|
account: {
|
|
@@ -260,7 +270,12 @@ export function createBetterConvexAuthOwned(component, options = {}, extraPlugin
|
|
|
260
270
|
},
|
|
261
271
|
emailVerification: workforce ? { ...emailVerification, autoSignInAfterVerification: false } : emailVerification,
|
|
262
272
|
plugins,
|
|
263
|
-
rateLimit: {
|
|
273
|
+
rateLimit: {
|
|
274
|
+
customStorage: rateLimitStorage,
|
|
275
|
+
enabled: true,
|
|
276
|
+
modelName: "rateLimit",
|
|
277
|
+
storage: "database"
|
|
278
|
+
},
|
|
264
279
|
session: {
|
|
265
280
|
expiresIn: 7 * 24 * 60 * 60,
|
|
266
281
|
updateAge: 24 * 60 * 60,
|
|
@@ -8,6 +8,7 @@ export { requireAuthOrigin } from './origin.js';
|
|
|
8
8
|
export { requireWritableAuthCtx } from './context.js';
|
|
9
9
|
export { convexAuth } from './plugin.js';
|
|
10
10
|
export { getConvexAuthProvider } from './provider.js';
|
|
11
|
+
export { createConvexAuthRateLimitStorage } from './rate-limit-storage.js';
|
|
11
12
|
export { createBetterAuthMcpAccessVerifier, verifyOAuthBearerToken } from './oauth-resource.js';
|
|
12
13
|
export { createUserProjectionTriggers } from './user-projection.js';
|
|
13
14
|
export type { AuthCtx, WritableAuthCtx } from './context.js';
|
|
@@ -6,5 +6,6 @@ export { requireAuthOrigin } from "./origin.js";
|
|
|
6
6
|
export { requireWritableAuthCtx } from "./context.js";
|
|
7
7
|
export { convexAuth } from "./plugin.js";
|
|
8
8
|
export { getConvexAuthProvider } from "./provider.js";
|
|
9
|
+
export { createConvexAuthRateLimitStorage } from "./rate-limit-storage.js";
|
|
9
10
|
export { createBetterAuthMcpAccessVerifier, verifyOAuthBearerToken } from "./oauth-resource.js";
|
|
10
11
|
export { createUserProjectionTriggers } from "./user-projection.js";
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
validateOAuthProviderProfile
|
|
38
38
|
} from "./oauth-security.js";
|
|
39
39
|
import { normalizeAuthOrigin } from "./origin.js";
|
|
40
|
+
import { isConvexAuthRateLimitStorage } from "./rate-limit-storage.js";
|
|
40
41
|
import { workforceSchemaPlugin } from "./workforce/schema.js";
|
|
41
42
|
import { isFullWorkforceSession } from "./workforce/session-assurance.js";
|
|
42
43
|
const forbiddenCustomClaims = /* @__PURE__ */ new Set(["aud", "exp", "iat", "iss", "jti", "nbf", "sub"]);
|
|
@@ -379,7 +380,7 @@ function hasSafeGlobalAuthRuntime(context) {
|
|
|
379
380
|
const ipAddress = context.options.advanced?.ipAddress;
|
|
380
381
|
const ipHeaders = ipAddress?.ipAddressHeaders;
|
|
381
382
|
const trustedProxies = ipAddress?.trustedProxies;
|
|
382
|
-
return context.options.rateLimit?.enabled === true && context.options.rateLimit.storage === "database" && context.options.rateLimit.modelName === "rateLimit" && context.options.rateLimit.customStorage
|
|
383
|
+
return context.options.rateLimit?.enabled === true && context.options.rateLimit.storage === "database" && context.options.rateLimit.modelName === "rateLimit" && isConvexAuthRateLimitStorage(context.options.rateLimit.customStorage) && context.options.rateLimit.customRules === void 0 && Array.isArray(ipHeaders) && ipHeaders.length === 1 && ipHeaders[0]?.toLowerCase() === VERIFIED_CLIENT_IP_HEADER && (ipAddress?.disableIpTracking === void 0 || ipAddress.disableIpTracking === false) && (trustedProxies === void 0 || Array.isArray(trustedProxies) && trustedProxies.length === 0) && (ipAddress?.ipv6Subnet === void 0 || ipAddress.ipv6Subnet === 64) && (context.options.advanced?.trustedProxyHeaders === void 0 || context.options.advanced.trustedProxyHeaders === false);
|
|
383
384
|
}
|
|
384
385
|
export function convexAuth(options) {
|
|
385
386
|
validateSessionJwt(options);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { BetterAuthOptions } from 'better-auth';
|
|
2
|
+
import type { GenericDataModel } from 'convex/server';
|
|
3
|
+
import { type AuthCtx } from './context.js';
|
|
4
|
+
import type { AuthAdapterComponentApi } from './types.js';
|
|
5
|
+
type BetterAuthRateLimitStorage = NonNullable<NonNullable<BetterAuthOptions['rateLimit']>['customStorage']>;
|
|
6
|
+
export declare function isConvexAuthRateLimitStorage(storage: BetterAuthRateLimitStorage | undefined): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Test seam for constructing a trusted in-memory storage fixture.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare function registerConvexAuthRateLimitStorageForTest(storage: BetterAuthRateLimitStorage): BetterAuthRateLimitStorage;
|
|
13
|
+
/**
|
|
14
|
+
* Create the one atomic Better Auth rate-limit store backed by the auth component.
|
|
15
|
+
* `maximumConfiguredRateLimitWindow` must include every custom plugin rule. The
|
|
16
|
+
* constructor owns the pinned Better Auth built-in 60-second floor. A final
|
|
17
|
+
* Convex OCC error is safe to retry because that mutation did not commit.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createConvexAuthRateLimitStorage<DataModel extends GenericDataModel>(ctx: AuthCtx<DataModel>, component: AuthAdapterComponentApi, maximumConfiguredRateLimitWindow: number): BetterAuthRateLimitStorage;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { requireWritableAuthCtx } from "./context.js";
|
|
2
|
+
const AUTH_RATE_LIMIT_CONTENTION_MAX_RETRIES = 5;
|
|
3
|
+
const BETTER_AUTH_MAX_BUILT_IN_RATE_LIMIT_WINDOW_SECONDS = 60;
|
|
4
|
+
const ownedStorages = /* @__PURE__ */ new WeakSet();
|
|
5
|
+
function isFinalConvexContention(error) {
|
|
6
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7
|
+
return /documents? read from or written to/iu.test(message) && /changed while this mutation was being run and on every subsequent retry/iu.test(message);
|
|
8
|
+
}
|
|
9
|
+
function retryDelay(attempt) {
|
|
10
|
+
const delayMs = Math.min(5 * 2 ** attempt, 80) + Math.floor(Math.random() * 7);
|
|
11
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
12
|
+
}
|
|
13
|
+
export function isConvexAuthRateLimitStorage(storage) {
|
|
14
|
+
return storage !== void 0 && ownedStorages.has(storage);
|
|
15
|
+
}
|
|
16
|
+
export function registerConvexAuthRateLimitStorageForTest(storage) {
|
|
17
|
+
ownedStorages.add(storage);
|
|
18
|
+
return storage;
|
|
19
|
+
}
|
|
20
|
+
export function createConvexAuthRateLimitStorage(ctx, component, maximumConfiguredRateLimitWindow) {
|
|
21
|
+
const storage = {
|
|
22
|
+
async consume(key, rule) {
|
|
23
|
+
requireWritableAuthCtx(ctx);
|
|
24
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
25
|
+
try {
|
|
26
|
+
return await ctx.runMutation(component.adapter.consumeRateLimit, {
|
|
27
|
+
key,
|
|
28
|
+
max: rule.max,
|
|
29
|
+
retentionWindow: Math.max(
|
|
30
|
+
BETTER_AUTH_MAX_BUILT_IN_RATE_LIMIT_WINDOW_SECONDS,
|
|
31
|
+
maximumConfiguredRateLimitWindow,
|
|
32
|
+
rule.window
|
|
33
|
+
),
|
|
34
|
+
window: rule.window
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if ("db" in ctx || attempt >= AUTH_RATE_LIMIT_CONTENTION_MAX_RETRIES || !isFinalConvexContention(error)) {
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
await retryDelay(attempt);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
return registerConvexAuthRateLimitStorageForTest(storage);
|
|
46
|
+
}
|
|
@@ -4,6 +4,11 @@ import type { AuthWhere } from './adapter/query.js';
|
|
|
4
4
|
type ReadCtx = GenericQueryCtx<GenericDataModel>;
|
|
5
5
|
type WriteCtx = GenericMutationCtx<GenericDataModel>;
|
|
6
6
|
type Row = Record<string, unknown>;
|
|
7
|
+
export type Beta3UserGenerationMigrationMode = 'forward' | 'rollback';
|
|
8
|
+
export interface Beta3UserGenerationMigrationResult {
|
|
9
|
+
patched: number;
|
|
10
|
+
scanned: number;
|
|
11
|
+
}
|
|
7
12
|
export interface SessionGenerationAuthority {
|
|
8
13
|
assuranceGenerationField: string;
|
|
9
14
|
securityGenerationField: string;
|
|
@@ -13,6 +18,11 @@ export interface SessionGenerationAuthority {
|
|
|
13
18
|
}
|
|
14
19
|
export declare const canonicalSessionGenerationAuthority: SessionGenerationAuthority;
|
|
15
20
|
export declare function sessionGenerationAuthority(metadata: AuthSchemaMetadata): SessionGenerationAuthority | null;
|
|
21
|
+
/**
|
|
22
|
+
* Temporary beta.3 cutover for small, pre-customer deployments.
|
|
23
|
+
* Legacy sessions must be deleted before this operator runs.
|
|
24
|
+
*/
|
|
25
|
+
export declare function migrateBeta3UserGeneration(ctx: WriteCtx, mode: Beta3UserGenerationMigrationMode, authority?: SessionGenerationAuthority): Promise<Beta3UserGenerationMigrationResult>;
|
|
16
26
|
/** Add component-owned generation fields before strict generated-schema normalization. */
|
|
17
27
|
export declare function prepareSessionGenerationCreate(ctx: ReadCtx, model: string, input: unknown, authority?: SessionGenerationAuthority): Promise<unknown>;
|
|
18
28
|
/** Generic adapter updates must never mint or move session-generation authority. */
|
|
@@ -33,6 +33,34 @@ function generation(value) {
|
|
|
33
33
|
async function readUser(ctx, userId, authority) {
|
|
34
34
|
return await ctx.db.query(authority.userModel).withIndex("id", (query) => query.eq("id", userId)).unique();
|
|
35
35
|
}
|
|
36
|
+
export async function migrateBeta3UserGeneration(ctx, mode, authority = canonicalSessionGenerationAuthority) {
|
|
37
|
+
const sessions = await ctx.db.query(authority.sessionModel).take(1);
|
|
38
|
+
if (sessions.length !== 0) throw new Error("AUTH_BETA3_CUTOVER_SESSIONS_REMAIN");
|
|
39
|
+
const users = await ctx.db.query(authority.userModel).take(129);
|
|
40
|
+
if (users.length > 128) throw new Error("AUTH_BETA3_CUTOVER_USER_LIMIT");
|
|
41
|
+
let patched = 0;
|
|
42
|
+
for (const user of users) {
|
|
43
|
+
if (typeof user._id !== "string" || !user._id || typeof user.id !== "string" || !user.id) {
|
|
44
|
+
throw new Error("AUTH_BETA3_CUTOVER_USER_INVALID");
|
|
45
|
+
}
|
|
46
|
+
const current = user[authority.securityGenerationField];
|
|
47
|
+
if (current !== void 0 && (!generation(current) || current !== 0)) {
|
|
48
|
+
throw new Error("AUTH_BETA3_CUTOVER_GENERATION_INVALID");
|
|
49
|
+
}
|
|
50
|
+
if (mode === "forward" && current === void 0) {
|
|
51
|
+
await ctx.db.patch(authority.userModel, user._id, {
|
|
52
|
+
[authority.securityGenerationField]: 0
|
|
53
|
+
});
|
|
54
|
+
patched += 1;
|
|
55
|
+
} else if (mode === "rollback" && current === 0) {
|
|
56
|
+
await ctx.db.patch(authority.userModel, user._id, {
|
|
57
|
+
[authority.securityGenerationField]: void 0
|
|
58
|
+
});
|
|
59
|
+
patched += 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { patched, scanned: users.length };
|
|
63
|
+
}
|
|
36
64
|
export async function prepareSessionGenerationCreate(ctx, model, input, authority = canonicalSessionGenerationAuthority) {
|
|
37
65
|
if (!input || typeof input !== "object" || Array.isArray(input)) return input;
|
|
38
66
|
const row = input;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const packedRuntimeFingerprint = "bcn-release-v1-
|
|
1
|
+
const packedRuntimeFingerprint = "bcn-release-v1-1a4ab089f02a76a2e4e1fa972a0160575dcecf5a3acc7f1958616549ddc3c81c";
|
|
2
2
|
const packedRuntimeFingerprintPattern = /^bcn-release-v1-[0-9a-f]{64}$/u;
|
|
3
3
|
export const PACKED_RUNTIME_FINGERPRINT_HEADER = "x-bcn-runtime-fingerprint";
|
|
4
4
|
export function getPackedRuntimeFingerprint() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lupinum/better-convex-nuxt",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.6",
|
|
4
4
|
"description": "Full-featured Convex integration for Nuxt with SSR, real-time subscriptions, and authentication",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"authentication",
|
|
@@ -195,7 +195,7 @@
|
|
|
195
195
|
},
|
|
196
196
|
"dependencies": {
|
|
197
197
|
"@better-auth/utils": "0.4.2",
|
|
198
|
-
"@lupinum/better-convex-vue": "1.0.0-beta.
|
|
198
|
+
"@lupinum/better-convex-vue": "1.0.0-beta.6",
|
|
199
199
|
"@nuxt/kit": "4.5.2",
|
|
200
200
|
"convex-helpers": "0.1.114",
|
|
201
201
|
"defu": "^6.1.5",
|
|
@@ -218,7 +218,7 @@
|
|
|
218
218
|
"@nuxt/test-utils": "^4.0.3",
|
|
219
219
|
"@types/node": "22.20.1",
|
|
220
220
|
"@vitejs/plugin-vue": "^6.0.8",
|
|
221
|
-
"@vitest/browser-playwright": "^4.1.
|
|
221
|
+
"@vitest/browser-playwright": "^4.1.11",
|
|
222
222
|
"arktype": "2.2.3",
|
|
223
223
|
"better-auth": "1.7.2",
|
|
224
224
|
"changelogen": "0.6.2",
|
|
@@ -236,7 +236,7 @@
|
|
|
236
236
|
"tar": "7.5.22",
|
|
237
237
|
"typescript": "~5.9.3",
|
|
238
238
|
"vite": "8.1.5",
|
|
239
|
-
"vitest": "^4.1.
|
|
239
|
+
"vitest": "^4.1.11",
|
|
240
240
|
"vitest-browser-vue": "^2.1.0",
|
|
241
241
|
"vue-tsc": "^3.3.7",
|
|
242
242
|
"yaml": "2.9.0",
|
|
@@ -148,9 +148,9 @@
|
|
|
148
148
|
"targetPath": "src/runtime/convex-auth/adapter/define-functions.ts",
|
|
149
149
|
"sourceCommit": "c628916b451a6b4cff0f5464f134475464b1a6da",
|
|
150
150
|
"sourceSha256": "7f537e1081bc22ee50d15df7b24e73e8869aa78a17af4aca18a5b0d261b186e3",
|
|
151
|
-
"targetSha256": "
|
|
151
|
+
"targetSha256": "cfabe28cf39cfe31075eddd06c7690728419f2e2a497fd58cd8e830ba603168b",
|
|
152
152
|
"classification": "adapted",
|
|
153
|
-
"modificationSummary": "Rebuilt component functions with strict validation, indexed relationships, failure-atomic writes and triggers, single-snapshot counts, guarded consumption and increments, authenticated signing-key rotation, component-owned session-generation invalidation, and workforce profile, admission, expiry, management, credential, factor, and replay operations.",
|
|
153
|
+
"modificationSummary": "Rebuilt component functions with strict validation, indexed relationships, failure-atomic writes and triggers, single-snapshot counts, one atomic rate-limit consume operation, guarded consumption and increments, authenticated signing-key rotation, component-owned session-generation invalidation, a temporary internal beta.3 user-generation cutover operator, and workforce profile, admission, expiry, management, credential, factor, and replay operations.",
|
|
154
154
|
"latestReviewedUpstreamPatch": "https://github.com/get-convex/better-auth/commit/c628916b451a6b4cff0f5464f134475464b1a6da",
|
|
155
155
|
"noticeRequired": true
|
|
156
156
|
},
|
|
@@ -203,9 +203,9 @@
|
|
|
203
203
|
"targetPath": "src/runtime/convex-auth/session-generation.ts",
|
|
204
204
|
"sourceCommit": "c628916b451a6b4cff0f5464f134475464b1a6da",
|
|
205
205
|
"sourceSha256": "70daca2ff67e790be91a69c92e35b9c9c373ff5b23402d796a3a50429cc002bc",
|
|
206
|
-
"targetSha256": "
|
|
206
|
+
"targetSha256": "e7f27dcd27e5a47a095647b9b54cac1a8c66b233ccc0e92049fda197dd5b168e",
|
|
207
207
|
"classification": "rewritten",
|
|
208
|
-
"modificationSummary": "Added component-owned user and session generations so the provider's canonical delete-all selector atomically invalidates every earlier session without an unbounded collection scan; stale sessions fail adapter reads and backend admission until scheduled physical expiry.",
|
|
208
|
+
"modificationSummary": "Added component-owned user and session generations so the provider's canonical delete-all selector atomically invalidates every earlier session without an unbounded collection scan; stale sessions fail adapter reads and backend admission until scheduled physical expiry. Added a bounded beta.3 user-generation cutover helper that requires legacy sessions to be deleted while preserving user IDs, accounts, passwords, and application references.",
|
|
209
209
|
"latestReviewedUpstreamPatch": "https://github.com/get-convex/better-auth/commit/c628916b451a6b4cff0f5464f134475464b1a6da",
|
|
210
210
|
"noticeRequired": false
|
|
211
211
|
},
|
|
@@ -225,9 +225,9 @@
|
|
|
225
225
|
"targetPath": "src/runtime/convex-auth/component/_generated/component.ts",
|
|
226
226
|
"sourceCommit": "c628916b451a6b4cff0f5464f134475464b1a6da",
|
|
227
227
|
"sourceSha256": "e337fe2ab868c4ba560868d24bcc725a7ca7e5672505adf57df6e5b6e61dbf4e",
|
|
228
|
-
"targetSha256": "
|
|
228
|
+
"targetSha256": "4746f502864388993e5cca6c795b8fc481865dc2a348dd328a5a89f9854dd98c",
|
|
229
229
|
"classification": "regenerated",
|
|
230
|
-
"modificationSummary": "Regenerated the public ComponentApi declaration including relationship-aware writes, guarded consumption and increments, signing-key lifecycle operations, schema-profile assertion, persisted-session admission, and workforce session management.",
|
|
230
|
+
"modificationSummary": "Regenerated the public ComponentApi declaration including relationship-aware writes, atomic rate-limit consumption, guarded consumption and increments, signing-key lifecycle operations, schema-profile assertion, persisted-session admission, and workforce session management.",
|
|
231
231
|
"latestReviewedUpstreamPatch": "https://github.com/get-convex/better-auth/commit/c628916b451a6b4cff0f5464f134475464b1a6da",
|
|
232
232
|
"noticeRequired": false
|
|
233
233
|
},
|
|
@@ -258,9 +258,9 @@
|
|
|
258
258
|
"targetPath": "src/runtime/convex-auth/component/adapter.ts",
|
|
259
259
|
"sourceCommit": "c628916b451a6b4cff0f5464f134475464b1a6da",
|
|
260
260
|
"sourceSha256": "1a373c633a7090ea60d0e0942a6cb0c6cff65c842c8ca8950e1546cfa50dcbe5",
|
|
261
|
-
"targetSha256": "
|
|
261
|
+
"targetSha256": "eebdfab80285ac45b78ce92f4d79abe67f405bf0ae5a2e190cf65f0f7febb416",
|
|
262
262
|
"classification": "rewritten",
|
|
263
|
-
"modificationSummary": "Reduced the component adapter to a thin assembly of the shared adapter functions and explicit workforce schema-profile and session-management functions, including scheduled expiry and atomic signing-key rotation.",
|
|
263
|
+
"modificationSummary": "Reduced the component adapter to a thin assembly of the shared adapter functions and explicit workforce schema-profile and session-management functions, including atomic rate-limit consumption, scheduled expiry, and atomic signing-key rotation.",
|
|
264
264
|
"latestReviewedUpstreamPatch": "https://github.com/get-convex/better-auth/commit/c628916b451a6b4cff0f5464f134475464b1a6da",
|
|
265
265
|
"noticeRequired": false
|
|
266
266
|
},
|
|
@@ -335,9 +335,9 @@
|
|
|
335
335
|
"targetPath": "src/runtime/convex-auth/create-better-convex-auth.ts",
|
|
336
336
|
"sourceCommit": "c628916b451a6b4cff0f5464f134475464b1a6da",
|
|
337
337
|
"sourceSha256": "610a4d7edfbb0fd3ab412d61e8103b9e416edaaf8e2719189c9cff345597150d",
|
|
338
|
-
"targetSha256": "
|
|
338
|
+
"targetSha256": "aa9e991096e808fc15a806da6604a47d2b9fb055051459490e0b7308aa7184ad",
|
|
339
339
|
"classification": "rewritten",
|
|
340
|
-
"modificationSummary": "Introduced one reviewed factory that owns adapter wiring, JWT and plugin ordering, route registration, keys, secrets, projections, and
|
|
340
|
+
"modificationSummary": "Introduced one reviewed factory that owns adapter wiring, JWT and plugin ordering, route registration, keys, secrets, projections, OAuth operations, and atomic database rate-limit storage; admits fail-closed request-scoped email and user-creation policy, supports one fixed workforce password-and-TOTP profile, keeps arbitrary plugins limited to the internal loopback-only test constructor, and lets that constructor revalidate its boundary for every auth instance.",
|
|
341
341
|
"latestReviewedUpstreamPatch": "https://github.com/get-convex/better-auth/commit/c628916b451a6b4cff0f5464f134475464b1a6da",
|
|
342
342
|
"noticeRequired": false
|
|
343
343
|
},
|
|
@@ -346,9 +346,9 @@
|
|
|
346
346
|
"targetPath": "src/runtime/convex-auth/index.ts",
|
|
347
347
|
"sourceCommit": "c628916b451a6b4cff0f5464f134475464b1a6da",
|
|
348
348
|
"sourceSha256": "300f9ebf00a0f236954653984709abfb70e93f1b1b9a37fabba4ee44661d2cf5",
|
|
349
|
-
"targetSha256": "
|
|
349
|
+
"targetSha256": "eb1124a91e1f672ec5e9305ff6fed383d7302cd498c3d1b369fd7325414aa805",
|
|
350
350
|
"classification": "rewritten",
|
|
351
|
-
"modificationSummary": "Replaced the compatibility barrel with the fixed narrow Better Convex Nuxt API, including the reviewed auth factory, maintained component, provider, OAuth resource verifier and operator, workforce schema-options helper, and rebuildable user projection.",
|
|
351
|
+
"modificationSummary": "Replaced the compatibility barrel with the fixed narrow Better Convex Nuxt API, including the reviewed auth factory, maintained component, atomic rate-limit storage constructor, provider, OAuth resource verifier and operator, workforce schema-options helper, and rebuildable user projection.",
|
|
352
352
|
"latestReviewedUpstreamPatch": "https://github.com/get-convex/better-auth/commit/c628916b451a6b4cff0f5464f134475464b1a6da",
|
|
353
353
|
"noticeRequired": false
|
|
354
354
|
},
|
|
@@ -363,14 +363,25 @@
|
|
|
363
363
|
"latestReviewedUpstreamPatch": "https://github.com/get-convex/better-auth/commit/c628916b451a6b4cff0f5464f134475464b1a6da",
|
|
364
364
|
"noticeRequired": false
|
|
365
365
|
},
|
|
366
|
+
{
|
|
367
|
+
"sourcePath": "src/auth-options.ts",
|
|
368
|
+
"targetPath": "src/runtime/convex-auth/rate-limit-storage.ts",
|
|
369
|
+
"sourceCommit": "c628916b451a6b4cff0f5464f134475464b1a6da",
|
|
370
|
+
"sourceSha256": "36b0218291f4b1ab577d2161a277e95fd62ce7e55e32e6f4d43ae41e001e7478",
|
|
371
|
+
"targetSha256": "36b67e5c5a4870a001fa98e3be72dc29e6a4d43c96f9dffa2ba629c15679f36e",
|
|
372
|
+
"classification": "rewritten",
|
|
373
|
+
"modificationSummary": "Added one package-owned Better Auth custom storage boundary that delegates the complete rate-limit decision and counter update to one atomic component mutation and retries only exact final uncommitted Convex contention from an action context with a fixed bound.",
|
|
374
|
+
"latestReviewedUpstreamPatch": "https://github.com/get-convex/better-auth/commit/c628916b451a6b4cff0f5464f134475464b1a6da",
|
|
375
|
+
"noticeRequired": false
|
|
376
|
+
},
|
|
366
377
|
{
|
|
367
378
|
"sourcePath": "src/plugins/convex/index.ts",
|
|
368
379
|
"targetPath": "src/runtime/convex-auth/plugin.ts",
|
|
369
380
|
"sourceCommit": "c628916b451a6b4cff0f5464f134475464b1a6da",
|
|
370
381
|
"sourceSha256": "48ab81541ecb0e5545bd74fb78ca426c0d695bbb156e03e6a402063255ef428a",
|
|
371
|
-
"targetSha256": "
|
|
382
|
+
"targetSha256": "a7852a3af404fb6d4b214646be6d49635b6044798a1e0d3d0f947a5f02ff3e0a",
|
|
372
383
|
"classification": "rewritten",
|
|
373
|
-
"modificationSummary": "Rewrote the Convex plugin as the composition, request-guard, and session-issuance owner; exact OAuth Provider compatibility and operator calls live behind fail-closed guards, while the fixed workforce profile adds provider request hooks and requires current full TOTP assurance before session-token issuance.",
|
|
384
|
+
"modificationSummary": "Rewrote the Convex plugin as the composition, request-guard, and session-issuance owner; exact OAuth Provider compatibility, package-owned atomic rate-limit storage, and operator calls live behind fail-closed guards, while the fixed workforce profile adds provider request hooks and requires current full TOTP assurance before session-token issuance.",
|
|
374
385
|
"latestReviewedUpstreamPatch": "https://github.com/get-convex/better-auth/commit/c628916b451a6b4cff0f5464f134475464b1a6da",
|
|
375
386
|
"noticeRequired": false
|
|
376
387
|
},
|