@odla-ai/chapter 0.46.0 → 0.47.1

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.
@@ -514,7 +514,7 @@ interface ChapterScheduling {
514
514
  *
515
515
  * - `"claim"` reads a role ladder from a JWT claim (the member-site model:
516
516
  * `provisional → member → admin`). Admin is the last (highest) rung; the
517
- * read-only `superAdmins` tier sits above it.
517
+ * Studio-managed super-admin tier sits above it.
518
518
  * - `"table"` gates on the deny-all `admins` allowlist (the hub model):
519
519
  * a binary "email is an admin or isn't", set only in odla Studio.
520
520
  *
@@ -526,10 +526,13 @@ interface ChapterAuth {
526
526
  /** Role ladder low→high, for `source: "claim"`. The last entry is admin.
527
527
  * Default `["provisional", "member", "admin"]`. */
528
528
  ladder?: readonly string[];
529
- /** Provision the read-only `superAdmins` tier table — the only tier that may
530
- * create/modify admins, and (like every namespace) deny-all + written only in
531
- * odla Studio. Default: `true` for `"claim"`, `false` for `"table"`. */
529
+ /** Enable the Studio-managed super-admin tier — the only tier that may
530
+ * create/modify admins. Default: `true` for `"claim"`, `false` for `"table"`. */
532
531
  superAdmins?: boolean;
532
+ /** Authority behind the super-admin tier. `"table"` preserves the legacy
533
+ * app allowlist; `"odla"` reads protected `$users.role`. Default `"table"`
534
+ * for backward compatibility. */
535
+ superAdminSource?: "table" | "odla";
533
536
  }
534
537
  /** The fully-resolved auth policy (defaults applied) carried on the {@link Chapter}. */
535
538
  interface ResolvedAuth {
@@ -539,6 +542,7 @@ interface ResolvedAuth {
539
542
  /** The highest ladder rung (last entry) — the "admin" gate. */
540
543
  adminRole: string;
541
544
  superAdmins: boolean;
545
+ superAdminSource: "none" | "table" | "odla";
542
546
  }
543
547
  /** The application status pipeline. Which statuses exist, and the subsets a site
544
548
  * allows a call to be booked from / an application approved from. Defaults to
@@ -514,7 +514,7 @@ interface ChapterScheduling {
514
514
  *
515
515
  * - `"claim"` reads a role ladder from a JWT claim (the member-site model:
516
516
  * `provisional → member → admin`). Admin is the last (highest) rung; the
517
- * read-only `superAdmins` tier sits above it.
517
+ * Studio-managed super-admin tier sits above it.
518
518
  * - `"table"` gates on the deny-all `admins` allowlist (the hub model):
519
519
  * a binary "email is an admin or isn't", set only in odla Studio.
520
520
  *
@@ -526,10 +526,13 @@ interface ChapterAuth {
526
526
  /** Role ladder low→high, for `source: "claim"`. The last entry is admin.
527
527
  * Default `["provisional", "member", "admin"]`. */
528
528
  ladder?: readonly string[];
529
- /** Provision the read-only `superAdmins` tier table — the only tier that may
530
- * create/modify admins, and (like every namespace) deny-all + written only in
531
- * odla Studio. Default: `true` for `"claim"`, `false` for `"table"`. */
529
+ /** Enable the Studio-managed super-admin tier — the only tier that may
530
+ * create/modify admins. Default: `true` for `"claim"`, `false` for `"table"`. */
532
531
  superAdmins?: boolean;
532
+ /** Authority behind the super-admin tier. `"table"` preserves the legacy
533
+ * app allowlist; `"odla"` reads protected `$users.role`. Default `"table"`
534
+ * for backward compatibility. */
535
+ superAdminSource?: "table" | "odla";
533
536
  }
534
537
  /** The fully-resolved auth policy (defaults applied) carried on the {@link Chapter}. */
535
538
  interface ResolvedAuth {
@@ -539,6 +542,7 @@ interface ResolvedAuth {
539
542
  /** The highest ladder rung (last entry) — the "admin" gate. */
540
543
  adminRole: string;
541
544
  superAdmins: boolean;
545
+ superAdminSource: "none" | "table" | "odla";
542
546
  }
543
547
  /** The application status pipeline. Which statuses exist, and the subsets a site
544
548
  * allows a call to be booked from / an application approved from. Defaults to
@@ -80,12 +80,16 @@ function createWorkerContext(options) {
80
80
  }
81
81
  async function isAdminEmail(db, email) {
82
82
  if (!email) return false;
83
- const { admins } = await db.query({ admins: { $: { where: { email: email.toLowerCase() }, limit: 1 } } });
83
+ const { admins } = await db.query({ admins: { $: { where: { email: email.trim().toLowerCase() }, limit: 1 } } });
84
84
  return Array.isArray(admins) && admins.length > 0;
85
85
  }
86
86
  async function isSuperAdminEmail(db, email) {
87
87
  if (!auth.superAdmins || !email) return false;
88
- const { superAdmins } = await db.query({ superAdmins: { $: { where: { email: email.toLowerCase() }, limit: 1 } } });
88
+ if (auth.superAdminSource === "odla") {
89
+ const { $users } = await db.query({ $users: { $: { where: { email: email.trim().toLowerCase(), role: "super_admin" }, limit: 1 } } });
90
+ return Array.isArray($users) && $users.length > 0;
91
+ }
92
+ const { superAdmins } = await db.query({ superAdmins: { $: { where: { email: email.trim().toLowerCase() }, limit: 1 } } });
89
93
  return Array.isArray(superAdmins) && superAdmins.length > 0;
90
94
  }
91
95
  async function roleFor(db, u) {
@@ -6091,9 +6095,39 @@ var IDENTIFIER = /^[a-z][a-zA-Z0-9_]*$/;
6091
6095
  var MAX_PAGE = 50;
6092
6096
  var MAX_OFFSET = 1e4;
6093
6097
  var MAX_SEARCH = 200;
6098
+ var MAX_STAGE = 100;
6094
6099
  var MAX_BODY = 32768;
6095
6100
  var RECORD_ID = /^[a-zA-Z0-9][a-zA-Z0-9:_-]{0,191}$/;
6096
6101
  var MUTATION_ID2 = /^[a-zA-Z0-9][a-zA-Z0-9:._-]{7,127}$/;
6102
+ var NETWORK_RECORD_SORTS = [
6103
+ { id: "updated_desc", label: "Recently updated" },
6104
+ { id: "updated_asc", label: "Least recently updated" },
6105
+ { id: "name_asc", label: "Name A\u2013Z" },
6106
+ { id: "name_desc", label: "Name Z\u2013A" }
6107
+ ];
6108
+ var NETWORK_RECORD_SORT_COLUMNS = {
6109
+ updated_desc: { field: "updatedAt", dir: "desc" },
6110
+ updated_asc: { field: "updatedAt", dir: "asc" },
6111
+ name_asc: { field: "name", dir: "asc" },
6112
+ name_desc: { field: "name", dir: "desc" }
6113
+ };
6114
+ function networkRecordWindow(params) {
6115
+ const limit = integer(params.get("limit"), 25, 1, MAX_PAGE);
6116
+ const offset = integer(params.get("offset"), 0, 0, MAX_OFFSET);
6117
+ const search = (params.get("search") ?? "").trim();
6118
+ const stage = (params.get("stage") ?? "").trim();
6119
+ const requestedSort = (params.get("sort") ?? "updated_desc").trim();
6120
+ const sortId = NETWORK_RECORD_SORTS.some((candidate) => candidate.id === requestedSort) ? requestedSort : null;
6121
+ if (limit == null || offset == null || search.length > MAX_SEARCH || stage.length > 0 && (stage.length > MAX_STAGE || /[\u0000-\u001f\u007f]/.test(stage)) || !sortId) return null;
6122
+ return {
6123
+ limit,
6124
+ offset,
6125
+ ...search ? { search } : {},
6126
+ ...stage ? { stage } : {},
6127
+ sortId,
6128
+ sort: NETWORK_RECORD_SORT_COLUMNS[sortId]
6129
+ };
6130
+ }
6097
6131
  function bodyTooLarge(body) {
6098
6132
  return new TextEncoder().encode(body).byteLength > MAX_BODY;
6099
6133
  }
@@ -6296,20 +6330,22 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
6296
6330
  const type = url.searchParams.get("type") ?? "";
6297
6331
  const allowed2 = reader.fields[type];
6298
6332
  if (!IDENTIFIER.test(type) || !allowed2) return json({ error: "record type is not shared with this reader" }, 403);
6299
- const limit = integer(url.searchParams.get("limit"), 25, 1, MAX_PAGE);
6300
- const offset = integer(url.searchParams.get("offset"), 0, 0, MAX_OFFSET);
6301
- const search = (url.searchParams.get("search") ?? "").trim();
6302
- if (limit == null || offset == null || search.length > MAX_SEARCH) {
6303
- return json({ error: "invalid pagination or search parameters" }, 400);
6333
+ const window = networkRecordWindow(url.searchParams);
6334
+ if (!window) return json({ error: "invalid record query parameters" }, 400);
6335
+ const typeDef = ctx.chapter.crm.type(type);
6336
+ const stages = typeDef.pipeline?.stages ?? [];
6337
+ if (window.stage && !stages.some((stage) => stage.id === window.stage)) {
6338
+ return json({ error: "stage is not defined for this record type" }, 400);
6304
6339
  }
6305
6340
  const page = await listRecords(
6306
6341
  { crm: ctx.chapter.crm, db },
6307
6342
  {
6308
6343
  type,
6309
- limit,
6310
- offset,
6311
- ...search ? { search } : {},
6312
- sort: { field: "updatedAt", dir: "desc" }
6344
+ limit: window.limit,
6345
+ offset: window.offset,
6346
+ ...window.search ? { search: window.search } : {},
6347
+ ...window.stage ? { stage: [window.stage] } : {},
6348
+ sort: window.sort
6313
6349
  }
6314
6350
  );
6315
6351
  const response = {
@@ -6319,7 +6355,15 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
6319
6355
  records: page.records.map((record4) => readerRecord(record4, reader, ctx.chapter.crm)),
6320
6356
  total: page.total,
6321
6357
  limit: page.limit,
6322
- offset: page.offset
6358
+ offset: page.offset,
6359
+ filters: {
6360
+ stages: stages.map((stage) => ({ id: stage.id, label: stage.label ?? stage.id })),
6361
+ sorts: [...NETWORK_RECORD_SORTS]
6362
+ },
6363
+ appliedQuery: {
6364
+ ...window.stage ? { stage: window.stage } : {},
6365
+ sort: window.sortId
6366
+ }
6323
6367
  };
6324
6368
  return json(response);
6325
6369
  };
@@ -6352,7 +6396,8 @@ async function readTargetRecords(db, env, ctx, target, query) {
6352
6396
  const upstream = body && "error" in body && typeof body.error === "string" ? body.error : void 0;
6353
6397
  return {
6354
6398
  ok: false,
6355
- error: upstream ?? (response.ok ? "invalid follower record response" : `follower returned ${response.status}`)
6399
+ error: upstream ?? (response.ok ? "invalid follower record response" : `follower returned ${response.status}`),
6400
+ ...response.status === 400 ? { status: 400 } : {}
6356
6401
  };
6357
6402
  }
6358
6403
  return {
@@ -6443,16 +6488,22 @@ var handleAdminNetworkRecords = async (req, url, env, ctx) => {
6443
6488
  const type = url.searchParams.get("type") ?? "";
6444
6489
  const target = targetFor2(ctx, targetId);
6445
6490
  if (!target || !IDENTIFIER.test(type)) return json({ error: "configured targetId and record type are required" }, 400);
6446
- const limit = integer(url.searchParams.get("limit"), 25, 1, MAX_PAGE);
6447
- const offset = integer(url.searchParams.get("offset"), 0, 0, MAX_OFFSET);
6448
- const search = (url.searchParams.get("search") ?? "").trim();
6449
- if (limit == null || offset == null || search.length > MAX_SEARCH) {
6450
- return json({ error: "invalid pagination or search parameters" }, 400);
6451
- }
6452
- const query = new URLSearchParams({ type, limit: String(limit), offset: String(offset) });
6453
- if (search) query.set("search", search);
6491
+ const window = networkRecordWindow(url.searchParams);
6492
+ if (!window) return json({ error: "invalid record query parameters" }, 400);
6493
+ const query = new URLSearchParams({
6494
+ type,
6495
+ limit: String(window.limit),
6496
+ offset: String(window.offset),
6497
+ sort: window.sortId
6498
+ });
6499
+ if (window.search) query.set("search", window.search);
6500
+ if (window.stage) query.set("stage", window.stage);
6454
6501
  const result = await readTargetRecords(got.db, env, ctx, target, query);
6455
- return result.ok ? json(result.page) : json({ error: result.error }, 502);
6502
+ if (!result.ok) return json({ error: result.error }, result.status ?? 502);
6503
+ if ((window.stage || url.searchParams.has("sort")) && (!result.page.appliedQuery || result.page.appliedQuery.sort !== window.sortId || result.page.appliedQuery.stage !== window.stage)) {
6504
+ return json({ error: "follower did not apply the requested record query" }, 502);
6505
+ }
6506
+ return json(result.page);
6456
6507
  };
6457
6508
 
6458
6509
  // src/worker-routes-network-notes.ts