@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.
- package/README.md +32 -4
- package/dist/{copy-context-CLvhJhrF.d.ts → copy-context-Vof2eTMt.d.ts} +5 -0
- package/dist/index.cjs +8 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/worker/index.cjs +123 -78
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +5 -0
- package/dist/worker/index.d.ts +5 -0
- package/dist/worker/index.js +120 -75
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/dist/worker/index.d.cts
CHANGED
|
@@ -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
|
}
|
package/dist/worker/index.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/worker/index.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
};
|