@odla-ai/chapter 0.42.0 → 0.42.2

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.
package/README.md CHANGED
@@ -888,6 +888,11 @@ export const chapter = defineChapter({
888
888
  },
889
889
  // Separate write consent: browsing never implies note permission.
890
890
  sharedNotes: ["person"],
891
+ // Separate again: name the exact profile fields and record types the
892
+ // parent may mutate. Omission keeps the edge read-only.
893
+ editableFields: { person: ["name", "email", "phone", "linkedin"] },
894
+ stageTransitions: ["person"],
895
+ admissionGrants: true,
891
896
  }],
892
897
  },
893
898
  });
@@ -896,14 +901,34 @@ export const chapter = defineChapter({
896
901
  `GET /api/network/records` requires the edge HMAC and rejects signed senders not
897
902
  listed in `network.readers`. Results are capped at 50 records, offset-paged,
898
903
  search-bounded, and projected to the reader's field list. Pipeline stage and
899
- record timestamps are the only metadata outside that list; application
904
+ record timestamps, optimistic `revision`, and explicitly delegated edit
905
+ metadata are the only metadata outside that list; application
900
906
  narrative, billing, account state, activities, and notes do not cross unless
901
907
  the follower explicitly names a corresponding CRM field.
902
908
 
903
909
  The leader's authenticated `GET /api/admin/network/records` proxy signs the
904
910
  follower request server-to-server, so neither the browser session nor the
905
- vaulted edge secret crosses sites. The Network detail view has two deliberate
906
- lanes:
911
+ vaulted edge secret crosses sites. When both halves of an edge opt in,
912
+ `PATCH /api/admin/network/records` accepts one bounded field patch or lifecycle
913
+ destination with the follower-authored `expectedRevision`, current stage, and a
914
+ stable mutation id. The follower revalidates its CRM schema, identity/email
915
+ rules, pipeline gates, and atomic revision guard; it remains source of truth and
916
+ records the leader peer plus initiating admin in the CRM activity audit. A
917
+ retry with the same input is idempotent, while stale state is `409`, an
918
+ undelegated operation is `403`, invalid fields or transitions are `422`, and
919
+ ordinary browse-only edges remain read-only.
920
+
921
+ Admission decisions use a separate default-deny lane. With
922
+ `admissionGrants: true` on both halves of an edge, the leader's
923
+ `POST /api/admin/network/admissions` resolves the person's current email from
924
+ its CRM, persists the complete desired target document, and delivers it through
925
+ the configured service binding or signed URL. Delivery state is durable as
926
+ `saved`, `pending`, `acknowledged`, `retrying`, or `rejected`; retry and revoke
927
+ routes publish the same or a higher desired revision. A follower may declare an
928
+ admission-only reader without `fields`, which does not make CRM records
929
+ browsable or editable.
930
+
931
+ The Network detail view also has two deliberate note lanes:
907
932
 
908
933
  - `GET/POST /api/admin/network/notes` stores private annotations in the
909
934
  leader's deny-all `networkNotes` namespace. They are keyed to the follower's
@@ -935,6 +960,9 @@ export const chapter = defineChapter({
935
960
  company: ["name", "domain", "industry", "location", "linkedin"],
936
961
  },
937
962
  sharedNotes: ["person"],
963
+ editableFields: { person: ["name", "email", "phone", "linkedin"] },
964
+ stageTransitions: ["person"],
965
+ admissionGrants: true,
938
966
  }],
939
967
  },
940
968
  });
@@ -945,7 +973,7 @@ Treat the network as websites connected by explicit delivery edges:
945
973
  - each website is a node with its own `appId`, ODLA tenants/keys, CRM config,
946
974
  Clerk application, publishable keys, issuer, roles, and local users;
947
975
  - each `network.targets[]` entry is a directed edge from one leader to one
948
- follower: `{ id, name, url, binding?, secretName?, fields }`;
976
+ follower: `{ id, name, url, binding?, secretName?, fields, editableFields?, stageTransitions?, admissionGrants? }`;
949
977
  - the payload is a versioned, allowlisted business record
950
978
  `{ version: 2, source: { siteId, recordId }, type, input }`, never an
951
979
  identity-provider session, another site's pipeline/account/billing state, or
@@ -1620,6 +1620,48 @@ function admissionGrantsWorkspace(label = "Admission") {
1620
1620
  render: (ctx) => /* @__PURE__ */ jsx16(AdmissionGrantsWorkspace, { getToken: ctx.getToken })
1621
1621
  };
1622
1622
  }
1623
+ function LeaderAdmissionAction(props) {
1624
+ const [decision, setDecision] = useState11("waive_interview");
1625
+ const [note, setNote] = useState11("");
1626
+ const [busy, setBusy] = useState11(false);
1627
+ const [error, setError] = useState11(null);
1628
+ const submit = async () => {
1629
+ setBusy(true);
1630
+ setError(null);
1631
+ try {
1632
+ const result = await adminFetch(props.getToken, "/api/admin/network/admissions", {
1633
+ method: "POST",
1634
+ body: JSON.stringify({
1635
+ targetId: props.targetId,
1636
+ personId: props.personId,
1637
+ decision,
1638
+ ...note.trim() ? { note: note.trim() } : {},
1639
+ mutationId: crypto.randomUUID()
1640
+ })
1641
+ });
1642
+ props.onSaved?.(result);
1643
+ } catch (cause) {
1644
+ setError(cause instanceof Error ? cause.message : String(cause));
1645
+ } finally {
1646
+ setBusy(false);
1647
+ }
1648
+ };
1649
+ return /* @__PURE__ */ jsxs13("div", { style: { display: "grid", gap: 8 }, children: [
1650
+ /* @__PURE__ */ jsxs13("label", { children: [
1651
+ "Interview",
1652
+ /* @__PURE__ */ jsxs13("select", { value: decision, onChange: (event) => setDecision(event.currentTarget.value), children: [
1653
+ /* @__PURE__ */ jsx16("option", { value: "waive_interview", children: "Already completed" }),
1654
+ /* @__PURE__ */ jsx16("option", { value: "require_interview", children: "Required" })
1655
+ ] })
1656
+ ] }),
1657
+ /* @__PURE__ */ jsxs13("label", { children: [
1658
+ "Internal note",
1659
+ /* @__PURE__ */ jsx16("input", { maxLength: 2e3, value: note, onInput: (event) => setNote(event.currentTarget.value) })
1660
+ ] }),
1661
+ /* @__PURE__ */ jsx16("button", { className: "btn secondary", type: "button", disabled: busy, onClick: () => void submit(), children: busy ? "Saving\u2026" : "Prepare for chapter" }),
1662
+ error ? /* @__PURE__ */ jsx16("p", { role: "alert", children: error }) : null
1663
+ ] });
1664
+ }
1623
1665
 
1624
1666
  // src/ui/admin-settings-workspace.tsx
1625
1667
  import { Tabs as Tabs2 } from "@odla-ai/ui/components";
@@ -2709,6 +2751,7 @@ export {
2709
2751
  LeaderNetworkOverview,
2710
2752
  AdmissionGrantsWorkspace,
2711
2753
  admissionGrantsWorkspace,
2754
+ LeaderAdmissionAction,
2712
2755
  availabilitySection,
2713
2756
  BuilderLibraryAdmin,
2714
2757
  builderLibrarySection,
@@ -2722,4 +2765,4 @@ export {
2722
2765
  defaultAdminSections,
2723
2766
  meetingsSection
2724
2767
  };
2725
- //# sourceMappingURL=chunk-MMDWXTJU.js.map
2768
+ //# sourceMappingURL=chunk-HWUTCXBV.js.map