@lupinum/better-convex-nuxt 1.0.0-beta.3 → 1.0.0-beta.5
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/convex.d.ts +8 -0
- package/dist/runtime/cli/convex.js +23 -0
- package/dist/runtime/cli/init.d.ts +1 -0
- package/dist/runtime/cli/init.js +33 -9
- package/dist/runtime/convex-auth/adapter/define-functions.d.ts +19 -1
- package/dist/runtime/convex-auth/adapter/define-functions.js +103 -8
- package/dist/runtime/convex-auth/adapter/generate-schema.js +35 -1
- package/dist/runtime/convex-auth/component/_generated/component.d.ts +9 -0
- package/dist/runtime/convex-auth/component/adapter.d.ts +19 -2
- package/dist/runtime/convex-auth/component/adapter.js +1 -0
- package/dist/runtime/convex-auth/component/schema.js +5 -3
- package/dist/runtime/convex-auth/component/schemaMetadata.d.ts +25 -1
- package/dist/runtime/convex-auth/component/schemaMetadata.js +25 -1
- 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 +28 -0
- package/dist/runtime/convex-auth/session-generation.js +98 -0
- package/dist/runtime/convex-auth/workforce/adapter-policy.d.ts +6 -2
- package/dist/runtime/convex-auth/workforce/adapter-policy.js +43 -6
- package/dist/runtime/convex-auth/workforce/admission.js +2 -1
- package/dist/runtime/convex-auth/workforce/schema.d.ts +2 -2
- package/dist/runtime/convex-auth/workforce/schema.js +12 -4
- package/dist/runtime/convex-auth/workforce/session-management.js +2 -1
- package/dist/runtime/shared/release-fingerprint.js +1 -1
- package/package.json +8 -8
- package/security/upstream-convex-better-auth.json +48 -26
package/dist/module.json
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
type ConvexAuthorityKind = 'deployment-key' | 'deployment-name' | 'self-hosted';
|
|
3
|
+
export interface ConvexAuthoritySnapshot {
|
|
4
|
+
digest: string;
|
|
5
|
+
development: boolean;
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
3
8
|
/** Validate that the fixed file contains one, and only one, deployment selector class. */
|
|
4
9
|
export declare function assertConvexAuthoritySelection(environment: Readonly<Record<string, string>>): ConvexAuthorityKind;
|
|
5
10
|
/** Build a child environment whose only Convex authority comes from the fixed file. */
|
|
6
11
|
export declare function buildConvexCommandEnvironment(inherited: Readonly<Record<string, string | undefined>>, authorityFile: Readonly<Record<string, string>>): Record<string, string>;
|
|
12
|
+
export declare function inspectConvexAuthority(cwd?: string): Promise<ConvexAuthoritySnapshot>;
|
|
7
13
|
export declare function runConvexCommand(arguments_: readonly string[], options?: {
|
|
8
14
|
cwd?: string;
|
|
15
|
+
developmentOnly?: boolean;
|
|
16
|
+
expectedAuthorityDigest?: string;
|
|
9
17
|
inheritedEnvironment?: NodeJS.ProcessEnv;
|
|
10
18
|
input?: string;
|
|
11
19
|
onStdout?: (output: string) => void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
3
4
|
import { realpathSync } from "node:fs";
|
|
4
5
|
import { stat, readFile } from "node:fs/promises";
|
|
5
6
|
import { resolve } from "node:path";
|
|
@@ -176,6 +177,21 @@ async function readAuthorityFile(cwd) {
|
|
|
176
177
|
throw new Error("The Convex authority file is missing, invalid, or too large.");
|
|
177
178
|
}
|
|
178
179
|
}
|
|
180
|
+
function snapshotAuthority(environment) {
|
|
181
|
+
const kind = assertConvexAuthoritySelection(environment);
|
|
182
|
+
const selector = TOKEN_SELECTORS.map((name) => environment[name]).find(Boolean) ?? environment.CONVEX_DEPLOYMENT ?? "self-hosted";
|
|
183
|
+
const publicSelector = selector.includes("|") ? selector.slice(0, selector.indexOf("|")) : selector;
|
|
184
|
+
const development = kind === "deployment-key" && publicSelector.startsWith("dev:") || kind === "deployment-name" && /^(?:anonymous:anonymous-|dev:|local:)/u.test(publicSelector);
|
|
185
|
+
const canonicalAuthority = Object.entries(environment).filter(([name]) => ALLOWED_CONVEX_FILE_NAMES.has(name)).sort(([left], [right]) => left.localeCompare(right));
|
|
186
|
+
return {
|
|
187
|
+
digest: createHash("sha256").update(JSON.stringify(canonicalAuthority)).digest("hex"),
|
|
188
|
+
development,
|
|
189
|
+
label: kind === "self-hosted" ? "self-hosted deployment" : publicSelector
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
export async function inspectConvexAuthority(cwd = process.cwd()) {
|
|
193
|
+
return snapshotAuthority(await readAuthorityFile(resolve(cwd)));
|
|
194
|
+
}
|
|
179
195
|
function usage() {
|
|
180
196
|
return [
|
|
181
197
|
"Run the pinned Convex CLI with one explicit deployment authority.",
|
|
@@ -224,6 +240,13 @@ export async function runConvexCommand(arguments_, options = {}) {
|
|
|
224
240
|
assertFileBoundArguments(commandArguments);
|
|
225
241
|
const authorityFile = await readAuthorityFile(cwd);
|
|
226
242
|
const authorityKind = assertConvexAuthoritySelection(authorityFile);
|
|
243
|
+
const authority = snapshotAuthority(authorityFile);
|
|
244
|
+
if (options.developmentOnly && !authority.development) {
|
|
245
|
+
throw new Error("This command requires development, local, or anonymous authority.");
|
|
246
|
+
}
|
|
247
|
+
if (options.expectedAuthorityDigest !== void 0 && authority.digest !== options.expectedAuthorityDigest) {
|
|
248
|
+
throw new Error("The Convex authority file changed after the operation was confirmed.");
|
|
249
|
+
}
|
|
227
250
|
const deploymentKey = TOKEN_SELECTORS.map((name) => authorityFile[name]).find(Boolean);
|
|
228
251
|
if (command === "deploy" && authorityKind === "deployment-name") {
|
|
229
252
|
throw new Error("Deploy requires a deployment-scoped key or self-hosted authority.");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export interface InitDependencies {
|
|
2
2
|
confirm(message: string): Promise<boolean>;
|
|
3
3
|
prompt(message: string, defaultValue: string): Promise<string>;
|
|
4
|
+
readDevelopmentAuthorityLabel(): Promise<string>;
|
|
4
5
|
runConvex(args: readonly string[], input?: string): Promise<number>;
|
|
5
6
|
readEnvironmentNames(): Promise<ReadonlySet<string>>;
|
|
6
7
|
generateSchema(args: readonly string[]): Promise<number>;
|
package/dist/runtime/cli/init.js
CHANGED
|
@@ -3,7 +3,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
5
|
import { runAuthSchemaCommand } from "./auth-schema.js";
|
|
6
|
-
import { runConvexCommand } from "./convex.js";
|
|
6
|
+
import { inspectConvexAuthority, runConvexCommand } from "./convex.js";
|
|
7
7
|
const templates = {
|
|
8
8
|
"convex/auth.config.ts": `import { getConvexAuthProvider } from '@lupinum/better-convex-nuxt/better-auth/server'
|
|
9
9
|
import type { AuthConfig } from 'convex/server'
|
|
@@ -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
|
|
|
@@ -88,7 +88,26 @@ async function readOptional(path) {
|
|
|
88
88
|
throw error;
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
-
function defaultDependencies() {
|
|
91
|
+
function defaultDependencies(root) {
|
|
92
|
+
let authorityPromise;
|
|
93
|
+
const developmentAuthority = async () => {
|
|
94
|
+
authorityPromise ??= inspectConvexAuthority(root);
|
|
95
|
+
const authority = await authorityPromise;
|
|
96
|
+
if (!authority.development) {
|
|
97
|
+
throw new Error("better-convex init refuses production or unclassified authority");
|
|
98
|
+
}
|
|
99
|
+
return authority;
|
|
100
|
+
};
|
|
101
|
+
const runDevelopmentConvex = async (args, options = {}) => {
|
|
102
|
+
const authority = await developmentAuthority();
|
|
103
|
+
return await runConvexCommand(args, {
|
|
104
|
+
...options,
|
|
105
|
+
cwd: root,
|
|
106
|
+
developmentOnly: true,
|
|
107
|
+
expectedAuthorityDigest: authority.digest,
|
|
108
|
+
quiet: true
|
|
109
|
+
});
|
|
110
|
+
};
|
|
92
111
|
async function question(message) {
|
|
93
112
|
const terminal = createInterface({ input: process.stdin, output: process.stdout });
|
|
94
113
|
try {
|
|
@@ -106,11 +125,13 @@ function defaultDependencies() {
|
|
|
106
125
|
const answer = (await question(`${message} [${defaultValue}] `)).trim();
|
|
107
126
|
return answer || defaultValue;
|
|
108
127
|
},
|
|
109
|
-
|
|
128
|
+
async readDevelopmentAuthorityLabel() {
|
|
129
|
+
return (await developmentAuthority()).label;
|
|
130
|
+
},
|
|
131
|
+
runConvex: async (args, input) => await runDevelopmentConvex(args, { input }),
|
|
110
132
|
async readEnvironmentNames() {
|
|
111
133
|
let output = "";
|
|
112
|
-
const status = await
|
|
113
|
-
quiet: true,
|
|
134
|
+
const status = await runDevelopmentConvex(["env", "list", "--names-only"], {
|
|
114
135
|
onStdout: (value) => {
|
|
115
136
|
output += value;
|
|
116
137
|
}
|
|
@@ -218,9 +239,9 @@ export async function runInitCommand(args, overrides = {}) {
|
|
|
218
239
|
console.log("Usage: better-convex init [--typed-client]");
|
|
219
240
|
return 0;
|
|
220
241
|
}
|
|
221
|
-
const defaults = defaultDependencies();
|
|
222
|
-
const dependencies = { ...defaults, ...overrides };
|
|
223
242
|
const root = process.cwd();
|
|
243
|
+
const defaults = defaultDependencies(root);
|
|
244
|
+
const dependencies = { ...defaults, ...overrides };
|
|
224
245
|
const schemaPath = join(root, "convex/betterAuth/schema.ts");
|
|
225
246
|
const metadataPath = join(root, "convex/betterAuth/schemaMetadata.ts");
|
|
226
247
|
const [schema, metadata] = await Promise.all([
|
|
@@ -256,7 +277,10 @@ export async function runInitCommand(args, overrides = {}) {
|
|
|
256
277
|
"Generated auth schema conflicts with the reviewed plugin profile. Run better-convex auth schema and review the diff manually."
|
|
257
278
|
);
|
|
258
279
|
}
|
|
259
|
-
|
|
280
|
+
const authorityLabel = await dependencies.readDevelopmentAuthorityLabel();
|
|
281
|
+
if (!await dependencies.confirm(
|
|
282
|
+
`Provision development secrets and the first signing key in ${authorityLabel}?`
|
|
283
|
+
)) {
|
|
260
284
|
return 0;
|
|
261
285
|
}
|
|
262
286
|
await provisionDevelopment(dependencies);
|
|
@@ -183,7 +183,13 @@ export declare function defineAuthAdapterFunctions<Schema extends SchemaDefiniti
|
|
|
183
183
|
cursor: string | null;
|
|
184
184
|
};
|
|
185
185
|
model: string;
|
|
186
|
-
}, Promise<
|
|
186
|
+
}, Promise<{
|
|
187
|
+
page: Record<string, string | number | boolean | string[] | number[] | null>[];
|
|
188
|
+
isDone: boolean;
|
|
189
|
+
continueCursor: import("convex/server").Cursor;
|
|
190
|
+
splitCursor?: import("convex/server").Cursor | null;
|
|
191
|
+
pageStatus?: "SplitRecommended" | "SplitRequired" | null;
|
|
192
|
+
}>>;
|
|
187
193
|
count: import("convex/server").RegisteredQuery<"public", {
|
|
188
194
|
where?: {
|
|
189
195
|
mode?: "sensitive" | "insensitive" | undefined;
|
|
@@ -294,6 +300,18 @@ export declare function defineAuthAdapterFunctions<Schema extends SchemaDefiniti
|
|
|
294
300
|
field: string;
|
|
295
301
|
}[];
|
|
296
302
|
}, Promise<Record<string, string | number | boolean | string[] | number[] | null> | null>>;
|
|
303
|
+
consumeRateLimit: import("convex/server").RegisteredMutation<"public", {
|
|
304
|
+
window: number;
|
|
305
|
+
key: string;
|
|
306
|
+
max: number;
|
|
307
|
+
retentionWindow: number;
|
|
308
|
+
}, Promise<{
|
|
309
|
+
allowed: boolean;
|
|
310
|
+
retryAfter: null;
|
|
311
|
+
} | {
|
|
312
|
+
allowed: boolean;
|
|
313
|
+
retryAfter: number;
|
|
314
|
+
}>>;
|
|
297
315
|
incrementOne: import("convex/server").RegisteredMutation<"public", {
|
|
298
316
|
set?: any;
|
|
299
317
|
workforce?: {
|
|
@@ -78,6 +78,7 @@ const readArgs = {
|
|
|
78
78
|
offset: v.optional(v.number())
|
|
79
79
|
};
|
|
80
80
|
const expireSessionReference = makeFunctionReference("adapter:expireWorkforceSession");
|
|
81
|
+
const pruneRateLimitsReference = makeFunctionReference("adapter:deleteMany");
|
|
81
82
|
function assertRecord(value, code) {
|
|
82
83
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(code);
|
|
83
84
|
}
|
|
@@ -222,7 +223,7 @@ export function defineAuthAdapterFunctions({
|
|
|
222
223
|
}) {
|
|
223
224
|
assertAuthSchemaMatchesMetadata(schema, metadata);
|
|
224
225
|
const workforce = hasWorkforceSchema(metadata);
|
|
225
|
-
const workforcePolicy = createWorkforceAdapterPolicy(workforce);
|
|
226
|
+
const workforcePolicy = createWorkforceAdapterPolicy(workforce, metadata);
|
|
226
227
|
const relationships = createAuthRelationshipEngine({
|
|
227
228
|
schema,
|
|
228
229
|
metadata,
|
|
@@ -272,7 +273,6 @@ export function defineAuthAdapterFunctions({
|
|
|
272
273
|
args: { storageId: v.id("session") },
|
|
273
274
|
returns: v.null(),
|
|
274
275
|
handler: async (ctx, args) => {
|
|
275
|
-
requireWorkforce();
|
|
276
276
|
const next = await expireWorkforceSession(ctx, args.storageId);
|
|
277
277
|
if (next !== null) await ctx.scheduler.runAt(next, expireSessionReference, args);
|
|
278
278
|
return null;
|
|
@@ -314,7 +314,11 @@ export function defineAuthAdapterFunctions({
|
|
|
314
314
|
workforceConsumedChallenge: v.optional(workforceConsumedChallengeValidator)
|
|
315
315
|
},
|
|
316
316
|
handler: async (ctx, args) => {
|
|
317
|
-
let row = normalizeCreate(
|
|
317
|
+
let row = normalizeCreate(
|
|
318
|
+
metadata,
|
|
319
|
+
args.model,
|
|
320
|
+
await workforcePolicy.prepareCreateInput(ctx, args.model, args.data)
|
|
321
|
+
);
|
|
318
322
|
await relationships.assertTargets(ctx, args.model, row);
|
|
319
323
|
await assertUniqueConstraints(ctx, schema, metadata, args.model, row);
|
|
320
324
|
row = await workforcePolicy.prepareCreate(
|
|
@@ -335,7 +339,7 @@ export function defineAuthAdapterFunctions({
|
|
|
335
339
|
if (!finalRow) throw new Error("AUTH_CREATE_TRIGGER_DELETED_ROW");
|
|
336
340
|
await workforcePolicy.scheduleCreatedSession(args.model, finalRow, async (expiresAt) => {
|
|
337
341
|
const sessionId = ctx.db.normalizeId("session", storageId);
|
|
338
|
-
if (!sessionId) throw new Error("
|
|
342
|
+
if (!sessionId) throw new Error("AUTH_SESSION_INVALID");
|
|
339
343
|
await ctx.scheduler.runAt(expiresAt, expireSessionReference, { storageId: sessionId });
|
|
340
344
|
});
|
|
341
345
|
return toBetterAuthDocument(finalRow);
|
|
@@ -349,10 +353,20 @@ export function defineAuthAdapterFunctions({
|
|
|
349
353
|
workforce: v.optional(workforceOperationValidator)
|
|
350
354
|
},
|
|
351
355
|
handler: async (ctx, args) => {
|
|
352
|
-
const
|
|
356
|
+
const requested = readShape(args);
|
|
357
|
+
const rows = await findAuthRows(
|
|
358
|
+
ctx,
|
|
359
|
+
schema,
|
|
360
|
+
metadata,
|
|
361
|
+
{
|
|
362
|
+
...requested,
|
|
363
|
+
select: workforcePolicy.prepareReadSelect(args.model, requested.select)
|
|
364
|
+
},
|
|
365
|
+
2
|
|
366
|
+
);
|
|
353
367
|
const row = oneOrNull(rows, "AUTH_FIND_ONE");
|
|
354
368
|
const view = await workforcePolicy.projectFind(ctx, args.model, row, args.workforce);
|
|
355
|
-
return toBetterAuthDocument(view,
|
|
369
|
+
return toBetterAuthDocument(view, requested.select);
|
|
356
370
|
}
|
|
357
371
|
}),
|
|
358
372
|
findMany: queryGeneric({
|
|
@@ -364,14 +378,21 @@ export function defineAuthAdapterFunctions({
|
|
|
364
378
|
paginationOpts: paginationOptsValidator
|
|
365
379
|
},
|
|
366
380
|
handler: async (ctx, args) => {
|
|
381
|
+
const requested = readShape(args);
|
|
367
382
|
const result = await paginateAuthRows(
|
|
368
383
|
ctx,
|
|
369
384
|
schema,
|
|
370
385
|
metadata,
|
|
371
|
-
|
|
386
|
+
{
|
|
387
|
+
...requested,
|
|
388
|
+
select: workforcePolicy.prepareReadSelect(args.model, requested.select)
|
|
389
|
+
},
|
|
372
390
|
args.paginationOpts
|
|
373
391
|
);
|
|
374
|
-
|
|
392
|
+
const page = (await Promise.all(
|
|
393
|
+
result.page.map((row) => workforcePolicy.projectFind(ctx, args.model, row))
|
|
394
|
+
)).filter((row) => row !== null).map((row) => toBetterAuthDocument(row, requested.select));
|
|
395
|
+
return { ...result, page: args.limit === void 0 ? page : page.slice(0, args.limit) };
|
|
375
396
|
}
|
|
376
397
|
}),
|
|
377
398
|
count: queryGeneric({
|
|
@@ -495,6 +516,12 @@ export function defineAuthAdapterFunctions({
|
|
|
495
516
|
onUpdateModels: v.optional(v.array(v.string()))
|
|
496
517
|
},
|
|
497
518
|
handler: async (ctx, args) => {
|
|
519
|
+
const invalidated = await workforcePolicy.invalidateSessionCollection(
|
|
520
|
+
ctx,
|
|
521
|
+
args.model,
|
|
522
|
+
args.where
|
|
523
|
+
);
|
|
524
|
+
if (invalidated !== null) return invalidated;
|
|
498
525
|
const rows = await workforcePolicy.expiredVerificationRows(ctx, args.model, args.where) ?? await relationships.collectOperationRows(ctx, readShape(args));
|
|
499
526
|
await relationships.applyDeletion(ctx, rows, args.model, args);
|
|
500
527
|
return rows.length;
|
|
@@ -521,6 +548,74 @@ export function defineAuthAdapterFunctions({
|
|
|
521
548
|
return toBetterAuthDocument(current);
|
|
522
549
|
}
|
|
523
550
|
}),
|
|
551
|
+
consumeRateLimit: mutationGeneric({
|
|
552
|
+
args: {
|
|
553
|
+
key: v.string(),
|
|
554
|
+
max: v.number(),
|
|
555
|
+
retentionWindow: v.number(),
|
|
556
|
+
window: v.number()
|
|
557
|
+
},
|
|
558
|
+
returns: v.object({
|
|
559
|
+
allowed: v.boolean(),
|
|
560
|
+
retryAfter: v.union(v.number(), v.null())
|
|
561
|
+
}),
|
|
562
|
+
handler: async (ctx, args) => {
|
|
563
|
+
const windowInMs = args.window * 1e3;
|
|
564
|
+
const retentionWindowInMs = args.retentionWindow * 1e3;
|
|
565
|
+
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)) {
|
|
566
|
+
throw new Error("AUTH_RATE_LIMIT_RULE_INVALID");
|
|
567
|
+
}
|
|
568
|
+
const now = Date.now();
|
|
569
|
+
const current = oneOrNull(
|
|
570
|
+
await findAuthRows(
|
|
571
|
+
ctx,
|
|
572
|
+
schema,
|
|
573
|
+
metadata,
|
|
574
|
+
{ model: "rateLimit", where: [{ field: "key", value: args.key }] },
|
|
575
|
+
2
|
|
576
|
+
),
|
|
577
|
+
"AUTH_RATE_LIMIT"
|
|
578
|
+
);
|
|
579
|
+
if (!current) {
|
|
580
|
+
await ctx.db.insert(
|
|
581
|
+
"rateLimit",
|
|
582
|
+
{ id: args.key, key: args.key, count: 1, lastRequest: now }
|
|
583
|
+
);
|
|
584
|
+
return { allowed: true, retryAfter: null };
|
|
585
|
+
}
|
|
586
|
+
if (typeof current.count !== "number" || !Number.isSafeInteger(current.count) || current.count < 0 || typeof current.lastRequest !== "number" || !Number.isFinite(current.lastRequest)) {
|
|
587
|
+
throw new Error("AUTH_RATE_LIMIT_ROW_INVALID");
|
|
588
|
+
}
|
|
589
|
+
if (now - current.lastRequest >= windowInMs) {
|
|
590
|
+
await ctx.db.patch(current._id, { count: 1, lastRequest: now });
|
|
591
|
+
await ctx.scheduler.runAfter(0, pruneRateLimitsReference, {
|
|
592
|
+
model: "rateLimit",
|
|
593
|
+
where: [
|
|
594
|
+
{
|
|
595
|
+
field: "lastRequest",
|
|
596
|
+
operator: "lt",
|
|
597
|
+
value: now - retentionWindowInMs
|
|
598
|
+
}
|
|
599
|
+
]
|
|
600
|
+
});
|
|
601
|
+
return { allowed: true, retryAfter: null };
|
|
602
|
+
}
|
|
603
|
+
if (current.count >= args.max) {
|
|
604
|
+
return {
|
|
605
|
+
allowed: false,
|
|
606
|
+
retryAfter: Math.max(1, Math.ceil((current.lastRequest + windowInMs - now) / 1e3))
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
await ctx.db.patch(
|
|
610
|
+
current._id,
|
|
611
|
+
{
|
|
612
|
+
count: current.count + 1,
|
|
613
|
+
lastRequest: now
|
|
614
|
+
}
|
|
615
|
+
);
|
|
616
|
+
return { allowed: true, retryAfter: null };
|
|
617
|
+
}
|
|
618
|
+
}),
|
|
524
619
|
incrementOne: mutationGeneric({
|
|
525
620
|
returns: v.union(authDocumentValidator, v.null()),
|
|
526
621
|
args: {
|
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
import {
|
|
2
2
|
fingerprintAuthSchemaModels
|
|
3
3
|
} from "./metadata.js";
|
|
4
|
+
const sessionGenerationFields = {
|
|
5
|
+
session: {
|
|
6
|
+
bcnAssuranceGeneration: {
|
|
7
|
+
type: "number",
|
|
8
|
+
required: true,
|
|
9
|
+
defaultValue: 0,
|
|
10
|
+
input: false,
|
|
11
|
+
returned: false
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
user: {
|
|
15
|
+
bcnSecurityGeneration: {
|
|
16
|
+
type: "number",
|
|
17
|
+
required: true,
|
|
18
|
+
defaultValue: 0,
|
|
19
|
+
input: false,
|
|
20
|
+
returned: false
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
function withSessionGenerationFields(tables) {
|
|
25
|
+
return Object.fromEntries(
|
|
26
|
+
Object.entries(tables).map(([name, table]) => {
|
|
27
|
+
if (name !== "user" && name !== "session") return [name, table];
|
|
28
|
+
for (const [fieldName, expected] of Object.entries(sessionGenerationFields[name])) {
|
|
29
|
+
const configured = table.fields[fieldName];
|
|
30
|
+
if (configured && (configured.type !== expected.type || configured.required !== expected.required || configured.defaultValue !== expected.defaultValue || configured.input !== expected.input || configured.returned !== expected.returned || configured.fieldName !== void 0 || configured.references !== void 0 || configured.unique === true)) {
|
|
31
|
+
throw new Error(`AUTH_SCHEMA_SESSION_GENERATION_FIELD_RESERVED:${name}.${fieldName}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return [name, { ...table, fields: { ...table.fields, ...sessionGenerationFields[name] } }];
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
}
|
|
4
38
|
const explicitIndexes = {
|
|
5
39
|
invitation: [
|
|
6
40
|
{ fields: ["email", "organizationId", "status"] },
|
|
@@ -311,7 +345,7 @@ ${fields}
|
|
|
311
345
|
].join("\n");
|
|
312
346
|
}
|
|
313
347
|
export function generateAuthSchemaArtifacts(tables) {
|
|
314
|
-
const metadata = buildMetadata(tables);
|
|
348
|
+
const metadata = buildMetadata(withSessionGenerationFields(tables));
|
|
315
349
|
return {
|
|
316
350
|
metadata,
|
|
317
351
|
metadataCode: renderMetadata(metadata),
|
|
@@ -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;
|
|
@@ -118,7 +129,13 @@ export declare const assertProfile: import("convex/server").RegisteredQuery<"pub
|
|
|
118
129
|
cursor: string | null;
|
|
119
130
|
};
|
|
120
131
|
model: string;
|
|
121
|
-
}, Promise<
|
|
132
|
+
}, Promise<{
|
|
133
|
+
page: Record<string, string | number | boolean | string[] | number[] | null>[];
|
|
134
|
+
isDone: boolean;
|
|
135
|
+
continueCursor: import("convex/server").Cursor;
|
|
136
|
+
splitCursor?: import("convex/server").Cursor | null;
|
|
137
|
+
pageStatus?: "SplitRecommended" | "SplitRequired" | null;
|
|
138
|
+
}>>, findOne: import("convex/server").RegisteredQuery<"public", {
|
|
122
139
|
join?: any;
|
|
123
140
|
select?: string[] | undefined;
|
|
124
141
|
workforce?: {
|
|
@@ -8,7 +8,8 @@ export const tables = {
|
|
|
8
8
|
emailVerified: v.boolean(),
|
|
9
9
|
image: v.union(v.null(), v.string()),
|
|
10
10
|
createdAt: v.number(),
|
|
11
|
-
updatedAt: v.number()
|
|
11
|
+
updatedAt: v.number(),
|
|
12
|
+
bcnSecurityGeneration: v.number()
|
|
12
13
|
}).index("id", ["id"]).index("name", ["name"]).index("email", ["email"]).index("createdAt", ["createdAt"]),
|
|
13
14
|
session: defineTable({
|
|
14
15
|
id: v.string(),
|
|
@@ -18,7 +19,8 @@ export const tables = {
|
|
|
18
19
|
updatedAt: v.number(),
|
|
19
20
|
ipAddress: v.union(v.null(), v.string()),
|
|
20
21
|
userAgent: v.union(v.null(), v.string()),
|
|
21
|
-
userId: v.string()
|
|
22
|
+
userId: v.string(),
|
|
23
|
+
bcnAssuranceGeneration: v.number()
|
|
22
24
|
}).index("id", ["id"]).index("expiresAt", ["expiresAt"]).index("userId_expiresAt", ["userId", "expiresAt"]).index("token", ["token"]).index("createdAt", ["createdAt"]).index("userId", ["userId"]),
|
|
23
25
|
account: defineTable({
|
|
24
26
|
id: v.string(),
|
|
@@ -176,6 +178,6 @@ export const tables = {
|
|
|
176
178
|
};
|
|
177
179
|
const schema = defineSchema(tables);
|
|
178
180
|
Object.defineProperty(schema, "__betterConvexNuxtAuthSchemaFingerprint", {
|
|
179
|
-
value: "bcn-auth-schema-v2:
|
|
181
|
+
value: "bcn-auth-schema-v2:de26f24c2d072824"
|
|
180
182
|
});
|
|
181
183
|
export default schema;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** This file is generated by Better Convex Nuxt. Do not edit. */
|
|
2
2
|
declare const schemaMetadata: {
|
|
3
|
-
readonly fingerprint: "bcn-auth-schema-v2:
|
|
3
|
+
readonly fingerprint: "bcn-auth-schema-v2:de26f24c2d072824";
|
|
4
4
|
readonly models: {
|
|
5
5
|
readonly user: {
|
|
6
6
|
readonly logicalName: "user";
|
|
@@ -90,6 +90,18 @@ declare const schemaMetadata: {
|
|
|
90
90
|
readonly unique: false;
|
|
91
91
|
readonly updatable: true;
|
|
92
92
|
};
|
|
93
|
+
readonly bcnSecurityGeneration: {
|
|
94
|
+
readonly logicalName: "bcnSecurityGeneration";
|
|
95
|
+
readonly physicalName: "bcnSecurityGeneration";
|
|
96
|
+
readonly kind: "number";
|
|
97
|
+
readonly nullable: false;
|
|
98
|
+
readonly required: true;
|
|
99
|
+
readonly indexed: false;
|
|
100
|
+
readonly selectable: true;
|
|
101
|
+
readonly sortable: false;
|
|
102
|
+
readonly unique: false;
|
|
103
|
+
readonly updatable: true;
|
|
104
|
+
};
|
|
93
105
|
};
|
|
94
106
|
readonly indexes: readonly [{
|
|
95
107
|
readonly descriptor: "id";
|
|
@@ -212,6 +224,18 @@ declare const schemaMetadata: {
|
|
|
212
224
|
readonly onDelete: "cascade";
|
|
213
225
|
};
|
|
214
226
|
};
|
|
227
|
+
readonly bcnAssuranceGeneration: {
|
|
228
|
+
readonly logicalName: "bcnAssuranceGeneration";
|
|
229
|
+
readonly physicalName: "bcnAssuranceGeneration";
|
|
230
|
+
readonly kind: "number";
|
|
231
|
+
readonly nullable: false;
|
|
232
|
+
readonly required: true;
|
|
233
|
+
readonly indexed: false;
|
|
234
|
+
readonly selectable: true;
|
|
235
|
+
readonly sortable: false;
|
|
236
|
+
readonly unique: false;
|
|
237
|
+
readonly updatable: true;
|
|
238
|
+
};
|
|
215
239
|
};
|
|
216
240
|
readonly indexes: readonly [{
|
|
217
241
|
readonly descriptor: "id";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const schemaMetadata = {
|
|
2
|
-
fingerprint: "bcn-auth-schema-v2:
|
|
2
|
+
fingerprint: "bcn-auth-schema-v2:de26f24c2d072824",
|
|
3
3
|
models: {
|
|
4
4
|
user: {
|
|
5
5
|
logicalName: "user",
|
|
@@ -88,6 +88,18 @@ const schemaMetadata = {
|
|
|
88
88
|
sortable: false,
|
|
89
89
|
unique: false,
|
|
90
90
|
updatable: true
|
|
91
|
+
},
|
|
92
|
+
bcnSecurityGeneration: {
|
|
93
|
+
logicalName: "bcnSecurityGeneration",
|
|
94
|
+
physicalName: "bcnSecurityGeneration",
|
|
95
|
+
kind: "number",
|
|
96
|
+
nullable: false,
|
|
97
|
+
required: true,
|
|
98
|
+
indexed: false,
|
|
99
|
+
selectable: true,
|
|
100
|
+
sortable: false,
|
|
101
|
+
unique: false,
|
|
102
|
+
updatable: true
|
|
91
103
|
}
|
|
92
104
|
},
|
|
93
105
|
indexes: [
|
|
@@ -215,6 +227,18 @@ const schemaMetadata = {
|
|
|
215
227
|
field: "id",
|
|
216
228
|
onDelete: "cascade"
|
|
217
229
|
}
|
|
230
|
+
},
|
|
231
|
+
bcnAssuranceGeneration: {
|
|
232
|
+
logicalName: "bcnAssuranceGeneration",
|
|
233
|
+
physicalName: "bcnAssuranceGeneration",
|
|
234
|
+
kind: "number",
|
|
235
|
+
nullable: false,
|
|
236
|
+
required: true,
|
|
237
|
+
indexed: false,
|
|
238
|
+
selectable: true,
|
|
239
|
+
sortable: false,
|
|
240
|
+
unique: false,
|
|
241
|
+
updatable: true
|
|
218
242
|
}
|
|
219
243
|
},
|
|
220
244
|
indexes: [
|
|
@@ -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';
|