@odla-ai/chapter 0.42.0 → 0.42.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.
@@ -1,8 +1,8 @@
1
1
  import * as preact from 'preact';
2
2
  import { ComponentChildren, JSX } from 'preact';
3
3
  import { CrmClient, Crm, CrmRecord } from '@odla-ai/crm';
4
- import { a as Chapter, b as ChapterCopy, M as MemberContentDocument, E as EffectiveMemberContent, c as MemberContentTierAuthority } from '../../copy-context-DuJkH4U9.js';
5
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-DuJkH4U9.js';
4
+ import { a as Chapter, b as ChapterCopy, M as MemberContentDocument, E as EffectiveMemberContent, c as MemberContentTierAuthority } from '../../copy-context-BNUzbL5g.js';
5
+ export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-BNUzbL5g.js';
6
6
  import { CrmLifecycleAdapter, CrmWorkspaceMasterContext, CrmWorkspaceRecordContext, RecordPanelTab } from '@odla-ai/crm/ui';
7
7
 
8
8
  /** Normalized public auth configuration consumed by the Clerk gate. */
@@ -1,5 +1,5 @@
1
1
  export { AdminAccountMenuProps, AdminBuilderLibraryData, AdminChrome, AdminMemberContentData, AdminNote, AdminPage, AdminResource, AdminRouteState, AdminRouteTarget, AdminRouting, AdminSection, AdminSectionContext, AdminWorkspace, AdminWorkspaceNavProps, AdmissionGrantsWorkspace, AvailabilitySectionOptions, BillingSectionOptions, BuilderLibraryAdmin, ChapterAdmin, ChapterAdminAuthAdapter, ChapterAdminAuthConfig, ChapterAdminAuthLoadContext, ChapterAdminProps, ChapterAdminUser, CollectionSectionOptions, DashboardSectionOptions, EmailSectionOptions, LeaderNetworkOverview, MeetingsSectionOptions, MemberContentAdmin, NetworkRecordsWorkspace, NetworkShareActions, NetworkShareActionsProps, Panel, PeopleSectionOptions, RecordActions, RecordActionsProps, ThemeWarning, adminFetch, adminRouteFromUrl, adminRouteHref, adminSectionFromUrl, adminSectionHref, admissionGrantsWorkspace, applicationLifecycleAdapter, availabilitySection, billingSection, builderLibrarySection, collectionSection, dashboardSection, defaultAdminSections, defaultAdminWorkspaces, emailSection, loadChapterAdminConfig, loadChapterAdminUser, meetingsSection, memberContentSection, networkRecordsWorkspace, peopleSection, useAdminResource } from './admin/index.js';
2
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../copy-context-DuJkH4U9.js';
2
+ export { C as ChapterCopyProvider, u as useChapterCopy } from '../copy-context-BNUzbL5g.js';
3
3
  export { ApiFn, BrandStyle, BrandStyleProps, GiftCodeRedeemResult, GiftCodeRedeemer, GiftCodeRedeemerCopy, GiftCodeRedeemerProps, GiftOfferCards, GiftOfferCardsMode, GiftOfferCardsProps, GiftPurchases, GiftPurchasesProps, JoinBookingSimulation, JoinConfig, JoinFlowState, JoinIsland, JoinIslandProps, JoinSimulation, JoinStepRenderContext, JoinSubmitRenderContext, JoinTier, JoinTierRenderContext, MEMBERSHIP_CARD_TOKEN_DEFAULTS, MemberContentArea, MemberContentAreaProps, MemberGifts, MemberProvisionalRenderContext, MembersArea, MembersAreaProps, MembershipCardTokenName, MembershipCards, MembershipCardsMode, MembershipCardsProps, PaymentPriceLines, PaymentStep, PaymentStepProps, PaymentStepSimulation, RescheduleProps, Rescheduler, Slot, SlotPicker, SlotPickerClasses, SlotPickerProps, SubResponse, TierPlaceholder, dayKey, dayLabel, fmtDate, fmtMoney, fullLabel, groupSlotsByDay, loadJoinResume, membershipApplicationHref, tierNeedsPayment, timeLabel, tzShort, useTierSelection } from './member/index.js';
4
4
  import 'preact';
5
5
  import '@odla-ai/crm';
@@ -1,7 +1,7 @@
1
1
  import * as preact from 'preact';
2
2
  import { ComponentChildren, JSX } from 'preact';
3
- import { d as MemberContentProjection, e as MemberContentPageProjection, b as ChapterCopy, f as ChapterBrand } from '../../copy-context-DuJkH4U9.js';
4
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-DuJkH4U9.js';
3
+ import { d as MemberContentProjection, e as MemberContentPageProjection, b as ChapterCopy, f as ChapterBrand } from '../../copy-context-BNUzbL5g.js';
4
+ export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-BNUzbL5g.js';
5
5
  import { FieldType, FieldConditions } from '@odla-ai/crm';
6
6
 
7
7
  /** A bookable slot: a start instant in epoch milliseconds. */
@@ -5376,12 +5376,22 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
5376
5376
  const got = await gate5(req, env, ctx);
5377
5377
  if ("response" in got) return got.response;
5378
5378
  return json({
5379
- targets: ctx.chapter.network.targets.map(({ id, name, url: targetUrl, fields, sharedNotes }) => ({
5379
+ targets: ctx.chapter.network.targets.map(({
5380
+ id,
5381
+ name,
5382
+ url: targetUrl,
5383
+ fields,
5384
+ sharedNotes,
5385
+ editableFields,
5386
+ stageTransitions
5387
+ }) => ({
5380
5388
  id,
5381
5389
  name,
5382
5390
  url: targetUrl,
5383
5391
  types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS),
5384
- sharedNotes
5392
+ sharedNotes,
5393
+ editableFields,
5394
+ stageTransitions
5385
5395
  }))
5386
5396
  });
5387
5397
  };
@@ -5499,19 +5509,20 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
5499
5509
  return json({ ok: results.every((result) => result.ok), results });
5500
5510
  };
5501
5511
 
5502
- // src/worker-routes-network-records.ts
5512
+ // src/worker-routes-network-records-follower.ts
5503
5513
  var import_crm7 = require("@odla-ai/crm");
5504
5514
  var import_db8 = require("@odla-ai/db");
5515
+
5516
+ // src/worker-routes-network-records-common.ts
5505
5517
  var IDENTIFIER = /^[a-z][a-zA-Z0-9_]*$/;
5506
5518
  var MAX_PAGE = 50;
5507
5519
  var MAX_OFFSET = 1e4;
5508
5520
  var MAX_SEARCH = 200;
5509
- async function gate6(req, env, ctx) {
5510
- const db = ctx.makeDb(env);
5511
- const user = await ctx.verifyUser(req, env);
5512
- if (!user) return { response: json({ error: "unauthorized" }, 401) };
5513
- if (!await ctx.isAdmin(db, user)) return { response: json({ error: "forbidden" }, 403) };
5514
- return { db, user };
5521
+ var MAX_BODY = 32768;
5522
+ var RECORD_ID = /^[a-zA-Z0-9][a-zA-Z0-9:_-]{0,191}$/;
5523
+ var MUTATION_ID = /^[a-zA-Z0-9][a-zA-Z0-9:._-]{7,127}$/;
5524
+ function bodyTooLarge(body) {
5525
+ return new TextEncoder().encode(body).byteLength > MAX_BODY;
5515
5526
  }
5516
5527
  function integer(value, fallback, min, max) {
5517
5528
  if (value == null || value === "") return fallback;
@@ -5519,10 +5530,22 @@ function integer(value, fallback, min, max) {
5519
5530
  const parsed = Number(value);
5520
5531
  return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : null;
5521
5532
  }
5522
- function projectRecord(record4, allowed2) {
5533
+ function projectRecord(record4, allowed2, editable, stagesEnabled, crm) {
5523
5534
  const fields = Object.fromEntries(
5524
5535
  allowed2.flatMap((name) => Object.prototype.hasOwnProperty.call(record4.fields, name) ? [[name, record4.fields[name]]] : [])
5525
5536
  );
5537
+ const def = crm.type(record4.type);
5538
+ const editFields = editable.map((id) => {
5539
+ const field = def.fields[id];
5540
+ return {
5541
+ id,
5542
+ label: field.label ?? id,
5543
+ type: field.type,
5544
+ required: field.required ?? false,
5545
+ ...field.options ? { options: field.options } : {}
5546
+ };
5547
+ });
5548
+ const allowedStages = stagesEnabled ? (def.pipeline?.stages ?? []).flatMap((stage) => crm.canTransition(record4.type, record4, stage.id) ? [] : [{ id: stage.id, label: stage.label ?? stage.id }]) : [];
5526
5549
  return {
5527
5550
  id: record4.id,
5528
5551
  type: record4.type,
@@ -5530,11 +5553,66 @@ function projectRecord(record4, allowed2) {
5530
5553
  ...record4.stage ? { stage: record4.stage } : {},
5531
5554
  fields,
5532
5555
  createdAt: record4.createdAt,
5533
- updatedAt: record4.updatedAt
5556
+ updatedAt: record4.updatedAt,
5557
+ revision: record4.updatedAt,
5558
+ ...editFields.length || stagesEnabled ? { edit: { fields: editFields, allowedStages } } : {}
5534
5559
  };
5535
5560
  }
5561
+ function readerRecord(record4, reader, crm) {
5562
+ return projectRecord(
5563
+ record4,
5564
+ reader.fields[record4.type] ?? [],
5565
+ reader.editableFields[record4.type] ?? [],
5566
+ reader.stageTransitions.includes(record4.type),
5567
+ crm
5568
+ );
5569
+ }
5570
+ function restrictedRecord(record4, target) {
5571
+ if (!record4.edit) return record4;
5572
+ const fields = record4.edit.fields.filter((field) => target.editableFields[record4.type]?.includes(field.id));
5573
+ const allowedStages = target.stageTransitions.includes(record4.type) ? record4.edit.allowedStages : [];
5574
+ const edit = fields.length || target.stageTransitions.includes(record4.type) ? { fields, allowedStages } : void 0;
5575
+ return { ...record4, ...edit ? { edit } : { edit: void 0 } };
5576
+ }
5577
+
5578
+ // src/worker-routes-network-records-follower.ts
5579
+ var NETWORK_MUTATION_META = "chapterNetworkMutation";
5580
+ function mutationFingerprint(input) {
5581
+ const fields = input.fields ? Object.fromEntries(Object.entries(input.fields).sort(([a], [b]) => a.localeCompare(b))) : void 0;
5582
+ return JSON.stringify({
5583
+ recordId: input.recordId,
5584
+ type: input.type,
5585
+ expectedRevision: input.expectedRevision,
5586
+ expectedStage: input.expectedStage,
5587
+ mutationId: input.mutationId,
5588
+ fields,
5589
+ stage: input.stage,
5590
+ actor: input.actor
5591
+ });
5592
+ }
5593
+ function mutationError(error) {
5594
+ if (error?.code === "transact_guard_failed") {
5595
+ return json({ error: "record changed since it was reviewed", code: "stale_revision" }, 409);
5596
+ }
5597
+ if (error instanceof import_crm7.CrmInputError) {
5598
+ return json({ error: error.message, code: "invalid_transition_or_fields", ...error.fields ? { fields: error.fields } : {} }, 422);
5599
+ }
5600
+ return json({ error: "follower mutation failed", code: "mutation_failed" }, 500);
5601
+ }
5602
+ async function priorMutation(db, id) {
5603
+ const result = await db.query({
5604
+ [import_crm7.CRM_NS.activity]: { $: { where: { id }, limit: 1 } }
5605
+ });
5606
+ const row = (result[import_crm7.CRM_NS.activity] ?? [])[0];
5607
+ const meta = row?.meta;
5608
+ const receipt = meta?.[NETWORK_MUTATION_META];
5609
+ return typeof receipt?.fingerprint === "string" ? { fingerprint: receipt.fingerprint } : null;
5610
+ }
5536
5611
  var handleNetworkRecords = async (req, url, env, ctx) => {
5537
- if (req.method !== "GET" || url.pathname !== "/api/network/records") return null;
5612
+ if (url.pathname !== "/api/network/records" || req.method !== "GET" && req.method !== "PATCH") return null;
5613
+ if (req.method === "PATCH" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY) {
5614
+ return json({ error: "mutation body is too large", code: "body_too_large" }, 413);
5615
+ }
5538
5616
  const db = ctx.makeDb(env);
5539
5617
  const secret = await getVaultSecret(db, "network_share_secret");
5540
5618
  const readers = ctx.chapter.network.readers;
@@ -5545,8 +5623,105 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
5545
5623
  });
5546
5624
  if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
5547
5625
  const reader = readers.find((candidate) => candidate.id === verified.sender);
5626
+ if (!reader) return json({ error: "unauthorized" }, 401);
5627
+ if (req.method === "PATCH") {
5628
+ if (bodyTooLarge(verified.body)) {
5629
+ return json({ error: "mutation body is too large", code: "body_too_large" }, 413);
5630
+ }
5631
+ let input;
5632
+ try {
5633
+ input = JSON.parse(verified.body);
5634
+ } catch {
5635
+ return json({ error: "invalid JSON body", code: "invalid_json" }, 422);
5636
+ }
5637
+ const fields = input?.fields;
5638
+ const hasFields = Boolean(fields && typeof fields === "object" && !Array.isArray(fields));
5639
+ const hasStage = typeof input?.stage === "string";
5640
+ const actor = input?.actor;
5641
+ if (!input || !IDENTIFIER.test(input.type) || !RECORD_ID.test(input.recordId) || !Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0 || !MUTATION_ID.test(input.mutationId) || !actor || typeof actor.id !== "string" || !RECORD_ID.test(actor.id) || hasFields === hasStage) {
5642
+ return json({ error: "invalid network mutation", code: "invalid_mutation" }, 422);
5643
+ }
5644
+ const editable = reader.editableFields[input.type];
5645
+ if (hasFields) {
5646
+ const names = Object.keys(fields);
5647
+ if (!editable || names.length === 0 || names.length > 32 || names.some((name) => !editable.includes(name))) {
5648
+ return json({ error: "profile fields are not delegated for this reader", code: "field_forbidden" }, 403);
5649
+ }
5650
+ } else if (!reader.stageTransitions.includes(input.type)) {
5651
+ return json({ error: "stage transitions are not delegated for this reader", code: "stage_forbidden" }, 403);
5652
+ }
5653
+ const activityId = `network-edit:${verified.sender}:${input.mutationId}`;
5654
+ const fingerprint = mutationFingerprint(input);
5655
+ const prior = await priorMutation(db, activityId);
5656
+ if (prior) {
5657
+ if (prior.fingerprint !== fingerprint) {
5658
+ return json({ error: "mutationId was already used for different input", code: "mutation_conflict" }, 409);
5659
+ }
5660
+ const replayed = await (0, import_crm7.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
5661
+ if (!replayed || replayed.type !== input.type) return json({ error: "record not found", code: "not_found" }, 404);
5662
+ const result = {
5663
+ version: 1,
5664
+ site: { id: ctx.chapter.id, name: ctx.chapter.name },
5665
+ record: readerRecord(replayed, reader, ctx.chapter.crm),
5666
+ duplicate: true
5667
+ };
5668
+ return json(result);
5669
+ }
5670
+ const record4 = await (0, import_crm7.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
5671
+ if (!record4 || record4.type !== input.type) return json({ error: "record not found", code: "not_found" }, 404);
5672
+ if (record4.updatedAt !== input.expectedRevision) {
5673
+ return json({ error: "record changed since it was reviewed", code: "stale_revision" }, 409);
5674
+ }
5675
+ if (hasStage && (input.expectedStage ?? null) !== (record4.stage ?? null)) {
5676
+ return json({ error: "stage changed since it was reviewed", code: "stale_stage" }, 409);
5677
+ }
5678
+ const actorEmail = typeof actor.email === "string" && actor.email.length <= 320 ? actor.email : void 0;
5679
+ const auditMeta = {
5680
+ [NETWORK_MUTATION_META]: {
5681
+ version: 1,
5682
+ sourceId: verified.sender,
5683
+ actorId: actor.id,
5684
+ ...actorEmail ? { actorEmail } : {},
5685
+ mutationId: input.mutationId,
5686
+ fingerprint
5687
+ }
5688
+ };
5689
+ try {
5690
+ const deps = { crm: ctx.chapter.crm, db };
5691
+ const updated = hasFields ? await (0, import_crm7.updateRecord)(deps, {
5692
+ id: record4.id,
5693
+ input: fields,
5694
+ mutationId: activityId,
5695
+ expectedUpdatedAt: input.expectedRevision,
5696
+ audit: {
5697
+ id: activityId,
5698
+ body: `Profile edited remotely by ${verified.sender}`,
5699
+ authorId: `network:${verified.sender}`,
5700
+ meta: auditMeta
5701
+ }
5702
+ }) : await (0, import_crm7.setStage)(deps, {
5703
+ id: record4.id,
5704
+ to: input.stage,
5705
+ authorId: `network:${verified.sender}`,
5706
+ mutationId: activityId,
5707
+ expectedUpdatedAt: input.expectedRevision,
5708
+ expectedStage: input.expectedStage ?? null,
5709
+ activityId,
5710
+ activityMeta: auditMeta
5711
+ });
5712
+ const result = {
5713
+ version: 1,
5714
+ site: { id: ctx.chapter.id, name: ctx.chapter.name },
5715
+ record: readerRecord(updated.record, reader, ctx.chapter.crm),
5716
+ duplicate: updated.duplicate
5717
+ };
5718
+ return json(result);
5719
+ } catch (error) {
5720
+ return mutationError(error);
5721
+ }
5722
+ }
5548
5723
  const type = url.searchParams.get("type") ?? "";
5549
- const allowed2 = reader?.fields[type];
5724
+ const allowed2 = reader.fields[type];
5550
5725
  if (!IDENTIFIER.test(type) || !allowed2) return json({ error: "record type is not shared with this reader" }, 403);
5551
5726
  const limit = integer(url.searchParams.get("limit"), 25, 1, MAX_PAGE);
5552
5727
  const offset = integer(url.searchParams.get("offset"), 0, 0, MAX_OFFSET);
@@ -5568,13 +5743,23 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
5568
5743
  version: 1,
5569
5744
  site: { id: ctx.chapter.id, name: ctx.chapter.name },
5570
5745
  type,
5571
- records: page.records.map((record4) => projectRecord(record4, allowed2)),
5746
+ records: page.records.map((record4) => readerRecord(record4, reader, ctx.chapter.crm)),
5572
5747
  total: page.total,
5573
5748
  limit: page.limit,
5574
5749
  offset: page.offset
5575
5750
  };
5576
5751
  return json(response);
5577
5752
  };
5753
+
5754
+ // src/worker-routes-network-records-admin.ts
5755
+ var import_db9 = require("@odla-ai/db");
5756
+ async function gate6(req, env, ctx) {
5757
+ const db = ctx.makeDb(env);
5758
+ const user = await ctx.verifyUser(req, env);
5759
+ if (!user) return { response: json({ error: "unauthorized" }, 401) };
5760
+ if (!await ctx.isAdmin(db, user)) return { response: json({ error: "forbidden" }, 403) };
5761
+ return { db, user };
5762
+ }
5578
5763
  function targetFor(ctx, id) {
5579
5764
  return ctx.chapter.network.targets.find((target) => target.id === id);
5580
5765
  }
@@ -5584,12 +5769,7 @@ async function readTargetRecords(db, env, ctx, target, query) {
5584
5769
  const destination = new URL("/api/network/records", target.url);
5585
5770
  destination.search = query.toString();
5586
5771
  try {
5587
- const headers = await (0, import_db8.signFederatedRequest)({
5588
- secret,
5589
- sender: ctx.chapter.id,
5590
- method: "GET",
5591
- url: destination
5592
- });
5772
+ const headers = await (0, import_db9.signFederatedRequest)({ secret, sender: ctx.chapter.id, method: "GET", url: destination });
5593
5773
  const response = await fetchNetworkTarget(env, target, destination, {
5594
5774
  headers,
5595
5775
  signal: AbortSignal.timeout(1e4)
@@ -5602,15 +5782,90 @@ async function readTargetRecords(db, env, ctx, target, query) {
5602
5782
  error: upstream ?? (response.ok ? "invalid follower record response" : `follower returned ${response.status}`)
5603
5783
  };
5604
5784
  }
5605
- return { ok: true, page: body };
5785
+ return {
5786
+ ok: true,
5787
+ page: { ...body, records: body.records.map((record4) => restrictedRecord(record4, target)) }
5788
+ };
5606
5789
  } catch (error) {
5607
5790
  return { ok: false, error: error instanceof Error ? error.message : "record read failed" };
5608
5791
  }
5609
5792
  }
5793
+ async function mutateTargetRecord(db, env, ctx, target, input) {
5794
+ const fields = input.fields;
5795
+ if (fields) {
5796
+ const allowed2 = target.editableFields[input.type];
5797
+ if (!allowed2 || Object.keys(fields).some((name) => !allowed2.includes(name))) {
5798
+ return json({ error: "profile fields are not enabled for this target", code: "field_forbidden" }, 403);
5799
+ }
5800
+ } else if (!target.stageTransitions.includes(input.type)) {
5801
+ return json({ error: "stage transitions are not enabled for this target", code: "stage_forbidden" }, 403);
5802
+ }
5803
+ const secret = await getVaultSecret(db, target.secretName);
5804
+ if (!secret) return json({ error: "edge secret is missing", code: "edge_unavailable" }, 502);
5805
+ const destination = new URL("/api/network/records", target.url);
5806
+ const body = JSON.stringify(input);
5807
+ try {
5808
+ const headers = await (0, import_db9.signFederatedRequest)({
5809
+ secret,
5810
+ sender: ctx.chapter.id,
5811
+ method: "PATCH",
5812
+ url: destination,
5813
+ body
5814
+ });
5815
+ const response = await fetchNetworkTarget(env, target, destination, {
5816
+ method: "PATCH",
5817
+ headers: { ...headers, "content-type": "application/json" },
5818
+ body,
5819
+ signal: AbortSignal.timeout(1e4)
5820
+ });
5821
+ const payload = await response.json().catch(() => null);
5822
+ const site = payload && "site" in payload ? payload.site : void 0;
5823
+ if (!response.ok || !payload || !("record" in payload) || site && site.id !== target.id) {
5824
+ const status = [401, 403, 404, 409, 422].includes(response.status) ? response.status : 502;
5825
+ return json({
5826
+ error: payload && "error" in payload && typeof payload.error === "string" ? payload.error : `follower returned ${response.status}`,
5827
+ ...payload && "code" in payload && typeof payload.code === "string" ? { code: payload.code } : {}
5828
+ }, status);
5829
+ }
5830
+ return json({ ...payload, record: restrictedRecord(payload.record, target) });
5831
+ } catch {
5832
+ return json({ error: "follower mutation request failed", code: "follower_unavailable" }, 502);
5833
+ }
5834
+ }
5610
5835
  var handleAdminNetworkRecords = async (req, url, env, ctx) => {
5611
- if (req.method !== "GET" || url.pathname !== "/api/admin/network/records") return null;
5836
+ if (url.pathname !== "/api/admin/network/records" || req.method !== "GET" && req.method !== "PATCH") return null;
5837
+ if (req.method === "PATCH" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY) {
5838
+ return json({ error: "mutation body is too large", code: "body_too_large" }, 413);
5839
+ }
5612
5840
  const got = await gate6(req, env, ctx);
5613
5841
  if ("response" in got) return got.response;
5842
+ if (req.method === "PATCH") {
5843
+ let input;
5844
+ try {
5845
+ const body = await req.text();
5846
+ if (bodyTooLarge(body)) {
5847
+ return json({ error: "mutation body is too large", code: "body_too_large" }, 413);
5848
+ }
5849
+ input = JSON.parse(body);
5850
+ } catch {
5851
+ return json({ error: "invalid JSON body", code: "invalid_json" }, 422);
5852
+ }
5853
+ const target2 = targetFor(ctx, input?.targetId);
5854
+ const hasFields = Boolean(input?.fields && typeof input.fields === "object" && !Array.isArray(input.fields));
5855
+ const hasStage = typeof input?.stage === "string";
5856
+ if (!target2 || !IDENTIFIER.test(input.type) || !RECORD_ID.test(input.recordId) || !Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0 || !MUTATION_ID.test(input.mutationId) || hasFields === hasStage) {
5857
+ return json({ error: "invalid network mutation", code: "invalid_mutation" }, 422);
5858
+ }
5859
+ return mutateTargetRecord(got.db, env, ctx, target2, {
5860
+ type: input.type,
5861
+ recordId: input.recordId,
5862
+ expectedRevision: input.expectedRevision,
5863
+ mutationId: input.mutationId,
5864
+ ...input.expectedStage !== void 0 ? { expectedStage: input.expectedStage } : {},
5865
+ ...hasFields ? { fields: input.fields } : { stage: input.stage },
5866
+ actor: { id: got.user.userId, ...got.user.email ? { email: got.user.email } : {} }
5867
+ });
5868
+ }
5614
5869
  const targetId = url.searchParams.get("targetId") ?? "";
5615
5870
  const type = url.searchParams.get("type") ?? "";
5616
5871
  const target = targetFor(ctx, targetId);
@@ -5623,19 +5878,13 @@ var handleAdminNetworkRecords = async (req, url, env, ctx) => {
5623
5878
  }
5624
5879
  const query = new URLSearchParams({ type, limit: String(limit), offset: String(offset) });
5625
5880
  if (search) query.set("search", search);
5626
- const result = await readTargetRecords(
5627
- got.db,
5628
- env,
5629
- ctx,
5630
- target,
5631
- query
5632
- );
5881
+ const result = await readTargetRecords(got.db, env, ctx, target, query);
5633
5882
  return result.ok ? json(result.page) : json({ error: result.error }, 502);
5634
5883
  };
5635
5884
 
5636
5885
  // src/worker-routes-network-notes.ts
5637
5886
  var IDENTIFIER2 = /^[a-z][a-zA-Z0-9_]*$/;
5638
- var RECORD_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
5887
+ var RECORD_ID2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
5639
5888
  var MAX_NOTE2 = 4e3;
5640
5889
  async function gate7(req, env, ctx) {
5641
5890
  const db = ctx.makeDb(env);
@@ -5649,7 +5898,7 @@ function noteIdentity(url, ctx) {
5649
5898
  const recordType = url.searchParams.get("type") ?? "";
5650
5899
  const recordId = url.searchParams.get("recordId") ?? "";
5651
5900
  const configured = ctx.chapter.network.targets.some((target) => target.id === targetId);
5652
- if (!configured || !IDENTIFIER2.test(recordType) || !RECORD_ID.test(recordId)) return null;
5901
+ if (!configured || !IDENTIFIER2.test(recordType) || !RECORD_ID2.test(recordId)) return null;
5653
5902
  return {
5654
5903
  targetId,
5655
5904
  recordType,
@@ -5719,10 +5968,10 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
5719
5968
 
5720
5969
  // src/worker-routes-network-shared-notes.ts
5721
5970
  var import_crm8 = require("@odla-ai/crm");
5722
- var import_db9 = require("@odla-ai/db");
5971
+ var import_db10 = require("@odla-ai/db");
5723
5972
  var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
5724
- var RECORD_ID2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
5725
- var MUTATION_ID = /^[A-Za-z0-9_-]{16,128}$/;
5973
+ var RECORD_ID3 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
5974
+ var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
5726
5975
  var MAX_NOTE3 = 4e3;
5727
5976
  var NETWORK_META = "chapterNetwork";
5728
5977
  async function gate8(req, env, ctx) {
@@ -5778,7 +6027,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
5778
6027
  const secret = await getVaultSecret(db, "network_share_secret");
5779
6028
  const readers = ctx.chapter.network.readers;
5780
6029
  if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
5781
- const verified = await (0, import_db9.verifyFederatedRequest)(req, {
6030
+ const verified = await (0, import_db10.verifyFederatedRequest)(req, {
5782
6031
  secret,
5783
6032
  senders: readers.map((reader2) => reader2.id)
5784
6033
  });
@@ -5786,7 +6035,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
5786
6035
  const reader = readers.find((candidate) => candidate.id === verified.sender);
5787
6036
  const recordType = url.searchParams.get("type") ?? "";
5788
6037
  const recordId = url.searchParams.get("recordId") ?? "";
5789
- if (!IDENTIFIER3.test(recordType) || !RECORD_ID2.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
6038
+ if (!IDENTIFIER3.test(recordType) || !RECORD_ID3.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
5790
6039
  return json({ error: "shared notes are not enabled for this reader and record type" }, 403);
5791
6040
  }
5792
6041
  const record4 = await (0, import_crm8.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
@@ -5806,7 +6055,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
5806
6055
  }
5807
6056
  const body = typeof input.body === "string" ? input.body.trim() : "";
5808
6057
  const mutationId = typeof input.mutationId === "string" ? input.mutationId : "";
5809
- if (!body || body.length > MAX_NOTE3 || !MUTATION_ID.test(mutationId)) {
6058
+ if (!body || body.length > MAX_NOTE3 || !MUTATION_ID2.test(mutationId)) {
5810
6059
  return json({ error: `body must be 1\u2013${MAX_NOTE3} characters and mutationId must be valid` }, 400);
5811
6060
  }
5812
6061
  const createdAt = Date.now();
@@ -5855,7 +6104,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
5855
6104
  const recordType = url.searchParams.get("type") ?? "";
5856
6105
  const recordId = url.searchParams.get("recordId") ?? "";
5857
6106
  const target = targetFor2(ctx, targetId);
5858
- if (!target || !target.sharedNotes.includes(recordType) || !IDENTIFIER3.test(recordType) || !RECORD_ID2.test(recordId)) {
6107
+ if (!target || !target.sharedNotes.includes(recordType) || !IDENTIFIER3.test(recordType) || !RECORD_ID3.test(recordId)) {
5859
6108
  return json({ error: "configured targetId, type, and recordId are required" }, 400);
5860
6109
  }
5861
6110
  let body = "";
@@ -5875,7 +6124,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
5875
6124
  const destination = new URL("/api/network/shared-notes", target.url);
5876
6125
  destination.search = new URLSearchParams({ type: recordType, recordId }).toString();
5877
6126
  try {
5878
- const headers = await (0, import_db9.signFederatedRequest)({
6127
+ const headers = await (0, import_db10.signFederatedRequest)({
5879
6128
  secret,
5880
6129
  sender: ctx.chapter.id,
5881
6130
  method: req.method,
@@ -5982,7 +6231,7 @@ var handleFormation = async (req, url, env, ctx) => {
5982
6231
  };
5983
6232
 
5984
6233
  // src/worker-routes-builder-library.ts
5985
- var import_db10 = require("@odla-ai/db");
6234
+ var import_db11 = require("@odla-ai/db");
5986
6235
 
5987
6236
  // src/builder-library-validation.ts
5988
6237
  var ID4 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
@@ -6265,7 +6514,7 @@ async function verifySource(req, db, ctx) {
6265
6514
  if (ctx.chapter.mode === "chapter") {
6266
6515
  const secret = await getVaultSecret(db, "network_share_secret");
6267
6516
  if (!secret) return null;
6268
- const verified = await (0, import_db10.verifyFederatedRequest)(req, {
6517
+ const verified = await (0, import_db11.verifyFederatedRequest)(req, {
6269
6518
  secret,
6270
6519
  senders: sources.map((source2) => source2.sourceId)
6271
6520
  });
@@ -6278,7 +6527,7 @@ async function verifySource(req, db, ctx) {
6278
6527
  if (!target) continue;
6279
6528
  const secret = await getVaultSecret(db, target.secretName);
6280
6529
  if (!secret) continue;
6281
- const verified = await (0, import_db10.verifyFederatedRequest)(req.clone(), { secret, senders: [source.sourceId] });
6530
+ const verified = await (0, import_db11.verifyFederatedRequest)(req.clone(), { secret, senders: [source.sourceId] });
6282
6531
  if (verified.ok) return { source, body: verified.body };
6283
6532
  }
6284
6533
  return null;
@@ -6467,11 +6716,11 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
6467
6716
  };
6468
6717
 
6469
6718
  // src/signup-control-delivery.ts
6470
- var import_db11 = require("@odla-ai/db");
6719
+ var import_db12 = require("@odla-ai/db");
6471
6720
  async function deliverSignupControl(options) {
6472
6721
  const destination = new URL(SIGNUP_CONTROL_PATH, options.targetUrl);
6473
6722
  const body = JSON.stringify(options.envelope);
6474
- const signed = await (0, import_db11.signFederatedRequest)({
6723
+ const signed = await (0, import_db12.signFederatedRequest)({
6475
6724
  secret: options.secret,
6476
6725
  sender: options.sender,
6477
6726
  method: "POST",
@@ -6492,11 +6741,11 @@ async function deliverSignupControl(options) {
6492
6741
  }
6493
6742
 
6494
6743
  // src/member-content-delivery.ts
6495
- var import_db12 = require("@odla-ai/db");
6744
+ var import_db13 = require("@odla-ai/db");
6496
6745
  async function deliverMemberContent(options) {
6497
6746
  const destination = new URL(MEMBER_CONTENT_CONTROL_PATH, options.targetUrl);
6498
6747
  const body = JSON.stringify(options.envelope);
6499
- const signed = await (0, import_db12.signFederatedRequest)({
6748
+ const signed = await (0, import_db13.signFederatedRequest)({
6500
6749
  secret: options.secret,
6501
6750
  sender: options.sender,
6502
6751
  method: "POST",
@@ -6517,11 +6766,11 @@ async function deliverMemberContent(options) {
6517
6766
  }
6518
6767
 
6519
6768
  // src/builder-library-delivery.ts
6520
- var import_db13 = require("@odla-ai/db");
6769
+ var import_db14 = require("@odla-ai/db");
6521
6770
  async function deliverBuilderLibrary(options) {
6522
6771
  const destination = new URL(BUILDER_LIBRARY_CONTROL_PATH, options.targetUrl);
6523
6772
  const body = JSON.stringify(options.envelope);
6524
- const signed = await (0, import_db13.signFederatedRequest)({ secret: options.secret, sender: options.sender, method: "POST", url: destination, body });
6773
+ const signed = await (0, import_db14.signFederatedRequest)({ secret: options.secret, sender: options.sender, method: "POST", url: destination, body });
6525
6774
  const response = await (options.fetch ?? globalThis.fetch)(destination, { method: "POST", headers: { ...signed, "content-type": "application/json" }, body });
6526
6775
  const result = await response.json();
6527
6776
  if (!response.ok || !("ok" in result) || result.ok !== true) {
@@ -6532,11 +6781,11 @@ async function deliverBuilderLibrary(options) {
6532
6781
  }
6533
6782
 
6534
6783
  // src/admission-grants-delivery.ts
6535
- var import_db14 = require("@odla-ai/db");
6784
+ var import_db15 = require("@odla-ai/db");
6536
6785
  async function deliverAdmissionGrants(options) {
6537
6786
  const destination = new URL(ADMISSION_GRANTS_PATH, options.targetUrl);
6538
6787
  const body = JSON.stringify(options.envelope);
6539
- const signed = await (0, import_db14.signFederatedRequest)({
6788
+ const signed = await (0, import_db15.signFederatedRequest)({
6540
6789
  secret: options.secret,
6541
6790
  sender: options.sender,
6542
6791
  method: "POST",