@odla-ai/chapter 0.26.0 → 0.26.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.
@@ -64,6 +64,10 @@ interface ChapterNetworkTarget {
64
64
  name?: string;
65
65
  /** Absolute follower origin, e.g. `https://chapter.example.com`. */
66
66
  url: string;
67
+ /** Optional Cloudflare service-binding name for Worker-to-Worker delivery.
68
+ * The URL still defines the signed request origin; the binding only supplies
69
+ * the transport when a `workers.dev` subrequest cannot enter another Worker. */
70
+ binding?: string;
67
71
  /** Leader-vault key holding this follower's share secret. Defaults to
68
72
  * `network_share_<id-with-underscores>`. */
69
73
  secretName?: string;
@@ -106,6 +110,7 @@ interface ResolvedNetworkTarget {
106
110
  id: string;
107
111
  name: string;
108
112
  url: string;
113
+ binding?: string;
109
114
  secretName: string;
110
115
  fields?: Record<string, readonly string[]>;
111
116
  }
@@ -64,6 +64,10 @@ interface ChapterNetworkTarget {
64
64
  name?: string;
65
65
  /** Absolute follower origin, e.g. `https://chapter.example.com`. */
66
66
  url: string;
67
+ /** Optional Cloudflare service-binding name for Worker-to-Worker delivery.
68
+ * The URL still defines the signed request origin; the binding only supplies
69
+ * the transport when a `workers.dev` subrequest cannot enter another Worker. */
70
+ binding?: string;
67
71
  /** Leader-vault key holding this follower's share secret. Defaults to
68
72
  * `network_share_<id-with-underscores>`. */
69
73
  secretName?: string;
@@ -106,6 +110,7 @@ interface ResolvedNetworkTarget {
106
110
  id: string;
107
111
  name: string;
108
112
  url: string;
113
+ binding?: string;
109
114
  secretName: string;
110
115
  fields?: Record<string, readonly string[]>;
111
116
  }
@@ -2615,8 +2615,118 @@ var handleAdminComms = async (req, url, env, ctx) => {
2615
2615
  };
2616
2616
 
2617
2617
  // src/worker-routes-network.ts
2618
- import { getRecord, recordDeliveryAttempt, typeSummary } from "@odla-ai/crm";
2619
- import { signFederatedRequest, verifyFederatedRequest as verifyFederatedRequest2 } from "@odla-ai/db";
2618
+ import { getRecord, typeSummary } from "@odla-ai/crm";
2619
+ import { signFederatedRequest as signFederatedRequest2, verifyFederatedRequest as verifyFederatedRequest2 } from "@odla-ai/db";
2620
+
2621
+ // src/worker-network-fetch.ts
2622
+ function targetFetcher(env, target) {
2623
+ if (!target.binding) return null;
2624
+ const candidate = env[target.binding];
2625
+ if (typeof candidate !== "object" || candidate === null || !("fetch" in candidate) || typeof candidate.fetch !== "function") {
2626
+ throw new Error(`service binding "${target.binding}" is unavailable`);
2627
+ }
2628
+ return candidate;
2629
+ }
2630
+ function fetchNetworkTarget(env, target, destination, init) {
2631
+ const binding = targetFetcher(env, target);
2632
+ return binding ? binding.fetch(new Request(destination, init)) : globalThis.fetch(destination, init);
2633
+ }
2634
+
2635
+ // src/worker-network-push.ts
2636
+ import { recordDeliveryAttempt } from "@odla-ai/crm";
2637
+ import { signFederatedRequest } from "@odla-ai/db";
2638
+ async function pushNetworkRecord(db, env, ctx, target, record) {
2639
+ const secret = await getVaultSecret(db, target.secretName);
2640
+ const crmDeps3 = {
2641
+ crm: ctx.chapter.crm,
2642
+ db,
2643
+ now: () => Date.now(),
2644
+ newId: () => crypto.randomUUID()
2645
+ };
2646
+ if (!secret) {
2647
+ const error = `vault secret "${target.secretName}" is missing`;
2648
+ await recordDeliveryAttempt(crmDeps3, {
2649
+ recordId: record.id,
2650
+ targetId: target.id,
2651
+ status: "failed",
2652
+ payloadVersion: 2,
2653
+ error
2654
+ });
2655
+ return { id: target.id, name: target.name, ok: false, error };
2656
+ }
2657
+ let payload;
2658
+ try {
2659
+ payload = sharedRecordFromCrm(ctx.chapter.crm, record, target, ctx.chapter.id);
2660
+ } catch (err) {
2661
+ return {
2662
+ id: target.id,
2663
+ name: target.name,
2664
+ ok: false,
2665
+ error: err instanceof Error ? err.message : "record is not shareable"
2666
+ };
2667
+ }
2668
+ const payloadBody = JSON.stringify(payload);
2669
+ const destination = new URL("/api/network/shared", target.url);
2670
+ try {
2671
+ const signed = await signFederatedRequest({
2672
+ secret,
2673
+ sender: ctx.chapter.id,
2674
+ method: "POST",
2675
+ url: destination,
2676
+ body: payloadBody
2677
+ });
2678
+ const res = await fetchNetworkTarget(env, target, destination, {
2679
+ method: "POST",
2680
+ headers: { ...signed, "content-type": "application/json" },
2681
+ body: payloadBody,
2682
+ signal: AbortSignal.timeout(1e4)
2683
+ });
2684
+ const responseBody = await res.json().catch(() => ({}));
2685
+ if (!res.ok) {
2686
+ const error = responseBody.error ?? "follower rejected the record";
2687
+ await recordDeliveryAttempt(crmDeps3, {
2688
+ recordId: record.id,
2689
+ targetId: target.id,
2690
+ status: "failed",
2691
+ payloadVersion: 2,
2692
+ error
2693
+ });
2694
+ return {
2695
+ id: target.id,
2696
+ name: target.name,
2697
+ ok: false,
2698
+ status: res.status,
2699
+ error
2700
+ };
2701
+ }
2702
+ await recordDeliveryAttempt(crmDeps3, {
2703
+ recordId: record.id,
2704
+ targetId: target.id,
2705
+ status: "delivered",
2706
+ payloadVersion: 2,
2707
+ ...responseBody.recordId ? { remoteRecordId: responseBody.recordId } : {}
2708
+ });
2709
+ return {
2710
+ id: target.id,
2711
+ name: target.name,
2712
+ ok: true,
2713
+ status: res.status,
2714
+ recordId: responseBody.recordId
2715
+ };
2716
+ } catch (err) {
2717
+ const error = err instanceof Error ? err.message : "delivery failed";
2718
+ await recordDeliveryAttempt(crmDeps3, {
2719
+ recordId: record.id,
2720
+ targetId: target.id,
2721
+ status: "failed",
2722
+ payloadVersion: 2,
2723
+ error
2724
+ });
2725
+ return { id: target.id, name: target.name, ok: false, error };
2726
+ }
2727
+ }
2728
+
2729
+ // src/worker-routes-network.ts
2620
2730
  async function gate4(req, env, ctx) {
2621
2731
  const db = ctx.makeDb(env);
2622
2732
  const user = await ctx.verifyUser(req, env);
@@ -2655,18 +2765,21 @@ var handleNetworkSnapshot = async (req, url, env, ctx) => {
2655
2765
  };
2656
2766
  return json(snapshot);
2657
2767
  };
2658
- async function snapshotOne(db, ctx, target) {
2768
+ async function snapshotOne(db, env, ctx, target) {
2659
2769
  const secret = await getVaultSecret(db, target.secretName);
2660
2770
  if (!secret) return { id: target.id, name: target.name, url: target.url, available: false, error: "edge secret is missing" };
2661
2771
  const destination = new URL("/api/network/snapshot", target.url);
2662
2772
  try {
2663
- const headers = await signFederatedRequest({
2773
+ const headers = await signFederatedRequest2({
2664
2774
  secret,
2665
2775
  sender: ctx.chapter.id,
2666
2776
  method: "GET",
2667
2777
  url: destination
2668
2778
  });
2669
- const response = await fetch(destination, { headers, signal: AbortSignal.timeout(1e4) });
2779
+ const response = await fetchNetworkTarget(env, target, destination, {
2780
+ headers,
2781
+ signal: AbortSignal.timeout(1e4)
2782
+ });
2670
2783
  const body = await response.json().catch(() => null);
2671
2784
  if (!response.ok || !body || !("version" in body) || body.version !== 1 || !("site" in body) || body.site.id !== target.id || !Array.isArray(body.types)) {
2672
2785
  const upstream = body && "error" in body && typeof body.error === "string" ? body.error : void 0;
@@ -2701,7 +2814,7 @@ var handleAdminNetworkRollup = async (req, url, env, ctx) => {
2701
2814
  const got = await gate4(req, env, ctx);
2702
2815
  if ("response" in got) return got.response;
2703
2816
  const targets = await Promise.all(
2704
- ctx.chapter.network.targets.map((target) => snapshotOne(got.db, ctx, target))
2817
+ ctx.chapter.network.targets.map((target) => snapshotOne(got.db, env, ctx, target))
2705
2818
  );
2706
2819
  const byType = /* @__PURE__ */ new Map();
2707
2820
  for (const target of targets) {
@@ -2723,74 +2836,6 @@ var handleAdminNetworkRollup = async (req, url, env, ctx) => {
2723
2836
  };
2724
2837
  return json(rollup);
2725
2838
  };
2726
- async function pushOne(db, ctx, target, record) {
2727
- const secret = await getVaultSecret(db, target.secretName);
2728
- const crmDeps3 = { crm: ctx.chapter.crm, db, now: () => Date.now(), newId: () => crypto.randomUUID() };
2729
- if (!secret) {
2730
- const error = `vault secret "${target.secretName}" is missing`;
2731
- await recordDeliveryAttempt(crmDeps3, {
2732
- recordId: record.id,
2733
- targetId: target.id,
2734
- status: "failed",
2735
- payloadVersion: 2,
2736
- error
2737
- });
2738
- return { id: target.id, name: target.name, ok: false, error };
2739
- }
2740
- let payload;
2741
- try {
2742
- payload = sharedRecordFromCrm(ctx.chapter.crm, record, target, ctx.chapter.id);
2743
- } catch (err) {
2744
- return { id: target.id, name: target.name, ok: false, error: err instanceof Error ? err.message : "record is not shareable" };
2745
- }
2746
- const payloadBody = JSON.stringify(payload);
2747
- const destination = new URL("/api/network/shared", target.url);
2748
- try {
2749
- const signed = await signFederatedRequest({
2750
- secret,
2751
- sender: ctx.chapter.id,
2752
- method: "POST",
2753
- url: destination,
2754
- body: payloadBody
2755
- });
2756
- const res = await fetch(destination, {
2757
- method: "POST",
2758
- headers: { ...signed, "content-type": "application/json" },
2759
- body: payloadBody,
2760
- signal: AbortSignal.timeout(1e4)
2761
- });
2762
- const responseBody = await res.json().catch(() => ({}));
2763
- if (!res.ok) {
2764
- const error = responseBody.error ?? "follower rejected the record";
2765
- await recordDeliveryAttempt(crmDeps3, {
2766
- recordId: record.id,
2767
- targetId: target.id,
2768
- status: "failed",
2769
- payloadVersion: 2,
2770
- error
2771
- });
2772
- return { id: target.id, name: target.name, ok: false, status: res.status, error };
2773
- }
2774
- await recordDeliveryAttempt(crmDeps3, {
2775
- recordId: record.id,
2776
- targetId: target.id,
2777
- status: "delivered",
2778
- payloadVersion: 2,
2779
- ...responseBody.recordId ? { remoteRecordId: responseBody.recordId } : {}
2780
- });
2781
- return { id: target.id, name: target.name, ok: true, status: res.status, recordId: responseBody.recordId };
2782
- } catch (err) {
2783
- const error = err instanceof Error ? err.message : "delivery failed";
2784
- await recordDeliveryAttempt(crmDeps3, {
2785
- recordId: record.id,
2786
- targetId: target.id,
2787
- status: "failed",
2788
- payloadVersion: 2,
2789
- error
2790
- });
2791
- return { id: target.id, name: target.name, ok: false, error };
2792
- }
2793
- }
2794
2839
  var handleAdminNetworkPush = async (req, url, env, ctx) => {
2795
2840
  if (req.method !== "POST" || url.pathname !== "/api/admin/network/push") return null;
2796
2841
  const got = await gate4(req, env, ctx);
@@ -2811,7 +2856,7 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
2811
2856
  const record = await getRecord({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
2812
2857
  if (!record) return json({ error: "record not found" }, 404);
2813
2858
  const results = await Promise.all(
2814
- targets.map((target) => pushOne(got.db, ctx, target, record))
2859
+ targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record))
2815
2860
  );
2816
2861
  return json({ ok: results.every((result) => result.ok), results });
2817
2862
  };