@somewhere-tech/cli 0.37.10 → 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.
@@ -54,7 +54,7 @@ var require_declared_data_contract = __commonJS({
54
54
  if (result.some((field) => typeof field !== "string" || !columns.has(field)) || new Set(result).size !== result.length) fail(name);
55
55
  return result.sort();
56
56
  }
57
- function policy(value, label) {
57
+ function policy(value, label, edges = 0) {
58
58
  record(value, label);
59
59
  const keys = Object.keys(value).sort().join(",");
60
60
  if (value.k === "o" && keys === "c,k" && identifier.test(value.c)) return { k: "o", c: value.c };
@@ -71,13 +71,31 @@ var require_declared_data_contract = __commonJS({
71
71
  if (left.k !== "o" || right.k !== "m") fail(label);
72
72
  return { k: "a", p: [left, right] };
73
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
+ }
74
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))) {
75
- const inherited = policy(value.p, label);
76
- 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);
77
84
  return { k: "p", v: value.v, t: value.t, pk: value.pk, p: inherited };
78
85
  }
79
86
  fail(label);
80
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
+ }
81
99
  function policyMembershipTables(value, out) {
82
100
  if (!value || typeof value !== "object") return;
83
101
  if (value.k === "m" && value.m) out.add(value.m.m);
@@ -164,7 +182,8 @@ var require_declared_data_contract = __commonJS({
164
182
  member = { g: [...g], m, u, mg: [...mg], ...normalizedOperations !== void 0 ? { o: [...normalizedOperations] } : {} };
165
183
  }
166
184
  const normalizedPolicy = intent === "policy" ? policy(table.policy, `${name}.policy`) : null;
167
- 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`);
168
187
  if (normalizedPolicy?.k === "a") {
169
188
  owner = normalizedPolicy.p[0].c;
170
189
  if ([...create || [], ...update || []].includes(owner) || (update || []).some((field) => normalizedPolicy.p[1].m.g.includes(field))) fail(`${name}.policy identity writes`);
@@ -337,11 +356,175 @@ var require_typed_data = __commonJS({
337
356
  }
338
357
  });
339
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
+
340
523
  // worker/containers/compile/runtime-types.cjs
341
524
  var require_runtime_types = __commonJS({
342
525
  "worker/containers/compile/runtime-types.cjs"(exports2, module2) {
343
526
  "use strict";
344
- var RUNTIME_CONTEXT_DECLARATION2 = `
527
+ var BASE_RUNTIME_CONTEXT_DECLARATION = `
345
528
  type __SomewhereJsonPrimitive = string | number | boolean | null;
346
529
  type __SomewhereJson = __SomewhereJsonPrimitive | { [key: string]: __SomewhereJson } | __SomewhereJson[];
347
530
  type SomewhereDbScalar = string | number | boolean | null;
@@ -1037,6 +1220,7 @@ type SomewherePaymentsStatus =
1037
1220
  };
1038
1221
  interface SomewherePaymentsLineItem { price?: string; amount?: number; currency?: string; name?: string; quantity?: number }
1039
1222
  interface SomewherePaymentsCheckoutOptions {
1223
+ row?: never;
1040
1224
  env?: SomewherePaymentsEnv;
1041
1225
  mode?: 'payment' | 'subscription';
1042
1226
  line_items?: readonly SomewherePaymentsLineItem[];
@@ -1050,6 +1234,44 @@ interface SomewherePaymentsCheckoutOptions {
1050
1234
  customer_email?: string;
1051
1235
  metadata?: Readonly<Record<string, string>>;
1052
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;
1053
1275
  interface SomewherePaymentsCheckoutForUserOptions extends SomewherePaymentsCheckoutOptions { plan: string }
1054
1276
  interface SomewherePaymentsCheckoutResult {
1055
1277
  session_id: string;
@@ -1138,6 +1360,7 @@ interface SomewhereRuntimePayments {
1138
1360
  onboard(opts?: SomewherePaymentsOnboardOptions | null): Promise<SomewherePaymentsOnboardResult>;
1139
1361
  quote: SomewhereQuoteFunction;
1140
1362
  status(opts?: { refresh?: boolean } | null): Promise<SomewherePaymentsStatus>;
1363
+ checkout(opts: SomewherePaymentsRowCheckoutOptions): Promise<SomewherePaymentsRowCheckoutResult>;
1141
1364
  checkout(opts: SomewherePaymentsCheckoutOptions): Promise<SomewherePaymentsCheckoutResult>;
1142
1365
  // The app user is the request's verified principal; no user id argument.
1143
1366
  checkoutForUser(opts: SomewherePaymentsCheckoutForUserOptions): Promise<SomewherePaymentsCheckoutResult>;
@@ -2700,6 +2923,10 @@ interface SomewhereJobsCreateOptions {
2700
2923
  // Earliest start: an ISO-8601 time with an explicit offset, e.g. '2026-10-05T09:00:00Z'. Must be in the future.
2701
2924
  run_at?: string;
2702
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 };
2703
2930
  }
2704
2931
  interface SomewhereJobsRecovery {
2705
2932
  dispatch_state: string;
@@ -2944,6 +3171,11 @@ type ServerFunction<Contract extends { input: unknown; output: unknown }> =
2944
3171
  interface Response { readonly webSocket: WebSocket | null }
2945
3172
  interface WebSocket { accept(): void }
2946
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({});
2947
3179
  var ENDPOINT_DECLARATION2 = `
2948
3180
  interface SomewhereEndpointUser {
2949
3181
  id: string;
@@ -3017,7 +3249,7 @@ interface SomewhereEndpointConfig<Auth extends SomewhereEndpointAuth, Schema ext
3017
3249
  });
3018
3250
  return "interface SomewhereDeclaredTables {\n" + entries.join("") + "}\n";
3019
3251
  }
3020
- 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 };
3021
3253
  }
3022
3254
  });
3023
3255
 
@@ -3038,20 +3270,32 @@ var require_typed_files = __commonJS({
3038
3270
  const scope = collection.scope || {};
3039
3271
  return scope.kind === "member" || scope.kind === "owner_or_member" ? scope.member : null;
3040
3272
  }
3041
- function describeCollection(collection) {
3273
+ function fieldOf(collection) {
3042
3274
  const member = memberOf(collection);
3043
- const groupKey = member ? JSON.stringify(member.group) : null;
3044
- const meta = member ? `FileMeta & { ${groupKey}: string }` : "FileMeta";
3045
- 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 }`;
3046
3289
  const ops = [];
3047
3290
  if (collection.client.read) {
3048
- ops.push(`list(options?: { ${groupOption.replace(": string | number; ", "?: string | number; ")}limit?: number; cursor?: string | null }): Promise<FileResult<{ items: Array<${meta}>; next: string | null; has_more: boolean }>>`);
3291
+ ops.push(`list(${listOptions}): Promise<FileResult<{ items: Array<${meta}>; next: string | null; has_more: boolean }>>`);
3049
3292
  ops.push(`get(id: string): Promise<FileResult<${meta}>>`);
3050
3293
  ops.push("url(id: string): string");
3051
3294
  ops.push("shareLink(id: string, options?: { expiresIn?: number }): Promise<FileResult<{ url: string; expires_at: string; expires_in: number }>>");
3052
3295
  }
3053
- 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>>`);
3054
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}>>`);
3055
3299
  if (collection.client.delete) ops.push("delete(id: string): Promise<FileResult<{ id: string; deleted: true }>>");
3056
3300
  if (collection.public) {
3057
3301
  ops.push(collection.scope.kind === "server_only" ? "publicUrl(path: string): string" : "publicUrl(id: string, name: string): string");
@@ -3071,13 +3315,14 @@ var require_typed_files = __commonJS({
3071
3315
  }
3072
3316
  `;
3073
3317
  const shapes = collections.map((c) => {
3074
- const member = memberOf(c);
3318
+ const field = fieldOf(c);
3075
3319
  return {
3076
3320
  name: c.name,
3077
3321
  root: c.path,
3078
3322
  ops: Object.keys(c.client).filter((op) => c.client[op]),
3079
3323
  public: c.public ? c.scope.kind === "server_only" ? "path" : "id" : null,
3080
- group: member ? member.group : null,
3324
+ group: field ? field.name : null,
3325
+ move: Boolean(field && field.parent && c.client.replace),
3081
3326
  maxSize: c.limits.maxSize,
3082
3327
  types: c.limits.types
3083
3328
  };
@@ -3150,6 +3395,7 @@ function makeCollection(shape){
3150
3395
  if(has("upload"))c.upload=(file,options)=>put("upload",null,file,options);
3151
3396
  if(has("replace"))c.replace=(id,file,options)=>put("replace",id,file,options);
3152
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)};
3153
3399
  if(shape.public)c.publicUrl=publicUrl;
3154
3400
  return Object.freeze(c);
3155
3401
  }
@@ -3205,13 +3451,20 @@ var require_schema_types = __commonJS({
3205
3451
  client?: Partial<Record<FileOperation, boolean>>;
3206
3452
  limits?: { maxSize?: string | number; types?: string[] };
3207
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'>> }
3208
3461
  interface TableOptions<Field extends string> {
3209
3462
  scope?: Scope; client?: ClientPermissions<Field>; indexes?: string[][];
3210
3463
  unique?: string[][]; relations?: Record<string, Relation>;
3211
3464
  }
3212
3465
  }
3213
3466
  `;
3214
- 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;
3215
3468
  function table<Columns extends Record<string, SomewhereSchemaDeclaration.Column | SomewhereSchemaDeclaration.ColumnMarker>>(columns: Columns, options?: SomewhereSchemaDeclaration.TableOptions<Extract<keyof Columns, string>>): SomewhereSchemaDeclaration.Table;
3216
3469
  function id(options?: { uuid?: boolean }): SomewhereSchemaDeclaration.Column;
3217
3470
  function text(options?: SomewhereSchemaDeclaration.ColumnOptions<string>): SomewhereSchemaDeclaration.Column;
@@ -3235,7 +3488,9 @@ function exported(): SomewhereSchemaDeclaration.TableMarker;
3235
3488
  `;
3236
3489
  var policySignatures = `function anyOf(owner: SomewhereSchemaDeclaration.OwnerScope, member: SomewhereSchemaDeclaration.MemberScope): SomewhereSchemaDeclaration.Scope;
3237
3490
  function anyOf(member: SomewhereSchemaDeclaration.MemberScope, owner: SomewhereSchemaDeclaration.OwnerScope): SomewhereSchemaDeclaration.Scope;
3238
- 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;
3239
3494
  `;
3240
3495
  var SCHEMA_DECLARATION2 = types + signatures.replace(/^function /gm, "declare function ") + `declare module 'somewhere/db' {
3241
3496
  ${(signatures + policySignatures).replace(/^function /gm, " export function ")}}
@@ -3253,7 +3508,8 @@ __export(declared_data_vendor_entry_exports, {
3253
3508
  SCHEMA_DECLARATION: () => import_schema_types.SCHEMA_DECLARATION,
3254
3509
  declaredTablesFromFiles: () => declaredTablesFromFiles,
3255
3510
  filesDeclarationFromFiles: () => filesDeclarationFromFiles,
3256
- generateFromFiles: () => generateFromFiles
3511
+ generateFromFiles: () => generateFromFiles,
3512
+ runtimeDeclarationFromFiles: () => runtimeDeclarationFromFiles
3257
3513
  });
3258
3514
  module.exports = __toCommonJS(declared_data_vendor_entry_exports);
3259
3515
 
@@ -3386,8 +3642,236 @@ function parseClientPermissions(value, columns, scope) {
3386
3642
  return { ok: true, permissions: { identity, read, publicRead, ...writes, delete: remove } };
3387
3643
  }
3388
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
+
3389
3862
  // worker/src/utils/authority/scope-enforcement.ts
3390
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;
3391
3875
  function bakedTableSchemaFromDeclared(declaredJson) {
3392
3876
  let shape;
3393
3877
  try {
@@ -3462,7 +3946,7 @@ function bakedTableSchemaFromDeclared(declaredJson) {
3462
3946
  }
3463
3947
  if (scope !== null && typeof scope === "object" && scope.kind === "policy") {
3464
3948
  const policy = bakedPolicyFromDeclared(scope.policy);
3465
- 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;
3466
3950
  return relations ? { columns: out, policy, relations, ...browser } : { columns: out, policy, ...browser };
3467
3951
  }
3468
3952
  const author = scope !== null && typeof scope === "object" && scope.kind === "shared" ? SHARED_AUTHOR_COLUMN : void 0;
@@ -3479,8 +3963,8 @@ function declaredPolicyIdentity(value) {
3479
3963
  }
3480
3964
  return {};
3481
3965
  }
3482
- function bakedPolicyFromDeclared(value, depth = 0) {
3483
- 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;
3484
3968
  const node = value;
3485
3969
  if (node.kind === "owner") {
3486
3970
  return typeof node.column === "string" && SAFE_SCOPE_IDENTIFIER.test(node.column) ? { k: "o", c: node.column.toLowerCase() } : null;
@@ -3490,14 +3974,26 @@ function bakedPolicyFromDeclared(value, depth = 0) {
3490
3974
  return member ? { k: "m", m: member } : null;
3491
3975
  }
3492
3976
  if (node.kind === "any_of" && Array.isArray(node.policies) && node.policies.length === 2) {
3493
- 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;
3494
3978
  const left = bakedPolicyFromDeclared(node.policies[0], depth + 1);
3495
3979
  const right = bakedPolicyFromDeclared(node.policies[1], depth + 1);
3496
3980
  return left && right ? { k: "a", p: [left, right] } : null;
3497
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
+ }
3498
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)) {
3499
- if (depth !== 0 || node.policy?.kind === "parent") return null;
3500
- 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);
3501
3997
  return inherited ? { k: "p", v: node.via.toLowerCase(), t: node.table.toLowerCase(), pk: node.parentKey.toLowerCase(), p: inherited } : null;
3502
3998
  }
3503
3999
  return null;
@@ -3777,6 +4273,90 @@ function matchingClose(tokens, open) {
3777
4273
  return -1;
3778
4274
  }
3779
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
+
3780
4360
  // worker/src/utils/db-schema-deploy/extract-schema-relations.ts
3781
4361
  function validateDeclaredRelations(tables, tableByName, knownColumns, markedGone, errors) {
3782
4362
  const columnByName = (t, col) => t.columns.find((c) => c.name === col);
@@ -3938,11 +4518,37 @@ function readFileOwner(r, fail, name) {
3938
4518
  }
3939
4519
  fail(key.line, `owner() on file collection "${name}" takes no options \u2014 the owner is always the signed-in uploader.`);
3940
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
+ }
3941
4549
  function readFileScope(r, fail, name) {
3942
4550
  const tok = r.expectIdent(`for the scope of file collection "${name}"`);
3943
- if (tok.value === "parent") {
3944
- fail(tok.line, `parent() is not available for file collections yet. Use owner(), member(), anyOf(owner(), member()), shared(), or serverOnly().`);
3945
- }
4551
+ if (tok.value === "parent") return { kind: "parent", parent: readFileParent(r, fail, name, tok.line) };
3946
4552
  if (tok.value === "owner") {
3947
4553
  readFileOwner(r, fail, name);
3948
4554
  return { kind: "owner" };
@@ -3971,7 +4577,7 @@ function readFileScope(r, fail, name) {
3971
4577
  if (owners !== 1 || !member) fail(tok.line, `anyOf() on file collection "${name}" requires exactly one owner() and one member().`);
3972
4578
  return { kind: "owner_or_member", member };
3973
4579
  }
3974
- 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().`);
3975
4581
  }
3976
4582
  function readCollection(r, fail, name, errors) {
3977
4583
  const callee = r.expectIdent(`for file collection "${name}"`);
@@ -4045,7 +4651,7 @@ function readCollection(r, fail, name, errors) {
4045
4651
  }
4046
4652
  r.tryPunct(",");
4047
4653
  r.expectPunct(")", "closing files()");
4048
- 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().`);
4049
4655
  const rootError = fileCollectionRootError(name, path);
4050
4656
  if (rootError) errors.push(rootError);
4051
4657
  const declaresClient = FILE_OPERATIONS.some((op) => client[op]);
@@ -4058,8 +4664,12 @@ function readCollection(r, fail, name, errors) {
4058
4664
  if (isPublic && (scope.kind === "member" || scope.kind === "owner_or_member")) {
4059
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().`);
4060
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
+ }
4061
4670
  return { name, path: typeof path === "string" ? path : "", scope, public: isPublic, client, limits };
4062
4671
  }
4672
+ var MAX_FILE_PARENT_EDGES = 4;
4063
4673
  function readFileCollections(r, fail, errors) {
4064
4674
  r.expectPunct("{", "opening schema option files");
4065
4675
  const out = [];
@@ -4086,6 +4696,30 @@ function validateFileCollections(collections, table, errors) {
4086
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.`);
4087
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.`);
4088
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
+ }
4089
4723
  if (a.scope.kind !== "member" && a.scope.kind !== "owner_or_member") continue;
4090
4724
  const m = a.scope.member;
4091
4725
  const membership = table(m.membership);
@@ -4106,7 +4740,7 @@ function canonicalFileCollectionsJson(collections) {
4106
4740
  collections: [...collections].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).map((c) => ({
4107
4741
  name: c.name,
4108
4742
  path: c.path,
4109
- 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" ? {
4110
4744
  kind: c.scope.kind,
4111
4745
  member: {
4112
4746
  group: c.scope.member.group,
@@ -4123,6 +4757,405 @@ function canonicalFileCollectionsJson(collections) {
4123
4757
  });
4124
4758
  }
4125
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
+
4126
5159
  // worker/src/utils/db-schema-deploy/extract-schema-ts.ts
4127
5160
  function policyOwner(policy) {
4128
5161
  if (policy.kind === "owner") return policy;
@@ -4138,6 +5171,13 @@ function declaredOwnerColumn(scope) {
4138
5171
  function declaredScopeIntent(scope) {
4139
5172
  return scope.kind === "owner" ? "scoped" : scope.kind;
4140
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
+ }
4141
5181
  var COLUMN_HELPERS = /* @__PURE__ */ new Set([
4142
5182
  "id",
4143
5183
  "text",
@@ -4149,7 +5189,7 @@ var COLUMN_HELPERS = /* @__PURE__ */ new Set([
4149
5189
  "json",
4150
5190
  "blob"
4151
5191
  ]);
4152
- 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"]);
4153
5193
  var TABLE_MARKER_HELPERS = /* @__PURE__ */ new Set(["removedTable", "exported"]);
4154
5194
  var SCHEMA_CALLABLE = /* @__PURE__ */ new Set([
4155
5195
  ...COLUMN_HELPERS,
@@ -4703,7 +5743,9 @@ function readParentScope(r, tableName, line) {
4703
5743
  if (typeof value !== "string" || !SAFE_IDENT.test(value) || value.length > MAX_NAME_LENGTH) {
4704
5744
  throw new SchemaTsError(`line ${opts.get("via").line}: parent({ via }) on table "${tableName}" must name a valid declared foreign-key column.`);
4705
5745
  }
4706
- 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 };
4707
5749
  }
4708
5750
  function readRelations(r, tableName) {
4709
5751
  r.expectPunct("{", `for "relations" of table "${tableName}"`);
@@ -4753,11 +5795,16 @@ function readScope(r, tableName) {
4753
5795
  const tok = r.expectIdent(`for the scope of table "${tableName}"`);
4754
5796
  if (!SCOPE_HELPERS.has(tok.value)) {
4755
5797
  throw new SchemaTsError(
4756
- `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().`
4757
5799
  );
4758
5800
  }
4759
5801
  if (tok.value === "anyOf") return readAnyOfScope(r, tableName, tok.line);
4760
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) };
4761
5808
  if (tok.value === "member") {
4762
5809
  r.expectPunct("(", 'after "member"');
4763
5810
  const scope = readMemberScope(r, tableName, tok.line);
@@ -5077,37 +6124,11 @@ function extractSchemaTs(source) {
5077
6124
  break;
5078
6125
  }
5079
6126
  }
5080
- let managedSearch;
5081
- let fileCollections;
5082
6127
  const fileErrors = [];
5083
- if (reader.tryPunct(",") && !(reader.peek().kind === "punct" && reader.peek().value === ")")) {
5084
- reader.expectPunct("{", "opening schema options");
5085
- while (!reader.tryPunct("}")) {
5086
- const option = reader.readKey("for a schema option");
5087
- if (option.name === "files" && !fileCollections) {
5088
- reader.expectPunct(":", 'after schema option "files"');
5089
- fileCollections = readFileCollections(reader, (line, message) => {
5090
- throw new SchemaTsError(`line ${line}: ${message}`);
5091
- }, fileErrors);
5092
- } else if (option.name === "search" && !managedSearch) {
5093
- reader.expectPunct(":", 'after schema option "search"');
5094
- const helper = reader.expectIdent('for schema option "search"');
5095
- if (helper.value !== "owner") {
5096
- throw new SchemaTsError(`line ${helper.line}: search ownership must be declared with owner().`);
5097
- }
5098
- reader.expectPunct("(", "after search: owner");
5099
- reader.expectPunct(")", "closing search: owner()");
5100
- managedSearch = { kind: "owner" };
5101
- } else {
5102
- 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.`);
5103
- }
5104
- if (!reader.tryPunct(",")) {
5105
- reader.expectPunct("}", "closing schema options");
5106
- break;
5107
- }
5108
- }
5109
- reader.tryPunct(",");
5110
- }
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);
5111
6132
  reader.expectPunct(")", "closing schema(\u2026)");
5112
6133
  reader.tryPunct(";");
5113
6134
  if (!reader.atEof()) {
@@ -5146,31 +6167,8 @@ function extractSchemaTs(source) {
5146
6167
  if (scope.kind === "policy" && scope.policy.kind !== "parent") return scope.policy;
5147
6168
  return null;
5148
6169
  };
5149
- for (const t of tables) {
5150
- if (t.scope.kind !== "policy" || t.scope.policy.kind !== "parent") continue;
5151
- const pending = t.scope.policy;
5152
- const fk = t.columns.find((column) => column.name === pending.via);
5153
- if (!fk || !fk.references) {
5154
- errors.push(`Table "${t.name}" uses parent({ via: '${pending.via}' }), but "${pending.via}" is not a declared scalar foreign key.`);
5155
- continue;
5156
- }
5157
- const parentTable = tableByName.get(fk.references);
5158
- const parentKey = parentTable?.columns.find((column) => column.helper === "id");
5159
- const inherited = parentTable ? privatePolicy(parentTable.scope) : null;
5160
- if (!parentTable || !parentKey || !inherited) {
5161
- 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.`);
5162
- continue;
5163
- }
5164
- 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;
5165
- if (!compatible) {
5166
- 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}".`);
5167
- continue;
5168
- }
5169
- t.scope.policy = { kind: "parent", via: pending.via, table: parentTable.name, parentKey: parentKey.name, policy: inherited };
5170
- if (t.client?.create && !t.client.create.includes(pending.via)) {
5171
- 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.`);
5172
- }
5173
- }
6170
+ resolveAppRoleScopes(tables, appRoles, errors);
6171
+ resolveGroupsAndParents(tables, tableByName, privatePolicy, groupRoles, errors);
5174
6172
  const memberPolicies = (scope) => {
5175
6173
  const out = [];
5176
6174
  const visit = (node) => {
@@ -5221,9 +6219,14 @@ function extractSchemaTs(source) {
5221
6219
  errors.push(...fileErrors);
5222
6220
  validateFileCollections(fileCollections, (name) => {
5223
6221
  const t = tableByName.get(name);
5224
- 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;
5225
6223
  }, errors);
5226
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);
5227
6230
  if (errors.length > 0) return { ok: false, errors };
5228
6231
  return {
5229
6232
  ok: true,
@@ -5232,7 +6235,12 @@ function extractSchemaTs(source) {
5232
6235
  removedTables: removedTables.sort(),
5233
6236
  exportedTables: exportedTables.sort(),
5234
6237
  ...managedSearch ? { search: managedSearch } : {},
5235
- ...fileCollections ? { files: fileCollections } : {}
6238
+ ...fileCollections ? { files: fileCollections } : {},
6239
+ ...groupRoles ? { groups: groupRoles } : {},
6240
+ ...appRoles ? { appRoles } : {},
6241
+ ...payments ? { payments } : {},
6242
+ ...jobs ? { jobs } : {},
6243
+ ...serviceRoles ? { serviceRoles } : {}
5236
6244
  }
5237
6245
  };
5238
6246
  } catch (err) {
@@ -5264,6 +6272,9 @@ function canonicalTableShape(t) {
5264
6272
  indexes: t.indexes,
5265
6273
  uniques: t.uniques,
5266
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 } : {},
5267
6278
  // conditional: a relation-free table keeps its slice-1 generation id. Ordered
5268
6279
  // by name (readRelations sorts) so the shape is deterministic. `kind` is
5269
6280
  // likewise conditional — absent means hasMany, so a declaration that uses
@@ -5313,10 +6324,18 @@ function declaredFilesFromSource(files) {
5313
6324
  const parsed = extractSchemaTs(source);
5314
6325
  return parsed.ok && parsed.declaration.files?.length ? canonicalFileCollectionsJson(parsed.declaration.files) : void 0;
5315
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
+ }
5316
6334
 
5317
6335
  // declared-data-vendor-entry.js
5318
6336
  var import_typed_data = __toESM(require_typed_data());
5319
6337
  var import_runtime_types = __toESM(require_runtime_types());
6338
+ var import_groups_types = __toESM(require_groups_types());
5320
6339
  var import_typed_files = __toESM(require_typed_files());
5321
6340
  var import_schema_types = __toESM(require_schema_types());
5322
6341
  var import_runtime_types2 = __toESM(require_runtime_types());
@@ -5325,6 +6344,9 @@ function generateFromFiles(files) {
5325
6344
  const authority = clientAuthorityFromSource(files);
5326
6345
  return authority ? (0, import_typed_data.generateDataClient)(authority) : void 0;
5327
6346
  }
6347
+ function runtimeDeclarationFromFiles(files) {
6348
+ return (0, import_runtime_types.runtimeContextDeclaration)((0, import_groups_types.declaredRoles)(declaredRolesFromSource(files)));
6349
+ }
5328
6350
  function declaredTablesFromFiles(files) {
5329
6351
  return (0, import_runtime_types.declaredTablesDeclaration)(schemaAuthorityFromSource(files));
5330
6352
  }
@@ -5340,5 +6362,6 @@ function filesDeclarationFromFiles(files) {
5340
6362
  SCHEMA_DECLARATION,
5341
6363
  declaredTablesFromFiles,
5342
6364
  filesDeclarationFromFiles,
5343
- generateFromFiles
6365
+ generateFromFiles,
6366
+ runtimeDeclarationFromFiles
5344
6367
  });