@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.
Files changed (31) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/cli/convex.d.ts +8 -0
  3. package/dist/runtime/cli/convex.js +23 -0
  4. package/dist/runtime/cli/init.d.ts +1 -0
  5. package/dist/runtime/cli/init.js +33 -9
  6. package/dist/runtime/convex-auth/adapter/define-functions.d.ts +19 -1
  7. package/dist/runtime/convex-auth/adapter/define-functions.js +103 -8
  8. package/dist/runtime/convex-auth/adapter/generate-schema.js +35 -1
  9. package/dist/runtime/convex-auth/component/_generated/component.d.ts +9 -0
  10. package/dist/runtime/convex-auth/component/adapter.d.ts +19 -2
  11. package/dist/runtime/convex-auth/component/adapter.js +1 -0
  12. package/dist/runtime/convex-auth/component/schema.js +5 -3
  13. package/dist/runtime/convex-auth/component/schemaMetadata.d.ts +25 -1
  14. package/dist/runtime/convex-auth/component/schemaMetadata.js +25 -1
  15. package/dist/runtime/convex-auth/create-better-convex-auth.js +16 -1
  16. package/dist/runtime/convex-auth/index.d.ts +1 -0
  17. package/dist/runtime/convex-auth/index.js +1 -0
  18. package/dist/runtime/convex-auth/plugin.js +2 -1
  19. package/dist/runtime/convex-auth/rate-limit-storage.d.ts +20 -0
  20. package/dist/runtime/convex-auth/rate-limit-storage.js +46 -0
  21. package/dist/runtime/convex-auth/session-generation.d.ts +28 -0
  22. package/dist/runtime/convex-auth/session-generation.js +98 -0
  23. package/dist/runtime/convex-auth/workforce/adapter-policy.d.ts +6 -2
  24. package/dist/runtime/convex-auth/workforce/adapter-policy.js +43 -6
  25. package/dist/runtime/convex-auth/workforce/admission.js +2 -1
  26. package/dist/runtime/convex-auth/workforce/schema.d.ts +2 -2
  27. package/dist/runtime/convex-auth/workforce/schema.js +12 -4
  28. package/dist/runtime/convex-auth/workforce/session-management.js +2 -1
  29. package/dist/runtime/shared/release-fingerprint.js +1 -1
  30. package/package.json +8 -8
  31. package/security/upstream-convex-better-auth.json +48 -26
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=4.5.2 <5"
6
6
  },
7
- "version": "1.0.0-beta.3",
7
+ "version": "1.0.0-beta.5",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
@@ -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>;
@@ -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
- runConvex: async (args, input) => await runConvexCommand(args, { input, quiet: true }),
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 runConvexCommand(["env", "list", "--names-only"], {
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
- if (!await dependencies.confirm("Provision development secrets and the first signing key?")) {
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<import("convex/server").PaginationResult<Record<string, string | number | boolean | string[] | number[] | null>>>>;
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(metadata, args.model, args.data);
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("AUTH_WORKFORCE_SESSION_INVALID");
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 rows = await findAuthRows(ctx, schema, metadata, readShape(args), 2);
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, args.select);
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
- readShape(args),
386
+ {
387
+ ...requested,
388
+ select: workforcePolicy.prepareReadSelect(args.model, requested.select)
389
+ },
372
390
  args.paginationOpts
373
391
  );
374
- return args.limit === void 0 ? result : { ...result, page: result.page.slice(0, args.limit) };
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>>, count: import("convex/server").RegisteredQuery<"public", {
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<import("convex/server").PaginationResult<Record<string, string | number | boolean | string[] | number[] | null>>>>, findOne: import("convex/server").RegisteredQuery<"public", {
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?: {
@@ -4,6 +4,7 @@ import schemaMetadata from "./schemaMetadata.js";
4
4
  export const {
5
5
  assertProfile,
6
6
  consumeOne,
7
+ consumeRateLimit,
7
8
  count,
8
9
  create,
9
10
  deleteMany,
@@ -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:e33d90ec0cf73ec1"
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:e33d90ec0cf73ec1";
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:e33d90ec0cf73ec1",
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: { enabled: true, modelName: "rateLimit", storage: "database" },
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';