@somewhere-tech/cli 0.37.9 → 0.37.11

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.
@@ -35,7 +35,13 @@ var require_declared_data_contract = __commonJS({
35
35
  "worker/containers/compile/declared-data-contract.cjs"(exports2, module2) {
36
36
  "use strict";
37
37
  var identifier = /^[a-z_][a-z0-9_]*$/;
38
- var types = /* @__PURE__ */ new Set(["integer", "number", "text", "boolean", "timestamp", "json", "blob"]);
38
+ var types = /* @__PURE__ */ new Set(["integer", "bigint", "number", "text", "boolean", "timestamp", "json", "blob"]);
39
+ function canonicalBigintText(value) {
40
+ if (typeof value !== "string" || !/^(0|-?[1-9][0-9]*)$/.test(value)) return false;
41
+ const digits = value.startsWith("-") ? value.slice(1) : value;
42
+ const limit = value.startsWith("-") ? "9223372036854775808" : "9223372036854775807";
43
+ return digits.length < limit.length || digits.length === limit.length && digits <= limit;
44
+ }
39
45
  function fail(detail) {
40
46
  throw new Error(`Invalid declared data contract: ${detail}`);
41
47
  }
@@ -48,7 +54,7 @@ var require_declared_data_contract = __commonJS({
48
54
  if (result.some((field) => typeof field !== "string" || !columns.has(field)) || new Set(result).size !== result.length) fail(name);
49
55
  return result.sort();
50
56
  }
51
- function policy(value, label) {
57
+ function policy(value, label, edges = 0) {
52
58
  record(value, label);
53
59
  const keys = Object.keys(value).sort().join(",");
54
60
  if (value.k === "o" && keys === "c,k" && identifier.test(value.c)) return { k: "o", c: value.c };
@@ -65,13 +71,31 @@ var require_declared_data_contract = __commonJS({
65
71
  if (left.k !== "o" || right.k !== "m") fail(label);
66
72
  return { k: "a", p: [left, right] };
67
73
  }
74
+ if (value.k === "g" && ["c,k,roles", "c,generation,k,roles"].includes(keys) && identifier.test(value.c)) {
75
+ return { k: "g", c: value.c, roles: policyRoles(value.roles, label) };
76
+ }
77
+ if (value.k === "r" && ["k,roles", "generation,k,roles"].includes(keys)) {
78
+ return { k: "r", roles: policyRoles(value.roles, label) };
79
+ }
68
80
  if (value.k === "p" && keys === "k,p,pk,t,v" && [value.v, value.t, value.pk].every((item) => typeof item === "string" && identifier.test(item))) {
69
- const inherited = policy(value.p, label);
70
- if (!["o", "m", "a"].includes(inherited.k)) fail(label);
81
+ if (edges + 1 > 4) fail(label);
82
+ const inherited = policy(value.p, label, edges + 1);
83
+ if (!["o", "m", "a", "p", "g", "r"].includes(inherited.k)) fail(label);
71
84
  return { k: "p", v: value.v, t: value.t, pk: value.pk, p: inherited };
72
85
  }
73
86
  fail(label);
74
87
  }
88
+ function policyRoles(value, label) {
89
+ record(value, label + ".roles");
90
+ if (Object.keys(value).sort().join(",") !== "create,delete,read,update") fail(label);
91
+ const roles = {};
92
+ for (const op of ["read", "create", "update", "delete"]) {
93
+ const list = value[op];
94
+ if (!Array.isArray(list) || list.some((role) => typeof role !== "string" || !/^[a-z][a-z0-9_]{0,63}$/.test(role)) || new Set(list).size !== list.length) fail(label);
95
+ roles[op] = [...list].sort();
96
+ }
97
+ return roles;
98
+ }
75
99
  function policyMembershipTables(value, out) {
76
100
  if (!value || typeof value !== "object") return;
77
101
  if (value.k === "m" && value.m) out.add(value.m.m);
@@ -120,7 +144,7 @@ var require_declared_data_contract = __commonJS({
120
144
  for (const [field, value] of entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
121
145
  const column = columns.find((column2) => column2.n === field);
122
146
  if (!column || ["json", "blob"].includes(column.t) || !(value === null || typeof value === "string" && value.length <= 256 || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value))) fail(`${name} public read predicate`);
123
- if (value === null ? column.nul !== 1 : column.t === "boolean" ? typeof value !== "boolean" : ["integer", "number"].includes(column.t) ? typeof value !== "number" : ["text", "timestamp"].includes(column.t) ? typeof value !== "string" : true) fail(`${name} public read predicate type`);
147
+ if (value === null ? column.nul !== 1 : column.t === "boolean" ? typeof value !== "boolean" : column.t === "integer" ? !Number.isSafeInteger(value) : column.t === "number" ? typeof value !== "number" : column.t === "bigint" ? !canonicalBigintText(value) : ["text", "timestamp"].includes(column.t) ? typeof value !== "string" : true) fail(`${name} public read predicate type`);
124
148
  where[field] = value;
125
149
  }
126
150
  publicRead = { where };
@@ -158,7 +182,8 @@ var require_declared_data_contract = __commonJS({
158
182
  member = { g: [...g], m, u, mg: [...mg], ...normalizedOperations !== void 0 ? { o: [...normalizedOperations] } : {} };
159
183
  }
160
184
  const normalizedPolicy = intent === "policy" ? policy(table.policy, `${name}.policy`) : null;
161
- if (normalizedPolicy && !["a", "p"].includes(normalizedPolicy.k)) fail(`${name}.policy root`);
185
+ if (normalizedPolicy && !["a", "p", "g", "r"].includes(normalizedPolicy.k)) fail(`${name}.policy root`);
186
+ if (normalizedPolicy?.k === "g" && create !== null && !create.includes(normalizedPolicy.c)) fail(`${name}.policy group create`);
162
187
  if (normalizedPolicy?.k === "a") {
163
188
  owner = normalizedPolicy.p[0].c;
164
189
  if ([...create || [], ...update || []].includes(owner) || (update || []).some((field) => normalizedPolicy.p[1].m.g.includes(field))) fail(`${name}.policy identity writes`);
@@ -211,7 +236,7 @@ var require_declared_data_contract = __commonJS({
211
236
  return JSON.stringify({ version: 1, tables });
212
237
  }
213
238
  function columnType(column) {
214
- const type = { integer: "number | string", number: "number", text: "string", timestamp: "string", boolean: "boolean", json: "Json", blob: "number[]" }[column.t];
239
+ const type = { integer: "number", bigint: "string", number: "number", text: "string", timestamp: "string", boolean: "boolean", json: "Json", blob: "number[]" }[column.t];
215
240
  return column.nul ? `${type} | null` : type;
216
241
  }
217
242
  function shape(columns, names, mode, required = /* @__PURE__ */ new Set()) {
@@ -331,11 +356,175 @@ var require_typed_data = __commonJS({
331
356
  }
332
357
  });
333
358
 
359
+ // worker/containers/compile/groups-types.cjs
360
+ var require_groups_types = __commonJS({
361
+ "worker/containers/compile/groups-types.cjs"(exports2, module2) {
362
+ "use strict";
363
+ var ROLE_NAME2 = /^[a-z][a-z0-9_]{0,63}$/;
364
+ var BUILT_IN_GROUP_ROLES3 = ["owner", "admin", "member"];
365
+ function fail(detail) {
366
+ throw new Error(`Invalid declared roles: ${detail}`);
367
+ }
368
+ function roleList(value, field) {
369
+ if (!Array.isArray(value)) fail(`${field} must be an array`);
370
+ const seen = /* @__PURE__ */ new Set();
371
+ for (const name of value) {
372
+ if (typeof name !== "string" || !ROLE_NAME2.test(name)) fail(`${field} has an invalid role name`);
373
+ if (seen.has(name)) fail(`${field} repeats ${name}`);
374
+ seen.add(name);
375
+ }
376
+ return [...value];
377
+ }
378
+ function declaredRoles2(text) {
379
+ if (text === void 0) return {};
380
+ if (typeof text !== "string") fail("expected canonical JSON text");
381
+ let value;
382
+ try {
383
+ value = JSON.parse(text);
384
+ } catch {
385
+ fail("not JSON");
386
+ }
387
+ if (!value || typeof value !== "object" || Array.isArray(value) || value.v !== 1) fail("expected { v: 1 }");
388
+ const keys = Object.keys(value).sort().join(",");
389
+ if (keys !== "adminGrantableGroupRoles,appRoles,groupRoles,v") fail("unexpected fields");
390
+ const groupRoles = roleList(value.groupRoles, "groupRoles");
391
+ if (groupRoles.some((name) => BUILT_IN_GROUP_ROLES3.includes(name))) fail("groupRoles repeats a built-in role");
392
+ const grantable = roleList(value.adminGrantableGroupRoles, "adminGrantableGroupRoles");
393
+ if (grantable.some((name) => !groupRoles.includes(name))) fail("adminGrantableGroupRoles names an undeclared role");
394
+ if (!Array.isArray(value.appRoles)) fail("appRoles must be an array");
395
+ const appRoles = roleList(value.appRoles.map((role) => {
396
+ if (!role || typeof role !== "object" || Array.isArray(role) || Object.keys(role).sort().join(",") !== "grantableBy,name") fail("appRoles entry");
397
+ return role.name;
398
+ }), "appRoles");
399
+ for (const role of value.appRoles) {
400
+ if (roleList(role.grantableBy, "grantableBy").some((name) => !appRoles.includes(name))) fail("grantableBy names an undeclared app role");
401
+ }
402
+ return { groupRoles, appRoles };
403
+ }
404
+ function groupsTypeDeclarations(roles = {}) {
405
+ const literal = (names) => names.map((name) => JSON.stringify(name)).join(" | ");
406
+ const groupRoles = literal([...BUILT_IN_GROUP_ROLES3, ...roles.groupRoles || []]);
407
+ const appRoles = (roles.appRoles || []).length ? literal(roles.appRoles) : "never";
408
+ return `
409
+ type SomewhereGroupRole = ${groupRoles};
410
+ type SomewhereAppRole = ${appRoles};
411
+
412
+ interface SomewhereGroup {
413
+ id: string;
414
+ name: string;
415
+ created_by: string;
416
+ created_at: number;
417
+ updated_at: number;
418
+ /** The signed-in user's role in this group. */
419
+ role: SomewhereGroupRole;
420
+ }
421
+
422
+ interface SomewhereGroupMember {
423
+ user_id: string;
424
+ role: SomewhereGroupRole;
425
+ created_at: number;
426
+ }
427
+
428
+ /** The group plus the FIRST roster page; continue with members(req, id, { cursor: members_next_cursor }). */
429
+ interface SomewhereGroupDetail extends SomewhereGroup {
430
+ members: SomewhereGroupMember[];
431
+ members_next_cursor: string | null;
432
+ }
433
+
434
+ /** One keyset page; next_cursor is null when nothing follows. No total count. */
435
+ interface SomewhereGroupsPage<T> {
436
+ items: T[];
437
+ next_cursor: string | null;
438
+ }
439
+
440
+ /** limit 1\u2013100 (default 50); cursor is a previous page's next_cursor. */
441
+ interface SomewhereGroupsPageOptions {
442
+ cursor?: string;
443
+ limit?: number;
444
+ }
445
+
446
+ interface SomewhereAppRoleGrant {
447
+ user_id: string;
448
+ role: SomewhereAppRole;
449
+ granted_by: string;
450
+ grant_source: 'platform_admin' | 'delegation';
451
+ created_at: number;
452
+ }
453
+
454
+ /** A sent group invitation: one email; acceptance completes the membership. */
455
+ interface SomewhereGroupInvitation {
456
+ invite: { id: string; email: string; status: 'pending'; redirect_uri: string; expires_at: number; created_at: number };
457
+ group: { group_id: string; role: SomewhereGroupRole };
458
+ delivery: 'sent' | 'pending';
459
+ }
460
+
461
+ /**
462
+ * Where an accepted invitation's membership stands. pending: not confirmed
463
+ * yet (the platform retries on this user's next list); refused: it will not
464
+ * complete (code says why, e.g. INVITATION_REVOKED, INVITATION_AUTHORITY_LOST).
465
+ */
466
+ interface SomewhereGroupInvitationOutcome {
467
+ invite_id: string;
468
+ group_id: string;
469
+ role: SomewhereGroupRole;
470
+ state: 'completed' | 'pending' | 'refused';
471
+ code?: string;
472
+ }
473
+
474
+ /** Expected refusals and unconfirmed outcomes resolve with retry:false; changes are never re-sent. */
475
+ interface SomewhereGroupsError {
476
+ code: string;
477
+ message: string;
478
+ status: number;
479
+ retry: false;
480
+ data?: Record<string, unknown>;
481
+ }
482
+
483
+ type SomewhereGroupsResult<T> = { data: T; error: null } | { data: null; error: SomewhereGroupsError };
484
+
485
+ /**
486
+ * Every method acts as the user signed in on the request this function was
487
+ * invoked with, read once from that original request. The \`req\` argument is
488
+ * accepted for compatibility and never read: passing another Request cannot
489
+ * change who acts, and no method accepts an acting user. Groups are available
490
+ * on the live app only; a preview, a dev run, a job delivery or a signed-out
491
+ * request resolves with data:null and a non-retryable error before any change.
492
+ */
493
+ interface SomewhereGroups {
494
+ create(req: Request, input: { name: string }): Promise<SomewhereGroupsResult<SomewhereGroupDetail>>;
495
+ /** Also completes (and reports) up to 3 of the signed-in user's accepted invitations. */
496
+ list(req: Request, opts?: SomewhereGroupsPageOptions): Promise<SomewhereGroupsResult<SomewhereGroupsPage<SomewhereGroup> & { invitations: SomewhereGroupInvitationOutcome[] }>>;
497
+ get(req: Request, groupId: string): Promise<SomewhereGroupsResult<SomewhereGroupDetail>>;
498
+ members(req: Request, groupId: string, opts?: SomewhereGroupsPageOptions): Promise<SomewhereGroupsResult<SomewhereGroupsPage<SomewhereGroupMember>>>;
499
+ /** Invite by email with a role the signed-in user may grant. The platform
500
+ * sends one email and adds the member when they accept; no app call is needed. */
501
+ invite(req: Request, groupId: string, input: { email: string; role: SomewhereGroupRole; redirect_uri: string; expires_in?: number }): Promise<SomewhereGroupsResult<SomewhereGroupInvitation>>;
502
+ /** Revoke in the invitation's own group, as its sender or a member who may grant its role. */
503
+ revokeInvitation(req: Request, inviteId: string): Promise<SomewhereGroupsResult<{ revoked: true }>>;
504
+ leave(req: Request, groupId: string): Promise<SomewhereGroupsResult<{ left: true }>>;
505
+ remove(req: Request, groupId: string, userId: string): Promise<SomewhereGroupsResult<{ removed: true }>>;
506
+ setRole(req: Request, groupId: string, userId: string, role: SomewhereGroupRole): Promise<SomewhereGroupsResult<{ user_id: string; role: SomewhereGroupRole }>>;
507
+ appRoles: {
508
+ list(req: Request, userId?: string, opts?: SomewhereGroupsPageOptions): Promise<SomewhereGroupsResult<SomewhereGroupsPage<SomewhereAppRoleGrant>>>;
509
+ grant(req: Request, userId: string, role: SomewhereAppRole): Promise<SomewhereGroupsResult<SomewhereAppRoleGrant>>;
510
+ revoke(req: Request, userId: string, role: SomewhereAppRole): Promise<SomewhereGroupsResult<{ revoked: boolean }>>;
511
+ };
512
+ }
513
+
514
+ interface SomewhereRuntimeContext {
515
+ readonly groups: SomewhereGroups;
516
+ }
517
+ `;
518
+ }
519
+ module2.exports = { groupsTypeDeclarations, declaredRoles: declaredRoles2 };
520
+ }
521
+ });
522
+
334
523
  // worker/containers/compile/runtime-types.cjs
335
524
  var require_runtime_types = __commonJS({
336
525
  "worker/containers/compile/runtime-types.cjs"(exports2, module2) {
337
526
  "use strict";
338
- var RUNTIME_CONTEXT_DECLARATION2 = `
527
+ var BASE_RUNTIME_CONTEXT_DECLARATION = `
339
528
  type __SomewhereJsonPrimitive = string | number | boolean | null;
340
529
  type __SomewhereJson = __SomewhereJsonPrimitive | { [key: string]: __SomewhereJson } | __SomewhereJson[];
341
530
  type SomewhereDbScalar = string | number | boolean | null;
@@ -1031,6 +1220,7 @@ type SomewherePaymentsStatus =
1031
1220
  };
1032
1221
  interface SomewherePaymentsLineItem { price?: string; amount?: number; currency?: string; name?: string; quantity?: number }
1033
1222
  interface SomewherePaymentsCheckoutOptions {
1223
+ row?: never;
1034
1224
  env?: SomewherePaymentsEnv;
1035
1225
  mode?: 'payment' | 'subscription';
1036
1226
  line_items?: readonly SomewherePaymentsLineItem[];
@@ -1044,6 +1234,44 @@ interface SomewherePaymentsCheckoutOptions {
1044
1234
  customer_email?: string;
1045
1235
  metadata?: Readonly<Record<string, string>>;
1046
1236
  }
1237
+ /** Row checkout is a live, one-time payment; amounts are integer cents and items use one currency. */
1238
+ interface SomewherePaymentsRowCheckoutLineItem {
1239
+ name: string;
1240
+ amount: number;
1241
+ currency: string;
1242
+ quantity?: number;
1243
+ price?: never;
1244
+ }
1245
+ interface SomewherePaymentsRowCheckoutOptions {
1246
+ row: { binding: string; id: string | number };
1247
+ env?: 'prod';
1248
+ mode?: 'payment';
1249
+ line_items: readonly SomewherePaymentsRowCheckoutLineItem[];
1250
+ success_url: string;
1251
+ cancel_url: string;
1252
+ customer_email?: string;
1253
+ metadata?: never;
1254
+ quote_id?: never;
1255
+ booking_id?: never;
1256
+ calendar_hold_token?: never;
1257
+ plan?: never;
1258
+ }
1259
+ interface SomewherePaymentsRowCheckoutSessionResult {
1260
+ binding_id: string;
1261
+ session_id: string;
1262
+ url: string | null;
1263
+ reused: boolean;
1264
+ stripe_mode: 'live';
1265
+ amount_total_cents: number;
1266
+ currency: string;
1267
+ }
1268
+ /** A pending result has no checkout URL; reconciliation_required:true needs operator reconciliation. */
1269
+ interface SomewherePaymentsRowCheckoutPendingResult {
1270
+ binding_id: string;
1271
+ status: 'pending';
1272
+ reconciliation_required: boolean;
1273
+ }
1274
+ type SomewherePaymentsRowCheckoutResult = SomewherePaymentsRowCheckoutSessionResult | SomewherePaymentsRowCheckoutPendingResult;
1047
1275
  interface SomewherePaymentsCheckoutForUserOptions extends SomewherePaymentsCheckoutOptions { plan: string }
1048
1276
  interface SomewherePaymentsCheckoutResult {
1049
1277
  session_id: string;
@@ -1132,6 +1360,7 @@ interface SomewhereRuntimePayments {
1132
1360
  onboard(opts?: SomewherePaymentsOnboardOptions | null): Promise<SomewherePaymentsOnboardResult>;
1133
1361
  quote: SomewhereQuoteFunction;
1134
1362
  status(opts?: { refresh?: boolean } | null): Promise<SomewherePaymentsStatus>;
1363
+ checkout(opts: SomewherePaymentsRowCheckoutOptions): Promise<SomewherePaymentsRowCheckoutResult>;
1135
1364
  checkout(opts: SomewherePaymentsCheckoutOptions): Promise<SomewherePaymentsCheckoutResult>;
1136
1365
  // The app user is the request's verified principal; no user id argument.
1137
1366
  checkoutForUser(opts: SomewherePaymentsCheckoutForUserOptions): Promise<SomewherePaymentsCheckoutResult>;
@@ -1792,6 +2021,20 @@ interface SomewhereRuntimeContext {
1792
2021
  // ---- sw.ai ----
1793
2022
  type SomewhereAiProvider = 'anthropic' | 'openai' | 'xai' | 'workers-ai' | 'deepseek' | 'deepinfra';
1794
2023
  interface SomewhereAiInputBlock { type: string; [key: string]: unknown }
2024
+ /** Native Anthropic image input. Encode file bytes without a data: URL prefix. */
2025
+ interface SomewhereAiImageInputBlock extends SomewhereAiInputBlock {
2026
+ type: 'image';
2027
+ source: {
2028
+ type: 'base64';
2029
+ media_type: 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp';
2030
+ data: string;
2031
+ };
2032
+ }
2033
+ /** Native Anthropic PDF input for one-shot extraction. */
2034
+ interface SomewhereAiDocumentInputBlock extends SomewhereAiInputBlock {
2035
+ type: 'document';
2036
+ source: { type: 'base64'; media_type: 'application/pdf'; data: string };
2037
+ }
1795
2038
  interface SomewhereAiMessage {
1796
2039
  role: 'user' | 'assistant' | 'system';
1797
2040
  content: string | readonly SomewhereAiInputBlock[];
@@ -1944,7 +2187,13 @@ interface SomewhereAiUserMemory {
1944
2187
  clear(): Promise<{ cleared: true }>;
1945
2188
  compact(schema: SomewhereJsonObject, options?: SomewhereAiUserMemoryCompactOptions): Promise<SomewhereJsonObject>;
1946
2189
  }
1947
- type SomewhereAiTranscribeOptions = { model?: string } & (
2190
+ type SomewhereAiTranscribeOptions = {
2191
+ model?: string;
2192
+ /** Spoken language hint for the default transcription model, e.g. 'en'. */
2193
+ language?: string;
2194
+ /** Names, vocabulary or context for the default transcription model. */
2195
+ prompt?: string;
2196
+ } & (
1948
2197
  | { audio: string; audio_url?: string }
1949
2198
  | { audio?: string; audio_url: string }
1950
2199
  );
@@ -2674,6 +2923,10 @@ interface SomewhereJobsCreateOptions {
2674
2923
  // Earliest start: an ISO-8601 time with an explicit offset, e.g. '2026-10-05T09:00:00Z'. Must be in the future.
2675
2924
  run_at?: string;
2676
2925
  agent?: { messages: readonly unknown[]; max_steps?: number; max_turns?: number; deployment_version?: string };
2926
+ // Row-bound job: acts for the signed-in user on ONE row of a job binding declared in db/schema.ts, with only that
2927
+ // binding's operations, until its deadline. id is the row's id(): a whole number, or a string for uuid ids.
2928
+ // Created only from that user's own request (not from another job); not combined with agent.
2929
+ row?: { binding: string; id: number | string };
2677
2930
  }
2678
2931
  interface SomewhereJobsRecovery {
2679
2932
  dispatch_state: string;
@@ -2720,9 +2973,17 @@ interface SomewhereJob {
2720
2973
  run_at: string | null;
2721
2974
  recovery: SomewhereJobsRecovery | null;
2722
2975
  }
2976
+ interface SomewhereJobsCancelResult {
2977
+ job_id: string;
2978
+ status: 'cancelled';
2979
+ ownership_status: 'app_user' | 'project_owner';
2980
+ owner_subject_id: string | null;
2981
+ }
2723
2982
  interface SomewhereRuntimeJobs {
2724
2983
  create(options: SomewhereJobsCreateOptions): Promise<SomewhereJobsCreateResult>;
2725
2984
  status(jobId: string): Promise<SomewhereJob>;
2985
+ // Cancels queued work. An ordinary handler already running finishes; its side effects remain.
2986
+ cancel(jobId: string): Promise<SomewhereJobsCancelResult>;
2726
2987
  // True only for a platform-signed job/queue/cron delivery; never throws.
2727
2988
  verifyInvocation(req: Request): Promise<boolean>;
2728
2989
  }
@@ -2910,6 +3171,11 @@ type ServerFunction<Contract extends { input: unknown; output: unknown }> =
2910
3171
  interface Response { readonly webSocket: WebSocket | null }
2911
3172
  interface WebSocket { accept(): void }
2912
3173
  `;
3174
+ var { groupsTypeDeclarations } = require_groups_types();
3175
+ function runtimeContextDeclaration2(roles) {
3176
+ return BASE_RUNTIME_CONTEXT_DECLARATION + groupsTypeDeclarations(roles);
3177
+ }
3178
+ var RUNTIME_CONTEXT_DECLARATION2 = runtimeContextDeclaration2({});
2913
3179
  var ENDPOINT_DECLARATION2 = `
2914
3180
  interface SomewhereEndpointUser {
2915
3181
  id: string;
@@ -2951,7 +3217,7 @@ interface SomewhereEndpointConfig<Auth extends SomewhereEndpointAuth, Schema ext
2951
3217
  ): unknown;
2952
3218
  }
2953
3219
  `;
2954
- var DECLARED_COLUMN_TYPES = { integer: "number | string", number: "number", text: "string", timestamp: "string", boolean: "boolean", json: "__SomewhereJson", blob: "number[]" };
3220
+ var DECLARED_COLUMN_TYPES = { integer: "number", bigint: "string", number: "number", text: "string", timestamp: "string", boolean: "boolean", json: "__SomewhereJson", blob: "number[]" };
2955
3221
  function declaredOwnerColumn2(name, table, scopes) {
2956
3222
  if (Object.prototype.hasOwnProperty.call(scopes, name)) return scopes[name];
2957
3223
  const find = (policy) => !policy ? null : policy.k === "o" ? policy.c : policy.k === "a" ? find(policy.p[0]) || find(policy.p[1]) : null;
@@ -2983,7 +3249,7 @@ interface SomewhereEndpointConfig<Auth extends SomewhereEndpointAuth, Schema ext
2983
3249
  });
2984
3250
  return "interface SomewhereDeclaredTables {\n" + entries.join("") + "}\n";
2985
3251
  }
2986
- module2.exports = { RUNTIME_CONTEXT_DECLARATION: RUNTIME_CONTEXT_DECLARATION2, ENDPOINT_DECLARATION: ENDPOINT_DECLARATION2, declaredTablesDeclaration: declaredTablesDeclaration2 };
3252
+ module2.exports = { RUNTIME_CONTEXT_DECLARATION: RUNTIME_CONTEXT_DECLARATION2, ENDPOINT_DECLARATION: ENDPOINT_DECLARATION2, declaredTablesDeclaration: declaredTablesDeclaration2, runtimeContextDeclaration: runtimeContextDeclaration2 };
2987
3253
  }
2988
3254
  });
2989
3255
 
@@ -3004,20 +3270,32 @@ var require_typed_files = __commonJS({
3004
3270
  const scope = collection.scope || {};
3005
3271
  return scope.kind === "member" || scope.kind === "owner_or_member" ? scope.member : null;
3006
3272
  }
3007
- function describeCollection(collection) {
3273
+ function fieldOf(collection) {
3008
3274
  const member = memberOf(collection);
3009
- const groupKey = member ? JSON.stringify(member.group) : null;
3010
- const meta = member ? `FileMeta & { ${groupKey}: string }` : "FileMeta";
3011
- const groupOption = member ? `${groupKey}: string | number; ` : "";
3275
+ if (member) return { name: member.group, input: "string | number", meta: "string", parent: false };
3276
+ const scope = collection.scope || {};
3277
+ if (scope.kind === "parent") {
3278
+ const type = scope.parent.keyType === "text" ? "string" : "number";
3279
+ return { name: scope.parent.via, input: type, meta: type, parent: true };
3280
+ }
3281
+ return null;
3282
+ }
3283
+ function describeCollection(collection) {
3284
+ const field = fieldOf(collection);
3285
+ const fieldKey = field ? JSON.stringify(field.name) : null;
3286
+ const meta = field ? `FileMeta & { ${fieldKey}: ${field.meta} }` : "FileMeta";
3287
+ const fieldOption = field ? `${fieldKey}: ${field.input}; ` : "";
3288
+ const listOptions = field && field.parent ? `options: { ${fieldOption}limit?: number; cursor?: string | null }` : `options?: { ${field ? `${fieldKey}?: ${field.input}; ` : ""}limit?: number; cursor?: string | null }`;
3012
3289
  const ops = [];
3013
3290
  if (collection.client.read) {
3014
- ops.push(`list(options?: { ${groupOption.replace(": string | number; ", "?: string | number; ")}limit?: number; cursor?: string | null }): Promise<FileResult<{ items: ${meta}[]; next: string | null; has_more: boolean }>>`);
3291
+ ops.push(`list(${listOptions}): Promise<FileResult<{ items: Array<${meta}>; next: string | null; has_more: boolean }>>`);
3015
3292
  ops.push(`get(id: string): Promise<FileResult<${meta}>>`);
3016
3293
  ops.push("url(id: string): string");
3017
3294
  ops.push("shareLink(id: string, options?: { expiresIn?: number }): Promise<FileResult<{ url: string; expires_at: string; expires_in: number }>>");
3018
3295
  }
3019
- if (collection.client.upload) ops.push(`upload(file: Blob, options${member ? "" : "?"}: { ${groupOption}name?: string; contentType?: string; onProgress?: (progress: { loaded: number; total: number }) => void }): Promise<FileResult<UploadedFile>>`);
3296
+ if (collection.client.upload) ops.push(`upload(file: Blob, options${field ? "" : "?"}: { ${fieldOption}name?: string; contentType?: string; onProgress?: (progress: { loaded: number; total: number }) => void }): Promise<FileResult<UploadedFile>>`);
3020
3297
  if (collection.client.replace) ops.push("replace(id: string, file: Blob, options?: { contentType?: string; onProgress?: (progress: { loaded: number; total: number }) => void }): Promise<FileResult<UploadedFile>>");
3298
+ if (collection.client.replace && field && field.parent) ops.push(`move(id: string, to: { ${fieldOption}}): Promise<FileResult<${meta}>>`);
3021
3299
  if (collection.client.delete) ops.push("delete(id: string): Promise<FileResult<{ id: string; deleted: true }>>");
3022
3300
  if (collection.public) {
3023
3301
  ops.push(collection.scope.kind === "server_only" ? "publicUrl(path: string): string" : "publicUrl(id: string, name: string): string");
@@ -3037,13 +3315,14 @@ var require_typed_files = __commonJS({
3037
3315
  }
3038
3316
  `;
3039
3317
  const shapes = collections.map((c) => {
3040
- const member = memberOf(c);
3318
+ const field = fieldOf(c);
3041
3319
  return {
3042
3320
  name: c.name,
3043
3321
  root: c.path,
3044
3322
  ops: Object.keys(c.client).filter((op) => c.client[op]),
3045
3323
  public: c.public ? c.scope.kind === "server_only" ? "path" : "id" : null,
3046
- group: member ? member.group : null,
3324
+ group: field ? field.name : null,
3325
+ move: Boolean(field && field.parent && c.client.replace),
3047
3326
  maxSize: c.limits.maxSize,
3048
3327
  types: c.limits.types
3049
3328
  };
@@ -3116,6 +3395,7 @@ function makeCollection(shape){
3116
3395
  if(has("upload"))c.upload=(file,options)=>put("upload",null,file,options);
3117
3396
  if(has("replace"))c.replace=(id,file,options)=>put("replace",id,file,options);
3118
3397
  if(has("delete"))c.delete=id=>call(shape.name,"delete",{id});
3398
+ if(shape.move)c.move=(id,to)=>{const input={id};if(to&&to[shape.group]!==undefined)input[shape.group]=to[shape.group];return call(shape.name,"move",input)};
3119
3399
  if(shape.public)c.publicUrl=publicUrl;
3120
3400
  return Object.freeze(c);
3121
3401
  }
@@ -3161,8 +3441,8 @@ var require_schema_types = __commonJS({
3161
3441
  references?: string; onDelete?: 'cascade' | 'restrict'; renamedFrom?: string;
3162
3442
  }
3163
3443
  interface ClientPermissions<Field extends string> {
3164
- identity?: 'authenticated' | 'visitor'; read?: boolean | Field[]; publicRead?: boolean | { where: Partial<Record<Field, string | number | boolean | null>> };
3165
- create?: Field[] | false | null; update?: Field[] | false | null; delete?: boolean;
3444
+ identity?: 'authenticated' | 'visitor'; read?: boolean | readonly Field[]; publicRead?: boolean | { where: Partial<Record<Field, string | number | boolean | null>> };
3445
+ create?: readonly Field[] | false | null; update?: readonly Field[] | false | null; delete?: boolean;
3166
3446
  }
3167
3447
  interface FileCollection { readonly __somewhereFileCollection: unique symbol }
3168
3448
  type FileOperation = 'read' | 'upload' | 'replace' | 'delete';
@@ -3171,18 +3451,26 @@ var require_schema_types = __commonJS({
3171
3451
  client?: Partial<Record<FileOperation, boolean>>;
3172
3452
  limits?: { maxSize?: string | number; types?: string[] };
3173
3453
  }
3454
+ type RowOperation = 'read' | 'update' | 'delete';
3455
+ interface PaymentBinding {
3456
+ table: string; amount: string; currency: string;
3457
+ onPaid: Record<string, { from: string[]; to: string }>;
3458
+ }
3459
+ interface JobBinding { table: string; operations: RowOperation[]; maxDurationSeconds: number }
3460
+ interface ServiceRole { tables: Record<string, Array<'read' | 'create' | 'update' | 'delete'>> }
3174
3461
  interface TableOptions<Field extends string> {
3175
3462
  scope?: Scope; client?: ClientPermissions<Field>; indexes?: string[][];
3176
3463
  unique?: string[][]; relations?: Record<string, Relation>;
3177
3464
  }
3178
3465
  }
3179
3466
  `;
3180
- var signatures = `function schema(tables: Record<string, SomewhereSchemaDeclaration.Table | SomewhereSchemaDeclaration.TableMarker>, options?: { search?: SomewhereSchemaDeclaration.OwnerScope; files?: Record<string, SomewhereSchemaDeclaration.FileCollection> }): unknown;
3467
+ var signatures = `function schema(tables: Record<string, SomewhereSchemaDeclaration.Table | SomewhereSchemaDeclaration.TableMarker>, options?: { search?: SomewhereSchemaDeclaration.OwnerScope; files?: Record<string, SomewhereSchemaDeclaration.FileCollection>; groups?: { roles?: string[]; adminGrantableRoles?: string[] }; appRoles?: Record<string, { grantableBy?: string[] }>; payments?: Record<string, SomewhereSchemaDeclaration.PaymentBinding>; jobs?: Record<string, SomewhereSchemaDeclaration.JobBinding>; serviceRoles?: Record<string, SomewhereSchemaDeclaration.ServiceRole> }): unknown;
3181
3468
  function table<Columns extends Record<string, SomewhereSchemaDeclaration.Column | SomewhereSchemaDeclaration.ColumnMarker>>(columns: Columns, options?: SomewhereSchemaDeclaration.TableOptions<Extract<keyof Columns, string>>): SomewhereSchemaDeclaration.Table;
3182
3469
  function id(options?: { uuid?: boolean }): SomewhereSchemaDeclaration.Column;
3183
3470
  function text(options?: SomewhereSchemaDeclaration.ColumnOptions<string>): SomewhereSchemaDeclaration.Column;
3184
3471
  function number(options?: SomewhereSchemaDeclaration.ColumnOptions<number>): SomewhereSchemaDeclaration.Column;
3185
3472
  function integer(options?: SomewhereSchemaDeclaration.ColumnOptions<number>): SomewhereSchemaDeclaration.Column;
3473
+ function bigint(options?: SomewhereSchemaDeclaration.ColumnOptions<string>): SomewhereSchemaDeclaration.Column;
3186
3474
  function boolean(options?: SomewhereSchemaDeclaration.ColumnOptions<boolean>): SomewhereSchemaDeclaration.Column;
3187
3475
  function timestamp(options?: SomewhereSchemaDeclaration.ColumnOptions<string>): SomewhereSchemaDeclaration.Column;
3188
3476
  function json(options?: SomewhereSchemaDeclaration.ColumnOptions<string>): SomewhereSchemaDeclaration.Column;
@@ -3200,7 +3488,9 @@ function exported(): SomewhereSchemaDeclaration.TableMarker;
3200
3488
  `;
3201
3489
  var policySignatures = `function anyOf(owner: SomewhereSchemaDeclaration.OwnerScope, member: SomewhereSchemaDeclaration.MemberScope): SomewhereSchemaDeclaration.Scope;
3202
3490
  function anyOf(member: SomewhereSchemaDeclaration.MemberScope, owner: SomewhereSchemaDeclaration.OwnerScope): SomewhereSchemaDeclaration.Scope;
3203
- function parent(options: { via: string }): SomewhereSchemaDeclaration.Scope;
3491
+ function parent(options: { via: string; table?: string }): SomewhereSchemaDeclaration.Scope;
3492
+ function group(options?: { column?: string; roles?: Partial<Record<'read' | 'create' | 'update' | 'delete', string[]>> }): SomewhereSchemaDeclaration.Scope;
3493
+ function appRole(options: { roles: Partial<Record<'read' | 'create' | 'update' | 'delete', string[]>> }): SomewhereSchemaDeclaration.Scope;
3204
3494
  `;
3205
3495
  var SCHEMA_DECLARATION2 = types + signatures.replace(/^function /gm, "declare function ") + `declare module 'somewhere/db' {
3206
3496
  ${(signatures + policySignatures).replace(/^function /gm, " export function ")}}
@@ -3218,13 +3508,23 @@ __export(declared_data_vendor_entry_exports, {
3218
3508
  SCHEMA_DECLARATION: () => import_schema_types.SCHEMA_DECLARATION,
3219
3509
  declaredTablesFromFiles: () => declaredTablesFromFiles,
3220
3510
  filesDeclarationFromFiles: () => filesDeclarationFromFiles,
3221
- generateFromFiles: () => generateFromFiles
3511
+ generateFromFiles: () => generateFromFiles,
3512
+ runtimeDeclarationFromFiles: () => runtimeDeclarationFromFiles
3222
3513
  });
3223
3514
  module.exports = __toCommonJS(declared_data_vendor_entry_exports);
3224
3515
 
3225
3516
  // worker/src/utils/db-schema-deploy/schema-ownership.ts
3226
3517
  var SHARED_AUTHOR_COLUMN = "_sw_author_id";
3227
3518
 
3519
+ // worker/src/utils/db-schema-deploy/bigint-text.ts
3520
+ function isCanonicalBigintText(value) {
3521
+ if (!/^(0|-?[1-9][0-9]*)$/.test(value)) return false;
3522
+ const negative = value.startsWith("-");
3523
+ const digits = negative ? value.slice(1) : value;
3524
+ const limit = negative ? "9223372036854775808" : "9223372036854775807";
3525
+ return digits.length < limit.length || digits.length === limit.length && digits <= limit;
3526
+ }
3527
+
3228
3528
  // worker/src/utils/db-schema-deploy/client-permissions.ts
3229
3529
  function parseClientPermissions(value, columns, scope) {
3230
3530
  const fail = (message) => ({ ok: false, message });
@@ -3268,7 +3568,7 @@ function parseClientPermissions(value, columns, scope) {
3268
3568
  if (!(literal === null || typeof literal === "boolean" || typeof literal === "string" || typeof literal === "number" && Number.isFinite(literal))) return fail(`client.publicRead.where field "${rawName}" must equal a literal scalar.`);
3269
3569
  if (typeof literal === "string" && literal.length > 256) return fail(`client.publicRead.where field "${rawName}" exceeds 256 characters.`);
3270
3570
  if (literal === null && !column.nullable) return fail(`client.publicRead.where field "${rawName}" cannot equal null because it is required.`);
3271
- if (literal !== null && (column.helper === "boolean" && typeof literal !== "boolean" || ["integer", "number"].includes(column.helper) && typeof literal !== "number" || ["text", "timestamp"].includes(column.helper) && typeof literal !== "string")) {
3571
+ if (literal !== null && (column.helper === "boolean" && typeof literal !== "boolean" || ["integer", "number"].includes(column.helper) && typeof literal !== "number" || column.helper === "integer" && !Number.isSafeInteger(literal) || column.helper === "bigint" && (typeof literal !== "string" || !isCanonicalBigintText(literal)) || ["text", "timestamp"].includes(column.helper) && typeof literal !== "string")) {
3272
3572
  return fail(`client.publicRead.where field "${rawName}" must match its declared type.`);
3273
3573
  }
3274
3574
  normalized[name] = literal;
@@ -3342,8 +3642,236 @@ function parseClientPermissions(value, columns, scope) {
3342
3642
  return { ok: true, permissions: { identity, read, publicRead, ...writes, delete: remove } };
3343
3643
  }
3344
3644
 
3645
+ // worker/src/services/jobs/limits.ts
3646
+ var STALE_JOB_WINDOW_MS = 45 * 60 * 1e3;
3647
+
3648
+ // worker/src/utils/db-schema-deploy/extract-schema-bindings.ts
3649
+ var BINDING_OPERATIONS = ["read", "create", "update", "delete"];
3650
+ var ROW_OPERATIONS = ["read", "update", "delete"];
3651
+ var MAX_JOB_BINDING_SECONDS = STALE_JOB_WINDOW_MS / 1e3;
3652
+ var NAME = /^[a-z][a-z0-9_]{0,63}$/;
3653
+ var BUILT_IN_GROUP_ROLES = ["admin", "member", "owner"];
3654
+ var lines = /* @__PURE__ */ new WeakMap();
3655
+ var byName = (list) => list.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
3656
+ function readString(r, fail, ctx) {
3657
+ const t = r.next();
3658
+ if (t.kind !== "string") fail(t.line, `${ctx} must be a quoted literal string.`);
3659
+ return t;
3660
+ }
3661
+ function readStrings(r, fail, ctx) {
3662
+ const open = r.next();
3663
+ if (open.kind !== "punct" || open.value !== "[") fail(open.line, `${ctx} must be a list of quoted literal strings, such as ['read'].`);
3664
+ const out = [];
3665
+ while (!r.tryPunct("]")) {
3666
+ const t = r.next();
3667
+ if (t.kind !== "string") fail(t.line, `${ctx} must be a list of quoted literal strings.`);
3668
+ if (out.some((seen) => seen.value === t.value)) fail(t.line, `${ctx} lists "${t.value}" twice.`);
3669
+ out.push(t);
3670
+ if (!r.tryPunct(",")) {
3671
+ r.expectPunct("]", `closing ${ctx}`);
3672
+ break;
3673
+ }
3674
+ }
3675
+ return out;
3676
+ }
3677
+ function readOperations(r, fail, ctx, allowed) {
3678
+ const list = readStrings(r, fail, ctx);
3679
+ if (list.length === 0) fail(r.peek().line, `${ctx} must name at least one operation.`);
3680
+ for (const t of list) {
3681
+ if (!allowed.includes(t.value)) fail(t.line, `${ctx} has an unknown operation "${t.value}". Allowed: ${allowed.join(", ")}.`);
3682
+ }
3683
+ const ops = BINDING_OPERATIONS.filter((op) => list.some((t) => t.value === op));
3684
+ if (!ops.includes("read") && (ops.includes("update") || ops.includes("delete"))) {
3685
+ fail(list[0].line, `${ctx} allows ${ops.filter((op) => op !== "create").join(" and ")} without read. Update and delete return the existing row, so they also need read; create alone is allowed because it returns only the row it created.`);
3686
+ }
3687
+ return ops;
3688
+ }
3689
+ function readNamed(r, fail, option, kind, keys, readField) {
3690
+ r.expectPunct("{", `opening schema option ${option}`);
3691
+ const out = [];
3692
+ while (!r.tryPunct("}")) {
3693
+ const key = r.readKey(`for a ${kind} name`);
3694
+ if (!NAME.test(key.name)) fail(key.line, `${kind} name "${key.name}" must be lowercase letters, digits and underscores, starting with a letter (at most 64).`);
3695
+ if (out.some((entry) => entry.name === key.name)) fail(key.line, `${kind} "${key.name}" is declared twice.`);
3696
+ r.expectPunct(":", `after ${kind} "${key.name}"`);
3697
+ r.expectPunct("{", `opening ${kind} "${key.name}"`);
3698
+ const fields = {};
3699
+ const fieldLines = { "": key.line };
3700
+ const seen = /* @__PURE__ */ new Set();
3701
+ while (!r.tryPunct("}")) {
3702
+ const opt = r.readKey(`for an option of ${kind} "${key.name}"`);
3703
+ if (!keys.includes(opt.name)) fail(opt.line, `${kind} "${key.name}" has an unknown option "${opt.name}". Allowed: ${keys.join(", ")}.`);
3704
+ if (seen.has(opt.name)) fail(opt.line, `${kind} "${key.name}" declares ${opt.name} twice.`);
3705
+ seen.add(opt.name);
3706
+ r.expectPunct(":", `after ${option}.${key.name}.${opt.name}`);
3707
+ fieldLines[opt.name] = r.peek().line;
3708
+ readField(opt.name, fields, key.name);
3709
+ if (!r.tryPunct(",")) {
3710
+ r.expectPunct("}", `closing ${kind} "${key.name}"`);
3711
+ break;
3712
+ }
3713
+ }
3714
+ const missing = keys.filter((k) => !seen.has(k));
3715
+ if (missing.length) fail(key.line, `${kind} "${key.name}" is missing ${missing.join(", ")}.`);
3716
+ lines.set(fields, fieldLines);
3717
+ out.push({ name: key.name, line: key.line, fields });
3718
+ if (!r.tryPunct(",")) {
3719
+ r.expectPunct("}", `closing schema option ${option}`);
3720
+ break;
3721
+ }
3722
+ }
3723
+ return out;
3724
+ }
3725
+ function readPaymentBindings(r, fail) {
3726
+ const entries = readNamed(r, fail, "payments", "payment binding", ["table", "amount", "currency", "onPaid"], (key, fields, name) => {
3727
+ if (key !== "onPaid") {
3728
+ fields[key] = readString(r, fail, `payments.${name}.${key}`).value.toLowerCase();
3729
+ return;
3730
+ }
3731
+ r.expectPunct("{", `opening payments.${name}.onPaid \u2014 declare one transition, { status: { from: [...], to: '...' } }`);
3732
+ const column = r.readKey(`for the status column in payments.${name}.onPaid`);
3733
+ r.expectPunct(":", `after payments.${name}.onPaid.${column.name}`);
3734
+ r.expectPunct("{", `opening payments.${name}.onPaid.${column.name} \u2014 declare { from: [...], to: '...' }`);
3735
+ const transition = {};
3736
+ while (!r.tryPunct("}")) {
3737
+ const opt = r.readKey(`for an option of payments.${name}.onPaid.${column.name}`);
3738
+ if (opt.name !== "from" && opt.name !== "to") fail(opt.line, `payments.${name}.onPaid.${column.name} has an unknown option "${opt.name}". Allowed: from, to.`);
3739
+ if (transition[opt.name]) fail(opt.line, `payments.${name}.onPaid.${column.name} declares ${opt.name} twice.`);
3740
+ r.expectPunct(":", `after ${opt.name}`);
3741
+ if (opt.name === "from") transition.from = readStrings(r, fail, `payments.${name}.onPaid.${column.name}.from`);
3742
+ else transition.to = readString(r, fail, `payments.${name}.onPaid.${column.name}.to`);
3743
+ if (!r.tryPunct(",")) {
3744
+ r.expectPunct("}", `closing payments.${name}.onPaid.${column.name}`);
3745
+ break;
3746
+ }
3747
+ }
3748
+ if (!transition.from || !transition.to) fail(column.line, `payments.${name}.onPaid.${column.name} needs both from and to.`);
3749
+ r.tryPunct(",");
3750
+ if (!r.tryPunct("}")) {
3751
+ const extra = r.peek();
3752
+ fail(extra.line, `payments.${name}.onPaid declares one status transition only; remove the second entry. Other paid effects run from the signed payment event.`);
3753
+ }
3754
+ const values = [...transition.from, transition.to];
3755
+ for (const t of values) {
3756
+ if (t.value.length < 1 || t.value.length > 64 || /[\u0000-\u001f]/.test(t.value)) fail(t.line, `payments.${name}.onPaid.${column.name} status values must be 1 to 64 characters, without control characters.`);
3757
+ }
3758
+ if (transition.from.length === 0) fail(column.line, `payments.${name}.onPaid.${column.name}.from must list at least one unpaid state.`);
3759
+ if (transition.from.some((t) => t.value === transition.to.value)) fail(transition.to.line, `payments.${name}.onPaid.${column.name}.to must differ from every from state.`);
3760
+ fields.status = { column: column.name.toLowerCase(), from: transition.from.map((t) => t.value).sort(), to: transition.to.value };
3761
+ });
3762
+ return byName(entries.map(({ name, fields }) => {
3763
+ const binding = { name, ...fields };
3764
+ lines.set(binding, lines.get(fields));
3765
+ return binding;
3766
+ }));
3767
+ }
3768
+ function readJobBindings(r, fail) {
3769
+ const entries = readNamed(r, fail, "jobs", "job binding", ["table", "operations", "maxDurationSeconds"], (key, fields, name) => {
3770
+ if (key === "table") fields.table = readString(r, fail, `jobs.${name}.table`).value.toLowerCase();
3771
+ else if (key === "operations") fields.operations = readOperations(r, fail, `jobs.${name}.operations`, ROW_OPERATIONS);
3772
+ else {
3773
+ const t = r.next();
3774
+ if (t.kind !== "number" || !/^[1-9][0-9]{0,9}$/.test(t.value) || Number(t.value) > MAX_JOB_BINDING_SECONDS) {
3775
+ fail(t.line, `jobs.${name}.maxDurationSeconds must be a whole number of seconds from 1 to ${MAX_JOB_BINDING_SECONDS}: how long the row grant lasts from the job's scheduled start, retries included.`);
3776
+ }
3777
+ fields.maxDurationSeconds = Number(t.value);
3778
+ }
3779
+ });
3780
+ return byName(entries.map(({ name, fields }) => {
3781
+ const binding = { name, ...fields };
3782
+ lines.set(binding, lines.get(fields));
3783
+ return binding;
3784
+ }));
3785
+ }
3786
+ function readServiceRoles(r, fail) {
3787
+ const entries = readNamed(r, fail, "serviceRoles", "service role", ["tables"], (_key, fields, name) => {
3788
+ r.expectPunct("{", `opening serviceRoles.${name}.tables \u2014 declare { table: ['read', ...] }`);
3789
+ const tables = [];
3790
+ const tableLines = {};
3791
+ while (!r.tryPunct("}")) {
3792
+ const t = r.readKey(`for a table in serviceRoles.${name}.tables`);
3793
+ const table = t.name.toLowerCase();
3794
+ if (tables.some((entry) => entry.table === table)) fail(t.line, `serviceRoles.${name}.tables names "${t.name}" twice.`);
3795
+ r.expectPunct(":", `after serviceRoles.${name}.tables.${t.name}`);
3796
+ tables.push({ table, operations: readOperations(r, fail, `serviceRoles.${name}.tables.${t.name}`, BINDING_OPERATIONS) });
3797
+ tableLines[table] = t.line;
3798
+ if (!r.tryPunct(",")) {
3799
+ r.expectPunct("}", `closing serviceRoles.${name}.tables`);
3800
+ break;
3801
+ }
3802
+ }
3803
+ if (tables.length === 0) fail(r.peek().line, `serviceRoles.${name}.tables must grant at least one table.`);
3804
+ fields.tables = tables.sort((a, b) => a.table < b.table ? -1 : a.table > b.table ? 1 : 0);
3805
+ lines.set(fields.tables, tableLines);
3806
+ });
3807
+ return byName(entries.map(({ name, fields }) => {
3808
+ const role = { name, tables: fields.tables };
3809
+ lines.set(role, lines.get(fields));
3810
+ return role;
3811
+ }));
3812
+ }
3813
+ function validateBindings(input, tableInfo, errors) {
3814
+ const at = (entry, field) => `line ${lines.get(entry)?.[field] ?? lines.get(entry)?.[""] ?? 0}: `;
3815
+ const rowTable = (entry, kind) => {
3816
+ const info = tableInfo(entry.table);
3817
+ if (!info) {
3818
+ errors.push(`${at(entry, "table")}${kind} "${entry.name}" names table "${entry.table}", which is not declared in db/schema.ts.`);
3819
+ return null;
3820
+ }
3821
+ if (!info.privateUser) errors.push(`${at(entry, "table")}${kind} "${entry.name}" names table "${entry.table}", whose scope is not owner(), member(), anyOf(), group(), appRole() or parent(). It binds rows a signed-in user holds; use a server function for other tables.`);
3822
+ if (!info.key) errors.push(`${at(entry, "table")}${kind} "${entry.name}" names table "${entry.table}", which needs exactly one id() key.`);
3823
+ return info;
3824
+ };
3825
+ for (const p of input.payments ?? []) {
3826
+ const info = rowTable(p, "payment binding");
3827
+ if (!info) continue;
3828
+ const column = (name) => info.table.columns.find((c) => c.name === name);
3829
+ const checks = [
3830
+ ["amount", p.amount, "integer", "a required integer() column holding the amount in minor units (cents)"],
3831
+ ["currency", p.currency, "text", "a required text() column holding the currency code"],
3832
+ ["onPaid", p.status.column, "text", "a required text() status column"]
3833
+ ];
3834
+ for (const [field, name, helper, what] of checks) {
3835
+ const c = column(name);
3836
+ if (!c) {
3837
+ errors.push(`${at(p, field)}payment binding "${p.name}" names "${name}", which is not a declared column of "${p.table}".`);
3838
+ continue;
3839
+ }
3840
+ if (c.helper !== helper || c.nullable) errors.push(`${at(p, field)}payment binding "${p.name}": "${p.table}"."${name}" must be ${what}.`);
3841
+ if (info.identity.includes(name)) errors.push(`${at(p, field)}payment binding "${p.name}": "${p.table}"."${name}" is the table's access column; a payment never moves a row.`);
3842
+ const writable = [...info.table.client?.create ?? [], ...info.table.client?.update ?? []];
3843
+ if (writable.includes(name)) errors.push(`${at(p, field)}payment binding "${p.name}": "${p.table}"."${name}" is browser-writable through client.create or client.update. Remove it there; the server sets prices and the payment sets the status.`);
3844
+ }
3845
+ if ((/* @__PURE__ */ new Set([p.amount, p.currency, p.status.column])).size !== 3) errors.push(`${at(p, "")}payment binding "${p.name}" must name three different columns for amount, currency and status.`);
3846
+ const status = column(p.status.column);
3847
+ if (status && !(status.hasDefault && typeof status.default === "string" && p.status.from.includes(status.default))) {
3848
+ errors.push(`${at(p, "onPaid")}payment binding "${p.name}": "${p.table}"."${p.status.column}" must declare a default that is one of its unpaid from states (${p.status.from.join(", ")}), so a new row starts unpaid.`);
3849
+ }
3850
+ }
3851
+ for (const j of input.jobs ?? []) rowTable(j, "job binding");
3852
+ const taken = /* @__PURE__ */ new Set([...BUILT_IN_GROUP_ROLES, ...input.groups?.roles ?? [], ...(input.appRoles ?? []).map((role) => role.name)]);
3853
+ for (const s of input.serviceRoles ?? []) {
3854
+ if (taken.has(s.name)) errors.push(`${at(s, "")}service role "${s.name}" has the name of a group or app role. Service roles are a separate namespace for unattended work; pick another name.`);
3855
+ const tableLines = lines.get(s.tables) ?? {};
3856
+ for (const grant of s.tables) {
3857
+ if (!tableInfo(grant.table)) errors.push(`line ${tableLines[grant.table] ?? 0}: service role "${s.name}" grants table "${grant.table}", which is not declared in db/schema.ts.`);
3858
+ }
3859
+ }
3860
+ }
3861
+
3345
3862
  // worker/src/utils/authority/scope-enforcement.ts
3346
3863
  var SAFE_SCOPE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
3864
+ var GROUP_ROLE_NAME = /^[a-z][a-z0-9_]{0,63}$/;
3865
+ function validGroupRoles(value) {
3866
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
3867
+ const roles = value;
3868
+ return exactKeys(roles, ["read", "create", "update", "delete"]) && ["read", "create", "update", "delete"].every((op) => Array.isArray(roles[op]) && roles[op].every((role) => typeof role === "string" && GROUP_ROLE_NAME.test(role)) && new Set(roles[op]).size === roles[op].length);
3869
+ }
3870
+ function exactKeys(value, keys) {
3871
+ const actual = Object.keys(value).sort();
3872
+ return actual.length === keys.length && actual.every((key, index) => key === [...keys].sort()[index]);
3873
+ }
3874
+ var MAX_PARENT_EDGES = 4;
3347
3875
  function bakedTableSchemaFromDeclared(declaredJson) {
3348
3876
  let shape;
3349
3877
  try {
@@ -3367,7 +3895,7 @@ function bakedTableSchemaFromDeclared(declaredJson) {
3367
3895
  if (col.helper === "id") {
3368
3896
  t = col.uuid === true ? "text" : "integer";
3369
3897
  implicitDefault = true;
3370
- } else if (col.helper === "integer" || col.helper === "number" || col.helper === "text" || col.helper === "boolean" || col.helper === "timestamp" || col.helper === "json" || col.helper === "blob") {
3898
+ } else if (col.helper === "integer" || col.helper === "bigint" || col.helper === "number" || col.helper === "text" || col.helper === "boolean" || col.helper === "timestamp" || col.helper === "json" || col.helper === "blob") {
3371
3899
  t = col.helper;
3372
3900
  } else {
3373
3901
  return null;
@@ -3418,7 +3946,7 @@ function bakedTableSchemaFromDeclared(declaredJson) {
3418
3946
  }
3419
3947
  if (scope !== null && typeof scope === "object" && scope.kind === "policy") {
3420
3948
  const policy = bakedPolicyFromDeclared(scope.policy);
3421
- if (!policy || policy.k !== "a" && policy.k !== "p") return null;
3949
+ if (!policy || policy.k !== "a" && policy.k !== "p" && policy.k !== "g" && policy.k !== "r") return null;
3422
3950
  return relations ? { columns: out, policy, relations, ...browser } : { columns: out, policy, ...browser };
3423
3951
  }
3424
3952
  const author = scope !== null && typeof scope === "object" && scope.kind === "shared" ? SHARED_AUTHOR_COLUMN : void 0;
@@ -3435,8 +3963,8 @@ function declaredPolicyIdentity(value) {
3435
3963
  }
3436
3964
  return {};
3437
3965
  }
3438
- function bakedPolicyFromDeclared(value, depth = 0) {
3439
- if (depth > 2 || value === null || typeof value !== "object") return null;
3966
+ function bakedPolicyFromDeclared(value, depth = 0, edges = 0, underParent = false) {
3967
+ if (edges > MAX_PARENT_EDGES || value === null || typeof value !== "object") return null;
3440
3968
  const node = value;
3441
3969
  if (node.kind === "owner") {
3442
3970
  return typeof node.column === "string" && SAFE_SCOPE_IDENTIFIER.test(node.column) ? { k: "o", c: node.column.toLowerCase() } : null;
@@ -3446,14 +3974,26 @@ function bakedPolicyFromDeclared(value, depth = 0) {
3446
3974
  return member ? { k: "m", m: member } : null;
3447
3975
  }
3448
3976
  if (node.kind === "any_of" && Array.isArray(node.policies) && node.policies.length === 2) {
3449
- if (depth > 1 || node.policies[0]?.kind !== "owner" || node.policies[1]?.kind !== "member") return null;
3977
+ if (depth !== 0 && !underParent || node.policies[0]?.kind !== "owner" || node.policies[1]?.kind !== "member") return null;
3450
3978
  const left = bakedPolicyFromDeclared(node.policies[0], depth + 1);
3451
3979
  const right = bakedPolicyFromDeclared(node.policies[1], depth + 1);
3452
3980
  return left && right ? { k: "a", p: [left, right] } : null;
3453
3981
  }
3982
+ if (node.kind === "group") {
3983
+ if (depth !== 0 && !underParent) return null;
3984
+ const roles = node.roles;
3985
+ const baked = { read: roles?.read, create: roles?.create, update: roles?.update, delete: roles?.delete };
3986
+ return typeof node.column === "string" && SAFE_SCOPE_IDENTIFIER.test(node.column) && validGroupRoles(baked) ? { k: "g", c: node.column.toLowerCase(), roles: { read: [...baked.read], create: [...baked.create], update: [...baked.update], delete: [...baked.delete] }, generation: 0 } : null;
3987
+ }
3988
+ if (node.kind === "app_role") {
3989
+ if (depth !== 0 && !underParent || !exactKeys(node, ["kind", "roles"]) || !validGroupRoles(node.roles)) return null;
3990
+ const roles = node.roles;
3991
+ const sorted3 = (list) => list.every((role, i) => i === 0 || list[i - 1] < role);
3992
+ return ["read", "create", "update", "delete"].every((op) => sorted3(roles[op])) ? { k: "r", roles: { read: [...roles.read], create: [...roles.create], update: [...roles.update], delete: [...roles.delete] }, generation: 0 } : null;
3993
+ }
3454
3994
  if (node.kind === "parent" && typeof node.via === "string" && SAFE_SCOPE_IDENTIFIER.test(node.via) && typeof node.table === "string" && SAFE_SCOPE_IDENTIFIER.test(node.table) && typeof node.parentKey === "string" && SAFE_SCOPE_IDENTIFIER.test(node.parentKey)) {
3455
- if (depth !== 0 || node.policy?.kind === "parent") return null;
3456
- const inherited = bakedPolicyFromDeclared(node.policy, depth + 1);
3995
+ if (depth !== 0 && !underParent || edges + 1 > MAX_PARENT_EDGES) return null;
3996
+ const inherited = bakedPolicyFromDeclared(node.policy, depth + 1, edges + 1, true);
3457
3997
  return inherited ? { k: "p", v: node.via.toLowerCase(), t: node.table.toLowerCase(), pk: node.parentKey.toLowerCase(), p: inherited } : null;
3458
3998
  }
3459
3999
  return null;
@@ -3510,6 +4050,313 @@ function bakedMemberFromDeclared(scope) {
3510
4050
  return { g, m: s.membership.toLowerCase(), u: s.memberUser.toLowerCase(), mg, ...o !== void 0 ? { o } : {} };
3511
4051
  }
3512
4052
 
4053
+ // worker/src/utils/db-schema-deploy/schema-constants.ts
4054
+ var LITERAL_WORDS = /* @__PURE__ */ new Set(["true", "false", "null"]);
4055
+ var STATEMENT_WORDS = /* @__PURE__ */ new Set([
4056
+ "import",
4057
+ "export",
4058
+ "default",
4059
+ "from",
4060
+ "const",
4061
+ "let",
4062
+ "var",
4063
+ "as",
4064
+ "function",
4065
+ "class"
4066
+ ]);
4067
+ var isPunct = (t, value) => !!t && t.kind === "punct" && t.value === value;
4068
+ var OPENERS = "([{";
4069
+ var CLOSERS = ")]}";
4070
+ function append(into, tokens) {
4071
+ for (const t of tokens) into.push(t);
4072
+ }
4073
+ var isIdent = (t, value) => !!t && t.kind === "ident" && t.value === value;
4074
+ function resolveSchemaConstants(tokens, callable, maxTokens, fail) {
4075
+ const out = [];
4076
+ let i = 0;
4077
+ const imported = /* @__PURE__ */ new Set();
4078
+ while (isIdent(tokens[i], "import")) {
4079
+ const start = i;
4080
+ while (tokens[i].kind !== "eof" && !isIdent(tokens[i], "from")) {
4081
+ if (tokens[i].kind === "ident" && i > start) imported.add(tokens[i].value);
4082
+ i++;
4083
+ }
4084
+ if (isIdent(tokens[i], "from")) i += 2;
4085
+ if (isPunct(tokens[i], ";")) i++;
4086
+ append(out, tokens.slice(start, i));
4087
+ }
4088
+ const constants = /* @__PURE__ */ new Map();
4089
+ const declaredLater = /* @__PURE__ */ new Map();
4090
+ for (let k = i; k < tokens.length; k++) {
4091
+ const t = tokens[k];
4092
+ if ((isIdent(t, "const") || isIdent(t, "let") || isIdent(t, "var")) && tokens[k + 1]?.kind === "ident") {
4093
+ if (!declaredLater.has(tokens[k + 1].value)) declaredLater.set(tokens[k + 1].value, tokens[k + 1].line);
4094
+ }
4095
+ }
4096
+ let expanded = out.length;
4097
+ const budget = (line, adding) => {
4098
+ expanded += adding;
4099
+ if (expanded > maxTokens) {
4100
+ fail(line, "reusing constants here expands the declaration past the size db/schema.ts allows. Reuse fewer copies, or write the shared part once.");
4101
+ }
4102
+ };
4103
+ const expandRange = (from, to, into, owner) => {
4104
+ const open2 = [];
4105
+ for (let k = from; k < to; k++) {
4106
+ const t = tokens[k];
4107
+ if (t.kind === "punct" && OPENERS.includes(t.value)) open2.push(t.value);
4108
+ else if (t.kind === "punct" && CLOSERS.includes(t.value)) open2.pop();
4109
+ if (isPunct(t, ".") && isPunct(tokens[k + 1], ".") && isPunct(tokens[k + 2], ".")) {
4110
+ const target = tokens[k + 3];
4111
+ const opener = open2.length > 0 ? open2[open2.length - 1] : null;
4112
+ if (!target || target.kind !== "ident") {
4113
+ fail(t.line, "a spread (...) must name a constant declared above, as in ...sharedColumns.");
4114
+ }
4115
+ const constant = lookup(target, owner);
4116
+ if (opener !== "{") {
4117
+ fail(t.line, `...${target.value} is only supported inside an object literal { \u2026 }. Reference ${target.value} directly, or write the values inline.`);
4118
+ }
4119
+ if (!constant.objectLiteral) {
4120
+ fail(t.line, `...${target.value} spreads a constant that is not an object literal { \u2026 }. Only object constants can be spread.`);
4121
+ }
4122
+ const inner = constant.tokens.slice(1, -1);
4123
+ if (inner.length > 0 && isPunct(inner[inner.length - 1], ",")) inner.pop();
4124
+ budget(t.line, Math.max(inner.length, 4));
4125
+ append(into, inner);
4126
+ if (inner.length === 0 && isPunct(tokens[k + 4], ",")) k++;
4127
+ k += 3;
4128
+ continue;
4129
+ }
4130
+ if (t.kind !== "ident") {
4131
+ into.push(t);
4132
+ continue;
4133
+ }
4134
+ const next = tokens[k + 1];
4135
+ const prev = tokens[k - 1];
4136
+ const keyPosition = isPunct(next, ":") && (isPunct(prev, "{") || isPunct(prev, ","));
4137
+ if (keyPosition || LITERAL_WORDS.has(t.value) || isPunct(prev, ".")) {
4138
+ into.push(t);
4139
+ continue;
4140
+ }
4141
+ if (owner === null && STATEMENT_WORDS.has(t.value)) {
4142
+ if (isIdent(t, "export") && isIdent(next, "const")) {
4143
+ fail(t.line, '"export const" is not supported in db/schema.ts; the file exports only the schema. Remove "export" from the constant.');
4144
+ }
4145
+ into.push(t);
4146
+ continue;
4147
+ }
4148
+ if (constants.has(t.value) || declaredLater.has(t.value)) {
4149
+ const constant = lookup(t, owner);
4150
+ if (isPunct(next, "(")) {
4151
+ fail(t.line, `${t.value} is a constant, not a function \u2014 write ${t.value} without "( \u2026 )".`);
4152
+ }
4153
+ if (isPunct(next, ".")) {
4154
+ fail(t.line, `${t.value}.${tokens[k + 2]?.value ?? ""} reads a property of a constant, which db/schema.ts cannot evaluate. Declare that part as its own constant, or write it inline.`);
4155
+ }
4156
+ budget(t.line, constant.tokens.length);
4157
+ append(into, constant.tokens);
4158
+ continue;
4159
+ }
4160
+ if (callable.has(t.value)) {
4161
+ into.push(t);
4162
+ continue;
4163
+ }
4164
+ if ((isPunct(next, "(") || isPunct(next, ".")) && owner === null) {
4165
+ into.push(t);
4166
+ continue;
4167
+ }
4168
+ if (isPunct(next, ".")) {
4169
+ fail(t.line, `${t.value}.${tokens[k + 2]?.value ?? ""} reads a value at run time; db/schema.ts is read without running it. Write the literal value instead.`);
4170
+ }
4171
+ if (isPunct(next, "(")) {
4172
+ fail(t.line, `${t.value}(\u2026) is a function call; db/schema.ts is read without running it, so only the schema helpers can be called. Write the value inline, or declare it with const using schema helpers and literals.`);
4173
+ }
4174
+ fail(t.line, `"${t.value}" is not defined in db/schema.ts. Declare it above export default as const ${t.value} = \u2026; or write the value inline.`);
4175
+ }
4176
+ };
4177
+ const lookup = (t, owner) => {
4178
+ const constant = constants.get(t.value);
4179
+ if (constant) return constant;
4180
+ const at = declaredLater.get(t.value);
4181
+ if (owner === t.value) {
4182
+ fail(t.line, `constant ${t.value} refers to itself. A constant must be built from values declared above it.`);
4183
+ }
4184
+ if (at !== void 0) {
4185
+ fail(t.line, owner ? `constant ${owner} uses ${t.value}, which is declared later (line ${at}). Move ${t.value} above ${owner}; constants that refer to each other in a cycle cannot be resolved.` : `${t.value} is used before its declaration on line ${at}. Move the const above export default.`);
4186
+ }
4187
+ return fail(t.line, `"${t.value}" is not defined in db/schema.ts. Declare it above export default as const ${t.value} = \u2026; or write the value inline.`);
4188
+ };
4189
+ while (!isIdent(tokens[i], "export")) {
4190
+ const t = tokens[i];
4191
+ if (t.kind === "eof") return [...out, t];
4192
+ if (isIdent(t, "let") || isIdent(t, "var")) {
4193
+ fail(t.line, `"${t.value}" declares a changeable binding; db/schema.ts allows only const. Change "${t.value}" to "const".`);
4194
+ }
4195
+ if (!isIdent(t, "const")) {
4196
+ if (t.kind === "ident" && constants.has(t.value)) {
4197
+ fail(t.line, `${t.value} is changed after its declaration; constants in db/schema.ts cannot be reassigned or mutated. Declare the final value once with const.`);
4198
+ }
4199
+ if (isIdent(t, "import")) fail(t.line, "imports must come first in db/schema.ts, before any const.");
4200
+ return [...out, ...tokens.slice(i)];
4201
+ }
4202
+ const nameTok = tokens[i + 1];
4203
+ if (!nameTok || nameTok.kind !== "ident") {
4204
+ fail(t.line, "destructuring (const { \u2026 } = / const [ \u2026 ] =) is not supported in db/schema.ts. Declare one constant per name: const name = \u2026;");
4205
+ }
4206
+ const name = nameTok.value;
4207
+ if (callable.has(name) || LITERAL_WORDS.has(name) || STATEMENT_WORDS.has(name) || name === "schema") {
4208
+ fail(nameTok.line, `const ${name} shadows a db/schema.ts helper or keyword. Choose another name, such as ${name}Rule.`);
4209
+ }
4210
+ if (imported.has(name)) {
4211
+ fail(nameTok.line, `const ${name} has the same name as an import. Choose another name.`);
4212
+ }
4213
+ if (constants.has(name)) fail(nameTok.line, `const ${name} is declared twice. Keep one declaration.`);
4214
+ if (isPunct(tokens[i + 2], ":")) {
4215
+ fail(tokens[i + 2].line, `const ${name} has a type annotation; db/schema.ts constants take their type from the helpers. Remove ": \u2026".`);
4216
+ }
4217
+ if (!isPunct(tokens[i + 2], "=")) fail(nameTok.line, `expected "=" after const ${name}.`);
4218
+ const start = i + 3;
4219
+ let k = start;
4220
+ let depth = 0;
4221
+ for (; tokens[k].kind !== "eof"; k++) {
4222
+ const v = tokens[k];
4223
+ if (v.kind === "punct" && OPENERS.includes(v.value)) depth++;
4224
+ else if (v.kind === "punct" && CLOSERS.includes(v.value)) {
4225
+ depth--;
4226
+ if (depth < 0) fail(v.line, `const ${name} has an unmatched "${v.value}". Balance its brackets.`);
4227
+ } else if (depth === 0 && (isPunct(v, ";") || isPunct(v, ",") || isIdent(v, "const") && !isIdent(tokens[k - 1], "as") || isIdent(v, "let") || isIdent(v, "var") || isIdent(v, "export"))) break;
4228
+ if (depth === 0 && isPunct(v, "=")) fail(v.line, `${name} is assigned twice; give each constant one value.`);
4229
+ }
4230
+ if (depth !== 0) fail(nameTok.line, `const ${name} has an unclosed bracket. Balance its brackets.`);
4231
+ let end2 = k;
4232
+ if (end2 - start >= 2 && isIdent(tokens[end2 - 2], "as") && isIdent(tokens[end2 - 1], "const")) end2 -= 2;
4233
+ if (end2 === start) fail(nameTok.line, `const ${name} has no value.`);
4234
+ if (isPunct(tokens[k], ",")) {
4235
+ fail(tokens[k].line, `one declaration declares several constants; write one const per name (const ${name} = \u2026;).`);
4236
+ }
4237
+ const value = [];
4238
+ expandRange(start, end2, value, name);
4239
+ const objectLiteral = isPunct(tokens[start], "{") && matchingClose(tokens, start) === end2 - 1;
4240
+ constants.set(name, { name, line: nameTok.line, tokens: value, objectLiteral });
4241
+ i = isPunct(tokens[k], ";") ? k + 1 : k;
4242
+ }
4243
+ if (isIdent(tokens[i], "export") && isIdent(tokens[i + 1], "const")) {
4244
+ fail(tokens[i].line, '"export const" is not supported in db/schema.ts; the file exports only the schema. Remove "export" from the constant.');
4245
+ }
4246
+ const body = [];
4247
+ const open = tokens.findIndex((v, k) => k > i && isPunct(v, "(") && isIdent(tokens[k - 1], "schema"));
4248
+ const close = open < 0 ? -1 : matchingClose(tokens, open);
4249
+ const end = close < 0 ? tokens.length : close + 1;
4250
+ expandRange(i, end, body, null);
4251
+ for (let k = end; k < tokens.length; k++) {
4252
+ const v = tokens[k];
4253
+ if (v.kind === "ident" && constants.has(v.value) && (isPunct(tokens[k + 1], "=") || isPunct(tokens[k + 1], "."))) {
4254
+ fail(v.line, `${v.value} is changed after its declaration; constants in db/schema.ts cannot be reassigned or mutated. Declare the final value once with const.`);
4255
+ }
4256
+ body.push(v);
4257
+ }
4258
+ if (out.length + body.length > maxTokens) {
4259
+ fail(tokens[i].line, "reusing constants here expands the declaration past the size db/schema.ts allows. Reuse fewer copies, or write the shared part once.");
4260
+ }
4261
+ return [...out, ...body];
4262
+ }
4263
+ function matchingClose(tokens, open) {
4264
+ let depth = 0;
4265
+ for (let k = open; k < tokens.length; k++) {
4266
+ const v = tokens[k];
4267
+ if (v.kind === "punct" && "([{".includes(v.value)) depth++;
4268
+ else if (v.kind === "punct" && ")]}".includes(v.value)) {
4269
+ depth--;
4270
+ if (depth === 0) return k;
4271
+ }
4272
+ }
4273
+ return -1;
4274
+ }
4275
+
4276
+ // worker/src/utils/db-schema-deploy/extract-schema-parents.ts
4277
+ var MAX_PARENT_EDGES2 = 4;
4278
+ var parentScopeLines = /* @__PURE__ */ new WeakMap();
4279
+ function rememberParentLine(sentinel, line) {
4280
+ parentScopeLines.set(sentinel, line);
4281
+ }
4282
+ function resolveParentChains(tables, tableByName, privatePolicy, errors) {
4283
+ const parentEdges = /* @__PURE__ */ new Map();
4284
+ const resolvingParents = /* @__PURE__ */ new Set();
4285
+ const resolveParent = (t) => {
4286
+ if (t.scope.kind !== "policy" || t.scope.policy.kind !== "parent") return 0;
4287
+ if (parentEdges.has(t.name)) return parentEdges.get(t.name);
4288
+ const pending = t.scope.policy;
4289
+ const at = parentScopeLines.get(pending);
4290
+ const where = at === void 0 ? "" : `line ${at}: `;
4291
+ if (resolvingParents.has(t.name)) {
4292
+ errors.push(`${where}Table "${t.name}" uses parent({ via: '${pending.via}' }), but its parent chain leads back to "${t.name}". Parent chains cannot form a cycle.`);
4293
+ parentEdges.set(t.name, null);
4294
+ return null;
4295
+ }
4296
+ resolvingParents.add(t.name);
4297
+ const fail = (message) => {
4298
+ errors.push(where + message);
4299
+ parentEdges.set(t.name, null);
4300
+ resolvingParents.delete(t.name);
4301
+ return null;
4302
+ };
4303
+ const fk = t.columns.find((column) => column.name === pending.via);
4304
+ if (!fk || !fk.references) {
4305
+ return fail(`Table "${t.name}" uses parent({ via: '${pending.via}' }), but "${pending.via}" is not a declared scalar foreign key.`);
4306
+ }
4307
+ const parentTable = tableByName.get(fk.references);
4308
+ const parentKeys = parentTable ? parentTable.columns.filter((column) => column.helper === "id") : [];
4309
+ const parentKey = parentKeys.length === 1 ? parentKeys[0] : void 0;
4310
+ let inherited = null;
4311
+ let edges = 1;
4312
+ if (parentTable && parentTable.scope.kind === "policy" && parentTable.scope.policy.kind === "parent") {
4313
+ const above = resolveParent(parentTable);
4314
+ if (above === null) {
4315
+ if (parentEdges.get(t.name) !== null) parentEdges.set(t.name, null);
4316
+ resolvingParents.delete(t.name);
4317
+ return null;
4318
+ }
4319
+ inherited = parentTable.scope.policy;
4320
+ edges = above + 1;
4321
+ } else if (parentTable) {
4322
+ inherited = privatePolicy(parentTable.scope);
4323
+ }
4324
+ if (!parentTable || !parentKey || !inherited) {
4325
+ return fail(`Table "${t.name}" uses parent({ via: '${pending.via}' }), but its referenced parent must have exactly one id() key and a direct owner(), group(), appRole(), member(), anyOf(owner(), member()) or parent() private scope.`);
4326
+ }
4327
+ if (edges > MAX_PARENT_EDGES2) {
4328
+ return fail(`Table "${t.name}" uses parent({ via: '${pending.via}' }), which makes a parent chain of ${edges} edges. At most ${MAX_PARENT_EDGES2} are allowed; inherit from a table nearer the root of the chain.`);
4329
+ }
4330
+ const compatible = fk.helper === parentKey.helper && (fk.helper !== "id" || fk.uuid === parentKey.uuid) || fk.helper === "integer" && parentKey.helper === "id" && !parentKey.uuid || fk.helper === "text" && parentKey.helper === "id" && parentKey.uuid;
4331
+ if (!compatible) {
4332
+ return fail(`Table "${t.name}" uses parent({ via: '${pending.via}' }), but that foreign key does not have the same scalar type as "${parentTable.name}"."${parentKey.name}".`);
4333
+ }
4334
+ t.scope.policy = { kind: "parent", via: pending.via, table: parentTable.name, parentKey: parentKey.name, policy: inherited };
4335
+ if (t.client?.create && !t.client.create.includes(pending.via)) {
4336
+ errors.push(`Table "${t.name}" uses parent({ via: '${pending.via}' }) and client.create must include "${pending.via}" so every new row explicitly selects an authorized parent.`);
4337
+ }
4338
+ resolvingParents.delete(t.name);
4339
+ parentEdges.set(t.name, edges);
4340
+ return edges;
4341
+ };
4342
+ for (const t of tables) resolveParent(t);
4343
+ }
4344
+ function policyEdges(policy) {
4345
+ return policy.kind === "parent" ? 1 + policyEdges(policy.policy) : 0;
4346
+ }
4347
+ function fileParentTarget(t) {
4348
+ const keys = t.columns.filter((column) => column.helper === "id");
4349
+ const key = keys.length === 1 ? keys[0] : null;
4350
+ const scope = t.scope;
4351
+ const privateUserPolicy = scope.kind === "owner" && !scope.visitors || scope.kind === "member" || scope.kind === "policy" && ["owner", "member", "any_of", "parent", "group", "app_role"].includes(scope.policy.kind);
4352
+ return {
4353
+ key: key ? key.name : null,
4354
+ keyType: key?.uuid ? "text" : "integer",
4355
+ privateUserPolicy,
4356
+ edges: scope.kind === "policy" ? policyEdges(scope.policy) : 0
4357
+ };
4358
+ }
4359
+
3513
4360
  // worker/src/utils/db-schema-deploy/extract-schema-relations.ts
3514
4361
  function validateDeclaredRelations(tables, tableByName, knownColumns, markedGone, errors) {
3515
4362
  const columnByName = (t, col) => t.columns.find((c) => c.name === col);
@@ -3671,11 +4518,37 @@ function readFileOwner(r, fail, name) {
3671
4518
  }
3672
4519
  fail(key.line, `owner() on file collection "${name}" takes no options \u2014 the owner is always the signed-in uploader.`);
3673
4520
  }
4521
+ function readFileParent(r, fail, name, line) {
4522
+ r.expectPunct("(", 'after "parent"');
4523
+ r.expectPunct("{", `in parent() for file collection "${name}" \u2014 parent() needs { table, via }`);
4524
+ const seen = /* @__PURE__ */ new Map();
4525
+ while (!r.tryPunct("}")) {
4526
+ const key = r.readKey(`for a parent() option of file collection "${name}"`);
4527
+ r.expectPunct(":", `after "${key.name}" in parent() for file collection "${name}"`);
4528
+ if (key.name !== "table" && key.name !== "via") {
4529
+ fail(key.line, `parent() on file collection "${name}" has an unknown option "${key.name}". Allowed: table, via.`);
4530
+ }
4531
+ const t = r.next();
4532
+ if (t.kind !== "string") fail(t.line, `parent({ ${key.name} }) on file collection "${name}" must be one quoted name.`);
4533
+ if (!COLUMN_NAME.test(t.value)) fail(t.line, `parent({ ${key.name} }) on file collection "${name}" names an invalid ${key.name === "table" ? "table" : "field"} "${t.value}".`);
4534
+ if (seen.has(key.name)) fail(key.line, `parent() on file collection "${name}" declares ${key.name} twice.`);
4535
+ seen.set(key.name, key.name === "table" ? t.value.toLowerCase() : t.value);
4536
+ if (!r.tryPunct(",")) {
4537
+ r.expectPunct("}", `closing parent() for file collection "${name}"`);
4538
+ break;
4539
+ }
4540
+ }
4541
+ r.tryPunct(",");
4542
+ r.expectPunct(")", "closing parent()");
4543
+ const missing = ["table", "via"].filter((k) => !seen.has(k));
4544
+ if (missing.length > 0) {
4545
+ fail(line, `parent() on file collection "${name}" is missing ${missing.join(" and ")}. It needs parent({ table: 'expenses', via: 'expenseId' }).`);
4546
+ }
4547
+ return { table: seen.get("table"), via: seen.get("via"), key: "", keyType: "integer" };
4548
+ }
3674
4549
  function readFileScope(r, fail, name) {
3675
4550
  const tok = r.expectIdent(`for the scope of file collection "${name}"`);
3676
- if (tok.value === "parent") {
3677
- fail(tok.line, `parent() is not available for file collections yet. Use owner(), member(), anyOf(owner(), member()), shared(), or serverOnly().`);
3678
- }
4551
+ if (tok.value === "parent") return { kind: "parent", parent: readFileParent(r, fail, name, tok.line) };
3679
4552
  if (tok.value === "owner") {
3680
4553
  readFileOwner(r, fail, name);
3681
4554
  return { kind: "owner" };
@@ -3704,7 +4577,7 @@ function readFileScope(r, fail, name) {
3704
4577
  if (owners !== 1 || !member) fail(tok.line, `anyOf() on file collection "${name}" requires exactly one owner() and one member().`);
3705
4578
  return { kind: "owner_or_member", member };
3706
4579
  }
3707
- return fail(tok.line, `unknown scope "${tok.value}" on file collection "${name}". Use owner(), member(), anyOf(owner(), member()), shared(), or serverOnly().`);
4580
+ return fail(tok.line, `unknown scope "${tok.value}" on file collection "${name}". Use owner(), member(), anyOf(owner(), member()), parent({ table, via }), shared(), or serverOnly().`);
3708
4581
  }
3709
4582
  function readCollection(r, fail, name, errors) {
3710
4583
  const callee = r.expectIdent(`for file collection "${name}"`);
@@ -3778,7 +4651,7 @@ function readCollection(r, fail, name, errors) {
3778
4651
  }
3779
4652
  r.tryPunct(",");
3780
4653
  r.expectPunct(")", "closing files()");
3781
- if (!scope) return fail(callee.line, `files() for "${name}" needs a scope: owner(), member(), anyOf(owner(), member()), shared(), or serverOnly().`);
4654
+ if (!scope) return fail(callee.line, `files() for "${name}" needs a scope: owner(), member(), anyOf(owner(), member()), parent({ table, via }), shared(), or serverOnly().`);
3782
4655
  const rootError = fileCollectionRootError(name, path);
3783
4656
  if (rootError) errors.push(rootError);
3784
4657
  const declaresClient = FILE_OPERATIONS.some((op) => client[op]);
@@ -3791,8 +4664,12 @@ function readCollection(r, fail, name, errors) {
3791
4664
  if (isPublic && (scope.kind === "member" || scope.kind === "owner_or_member")) {
3792
4665
  errors.push(`File collection "${name}" is public: true on a member() scope, which would let anyone read team files. Remove public, or use owner() or serverOnly().`);
3793
4666
  }
4667
+ if (isPublic && scope.kind === "parent") {
4668
+ errors.push(`File collection "${name}" is public: true on a parent() scope, which would let anyone read files that follow a private row. Remove public.`);
4669
+ }
3794
4670
  return { name, path: typeof path === "string" ? path : "", scope, public: isPublic, client, limits };
3795
4671
  }
4672
+ var MAX_FILE_PARENT_EDGES = 4;
3796
4673
  function readFileCollections(r, fail, errors) {
3797
4674
  r.expectPunct("{", "opening schema option files");
3798
4675
  const out = [];
@@ -3819,6 +4696,30 @@ function validateFileCollections(collections, table, errors) {
3819
4696
  if (a.path === b.path && a.name < b.name) errors.push(`File collections "${a.name}" and "${b.name}" both use the path "${a.path}". Each collection needs its own folder.`);
3820
4697
  else if (b.path.startsWith(a.path + "/")) errors.push(`File collection "${b.name}" (${b.path}) is nested inside "${a.name}" (${a.path}). Collection folders cannot overlap.`);
3821
4698
  }
4699
+ if (a.scope.kind === "parent") {
4700
+ const parent = a.scope.parent;
4701
+ const target = table(parent.table);
4702
+ const facts = target?.parentTarget;
4703
+ if (!target || !facts) {
4704
+ errors.push(`File collection "${a.name}" uses parent({ table: '${parent.table}' }), which is not a table declared in db/schema.ts.`);
4705
+ continue;
4706
+ }
4707
+ if (!facts.privateUserPolicy) {
4708
+ errors.push(`File collection "${a.name}" uses parent({ table: '${parent.table}' }), but that table's scope is not owner(), member(), anyOf(owner(), member()), group(), appRole() or parent(). Files can follow only rows a signed-in user holds.`);
4709
+ continue;
4710
+ }
4711
+ if (!facts.key) {
4712
+ errors.push(`File collection "${a.name}" uses parent({ table: '${parent.table}' }), but that table needs exactly one id() key for files to point at.`);
4713
+ continue;
4714
+ }
4715
+ if (facts.edges + 1 > MAX_FILE_PARENT_EDGES) {
4716
+ errors.push(`File collection "${a.name}" uses parent({ table: '${parent.table}' }), which makes a chain of ${facts.edges + 1} parent edges counting the file. At most ${MAX_FILE_PARENT_EDGES} are allowed; attach the files to a table nearer the root of the chain.`);
4717
+ continue;
4718
+ }
4719
+ parent.key = facts.key;
4720
+ parent.keyType = facts.keyType;
4721
+ continue;
4722
+ }
3822
4723
  if (a.scope.kind !== "member" && a.scope.kind !== "owner_or_member") continue;
3823
4724
  const m = a.scope.member;
3824
4725
  const membership = table(m.membership);
@@ -3839,7 +4740,7 @@ function canonicalFileCollectionsJson(collections) {
3839
4740
  collections: [...collections].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).map((c) => ({
3840
4741
  name: c.name,
3841
4742
  path: c.path,
3842
- scope: c.scope.kind === "member" || c.scope.kind === "owner_or_member" ? {
4743
+ scope: c.scope.kind === "parent" ? { kind: "parent", parent: { table: c.scope.parent.table, via: c.scope.parent.via, key: c.scope.parent.key, keyType: c.scope.parent.keyType } } : c.scope.kind === "member" || c.scope.kind === "owner_or_member" ? {
3843
4744
  kind: c.scope.kind,
3844
4745
  member: {
3845
4746
  group: c.scope.member.group,
@@ -3856,6 +4757,405 @@ function canonicalFileCollectionsJson(collections) {
3856
4757
  });
3857
4758
  }
3858
4759
 
4760
+ // worker/src/utils/managed-groups/policy.ts
4761
+ var BUILT_IN_GROUP_ROLES2 = ["owner", "admin", "member"];
4762
+ var ROLE_NAME = /^[a-z][a-z0-9_]{0,63}$/;
4763
+ function isPrototypeName(name) {
4764
+ return name === "prototype" || Object.prototype.hasOwnProperty.call(Object.prototype, name);
4765
+ }
4766
+ function roleVocabularyErrors(policy) {
4767
+ const errors = [];
4768
+ const dupes = (list, what) => {
4769
+ const seen = /* @__PURE__ */ new Set();
4770
+ for (const name of list) {
4771
+ if (seen.has(name)) errors.push(`${what} lists "${name}" twice.`);
4772
+ seen.add(name);
4773
+ }
4774
+ };
4775
+ for (const [list, what] of [[policy.groupRoles, "Group role"], [policy.appRoles, "App role"]]) {
4776
+ for (const role of list) {
4777
+ if (!ROLE_NAME.test(role)) errors.push(`${what} "${role}" must be lowercase letters, digits and underscores, starting with a letter (at most 64).`);
4778
+ else if (isPrototypeName(role)) errors.push(`${what} "${role}" is a reserved name.`);
4779
+ }
4780
+ }
4781
+ dupes(policy.groupRoles, "groups.roles");
4782
+ dupes(policy.adminGrantableGroupRoles, "groups.adminGrantableRoles");
4783
+ dupes(policy.appRoles, "appRoles");
4784
+ for (const role of policy.groupRoles) {
4785
+ if (BUILT_IN_GROUP_ROLES2.includes(role)) errors.push(`"${role}" is a built-in group role; declare only additional roles.`);
4786
+ }
4787
+ for (const role of policy.adminGrantableGroupRoles) {
4788
+ if (!policy.groupRoles.includes(role)) errors.push(`groups.adminGrantableRoles names "${role}", which is not in groups.roles.`);
4789
+ }
4790
+ for (const role of Object.keys(policy.appRoleDelegation)) {
4791
+ const grantors = policy.appRoleDelegation[role];
4792
+ if (!policy.appRoles.includes(role)) errors.push(`App role delegation names undeclared app role "${role}".`);
4793
+ dupes(grantors, `appRoles.${role}.grantableBy`);
4794
+ for (const grantor of grantors) {
4795
+ if (!policy.appRoles.includes(grantor)) errors.push(`appRoles.${role}.grantableBy names "${grantor}", which is not a declared app role.`);
4796
+ }
4797
+ }
4798
+ return errors;
4799
+ }
4800
+
4801
+ // worker/src/utils/db-schema-deploy/extract-schema-groups.ts
4802
+ var GROUP_OPERATIONS = ["read", "create", "update", "delete"];
4803
+ var DEFAULT_GROUP_COLUMN = "group_id";
4804
+ var IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
4805
+ var declaredGroupRoles = /* @__PURE__ */ new WeakMap();
4806
+ function readRoleList(r, fail, ctx) {
4807
+ const open = r.next();
4808
+ if (open.kind !== "punct" || open.value !== "[") fail(open.line, `${ctx} must be a list of quoted role names, such as ['owner', 'admin'].`);
4809
+ const out = [];
4810
+ while (!r.tryPunct("]")) {
4811
+ const t = r.next();
4812
+ if (t.kind !== "string") fail(t.line, `${ctx} must be a list of quoted role names; values must be literal strings.`);
4813
+ if (out.includes(t.value)) fail(t.line, `${ctx} lists "${t.value}" twice.`);
4814
+ out.push(t.value);
4815
+ if (!r.tryPunct(",")) {
4816
+ r.expectPunct("]", `closing ${ctx}`);
4817
+ break;
4818
+ }
4819
+ }
4820
+ return out;
4821
+ }
4822
+ function readGroupScope(r, tableName, line, fail) {
4823
+ r.expectPunct("(", 'after "group"');
4824
+ let column = DEFAULT_GROUP_COLUMN;
4825
+ const explicit = {};
4826
+ if (!r.tryPunct(")")) {
4827
+ r.expectPunct("{", `in group() options for table "${tableName}"`);
4828
+ const seen = /* @__PURE__ */ new Set();
4829
+ while (!r.tryPunct("}")) {
4830
+ const key = r.readKey(`for a group() option of table "${tableName}"`);
4831
+ if (seen.has(key.name)) fail(key.line, `group() on table "${tableName}" declares ${key.name} twice.`);
4832
+ seen.add(key.name);
4833
+ r.expectPunct(":", `after "${key.name}" in group() for table "${tableName}"`);
4834
+ if (key.name === "column") {
4835
+ const t = r.next();
4836
+ if (t.kind !== "string" || !IDENT.test(t.value) || t.value.length > 64) fail(t.line, `group({ column }) on table "${tableName}" must name a declared text column.`);
4837
+ column = t.value.toLowerCase();
4838
+ } else if (key.name === "roles") {
4839
+ r.expectPunct("{", `opening group({ roles }) on table "${tableName}"`);
4840
+ while (!r.tryPunct("}")) {
4841
+ const op = r.readKey(`for an operation in group({ roles }) on table "${tableName}"`);
4842
+ if (!GROUP_OPERATIONS.includes(op.name)) {
4843
+ fail(op.line, `group({ roles }) on table "${tableName}" has an unknown operation "${op.name}". Allowed: read, create, update, delete.`);
4844
+ }
4845
+ if (explicit[op.name]) fail(op.line, `group({ roles }) on table "${tableName}" declares ${op.name} twice.`);
4846
+ r.expectPunct(":", `after "${op.name}" in group({ roles }) on table "${tableName}"`);
4847
+ explicit[op.name] = readRoleList(r, fail, `group({ roles: { ${op.name} } }) on table "${tableName}"`);
4848
+ if (!r.tryPunct(",")) {
4849
+ r.expectPunct("}", `closing group({ roles }) on table "${tableName}"`);
4850
+ break;
4851
+ }
4852
+ }
4853
+ } else {
4854
+ fail(key.line, `group() on table "${tableName}" has an unknown option "${key.name}". Allowed: column, roles.`);
4855
+ }
4856
+ if (!r.tryPunct(",")) {
4857
+ r.expectPunct("}", `closing group() options for table "${tableName}"`);
4858
+ break;
4859
+ }
4860
+ }
4861
+ r.tryPunct(",");
4862
+ r.expectPunct(")", "closing group()");
4863
+ }
4864
+ const node = { kind: "group", column, roles: { read: [], create: [], update: [], delete: [] } };
4865
+ declaredGroupRoles.set(node, { line, explicit });
4866
+ return node;
4867
+ }
4868
+ var sorted = (list) => [...list].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
4869
+ function resolveGroupScopes(tables, vocabulary, errors) {
4870
+ const allRoles = [...BUILT_IN_GROUP_ROLES2, ...vocabulary?.roles ?? []];
4871
+ for (const t of tables) {
4872
+ if (t.scope.kind !== "policy" || t.scope.policy.kind !== "group") continue;
4873
+ const node = t.scope.policy;
4874
+ const declared = declaredGroupRoles.get(node);
4875
+ if (!declared) continue;
4876
+ const where = `line ${declared.line}: `;
4877
+ const column = t.columns.find((c) => c.name === node.column);
4878
+ if (!column) {
4879
+ errors.push(`${where}Table "${t.name}" uses group({ column: '${node.column}' }), but "${node.column}" is not one of its declared columns. Declare it as ${node.column}: text().`);
4880
+ } else if (column.helper !== "text" || column.nullable) {
4881
+ errors.push(`${where}Table "${t.name}" uses group({ column: '${node.column}' }), which must be a non-nullable text() column: every row belongs to exactly one group.`);
4882
+ }
4883
+ for (const op of GROUP_OPERATIONS) {
4884
+ const list = declared.explicit[op] ?? allRoles;
4885
+ for (const role of list) {
4886
+ if (!allRoles.includes(role)) {
4887
+ errors.push(`${where}group({ roles: { ${op} } }) on table "${t.name}" names "${role}", which is not a group role. Use owner, admin, member, or a role declared in groups.roles.`);
4888
+ }
4889
+ }
4890
+ node.roles[op] = sorted(list);
4891
+ }
4892
+ for (const op of ["update", "delete"]) {
4893
+ const missing = node.roles[op].filter((role) => !node.roles.read.includes(role));
4894
+ if (missing.length) {
4895
+ errors.push(`${where}group() on table "${t.name}" lets ${missing.join(", ")} ${op} rows they cannot read. A role that may ${op} must also read, because the write returns the row.`);
4896
+ }
4897
+ }
4898
+ if (t.client?.create && !t.client.create.includes(node.column)) {
4899
+ errors.push(`${where}Table "${t.name}" uses group() and client.create must include "${node.column}" so every new row names the group it belongs to.`);
4900
+ }
4901
+ declaredGroupRoles.delete(node);
4902
+ }
4903
+ }
4904
+ function groupLeafOf(t) {
4905
+ if (t.scope.kind !== "policy") return null;
4906
+ let policy = t.scope.policy;
4907
+ let inherited = false;
4908
+ while (policy.kind === "parent") {
4909
+ policy = policy.policy;
4910
+ inherited = true;
4911
+ }
4912
+ return policy.kind === "group" ? { node: policy, inherited } : null;
4913
+ }
4914
+ function resolveGroupReferences(tables, errors) {
4915
+ const byName2 = new Map(tables.map((t) => [t.name, t]));
4916
+ for (const t of tables) {
4917
+ const own = groupLeafOf(t);
4918
+ const refs = [];
4919
+ const via = t.scope.kind === "policy" && t.scope.policy.kind === "parent" ? t.scope.policy.via : null;
4920
+ for (const column of t.columns) {
4921
+ if (!column.references) continue;
4922
+ const target = byName2.get(column.references);
4923
+ if (!target) continue;
4924
+ const theirs = groupLeafOf(target);
4925
+ if (!own || !theirs || column.name === via) continue;
4926
+ const keys = target.columns.filter((c) => c.helper === "id");
4927
+ if (keys.length !== 1) {
4928
+ errors.push(`Table "${t.name}" references the group-owned table "${target.name}" through "${column.name}", which needs exactly one id() key on "${target.name}".`);
4929
+ continue;
4930
+ }
4931
+ if (own.inherited || theirs.inherited) continue;
4932
+ refs.push({ column: column.name, group: own.node.column, table: target.name, targetGroup: theirs.node.column, targetKey: keys[0].name });
4933
+ }
4934
+ if (refs.length > 0) t.groupRefs = refs.sort((a, b) => a.column < b.column ? -1 : a.column > b.column ? 1 : 0);
4935
+ }
4936
+ }
4937
+ function resolveGroupsAndParents(tables, tableByName, privatePolicy, vocabulary, errors) {
4938
+ resolveGroupScopes(tables, vocabulary, errors);
4939
+ resolveParentChains(tables, tableByName, privatePolicy, errors);
4940
+ resolveGroupReferences(tables, errors);
4941
+ }
4942
+
4943
+ // worker/src/utils/db-schema-deploy/extract-schema-app-roles.ts
4944
+ var APP_ROLE_OPERATIONS = ["read", "create", "update", "delete"];
4945
+ var declaredLines = /* @__PURE__ */ new WeakMap();
4946
+ function readAppRoleScope(r, tableName, line, fail) {
4947
+ const where = `appRole() on table "${tableName}"`;
4948
+ r.expectPunct("(", 'after "appRole"');
4949
+ r.expectPunct("{", `in ${where} \u2014 it needs appRole({ roles: { read: [...], ... } })`);
4950
+ const node = { kind: "app_role", roles: { read: [], create: [], update: [], delete: [] } };
4951
+ const names = [];
4952
+ let sawRoles = false;
4953
+ while (!r.tryPunct("}")) {
4954
+ const key = r.readKey(`for an option of ${where}`);
4955
+ if (key.name !== "roles") fail(key.line, `${where} has an unknown option "${key.name}". Allowed: roles.`);
4956
+ if (sawRoles) fail(key.line, `${where} declares roles twice.`);
4957
+ sawRoles = true;
4958
+ r.expectPunct(":", `after "roles" in ${where}`);
4959
+ r.expectPunct("{", `opening appRole({ roles }) on table "${tableName}"`);
4960
+ const seenOps = /* @__PURE__ */ new Set();
4961
+ while (!r.tryPunct("}")) {
4962
+ const op = r.readKey(`for an operation in appRole({ roles }) on table "${tableName}"`);
4963
+ if (!APP_ROLE_OPERATIONS.includes(op.name)) {
4964
+ fail(op.line, `appRole({ roles }) on table "${tableName}" has an unknown operation "${op.name}". Allowed: read, create, update, delete.`);
4965
+ }
4966
+ if (seenOps.has(op.name)) fail(op.line, `appRole({ roles }) on table "${tableName}" declares ${op.name} twice.`);
4967
+ seenOps.add(op.name);
4968
+ r.expectPunct(":", `after "${op.name}" in appRole({ roles }) on table "${tableName}"`);
4969
+ const ctx = `appRole({ roles: { ${op.name} } }) on table "${tableName}"`;
4970
+ const open = r.next();
4971
+ if (open.kind !== "punct" || open.value !== "[") fail(open.line, `${ctx} must be a list of quoted app role names, such as ['staff'].`);
4972
+ const list = [];
4973
+ while (!r.tryPunct("]")) {
4974
+ const t = r.next();
4975
+ if (t.kind !== "string") fail(t.line, `${ctx} must be a list of quoted app role names; values must be literal strings.`);
4976
+ if (list.includes(t.value)) fail(t.line, `${ctx} lists "${t.value}" twice.`);
4977
+ list.push(t.value);
4978
+ names.push({ op: op.name, name: t.value, line: t.line });
4979
+ if (!r.tryPunct(",")) {
4980
+ r.expectPunct("]", `closing ${ctx}`);
4981
+ break;
4982
+ }
4983
+ }
4984
+ node.roles[op.name] = [...list].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
4985
+ if (!r.tryPunct(",")) {
4986
+ r.expectPunct("}", `closing appRole({ roles }) on table "${tableName}"`);
4987
+ break;
4988
+ }
4989
+ }
4990
+ if (!r.tryPunct(",")) {
4991
+ r.expectPunct("}", `closing ${where}`);
4992
+ break;
4993
+ }
4994
+ }
4995
+ r.tryPunct(",");
4996
+ r.expectPunct(")", "closing appRole()");
4997
+ if (!sawRoles) fail(line, `${where} needs roles: appRole({ roles: { read: [...], ... } }). An omitted operation allows nobody.`);
4998
+ declaredLines.set(node, { line, names });
4999
+ return node;
5000
+ }
5001
+ function resolveAppRoleScopes(tables, appRoles, errors) {
5002
+ const vocabulary = new Set((appRoles ?? []).map((role) => role.name));
5003
+ for (const t of tables) {
5004
+ if (t.scope.kind !== "policy" || t.scope.policy.kind !== "app_role") continue;
5005
+ const node = t.scope.policy;
5006
+ const declared = declaredLines.get(node);
5007
+ if (!declared) continue;
5008
+ for (const { op, name, line } of declared.names) {
5009
+ if (!vocabulary.has(name)) {
5010
+ errors.push(`line ${line}: appRole({ roles: { ${op} } }) on table "${t.name}" names "${name}", which is not declared in the schema's appRoles option. List only app roles declared there; group roles and admin do not apply.`);
5011
+ }
5012
+ }
5013
+ for (const op of ["update", "delete"]) {
5014
+ const missing = node.roles[op].filter((role) => !node.roles.read.includes(role));
5015
+ if (missing.length) {
5016
+ errors.push(`line ${declared.line}: appRole() on table "${t.name}" lets ${missing.join(", ")} ${op} rows they cannot read. A role that may ${op} must also read, because the write returns the row.`);
5017
+ }
5018
+ }
5019
+ declaredLines.delete(node);
5020
+ }
5021
+ }
5022
+
5023
+ // worker/src/utils/db-schema-deploy/extract-schema-roles.ts
5024
+ var GROUP_KEYS = ["roles", "adminGrantableRoles"];
5025
+ var APP_ROLE_KEYS = ["grantableBy"];
5026
+ function readStringList2(r, fail, ctx) {
5027
+ const open = r.peek();
5028
+ if (open.kind !== "punct" || open.value !== "[") fail(open.line, `${ctx} must be a list of quoted role names, such as ['viewer'].`);
5029
+ r.next();
5030
+ const out = [];
5031
+ while (!r.tryPunct("]")) {
5032
+ const t = r.next();
5033
+ if (t.kind !== "string") fail(t.line, `${ctx} must be a list of quoted role names; found ${t.kind === "eof" ? "the end of the file" : `"${t.value}"`}. Values must be literal strings.`);
5034
+ out.push(t.value);
5035
+ if (!r.tryPunct(",")) {
5036
+ r.expectPunct("]", `closing ${ctx}`);
5037
+ break;
5038
+ }
5039
+ }
5040
+ return out;
5041
+ }
5042
+ function readGroupRoles(r, fail) {
5043
+ r.expectPunct("{", "opening schema option groups");
5044
+ const seen = /* @__PURE__ */ new Set();
5045
+ const out = { roles: [], adminGrantableRoles: [] };
5046
+ while (!r.tryPunct("}")) {
5047
+ const key = r.readKey("for a groups option");
5048
+ if (!GROUP_KEYS.includes(key.name)) {
5049
+ fail(key.line, `groups has an unknown option "${key.name}". Allowed: ${GROUP_KEYS.join(", ")}. owner, admin and member are built in.`);
5050
+ }
5051
+ if (seen.has(key.name)) fail(key.line, `groups declares ${key.name} twice.`);
5052
+ seen.add(key.name);
5053
+ r.expectPunct(":", `after groups.${key.name}`);
5054
+ out[key.name] = readStringList2(r, fail, `groups.${key.name}`);
5055
+ if (!r.tryPunct(",")) {
5056
+ r.expectPunct("}", "closing schema option groups");
5057
+ break;
5058
+ }
5059
+ }
5060
+ return out;
5061
+ }
5062
+ function readAppRoles(r, fail) {
5063
+ r.expectPunct("{", "opening schema option appRoles");
5064
+ const out = [];
5065
+ while (!r.tryPunct("}")) {
5066
+ const key = r.readKey("for an app role name");
5067
+ if (out.some((role2) => role2.name === key.name)) fail(key.line, `app role "${key.name}" is declared twice.`);
5068
+ r.expectPunct(":", `after app role "${key.name}"`);
5069
+ r.expectPunct("{", `opening app role "${key.name}" \u2014 declare it as { grantableBy: [...] }`);
5070
+ const role = { name: key.name, grantableBy: [] };
5071
+ const seen = /* @__PURE__ */ new Set();
5072
+ while (!r.tryPunct("}")) {
5073
+ const opt = r.readKey(`for an option of app role "${key.name}"`);
5074
+ if (!APP_ROLE_KEYS.includes(opt.name)) {
5075
+ fail(opt.line, `app role "${key.name}" has an unknown option "${opt.name}". Allowed: ${APP_ROLE_KEYS.join(", ")}.`);
5076
+ }
5077
+ if (seen.has(opt.name)) fail(opt.line, `app role "${key.name}" declares ${opt.name} twice.`);
5078
+ seen.add(opt.name);
5079
+ r.expectPunct(":", `after appRoles.${key.name}.${opt.name}`);
5080
+ role.grantableBy = readStringList2(r, fail, `appRoles.${key.name}.grantableBy`);
5081
+ if (!r.tryPunct(",")) {
5082
+ r.expectPunct("}", `closing app role "${key.name}"`);
5083
+ break;
5084
+ }
5085
+ }
5086
+ out.push(role);
5087
+ if (!r.tryPunct(",")) {
5088
+ r.expectPunct("}", "closing schema option appRoles");
5089
+ break;
5090
+ }
5091
+ }
5092
+ return out.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
5093
+ }
5094
+ var sorted2 = (list) => [...list].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
5095
+ function canonicalRoleVocabulary(groups, appRoles) {
5096
+ return {
5097
+ v: 1,
5098
+ groupRoles: sorted2(groups?.roles ?? []),
5099
+ adminGrantableGroupRoles: sorted2(groups?.adminGrantableRoles ?? []),
5100
+ appRoles: [...appRoles ?? []].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).map((role) => ({ name: role.name, grantableBy: sorted2(role.grantableBy) }))
5101
+ };
5102
+ }
5103
+ function validateDeclaredRoles(groups, appRoles) {
5104
+ const delegation = /* @__PURE__ */ Object.create(null);
5105
+ for (const role of appRoles ?? []) delegation[role.name] = role.grantableBy;
5106
+ return roleVocabularyErrors({
5107
+ groupRoles: groups?.roles ?? [],
5108
+ adminGrantableGroupRoles: groups?.adminGrantableRoles ?? [],
5109
+ appRoles: (appRoles ?? []).map((role) => role.name),
5110
+ appRoleDelegation: delegation
5111
+ });
5112
+ }
5113
+
5114
+ // worker/src/utils/db-schema-deploy/extract-schema-options.ts
5115
+ function readSchemaOptions(reader, fail, fileErrors) {
5116
+ const o = {};
5117
+ if (!reader.tryPunct(",") || reader.peek().kind === "punct" && reader.peek().value === ")") return o;
5118
+ reader.expectPunct("{", "opening schema options");
5119
+ while (!reader.tryPunct("}")) {
5120
+ const option = reader.readKey("for a schema option");
5121
+ const after = () => reader.expectPunct(":", `after schema option "${option.name}"`);
5122
+ if (option.name === "files" && !o.fileCollections) {
5123
+ after();
5124
+ o.fileCollections = readFileCollections(reader, fail, fileErrors);
5125
+ } else if (option.name === "groups" && !o.groupRoles) {
5126
+ after();
5127
+ o.groupRoles = readGroupRoles(reader, fail);
5128
+ } else if (option.name === "appRoles" && !o.appRoles) {
5129
+ after();
5130
+ o.appRoles = readAppRoles(reader, fail);
5131
+ } else if (option.name === "payments" && !o.payments) {
5132
+ after();
5133
+ o.payments = readPaymentBindings(reader, fail);
5134
+ } else if (option.name === "jobs" && !o.jobs) {
5135
+ after();
5136
+ o.jobs = readJobBindings(reader, fail);
5137
+ } else if (option.name === "serviceRoles" && !o.serviceRoles) {
5138
+ after();
5139
+ o.serviceRoles = readServiceRoles(reader, fail);
5140
+ } else if (option.name === "search" && !o.managedSearch) {
5141
+ after();
5142
+ const helper = reader.expectIdent('for schema option "search"');
5143
+ if (helper.value !== "owner") fail(helper.line, "search ownership must be declared with owner().");
5144
+ reader.expectPunct("(", "after search: owner");
5145
+ reader.expectPunct(")", "closing search: owner()");
5146
+ o.managedSearch = { kind: "owner" };
5147
+ } else {
5148
+ fail(option.line, `unknown schema option "${option.name}". The schema-level options are search: owner(), files: { \u2026 }, groups: { \u2026 }, appRoles: { \u2026 }, payments: { \u2026 }, jobs: { \u2026 } and serviceRoles: { \u2026 }, each at most once.`);
5149
+ }
5150
+ if (!reader.tryPunct(",")) {
5151
+ reader.expectPunct("}", "closing schema options");
5152
+ break;
5153
+ }
5154
+ }
5155
+ reader.tryPunct(",");
5156
+ return o;
5157
+ }
5158
+
3859
5159
  // worker/src/utils/db-schema-deploy/extract-schema-ts.ts
3860
5160
  function policyOwner(policy) {
3861
5161
  if (policy.kind === "owner") return policy;
@@ -3871,18 +5171,37 @@ function declaredOwnerColumn(scope) {
3871
5171
  function declaredScopeIntent(scope) {
3872
5172
  return scope.kind === "owner" ? "scoped" : scope.kind;
3873
5173
  }
5174
+ function declaredScopeIdentityColumns(scope) {
5175
+ if (scope.kind === "owner") return [scope.column];
5176
+ if (scope.kind === "member") return [...scope.group];
5177
+ if (scope.kind !== "policy") return [];
5178
+ const collect = (policy) => policy.kind === "owner" ? [policy.column] : policy.kind === "member" ? [...policy.group] : policy.kind === "any_of" ? policy.policies.flatMap(collect) : policy.kind === "group" ? [policy.column] : policy.kind === "app_role" ? [] : [policy.via];
5179
+ return [...new Set(collect(scope.policy))];
5180
+ }
3874
5181
  var COLUMN_HELPERS = /* @__PURE__ */ new Set([
3875
5182
  "id",
3876
5183
  "text",
3877
5184
  "number",
3878
5185
  "integer",
5186
+ "bigint",
3879
5187
  "boolean",
3880
5188
  "timestamp",
3881
5189
  "json",
3882
5190
  "blob"
3883
5191
  ]);
3884
- var SCOPE_HELPERS = /* @__PURE__ */ new Set(["owner", "shared", "serverOnly", "member", "anyOf", "parent"]);
5192
+ var SCOPE_HELPERS = /* @__PURE__ */ new Set(["owner", "shared", "serverOnly", "member", "anyOf", "parent", "group", "appRole"]);
3885
5193
  var TABLE_MARKER_HELPERS = /* @__PURE__ */ new Set(["removedTable", "exported"]);
5194
+ var SCHEMA_CALLABLE = /* @__PURE__ */ new Set([
5195
+ ...COLUMN_HELPERS,
5196
+ ...SCOPE_HELPERS,
5197
+ ...TABLE_MARKER_HELPERS,
5198
+ "schema",
5199
+ "table",
5200
+ "removed",
5201
+ "hasMany",
5202
+ "belongsTo",
5203
+ "files"
5204
+ ]);
3886
5205
  var SAFE_IDENT = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
3887
5206
  var RESERVED_PREFIXES = ["app_", "sqlite_", "d1_", "__sw_"];
3888
5207
  var MAX_TABLES = 100;
@@ -3979,7 +5298,7 @@ function tokenize(source) {
3979
5298
  i = j;
3980
5299
  continue;
3981
5300
  }
3982
- if ("(){}[]:,;.".includes(ch)) {
5301
+ if ("(){}[]:,;.=".includes(ch)) {
3983
5302
  tokens.push({ kind: "punct", value: ch, line });
3984
5303
  i++;
3985
5304
  continue;
@@ -4146,7 +5465,7 @@ function readColumn(r, tableName, rawName, line) {
4146
5465
  }
4147
5466
  if (!COLUMN_HELPERS.has(helper)) {
4148
5467
  throw new SchemaTsError(
4149
- `line ${helperTok.line}: unknown column type "${helper}" on "${tableName}"."${rawName}". Use one of: id, text, number, integer, boolean, timestamp, json, blob.`
5468
+ `line ${helperTok.line}: unknown column type "${helper}" on "${tableName}"."${rawName}". Use one of: id, text, number, integer, bigint, boolean, timestamp, json, blob.`
4150
5469
  );
4151
5470
  }
4152
5471
  r.expectPunct("(", `after "${helper}"`);
@@ -4196,14 +5515,17 @@ function readColumn(r, tableName, rawName, line) {
4196
5515
  col.autoNow = true;
4197
5516
  break;
4198
5517
  }
4199
- const okType = value === null || t === "boolean" && typeof value === "boolean" || (t === "number" || t === "integer") && typeof value === "number" || (t === "text" || t === "timestamp" || t === "json") && typeof value === "string";
5518
+ const okType = value === null || t === "boolean" && typeof value === "boolean" || (t === "number" || t === "integer") && typeof value === "number" || (t === "text" || t === "timestamp" || t === "json" || t === "bigint") && typeof value === "string";
4200
5519
  if (!okType) {
4201
5520
  throw new SchemaTsError(
4202
- `line ${optLine}: the default for "${tableName}"."${rawName}" (${t}) must be ${t === "boolean" ? "true or false" : t === "number" || t === "integer" ? "a number" : t === "blob" ? "omitted \u2014 blob columns cannot declare a default" : "a string"} or null.`
5521
+ `line ${optLine}: the default for "${tableName}"."${rawName}" (${t}) must be ${t === "boolean" ? "true or false" : t === "number" || t === "integer" ? "a number" : t === "bigint" ? "a whole number written as a string, like '9007199254740993'" : t === "blob" ? "omitted \u2014 blob columns cannot declare a default" : "a string"} or null.`
4203
5522
  );
4204
5523
  }
4205
- if (t === "integer" && typeof value === "number" && !Number.isInteger(value)) {
4206
- throw new SchemaTsError(`line ${optLine}: the default for integer column "${tableName}"."${rawName}" must be a whole number.`);
5524
+ if (t === "integer" && typeof value === "number" && !Number.isSafeInteger(value)) {
5525
+ throw new SchemaTsError(Number.isInteger(value) ? `line ${optLine}: the default for integer column "${tableName}"."${rawName}" is outside JavaScript's exact whole-number range (\xB19007199254740991). Declare the column bigint() and write the default as a string.` : `line ${optLine}: the default for integer column "${tableName}"."${rawName}" must be a whole number.`);
5526
+ }
5527
+ if (t === "bigint" && typeof value === "string" && !isCanonicalBigintText(value)) {
5528
+ throw new SchemaTsError(`line ${optLine}: the default for bigint column "${tableName}"."${rawName}" must be a whole number between -9223372036854775808 and 9223372036854775807, written as a string with no sign other than a leading minus and no leading zeros.`);
4207
5529
  }
4208
5530
  col.hasDefault = true;
4209
5531
  col.default = value;
@@ -4421,7 +5743,9 @@ function readParentScope(r, tableName, line) {
4421
5743
  if (typeof value !== "string" || !SAFE_IDENT.test(value) || value.length > MAX_NAME_LENGTH) {
4422
5744
  throw new SchemaTsError(`line ${opts.get("via").line}: parent({ via }) on table "${tableName}" must name a valid declared foreign-key column.`);
4423
5745
  }
4424
- return { kind: "policy", policy: { kind: "parent", via: value.toLowerCase(), table: "", parentKey: "", policy: { kind: "owner", column: "" } } };
5746
+ const sentinel = { kind: "parent", via: value.toLowerCase(), table: "", parentKey: "", policy: { kind: "owner", column: "" } };
5747
+ rememberParentLine(sentinel, line);
5748
+ return { kind: "policy", policy: sentinel };
4425
5749
  }
4426
5750
  function readRelations(r, tableName) {
4427
5751
  r.expectPunct("{", `for "relations" of table "${tableName}"`);
@@ -4471,11 +5795,16 @@ function readScope(r, tableName) {
4471
5795
  const tok = r.expectIdent(`for the scope of table "${tableName}"`);
4472
5796
  if (!SCOPE_HELPERS.has(tok.value)) {
4473
5797
  throw new SchemaTsError(
4474
- `line ${tok.line}: unknown scope "${tok.value}" on table "${tableName}". Use owner(), member(), anyOf(), parent(), shared(), or serverOnly().`
5798
+ `line ${tok.line}: unknown scope "${tok.value}" on table "${tableName}". Use owner(), group(), appRole(), member(), anyOf(), parent(), shared(), or serverOnly().`
4475
5799
  );
4476
5800
  }
4477
5801
  if (tok.value === "anyOf") return readAnyOfScope(r, tableName, tok.line);
4478
5802
  if (tok.value === "parent") return readParentScope(r, tableName, tok.line);
5803
+ const failAt = (line, message) => {
5804
+ throw new SchemaTsError(`line ${line}: ${message}`);
5805
+ };
5806
+ if (tok.value === "group") return { kind: "policy", policy: readGroupScope(r, tableName, tok.line, failAt) };
5807
+ if (tok.value === "appRole") return { kind: "policy", policy: readAppRoleScope(r, tableName, tok.line, failAt) };
4479
5808
  if (tok.value === "member") {
4480
5809
  r.expectPunct("(", 'after "member"');
4481
5810
  const scope = readMemberScope(r, tableName, tok.line);
@@ -4755,12 +6084,14 @@ function extractSchemaTs(source) {
4755
6084
  return { ok: false, errors: [`db/schema.ts is larger than ${Math.floor(MAX_SOURCE_BYTES / 1024)}KB \u2014 that is not a schema declaration.`] };
4756
6085
  }
4757
6086
  try {
4758
- const r = new Reader(tokenize(source));
6087
+ const r = new Reader(resolveSchemaConstants(tokenize(source), SCHEMA_CALLABLE, MAX_SOURCE_BYTES, (line, message) => {
6088
+ throw new SchemaTsError(`line ${line}: ${message}`);
6089
+ }));
4759
6090
  while (r.peek().kind === "ident" && r.peek().value === "import") skipImport(r);
4760
6091
  const exp = r.expectIdent("to start the declaration");
4761
6092
  if (exp.value !== "export") {
4762
6093
  throw new SchemaTsError(
4763
- `line ${exp.line}: db/schema.ts must contain only the schema declaration: optional imports, then "export default schema({ \u2026 })". Found "${exp.value}".`
6094
+ `line ${exp.line}: db/schema.ts must contain only the schema declaration: optional imports, optional const declarations, then "export default schema({ \u2026 })". Found "${exp.value}".`
4764
6095
  );
4765
6096
  }
4766
6097
  const def = r.expectIdent('after "export"');
@@ -4793,37 +6124,11 @@ function extractSchemaTs(source) {
4793
6124
  break;
4794
6125
  }
4795
6126
  }
4796
- let managedSearch;
4797
- let fileCollections;
4798
6127
  const fileErrors = [];
4799
- if (reader.tryPunct(",") && !(reader.peek().kind === "punct" && reader.peek().value === ")")) {
4800
- reader.expectPunct("{", "opening schema options");
4801
- while (!reader.tryPunct("}")) {
4802
- const option = reader.readKey("for a schema option");
4803
- if (option.name === "files" && !fileCollections) {
4804
- reader.expectPunct(":", 'after schema option "files"');
4805
- fileCollections = readFileCollections(reader, (line, message) => {
4806
- throw new SchemaTsError(`line ${line}: ${message}`);
4807
- }, fileErrors);
4808
- } else if (option.name === "search" && !managedSearch) {
4809
- reader.expectPunct(":", 'after schema option "search"');
4810
- const helper = reader.expectIdent('for schema option "search"');
4811
- if (helper.value !== "owner") {
4812
- throw new SchemaTsError(`line ${helper.line}: search ownership must be declared with owner().`);
4813
- }
4814
- reader.expectPunct("(", "after search: owner");
4815
- reader.expectPunct(")", "closing search: owner()");
4816
- managedSearch = { kind: "owner" };
4817
- } else {
4818
- throw new SchemaTsError(`line ${option.line}: unknown schema option "${option.name}". The schema-level options are search: owner() and files: { \u2026 }, each at most once.`);
4819
- }
4820
- if (!reader.tryPunct(",")) {
4821
- reader.expectPunct("}", "closing schema options");
4822
- break;
4823
- }
4824
- }
4825
- reader.tryPunct(",");
4826
- }
6128
+ const fail = (line, message) => {
6129
+ throw new SchemaTsError(`line ${line}: ${message}`);
6130
+ };
6131
+ const { managedSearch, fileCollections, groupRoles, appRoles, payments, jobs, serviceRoles } = readSchemaOptions(reader, fail, fileErrors);
4827
6132
  reader.expectPunct(")", "closing schema(\u2026)");
4828
6133
  reader.tryPunct(";");
4829
6134
  if (!reader.atEof()) {
@@ -4862,31 +6167,8 @@ function extractSchemaTs(source) {
4862
6167
  if (scope.kind === "policy" && scope.policy.kind !== "parent") return scope.policy;
4863
6168
  return null;
4864
6169
  };
4865
- for (const t of tables) {
4866
- if (t.scope.kind !== "policy" || t.scope.policy.kind !== "parent") continue;
4867
- const pending = t.scope.policy;
4868
- const fk = t.columns.find((column) => column.name === pending.via);
4869
- if (!fk || !fk.references) {
4870
- errors.push(`Table "${t.name}" uses parent({ via: '${pending.via}' }), but "${pending.via}" is not a declared scalar foreign key.`);
4871
- continue;
4872
- }
4873
- const parentTable = tableByName.get(fk.references);
4874
- const parentKey = parentTable?.columns.find((column) => column.helper === "id");
4875
- const inherited = parentTable ? privatePolicy(parentTable.scope) : null;
4876
- if (!parentTable || !parentKey || !inherited) {
4877
- errors.push(`Table "${t.name}" uses parent({ via: '${pending.via}' }), but its referenced parent must have one id() key and a direct owner(), member(), or anyOf(owner(), member()) private scope.`);
4878
- continue;
4879
- }
4880
- const compatible = fk.helper === parentKey.helper && (fk.helper !== "id" || fk.uuid === parentKey.uuid) || fk.helper === "integer" && parentKey.helper === "id" && !parentKey.uuid || fk.helper === "text" && parentKey.helper === "id" && parentKey.uuid;
4881
- if (!compatible) {
4882
- errors.push(`Table "${t.name}" uses parent({ via: '${pending.via}' }), but that foreign key does not have the same scalar type as "${parentTable.name}"."${parentKey.name}".`);
4883
- continue;
4884
- }
4885
- t.scope.policy = { kind: "parent", via: pending.via, table: parentTable.name, parentKey: parentKey.name, policy: inherited };
4886
- if (t.client?.create && !t.client.create.includes(pending.via)) {
4887
- errors.push(`Table "${t.name}" uses parent({ via: '${pending.via}' }) and client.create must include "${pending.via}" so every new row explicitly selects an authorized parent.`);
4888
- }
4889
- }
6170
+ resolveAppRoleScopes(tables, appRoles, errors);
6171
+ resolveGroupsAndParents(tables, tableByName, privatePolicy, groupRoles, errors);
4890
6172
  const memberPolicies = (scope) => {
4891
6173
  const out = [];
4892
6174
  const visit = (node) => {
@@ -4937,9 +6219,14 @@ function extractSchemaTs(source) {
4937
6219
  errors.push(...fileErrors);
4938
6220
  validateFileCollections(fileCollections, (name) => {
4939
6221
  const t = tableByName.get(name);
4940
- return t ? { columns: knownColumns(t), clientWrites: Boolean(t.client && (t.client.create !== null || t.client.update !== null || t.client.delete)) } : null;
6222
+ return t ? { columns: knownColumns(t), clientWrites: Boolean(t.client && (t.client.create !== null || t.client.update !== null || t.client.delete)), parentTarget: fileParentTarget(t) } : null;
4941
6223
  }, errors);
4942
6224
  }
6225
+ if (groupRoles || appRoles) errors.push(...validateDeclaredRoles(groupRoles, appRoles));
6226
+ validateBindings({ payments, jobs, serviceRoles, groups: groupRoles, appRoles }, (name) => {
6227
+ const t = tableByName.get(name);
6228
+ return t ? { table: t, key: fileParentTarget(t).key, privateUser: fileParentTarget(t).privateUserPolicy, identity: declaredScopeIdentityColumns(t.scope) } : null;
6229
+ }, errors);
4943
6230
  if (errors.length > 0) return { ok: false, errors };
4944
6231
  return {
4945
6232
  ok: true,
@@ -4948,7 +6235,12 @@ function extractSchemaTs(source) {
4948
6235
  removedTables: removedTables.sort(),
4949
6236
  exportedTables: exportedTables.sort(),
4950
6237
  ...managedSearch ? { search: managedSearch } : {},
4951
- ...fileCollections ? { files: fileCollections } : {}
6238
+ ...fileCollections ? { files: fileCollections } : {},
6239
+ ...groupRoles ? { groups: groupRoles } : {},
6240
+ ...appRoles ? { appRoles } : {},
6241
+ ...payments ? { payments } : {},
6242
+ ...jobs ? { jobs } : {},
6243
+ ...serviceRoles ? { serviceRoles } : {}
4952
6244
  }
4953
6245
  };
4954
6246
  } catch (err) {
@@ -4980,6 +6272,9 @@ function canonicalTableShape(t) {
4980
6272
  indexes: t.indexes,
4981
6273
  uniques: t.uniques,
4982
6274
  ...t.removedColumns.length > 0 ? { removedColumns: t.removedColumns } : {},
6275
+ // conditional: only a group() table with direct group references gains
6276
+ // it, so every other table keeps its generation id.
6277
+ ...t.groupRefs && t.groupRefs.length > 0 ? { groupRefs: t.groupRefs } : {},
4983
6278
  // conditional: a relation-free table keeps its slice-1 generation id. Ordered
4984
6279
  // by name (readRelations sorts) so the shape is deterministic. `kind` is
4985
6280
  // likewise conditional — absent means hasMany, so a declaration that uses
@@ -5029,10 +6324,18 @@ function declaredFilesFromSource(files) {
5029
6324
  const parsed = extractSchemaTs(source);
5030
6325
  return parsed.ok && parsed.declaration.files?.length ? canonicalFileCollectionsJson(parsed.declaration.files) : void 0;
5031
6326
  }
6327
+ function declaredRolesFromSource(files) {
6328
+ const source = files["db/schema.ts"];
6329
+ if (source === void 0) return void 0;
6330
+ const parsed = extractSchemaTs(source);
6331
+ if (!parsed.ok || !parsed.declaration.groups && !parsed.declaration.appRoles) return void 0;
6332
+ return JSON.stringify(canonicalRoleVocabulary(parsed.declaration.groups, parsed.declaration.appRoles));
6333
+ }
5032
6334
 
5033
6335
  // declared-data-vendor-entry.js
5034
6336
  var import_typed_data = __toESM(require_typed_data());
5035
6337
  var import_runtime_types = __toESM(require_runtime_types());
6338
+ var import_groups_types = __toESM(require_groups_types());
5036
6339
  var import_typed_files = __toESM(require_typed_files());
5037
6340
  var import_schema_types = __toESM(require_schema_types());
5038
6341
  var import_runtime_types2 = __toESM(require_runtime_types());
@@ -5041,6 +6344,9 @@ function generateFromFiles(files) {
5041
6344
  const authority = clientAuthorityFromSource(files);
5042
6345
  return authority ? (0, import_typed_data.generateDataClient)(authority) : void 0;
5043
6346
  }
6347
+ function runtimeDeclarationFromFiles(files) {
6348
+ return (0, import_runtime_types.runtimeContextDeclaration)((0, import_groups_types.declaredRoles)(declaredRolesFromSource(files)));
6349
+ }
5044
6350
  function declaredTablesFromFiles(files) {
5045
6351
  return (0, import_runtime_types.declaredTablesDeclaration)(schemaAuthorityFromSource(files));
5046
6352
  }
@@ -5056,5 +6362,6 @@ function filesDeclarationFromFiles(files) {
5056
6362
  SCHEMA_DECLARATION,
5057
6363
  declaredTablesFromFiles,
5058
6364
  filesDeclarationFromFiles,
5059
- generateFromFiles
6365
+ generateFromFiles,
6366
+ runtimeDeclarationFromFiles
5060
6367
  });