@odla-ai/chapter 0.26.0 → 0.27.0
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 +83 -7
- package/dist/{chunk-QBTFQUDL.js → chunk-IOHVFBXH.js} +2 -2
- package/dist/{chunk-OIWCKF2G.js → chunk-JAWTIROV.js} +2 -1
- package/dist/chunk-JAWTIROV.js.map +1 -0
- package/dist/{chunk-4QZEAWQL.js → chunk-L2SA47IO.js} +434 -79
- package/dist/chunk-L2SA47IO.js.map +1 -0
- package/dist/{copy-context-CLvhJhrF.d.ts → copy-context-efEMF_aD.d.ts} +38 -5
- package/dist/index.cjs +98 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +89 -11
- package/dist/index.d.ts +89 -11
- package/dist/index.js +98 -6
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +12 -3
- package/dist/ui/admin/index.js +6 -2
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +7 -3
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/ui/member/index.js +2 -2
- package/dist/worker/index.cjs +545 -92
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +40 -7
- package/dist/worker/index.d.ts +40 -7
- package/dist/worker/index.js +546 -89
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-4QZEAWQL.js.map +0 -1
- package/dist/chunk-OIWCKF2G.js.map +0 -1
- /package/dist/{chunk-QBTFQUDL.js.map → chunk-IOHVFBXH.js.map} +0 -0
package/dist/worker/index.js
CHANGED
|
@@ -1901,16 +1901,16 @@ var crmDeps2 = (db, ctx) => ({
|
|
|
1901
1901
|
});
|
|
1902
1902
|
var handleAdminCrmSync = async (req, url, env, ctx) => {
|
|
1903
1903
|
if (req.method !== "POST" || url.pathname !== "/api/admin/crm/sync") return null;
|
|
1904
|
-
const
|
|
1905
|
-
if (
|
|
1906
|
-
const result = await backfillCrm(crmDeps2(
|
|
1904
|
+
const gate8 = await adminGate(req, env, ctx);
|
|
1905
|
+
if (gate8 instanceof Response) return gate8;
|
|
1906
|
+
const result = await backfillCrm(crmDeps2(gate8.db, ctx));
|
|
1907
1907
|
return json({ ok: true, ...result });
|
|
1908
1908
|
};
|
|
1909
1909
|
var handleAdminPeople = async (req, url, env, ctx) => {
|
|
1910
1910
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people") return null;
|
|
1911
|
-
const
|
|
1912
|
-
if (
|
|
1913
|
-
const { db } =
|
|
1911
|
+
const gate8 = await adminGate(req, env, ctx);
|
|
1912
|
+
if (gate8 instanceof Response) return gate8;
|
|
1913
|
+
const { db } = gate8;
|
|
1914
1914
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
1915
1915
|
const [appsRes, usersRes, roleList] = await Promise.all([
|
|
1916
1916
|
db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 200 } } }),
|
|
@@ -1961,9 +1961,9 @@ var handleAdminPeople = async (req, url, env, ctx) => {
|
|
|
1961
1961
|
};
|
|
1962
1962
|
var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
1963
1963
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people/access") return null;
|
|
1964
|
-
const
|
|
1965
|
-
if (
|
|
1966
|
-
const { db } =
|
|
1964
|
+
const gate8 = await adminGate(req, env, ctx);
|
|
1965
|
+
if (gate8 instanceof Response) return gate8;
|
|
1966
|
+
const { db } = gate8;
|
|
1967
1967
|
const targetId = url.searchParams.get("userId") ?? "";
|
|
1968
1968
|
if (!targetId.startsWith("user_")) return json({ error: "invalid userId" }, 400);
|
|
1969
1969
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
@@ -1974,9 +1974,9 @@ var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
|
1974
1974
|
};
|
|
1975
1975
|
var handleAdminPeopleRole = async (req, url, env, ctx) => {
|
|
1976
1976
|
if (req.method !== "POST" || url.pathname !== "/api/admin/people/role") return null;
|
|
1977
|
-
const
|
|
1978
|
-
if (
|
|
1979
|
-
const { db, actor } =
|
|
1977
|
+
const gate8 = await adminGate(req, env, ctx);
|
|
1978
|
+
if (gate8 instanceof Response) return gate8;
|
|
1979
|
+
const { db, actor } = gate8;
|
|
1980
1980
|
let body;
|
|
1981
1981
|
try {
|
|
1982
1982
|
body = await req.json();
|
|
@@ -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);
|
|
@@ -2629,11 +2739,12 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
|
2629
2739
|
const got = await gate4(req, env, ctx);
|
|
2630
2740
|
if ("response" in got) return got.response;
|
|
2631
2741
|
return json({
|
|
2632
|
-
targets: ctx.chapter.network.targets.map(({ id, name, url: targetUrl, fields }) => ({
|
|
2742
|
+
targets: ctx.chapter.network.targets.map(({ id, name, url: targetUrl, fields, sharedNotes }) => ({
|
|
2633
2743
|
id,
|
|
2634
2744
|
name,
|
|
2635
2745
|
url: targetUrl,
|
|
2636
|
-
types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS)
|
|
2746
|
+
types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS),
|
|
2747
|
+
sharedNotes
|
|
2637
2748
|
}))
|
|
2638
2749
|
});
|
|
2639
2750
|
};
|
|
@@ -2655,18 +2766,21 @@ var handleNetworkSnapshot = async (req, url, env, ctx) => {
|
|
|
2655
2766
|
};
|
|
2656
2767
|
return json(snapshot);
|
|
2657
2768
|
};
|
|
2658
|
-
async function snapshotOne(db, ctx, target) {
|
|
2769
|
+
async function snapshotOne(db, env, ctx, target) {
|
|
2659
2770
|
const secret = await getVaultSecret(db, target.secretName);
|
|
2660
2771
|
if (!secret) return { id: target.id, name: target.name, url: target.url, available: false, error: "edge secret is missing" };
|
|
2661
2772
|
const destination = new URL("/api/network/snapshot", target.url);
|
|
2662
2773
|
try {
|
|
2663
|
-
const headers = await
|
|
2774
|
+
const headers = await signFederatedRequest2({
|
|
2664
2775
|
secret,
|
|
2665
2776
|
sender: ctx.chapter.id,
|
|
2666
2777
|
method: "GET",
|
|
2667
2778
|
url: destination
|
|
2668
2779
|
});
|
|
2669
|
-
const response = await
|
|
2780
|
+
const response = await fetchNetworkTarget(env, target, destination, {
|
|
2781
|
+
headers,
|
|
2782
|
+
signal: AbortSignal.timeout(1e4)
|
|
2783
|
+
});
|
|
2670
2784
|
const body = await response.json().catch(() => null);
|
|
2671
2785
|
if (!response.ok || !body || !("version" in body) || body.version !== 1 || !("site" in body) || body.site.id !== target.id || !Array.isArray(body.types)) {
|
|
2672
2786
|
const upstream = body && "error" in body && typeof body.error === "string" ? body.error : void 0;
|
|
@@ -2701,7 +2815,7 @@ var handleAdminNetworkRollup = async (req, url, env, ctx) => {
|
|
|
2701
2815
|
const got = await gate4(req, env, ctx);
|
|
2702
2816
|
if ("response" in got) return got.response;
|
|
2703
2817
|
const targets = await Promise.all(
|
|
2704
|
-
ctx.chapter.network.targets.map((target) => snapshotOne(got.db, ctx, target))
|
|
2818
|
+
ctx.chapter.network.targets.map((target) => snapshotOne(got.db, env, ctx, target))
|
|
2705
2819
|
);
|
|
2706
2820
|
const byType = /* @__PURE__ */ new Map();
|
|
2707
2821
|
for (const target of targets) {
|
|
@@ -2723,74 +2837,6 @@ var handleAdminNetworkRollup = async (req, url, env, ctx) => {
|
|
|
2723
2837
|
};
|
|
2724
2838
|
return json(rollup);
|
|
2725
2839
|
};
|
|
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
2840
|
var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
2795
2841
|
if (req.method !== "POST" || url.pathname !== "/api/admin/network/push") return null;
|
|
2796
2842
|
const got = await gate4(req, env, ctx);
|
|
@@ -2811,11 +2857,417 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
2811
2857
|
const record = await getRecord({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
|
|
2812
2858
|
if (!record) return json({ error: "record not found" }, 404);
|
|
2813
2859
|
const results = await Promise.all(
|
|
2814
|
-
targets.map((target) =>
|
|
2860
|
+
targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record))
|
|
2815
2861
|
);
|
|
2816
2862
|
return json({ ok: results.every((result) => result.ok), results });
|
|
2817
2863
|
};
|
|
2818
2864
|
|
|
2865
|
+
// src/worker-routes-network-records.ts
|
|
2866
|
+
import { listRecords } from "@odla-ai/crm";
|
|
2867
|
+
import { signFederatedRequest as signFederatedRequest3, verifyFederatedRequest as verifyFederatedRequest3 } from "@odla-ai/db";
|
|
2868
|
+
var IDENTIFIER = /^[a-z][a-zA-Z0-9_]*$/;
|
|
2869
|
+
var MAX_PAGE = 50;
|
|
2870
|
+
var MAX_OFFSET = 1e4;
|
|
2871
|
+
var MAX_SEARCH = 200;
|
|
2872
|
+
async function gate5(req, env, ctx) {
|
|
2873
|
+
const db = ctx.makeDb(env);
|
|
2874
|
+
const user = await ctx.verifyUser(req, env);
|
|
2875
|
+
if (!user) return { response: json({ error: "unauthorized" }, 401) };
|
|
2876
|
+
if (!await ctx.isAdmin(db, user)) return { response: json({ error: "forbidden" }, 403) };
|
|
2877
|
+
return { db, user };
|
|
2878
|
+
}
|
|
2879
|
+
function integer(value, fallback, min, max) {
|
|
2880
|
+
if (value == null || value === "") return fallback;
|
|
2881
|
+
if (!/^\d+$/.test(value)) return null;
|
|
2882
|
+
const parsed = Number(value);
|
|
2883
|
+
return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : null;
|
|
2884
|
+
}
|
|
2885
|
+
function projectRecord(record, allowed) {
|
|
2886
|
+
const fields = Object.fromEntries(
|
|
2887
|
+
allowed.flatMap((name) => Object.prototype.hasOwnProperty.call(record.fields, name) ? [[name, record.fields[name]]] : [])
|
|
2888
|
+
);
|
|
2889
|
+
return {
|
|
2890
|
+
id: record.id,
|
|
2891
|
+
type: record.type,
|
|
2892
|
+
name: record.name,
|
|
2893
|
+
...record.stage ? { stage: record.stage } : {},
|
|
2894
|
+
fields,
|
|
2895
|
+
createdAt: record.createdAt,
|
|
2896
|
+
updatedAt: record.updatedAt
|
|
2897
|
+
};
|
|
2898
|
+
}
|
|
2899
|
+
var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
2900
|
+
if (req.method !== "GET" || url.pathname !== "/api/network/records") return null;
|
|
2901
|
+
const db = ctx.makeDb(env);
|
|
2902
|
+
const secret = await getVaultSecret(db, "network_share_secret");
|
|
2903
|
+
const readers = ctx.chapter.network.readers;
|
|
2904
|
+
if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
|
|
2905
|
+
const verified = await verifyFederatedRequest3(req, {
|
|
2906
|
+
secret,
|
|
2907
|
+
senders: readers.map((reader2) => reader2.id)
|
|
2908
|
+
});
|
|
2909
|
+
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
2910
|
+
const reader = readers.find((candidate) => candidate.id === verified.sender);
|
|
2911
|
+
const type = url.searchParams.get("type") ?? "";
|
|
2912
|
+
const allowed = reader?.fields[type];
|
|
2913
|
+
if (!IDENTIFIER.test(type) || !allowed) return json({ error: "record type is not shared with this reader" }, 403);
|
|
2914
|
+
const limit = integer(url.searchParams.get("limit"), 25, 1, MAX_PAGE);
|
|
2915
|
+
const offset = integer(url.searchParams.get("offset"), 0, 0, MAX_OFFSET);
|
|
2916
|
+
const search = (url.searchParams.get("search") ?? "").trim();
|
|
2917
|
+
if (limit == null || offset == null || search.length > MAX_SEARCH) {
|
|
2918
|
+
return json({ error: "invalid pagination or search parameters" }, 400);
|
|
2919
|
+
}
|
|
2920
|
+
const page = await listRecords(
|
|
2921
|
+
{ crm: ctx.chapter.crm, db },
|
|
2922
|
+
{
|
|
2923
|
+
type,
|
|
2924
|
+
limit,
|
|
2925
|
+
offset,
|
|
2926
|
+
...search ? { search } : {},
|
|
2927
|
+
sort: { field: "updatedAt", dir: "desc" }
|
|
2928
|
+
}
|
|
2929
|
+
);
|
|
2930
|
+
const response = {
|
|
2931
|
+
version: 1,
|
|
2932
|
+
site: { id: ctx.chapter.id, name: ctx.chapter.name },
|
|
2933
|
+
type,
|
|
2934
|
+
records: page.records.map((record) => projectRecord(record, allowed)),
|
|
2935
|
+
total: page.total,
|
|
2936
|
+
limit: page.limit,
|
|
2937
|
+
offset: page.offset
|
|
2938
|
+
};
|
|
2939
|
+
return json(response);
|
|
2940
|
+
};
|
|
2941
|
+
function targetFor(ctx, id) {
|
|
2942
|
+
return ctx.chapter.network.targets.find((target) => target.id === id);
|
|
2943
|
+
}
|
|
2944
|
+
async function readTargetRecords(db, env, ctx, target, query) {
|
|
2945
|
+
const secret = await getVaultSecret(db, target.secretName);
|
|
2946
|
+
if (!secret) return { ok: false, error: "edge secret is missing" };
|
|
2947
|
+
const destination = new URL("/api/network/records", target.url);
|
|
2948
|
+
destination.search = query.toString();
|
|
2949
|
+
try {
|
|
2950
|
+
const headers = await signFederatedRequest3({
|
|
2951
|
+
secret,
|
|
2952
|
+
sender: ctx.chapter.id,
|
|
2953
|
+
method: "GET",
|
|
2954
|
+
url: destination
|
|
2955
|
+
});
|
|
2956
|
+
const response = await fetchNetworkTarget(env, target, destination, {
|
|
2957
|
+
headers,
|
|
2958
|
+
signal: AbortSignal.timeout(1e4)
|
|
2959
|
+
});
|
|
2960
|
+
const body = await response.json().catch(() => null);
|
|
2961
|
+
if (!response.ok || !body || !("version" in body) || body.version !== 1 || !("site" in body) || body.site.id !== target.id || !Array.isArray(body.records)) {
|
|
2962
|
+
const upstream = body && "error" in body && typeof body.error === "string" ? body.error : void 0;
|
|
2963
|
+
return {
|
|
2964
|
+
ok: false,
|
|
2965
|
+
error: upstream ?? (response.ok ? "invalid follower record response" : `follower returned ${response.status}`)
|
|
2966
|
+
};
|
|
2967
|
+
}
|
|
2968
|
+
return { ok: true, page: body };
|
|
2969
|
+
} catch (error) {
|
|
2970
|
+
return { ok: false, error: error instanceof Error ? error.message : "record read failed" };
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
var handleAdminNetworkRecords = async (req, url, env, ctx) => {
|
|
2974
|
+
if (req.method !== "GET" || url.pathname !== "/api/admin/network/records") return null;
|
|
2975
|
+
const got = await gate5(req, env, ctx);
|
|
2976
|
+
if ("response" in got) return got.response;
|
|
2977
|
+
const targetId = url.searchParams.get("targetId") ?? "";
|
|
2978
|
+
const type = url.searchParams.get("type") ?? "";
|
|
2979
|
+
const target = targetFor(ctx, targetId);
|
|
2980
|
+
if (!target || !IDENTIFIER.test(type)) return json({ error: "configured targetId and record type are required" }, 400);
|
|
2981
|
+
const limit = integer(url.searchParams.get("limit"), 25, 1, MAX_PAGE);
|
|
2982
|
+
const offset = integer(url.searchParams.get("offset"), 0, 0, MAX_OFFSET);
|
|
2983
|
+
const search = (url.searchParams.get("search") ?? "").trim();
|
|
2984
|
+
if (limit == null || offset == null || search.length > MAX_SEARCH) {
|
|
2985
|
+
return json({ error: "invalid pagination or search parameters" }, 400);
|
|
2986
|
+
}
|
|
2987
|
+
const query = new URLSearchParams({ type, limit: String(limit), offset: String(offset) });
|
|
2988
|
+
if (search) query.set("search", search);
|
|
2989
|
+
const result = await readTargetRecords(
|
|
2990
|
+
got.db,
|
|
2991
|
+
env,
|
|
2992
|
+
ctx,
|
|
2993
|
+
target,
|
|
2994
|
+
query
|
|
2995
|
+
);
|
|
2996
|
+
return result.ok ? json(result.page) : json({ error: result.error }, 502);
|
|
2997
|
+
};
|
|
2998
|
+
|
|
2999
|
+
// src/worker-routes-network-notes.ts
|
|
3000
|
+
var IDENTIFIER2 = /^[a-z][a-zA-Z0-9_]*$/;
|
|
3001
|
+
var RECORD_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
|
|
3002
|
+
var MAX_NOTE = 4e3;
|
|
3003
|
+
async function gate6(req, env, ctx) {
|
|
3004
|
+
const db = ctx.makeDb(env);
|
|
3005
|
+
const user = await ctx.verifyUser(req, env);
|
|
3006
|
+
if (!user) return { response: json({ error: "unauthorized" }, 401) };
|
|
3007
|
+
if (!await ctx.isAdmin(db, user)) return { response: json({ error: "forbidden" }, 403) };
|
|
3008
|
+
return { db, user };
|
|
3009
|
+
}
|
|
3010
|
+
function noteIdentity(url, ctx) {
|
|
3011
|
+
const targetId = url.searchParams.get("targetId") ?? "";
|
|
3012
|
+
const recordType = url.searchParams.get("type") ?? "";
|
|
3013
|
+
const recordId = url.searchParams.get("recordId") ?? "";
|
|
3014
|
+
const configured = ctx.chapter.network.targets.some((target) => target.id === targetId);
|
|
3015
|
+
if (!configured || !IDENTIFIER2.test(recordType) || !RECORD_ID.test(recordId)) return null;
|
|
3016
|
+
return {
|
|
3017
|
+
targetId,
|
|
3018
|
+
recordType,
|
|
3019
|
+
recordId,
|
|
3020
|
+
recordKey: `${targetId}:${recordType}:${recordId}`
|
|
3021
|
+
};
|
|
3022
|
+
}
|
|
3023
|
+
function asNote(row) {
|
|
3024
|
+
return {
|
|
3025
|
+
id: String(row.id),
|
|
3026
|
+
targetId: String(row.targetId),
|
|
3027
|
+
recordType: String(row.recordType),
|
|
3028
|
+
recordId: String(row.recordId),
|
|
3029
|
+
body: String(row.body),
|
|
3030
|
+
authorId: String(row.authorId),
|
|
3031
|
+
...typeof row.authorEmail === "string" ? { authorEmail: row.authorEmail } : {},
|
|
3032
|
+
createdAt: Number(row.createdAt)
|
|
3033
|
+
};
|
|
3034
|
+
}
|
|
3035
|
+
var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
3036
|
+
if (url.pathname !== "/api/admin/network/notes" || req.method !== "GET" && req.method !== "POST") return null;
|
|
3037
|
+
const got = await gate6(req, env, ctx);
|
|
3038
|
+
if ("response" in got) return got.response;
|
|
3039
|
+
const identity = noteIdentity(url, ctx);
|
|
3040
|
+
if (!identity) return json({ error: "valid targetId, type, and recordId are required" }, 400);
|
|
3041
|
+
if (req.method === "GET") {
|
|
3042
|
+
const result = await got.db.query({
|
|
3043
|
+
networkNotes: {
|
|
3044
|
+
$: {
|
|
3045
|
+
where: { recordKey: identity.recordKey },
|
|
3046
|
+
order: { createdAt: "desc" },
|
|
3047
|
+
limit: 100
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
});
|
|
3051
|
+
return json({ notes: (result.networkNotes ?? []).map((row) => asNote(row)) });
|
|
3052
|
+
}
|
|
3053
|
+
const contentLength = Number(req.headers.get("content-length") ?? 0);
|
|
3054
|
+
if (contentLength > MAX_NOTE * 2) return json({ error: "note is too large" }, 413);
|
|
3055
|
+
let input;
|
|
3056
|
+
try {
|
|
3057
|
+
input = await req.json();
|
|
3058
|
+
} catch {
|
|
3059
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
3060
|
+
}
|
|
3061
|
+
const body = typeof input.body === "string" ? input.body.trim() : "";
|
|
3062
|
+
if (!body || body.length > MAX_NOTE) return json({ error: `note must be 1\u2013${MAX_NOTE} characters` }, 400);
|
|
3063
|
+
const id = crypto.randomUUID();
|
|
3064
|
+
const note = {
|
|
3065
|
+
id,
|
|
3066
|
+
targetId: identity.targetId,
|
|
3067
|
+
recordType: identity.recordType,
|
|
3068
|
+
recordId: identity.recordId,
|
|
3069
|
+
body,
|
|
3070
|
+
authorId: got.user.userId,
|
|
3071
|
+
...got.user.email ? { authorEmail: got.user.email } : {},
|
|
3072
|
+
createdAt: Date.now()
|
|
3073
|
+
};
|
|
3074
|
+
await got.db.transact([{
|
|
3075
|
+
t: "update",
|
|
3076
|
+
ns: "networkNotes",
|
|
3077
|
+
id,
|
|
3078
|
+
attrs: { ...note, recordKey: identity.recordKey }
|
|
3079
|
+
}]);
|
|
3080
|
+
return json({ note }, 201);
|
|
3081
|
+
};
|
|
3082
|
+
|
|
3083
|
+
// src/worker-routes-network-shared-notes.ts
|
|
3084
|
+
import {
|
|
3085
|
+
addActivity,
|
|
3086
|
+
CRM_NS,
|
|
3087
|
+
getRecord as getRecord2
|
|
3088
|
+
} from "@odla-ai/crm";
|
|
3089
|
+
import { signFederatedRequest as signFederatedRequest4, verifyFederatedRequest as verifyFederatedRequest4 } from "@odla-ai/db";
|
|
3090
|
+
var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
|
|
3091
|
+
var RECORD_ID2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
|
|
3092
|
+
var MUTATION_ID = /^[A-Za-z0-9_-]{16,128}$/;
|
|
3093
|
+
var MAX_NOTE2 = 4e3;
|
|
3094
|
+
var NETWORK_META = "chapterNetwork";
|
|
3095
|
+
async function gate7(req, env, ctx) {
|
|
3096
|
+
const db = ctx.makeDb(env);
|
|
3097
|
+
const user = await ctx.verifyUser(req, env);
|
|
3098
|
+
if (!user) return { response: json({ error: "unauthorized" }, 401) };
|
|
3099
|
+
if (!await ctx.isAdmin(db, user)) return { response: json({ error: "forbidden" }, 403) };
|
|
3100
|
+
return { db, user };
|
|
3101
|
+
}
|
|
3102
|
+
function sharedNote(activity, sourceId) {
|
|
3103
|
+
const meta = activity.meta?.[NETWORK_META];
|
|
3104
|
+
if (!meta || typeof meta !== "object" || Array.isArray(meta)) return null;
|
|
3105
|
+
const value = meta;
|
|
3106
|
+
if (value.version !== 1 || value.sourceId !== sourceId || typeof value.body !== "string") return null;
|
|
3107
|
+
return {
|
|
3108
|
+
id: activity.id,
|
|
3109
|
+
sourceId,
|
|
3110
|
+
recordType: String(value.recordType ?? ""),
|
|
3111
|
+
recordId: activity.recordId,
|
|
3112
|
+
body: value.body,
|
|
3113
|
+
createdAt: activity.createdAt
|
|
3114
|
+
};
|
|
3115
|
+
}
|
|
3116
|
+
async function listSharedNotes(db, sourceId, recordId) {
|
|
3117
|
+
const result = await db.query({
|
|
3118
|
+
[CRM_NS.activity]: {
|
|
3119
|
+
$: {
|
|
3120
|
+
where: {
|
|
3121
|
+
and: [
|
|
3122
|
+
{ recordId },
|
|
3123
|
+
{ authorId: `network:${sourceId}` }
|
|
3124
|
+
]
|
|
3125
|
+
},
|
|
3126
|
+
order: { createdAt: "desc" },
|
|
3127
|
+
limit: 100
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
});
|
|
3131
|
+
return (result[CRM_NS.activity] ?? []).flatMap((activity) => {
|
|
3132
|
+
const note = sharedNote(activity, sourceId);
|
|
3133
|
+
return note ? [note] : [];
|
|
3134
|
+
});
|
|
3135
|
+
}
|
|
3136
|
+
function sourceLabel(sourceId) {
|
|
3137
|
+
return sourceId.split("-").map((word) => word ? `${word[0]?.toUpperCase()}${word.slice(1)}` : word).join(" ");
|
|
3138
|
+
}
|
|
3139
|
+
var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
3140
|
+
if (url.pathname !== "/api/network/shared-notes" || req.method !== "GET" && req.method !== "POST") return null;
|
|
3141
|
+
if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_NOTE2 * 2) {
|
|
3142
|
+
return json({ error: "note is too large" }, 413);
|
|
3143
|
+
}
|
|
3144
|
+
const db = ctx.makeDb(env);
|
|
3145
|
+
const secret = await getVaultSecret(db, "network_share_secret");
|
|
3146
|
+
const readers = ctx.chapter.network.readers;
|
|
3147
|
+
if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
|
|
3148
|
+
const verified = await verifyFederatedRequest4(req, {
|
|
3149
|
+
secret,
|
|
3150
|
+
senders: readers.map((reader2) => reader2.id)
|
|
3151
|
+
});
|
|
3152
|
+
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
3153
|
+
const reader = readers.find((candidate) => candidate.id === verified.sender);
|
|
3154
|
+
const recordType = url.searchParams.get("type") ?? "";
|
|
3155
|
+
const recordId = url.searchParams.get("recordId") ?? "";
|
|
3156
|
+
if (!IDENTIFIER3.test(recordType) || !RECORD_ID2.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
|
|
3157
|
+
return json({ error: "shared notes are not enabled for this reader and record type" }, 403);
|
|
3158
|
+
}
|
|
3159
|
+
const record = await getRecord2({ crm: ctx.chapter.crm, db }, recordId);
|
|
3160
|
+
if (!record || record.type !== recordType) return json({ error: "record not found" }, 404);
|
|
3161
|
+
if (req.method === "GET") {
|
|
3162
|
+
return json({
|
|
3163
|
+
version: 1,
|
|
3164
|
+
site: { id: ctx.chapter.id, name: ctx.chapter.name },
|
|
3165
|
+
notes: await listSharedNotes(db, verified.sender, recordId)
|
|
3166
|
+
});
|
|
3167
|
+
}
|
|
3168
|
+
let input;
|
|
3169
|
+
try {
|
|
3170
|
+
input = JSON.parse(verified.body);
|
|
3171
|
+
} catch {
|
|
3172
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
3173
|
+
}
|
|
3174
|
+
const body = typeof input.body === "string" ? input.body.trim() : "";
|
|
3175
|
+
const mutationId = typeof input.mutationId === "string" ? input.mutationId : "";
|
|
3176
|
+
if (!body || body.length > MAX_NOTE2 || !MUTATION_ID.test(mutationId)) {
|
|
3177
|
+
return json({ error: `body must be 1\u2013${MAX_NOTE2} characters and mutationId must be valid` }, 400);
|
|
3178
|
+
}
|
|
3179
|
+
const createdAt = Date.now();
|
|
3180
|
+
const id = `network:${verified.sender}:${mutationId}`;
|
|
3181
|
+
const result = await addActivity(
|
|
3182
|
+
{
|
|
3183
|
+
crm: ctx.chapter.crm,
|
|
3184
|
+
db,
|
|
3185
|
+
now: () => createdAt,
|
|
3186
|
+
newId: () => id
|
|
3187
|
+
},
|
|
3188
|
+
{
|
|
3189
|
+
recordId,
|
|
3190
|
+
kind: "note",
|
|
3191
|
+
body: `Shared by ${sourceLabel(verified.sender)}: ${body}`,
|
|
3192
|
+
authorId: `network:${verified.sender}`,
|
|
3193
|
+
meta: {
|
|
3194
|
+
[NETWORK_META]: {
|
|
3195
|
+
version: 1,
|
|
3196
|
+
sourceId: verified.sender,
|
|
3197
|
+
recordType,
|
|
3198
|
+
body
|
|
3199
|
+
}
|
|
3200
|
+
},
|
|
3201
|
+
mutationId: id
|
|
3202
|
+
}
|
|
3203
|
+
);
|
|
3204
|
+
const note = {
|
|
3205
|
+
id,
|
|
3206
|
+
sourceId: verified.sender,
|
|
3207
|
+
recordType,
|
|
3208
|
+
recordId,
|
|
3209
|
+
body,
|
|
3210
|
+
createdAt
|
|
3211
|
+
};
|
|
3212
|
+
return json({ version: 1, site: { id: ctx.chapter.id, name: ctx.chapter.name }, note, duplicate: result.duplicate }, 201);
|
|
3213
|
+
};
|
|
3214
|
+
function targetFor2(ctx, id) {
|
|
3215
|
+
return ctx.chapter.network.targets.find((target) => target.id === id);
|
|
3216
|
+
}
|
|
3217
|
+
var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
3218
|
+
if (url.pathname !== "/api/admin/network/shared-notes" || req.method !== "GET" && req.method !== "POST") return null;
|
|
3219
|
+
const got = await gate7(req, env, ctx);
|
|
3220
|
+
if ("response" in got) return got.response;
|
|
3221
|
+
const targetId = url.searchParams.get("targetId") ?? "";
|
|
3222
|
+
const recordType = url.searchParams.get("type") ?? "";
|
|
3223
|
+
const recordId = url.searchParams.get("recordId") ?? "";
|
|
3224
|
+
const target = targetFor2(ctx, targetId);
|
|
3225
|
+
if (!target || !target.sharedNotes.includes(recordType) || !IDENTIFIER3.test(recordType) || !RECORD_ID2.test(recordId)) {
|
|
3226
|
+
return json({ error: "configured targetId, type, and recordId are required" }, 400);
|
|
3227
|
+
}
|
|
3228
|
+
let body = "";
|
|
3229
|
+
if (req.method === "POST") {
|
|
3230
|
+
let input;
|
|
3231
|
+
try {
|
|
3232
|
+
input = await req.json();
|
|
3233
|
+
} catch {
|
|
3234
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
3235
|
+
}
|
|
3236
|
+
const note = typeof input.body === "string" ? input.body.trim() : "";
|
|
3237
|
+
if (!note || note.length > MAX_NOTE2) return json({ error: `note must be 1\u2013${MAX_NOTE2} characters` }, 400);
|
|
3238
|
+
body = JSON.stringify({ body: note, mutationId: crypto.randomUUID() });
|
|
3239
|
+
}
|
|
3240
|
+
const secret = await getVaultSecret(got.db, target.secretName);
|
|
3241
|
+
if (!secret) return json({ error: "edge secret is missing" }, 502);
|
|
3242
|
+
const destination = new URL("/api/network/shared-notes", target.url);
|
|
3243
|
+
destination.search = new URLSearchParams({ type: recordType, recordId }).toString();
|
|
3244
|
+
try {
|
|
3245
|
+
const headers = await signFederatedRequest4({
|
|
3246
|
+
secret,
|
|
3247
|
+
sender: ctx.chapter.id,
|
|
3248
|
+
method: req.method,
|
|
3249
|
+
url: destination,
|
|
3250
|
+
...body ? { body } : {}
|
|
3251
|
+
});
|
|
3252
|
+
const response = await fetchNetworkTarget(env, target, destination, {
|
|
3253
|
+
method: req.method,
|
|
3254
|
+
headers: { ...headers, ...body ? { "content-type": "application/json" } : {} },
|
|
3255
|
+
...body ? { body } : {},
|
|
3256
|
+
signal: AbortSignal.timeout(1e4)
|
|
3257
|
+
});
|
|
3258
|
+
const payload = await response.json().catch(() => null);
|
|
3259
|
+
const site = payload?.site;
|
|
3260
|
+
if (!response.ok || !payload || site?.id !== target.id) {
|
|
3261
|
+
return json({
|
|
3262
|
+
error: typeof payload?.error === "string" ? payload.error : `follower returned ${response.status}`
|
|
3263
|
+
}, 502);
|
|
3264
|
+
}
|
|
3265
|
+
return json(payload, req.method === "POST" ? 201 : 200);
|
|
3266
|
+
} catch (error) {
|
|
3267
|
+
return json({ error: error instanceof Error ? error.message : "shared note request failed" }, 502);
|
|
3268
|
+
}
|
|
3269
|
+
};
|
|
3270
|
+
|
|
2819
3271
|
// src/worker-routes-formation.ts
|
|
2820
3272
|
import { createRecord as createRecord3 } from "@odla-ai/crm";
|
|
2821
3273
|
|
|
@@ -2918,6 +3370,8 @@ var BUILTIN_ROUTES = [
|
|
|
2918
3370
|
handleCrm,
|
|
2919
3371
|
handleNetworkShared,
|
|
2920
3372
|
handleNetworkSnapshot,
|
|
3373
|
+
handleNetworkRecords,
|
|
3374
|
+
handleNetworkSharedNotes,
|
|
2921
3375
|
handleFormation,
|
|
2922
3376
|
handleMember,
|
|
2923
3377
|
handleSchedule,
|
|
@@ -2948,6 +3402,9 @@ var BUILTIN_ROUTES = [
|
|
|
2948
3402
|
handleAdminNetworkTargets,
|
|
2949
3403
|
handleAdminNetworkRollup,
|
|
2950
3404
|
handleAdminNetworkPush,
|
|
3405
|
+
handleAdminNetworkRecords,
|
|
3406
|
+
handleAdminNetworkNotes,
|
|
3407
|
+
handleAdminNetworkSharedNotes,
|
|
2951
3408
|
// API requests must never fall through to an SPA asset response. Hosts still
|
|
2952
3409
|
// get first refusal through options.routes, then this terminates unknown API
|
|
2953
3410
|
// paths with an explicit machine-readable 404.
|