@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.cjs
CHANGED
|
@@ -1923,16 +1923,16 @@ var crmDeps2 = (db, ctx) => ({
|
|
|
1923
1923
|
});
|
|
1924
1924
|
var handleAdminCrmSync = async (req, url, env, ctx) => {
|
|
1925
1925
|
if (req.method !== "POST" || url.pathname !== "/api/admin/crm/sync") return null;
|
|
1926
|
-
const
|
|
1927
|
-
if (
|
|
1928
|
-
const result = await backfillCrm(crmDeps2(
|
|
1926
|
+
const gate8 = await adminGate(req, env, ctx);
|
|
1927
|
+
if (gate8 instanceof Response) return gate8;
|
|
1928
|
+
const result = await backfillCrm(crmDeps2(gate8.db, ctx));
|
|
1929
1929
|
return json({ ok: true, ...result });
|
|
1930
1930
|
};
|
|
1931
1931
|
var handleAdminPeople = async (req, url, env, ctx) => {
|
|
1932
1932
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people") return null;
|
|
1933
|
-
const
|
|
1934
|
-
if (
|
|
1935
|
-
const { db } =
|
|
1933
|
+
const gate8 = await adminGate(req, env, ctx);
|
|
1934
|
+
if (gate8 instanceof Response) return gate8;
|
|
1935
|
+
const { db } = gate8;
|
|
1936
1936
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
1937
1937
|
const [appsRes, usersRes, roleList] = await Promise.all([
|
|
1938
1938
|
db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 200 } } }),
|
|
@@ -1983,9 +1983,9 @@ var handleAdminPeople = async (req, url, env, ctx) => {
|
|
|
1983
1983
|
};
|
|
1984
1984
|
var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
1985
1985
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people/access") return null;
|
|
1986
|
-
const
|
|
1987
|
-
if (
|
|
1988
|
-
const { db } =
|
|
1986
|
+
const gate8 = await adminGate(req, env, ctx);
|
|
1987
|
+
if (gate8 instanceof Response) return gate8;
|
|
1988
|
+
const { db } = gate8;
|
|
1989
1989
|
const targetId = url.searchParams.get("userId") ?? "";
|
|
1990
1990
|
if (!targetId.startsWith("user_")) return json({ error: "invalid userId" }, 400);
|
|
1991
1991
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
@@ -1996,9 +1996,9 @@ var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
|
1996
1996
|
};
|
|
1997
1997
|
var handleAdminPeopleRole = async (req, url, env, ctx) => {
|
|
1998
1998
|
if (req.method !== "POST" || url.pathname !== "/api/admin/people/role") return null;
|
|
1999
|
-
const
|
|
2000
|
-
if (
|
|
2001
|
-
const { db, actor } =
|
|
1999
|
+
const gate8 = await adminGate(req, env, ctx);
|
|
2000
|
+
if (gate8 instanceof Response) return gate8;
|
|
2001
|
+
const { db, actor } = gate8;
|
|
2002
2002
|
let body;
|
|
2003
2003
|
try {
|
|
2004
2004
|
body = await req.json();
|
|
@@ -2637,8 +2637,118 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
2637
2637
|
};
|
|
2638
2638
|
|
|
2639
2639
|
// src/worker-routes-network.ts
|
|
2640
|
+
var import_crm5 = require("@odla-ai/crm");
|
|
2641
|
+
var import_db4 = require("@odla-ai/db");
|
|
2642
|
+
|
|
2643
|
+
// src/worker-network-fetch.ts
|
|
2644
|
+
function targetFetcher(env, target) {
|
|
2645
|
+
if (!target.binding) return null;
|
|
2646
|
+
const candidate = env[target.binding];
|
|
2647
|
+
if (typeof candidate !== "object" || candidate === null || !("fetch" in candidate) || typeof candidate.fetch !== "function") {
|
|
2648
|
+
throw new Error(`service binding "${target.binding}" is unavailable`);
|
|
2649
|
+
}
|
|
2650
|
+
return candidate;
|
|
2651
|
+
}
|
|
2652
|
+
function fetchNetworkTarget(env, target, destination, init) {
|
|
2653
|
+
const binding = targetFetcher(env, target);
|
|
2654
|
+
return binding ? binding.fetch(new Request(destination, init)) : globalThis.fetch(destination, init);
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
// src/worker-network-push.ts
|
|
2640
2658
|
var import_crm4 = require("@odla-ai/crm");
|
|
2641
2659
|
var import_db3 = require("@odla-ai/db");
|
|
2660
|
+
async function pushNetworkRecord(db, env, ctx, target, record) {
|
|
2661
|
+
const secret = await getVaultSecret(db, target.secretName);
|
|
2662
|
+
const crmDeps3 = {
|
|
2663
|
+
crm: ctx.chapter.crm,
|
|
2664
|
+
db,
|
|
2665
|
+
now: () => Date.now(),
|
|
2666
|
+
newId: () => crypto.randomUUID()
|
|
2667
|
+
};
|
|
2668
|
+
if (!secret) {
|
|
2669
|
+
const error = `vault secret "${target.secretName}" is missing`;
|
|
2670
|
+
await (0, import_crm4.recordDeliveryAttempt)(crmDeps3, {
|
|
2671
|
+
recordId: record.id,
|
|
2672
|
+
targetId: target.id,
|
|
2673
|
+
status: "failed",
|
|
2674
|
+
payloadVersion: 2,
|
|
2675
|
+
error
|
|
2676
|
+
});
|
|
2677
|
+
return { id: target.id, name: target.name, ok: false, error };
|
|
2678
|
+
}
|
|
2679
|
+
let payload;
|
|
2680
|
+
try {
|
|
2681
|
+
payload = sharedRecordFromCrm(ctx.chapter.crm, record, target, ctx.chapter.id);
|
|
2682
|
+
} catch (err) {
|
|
2683
|
+
return {
|
|
2684
|
+
id: target.id,
|
|
2685
|
+
name: target.name,
|
|
2686
|
+
ok: false,
|
|
2687
|
+
error: err instanceof Error ? err.message : "record is not shareable"
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
const payloadBody = JSON.stringify(payload);
|
|
2691
|
+
const destination = new URL("/api/network/shared", target.url);
|
|
2692
|
+
try {
|
|
2693
|
+
const signed = await (0, import_db3.signFederatedRequest)({
|
|
2694
|
+
secret,
|
|
2695
|
+
sender: ctx.chapter.id,
|
|
2696
|
+
method: "POST",
|
|
2697
|
+
url: destination,
|
|
2698
|
+
body: payloadBody
|
|
2699
|
+
});
|
|
2700
|
+
const res = await fetchNetworkTarget(env, target, destination, {
|
|
2701
|
+
method: "POST",
|
|
2702
|
+
headers: { ...signed, "content-type": "application/json" },
|
|
2703
|
+
body: payloadBody,
|
|
2704
|
+
signal: AbortSignal.timeout(1e4)
|
|
2705
|
+
});
|
|
2706
|
+
const responseBody = await res.json().catch(() => ({}));
|
|
2707
|
+
if (!res.ok) {
|
|
2708
|
+
const error = responseBody.error ?? "follower rejected the record";
|
|
2709
|
+
await (0, import_crm4.recordDeliveryAttempt)(crmDeps3, {
|
|
2710
|
+
recordId: record.id,
|
|
2711
|
+
targetId: target.id,
|
|
2712
|
+
status: "failed",
|
|
2713
|
+
payloadVersion: 2,
|
|
2714
|
+
error
|
|
2715
|
+
});
|
|
2716
|
+
return {
|
|
2717
|
+
id: target.id,
|
|
2718
|
+
name: target.name,
|
|
2719
|
+
ok: false,
|
|
2720
|
+
status: res.status,
|
|
2721
|
+
error
|
|
2722
|
+
};
|
|
2723
|
+
}
|
|
2724
|
+
await (0, import_crm4.recordDeliveryAttempt)(crmDeps3, {
|
|
2725
|
+
recordId: record.id,
|
|
2726
|
+
targetId: target.id,
|
|
2727
|
+
status: "delivered",
|
|
2728
|
+
payloadVersion: 2,
|
|
2729
|
+
...responseBody.recordId ? { remoteRecordId: responseBody.recordId } : {}
|
|
2730
|
+
});
|
|
2731
|
+
return {
|
|
2732
|
+
id: target.id,
|
|
2733
|
+
name: target.name,
|
|
2734
|
+
ok: true,
|
|
2735
|
+
status: res.status,
|
|
2736
|
+
recordId: responseBody.recordId
|
|
2737
|
+
};
|
|
2738
|
+
} catch (err) {
|
|
2739
|
+
const error = err instanceof Error ? err.message : "delivery failed";
|
|
2740
|
+
await (0, import_crm4.recordDeliveryAttempt)(crmDeps3, {
|
|
2741
|
+
recordId: record.id,
|
|
2742
|
+
targetId: target.id,
|
|
2743
|
+
status: "failed",
|
|
2744
|
+
payloadVersion: 2,
|
|
2745
|
+
error
|
|
2746
|
+
});
|
|
2747
|
+
return { id: target.id, name: target.name, ok: false, error };
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
// src/worker-routes-network.ts
|
|
2642
2752
|
async function gate4(req, env, ctx) {
|
|
2643
2753
|
const db = ctx.makeDb(env);
|
|
2644
2754
|
const user = await ctx.verifyUser(req, env);
|
|
@@ -2651,11 +2761,12 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
|
2651
2761
|
const got = await gate4(req, env, ctx);
|
|
2652
2762
|
if ("response" in got) return got.response;
|
|
2653
2763
|
return json({
|
|
2654
|
-
targets: ctx.chapter.network.targets.map(({ id, name, url: targetUrl, fields }) => ({
|
|
2764
|
+
targets: ctx.chapter.network.targets.map(({ id, name, url: targetUrl, fields, sharedNotes }) => ({
|
|
2655
2765
|
id,
|
|
2656
2766
|
name,
|
|
2657
2767
|
url: targetUrl,
|
|
2658
|
-
types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS)
|
|
2768
|
+
types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS),
|
|
2769
|
+
sharedNotes
|
|
2659
2770
|
}))
|
|
2660
2771
|
});
|
|
2661
2772
|
};
|
|
@@ -2664,10 +2775,10 @@ var handleNetworkSnapshot = async (req, url, env, ctx) => {
|
|
|
2664
2775
|
const db = ctx.makeDb(env);
|
|
2665
2776
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
2666
2777
|
if (!secret) return json({ error: "unauthorized" }, 401);
|
|
2667
|
-
const verified = await (0,
|
|
2778
|
+
const verified = await (0, import_db4.verifyFederatedRequest)(req, { secret });
|
|
2668
2779
|
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
2669
2780
|
const types = await Promise.all(
|
|
2670
|
-
Object.keys(ctx.chapter.crm.config.types).map((type) => (0,
|
|
2781
|
+
Object.keys(ctx.chapter.crm.config.types).map((type) => (0, import_crm5.typeSummary)({ crm: ctx.chapter.crm, db }, type))
|
|
2671
2782
|
);
|
|
2672
2783
|
const snapshot = {
|
|
2673
2784
|
version: 1,
|
|
@@ -2677,18 +2788,21 @@ var handleNetworkSnapshot = async (req, url, env, ctx) => {
|
|
|
2677
2788
|
};
|
|
2678
2789
|
return json(snapshot);
|
|
2679
2790
|
};
|
|
2680
|
-
async function snapshotOne(db, ctx, target) {
|
|
2791
|
+
async function snapshotOne(db, env, ctx, target) {
|
|
2681
2792
|
const secret = await getVaultSecret(db, target.secretName);
|
|
2682
2793
|
if (!secret) return { id: target.id, name: target.name, url: target.url, available: false, error: "edge secret is missing" };
|
|
2683
2794
|
const destination = new URL("/api/network/snapshot", target.url);
|
|
2684
2795
|
try {
|
|
2685
|
-
const headers = await (0,
|
|
2796
|
+
const headers = await (0, import_db4.signFederatedRequest)({
|
|
2686
2797
|
secret,
|
|
2687
2798
|
sender: ctx.chapter.id,
|
|
2688
2799
|
method: "GET",
|
|
2689
2800
|
url: destination
|
|
2690
2801
|
});
|
|
2691
|
-
const response = await
|
|
2802
|
+
const response = await fetchNetworkTarget(env, target, destination, {
|
|
2803
|
+
headers,
|
|
2804
|
+
signal: AbortSignal.timeout(1e4)
|
|
2805
|
+
});
|
|
2692
2806
|
const body = await response.json().catch(() => null);
|
|
2693
2807
|
if (!response.ok || !body || !("version" in body) || body.version !== 1 || !("site" in body) || body.site.id !== target.id || !Array.isArray(body.types)) {
|
|
2694
2808
|
const upstream = body && "error" in body && typeof body.error === "string" ? body.error : void 0;
|
|
@@ -2723,7 +2837,7 @@ var handleAdminNetworkRollup = async (req, url, env, ctx) => {
|
|
|
2723
2837
|
const got = await gate4(req, env, ctx);
|
|
2724
2838
|
if ("response" in got) return got.response;
|
|
2725
2839
|
const targets = await Promise.all(
|
|
2726
|
-
ctx.chapter.network.targets.map((target) => snapshotOne(got.db, ctx, target))
|
|
2840
|
+
ctx.chapter.network.targets.map((target) => snapshotOne(got.db, env, ctx, target))
|
|
2727
2841
|
);
|
|
2728
2842
|
const byType = /* @__PURE__ */ new Map();
|
|
2729
2843
|
for (const target of targets) {
|
|
@@ -2745,74 +2859,6 @@ var handleAdminNetworkRollup = async (req, url, env, ctx) => {
|
|
|
2745
2859
|
};
|
|
2746
2860
|
return json(rollup);
|
|
2747
2861
|
};
|
|
2748
|
-
async function pushOne(db, ctx, target, record) {
|
|
2749
|
-
const secret = await getVaultSecret(db, target.secretName);
|
|
2750
|
-
const crmDeps3 = { crm: ctx.chapter.crm, db, now: () => Date.now(), newId: () => crypto.randomUUID() };
|
|
2751
|
-
if (!secret) {
|
|
2752
|
-
const error = `vault secret "${target.secretName}" is missing`;
|
|
2753
|
-
await (0, import_crm4.recordDeliveryAttempt)(crmDeps3, {
|
|
2754
|
-
recordId: record.id,
|
|
2755
|
-
targetId: target.id,
|
|
2756
|
-
status: "failed",
|
|
2757
|
-
payloadVersion: 2,
|
|
2758
|
-
error
|
|
2759
|
-
});
|
|
2760
|
-
return { id: target.id, name: target.name, ok: false, error };
|
|
2761
|
-
}
|
|
2762
|
-
let payload;
|
|
2763
|
-
try {
|
|
2764
|
-
payload = sharedRecordFromCrm(ctx.chapter.crm, record, target, ctx.chapter.id);
|
|
2765
|
-
} catch (err) {
|
|
2766
|
-
return { id: target.id, name: target.name, ok: false, error: err instanceof Error ? err.message : "record is not shareable" };
|
|
2767
|
-
}
|
|
2768
|
-
const payloadBody = JSON.stringify(payload);
|
|
2769
|
-
const destination = new URL("/api/network/shared", target.url);
|
|
2770
|
-
try {
|
|
2771
|
-
const signed = await (0, import_db3.signFederatedRequest)({
|
|
2772
|
-
secret,
|
|
2773
|
-
sender: ctx.chapter.id,
|
|
2774
|
-
method: "POST",
|
|
2775
|
-
url: destination,
|
|
2776
|
-
body: payloadBody
|
|
2777
|
-
});
|
|
2778
|
-
const res = await fetch(destination, {
|
|
2779
|
-
method: "POST",
|
|
2780
|
-
headers: { ...signed, "content-type": "application/json" },
|
|
2781
|
-
body: payloadBody,
|
|
2782
|
-
signal: AbortSignal.timeout(1e4)
|
|
2783
|
-
});
|
|
2784
|
-
const responseBody = await res.json().catch(() => ({}));
|
|
2785
|
-
if (!res.ok) {
|
|
2786
|
-
const error = responseBody.error ?? "follower rejected the record";
|
|
2787
|
-
await (0, import_crm4.recordDeliveryAttempt)(crmDeps3, {
|
|
2788
|
-
recordId: record.id,
|
|
2789
|
-
targetId: target.id,
|
|
2790
|
-
status: "failed",
|
|
2791
|
-
payloadVersion: 2,
|
|
2792
|
-
error
|
|
2793
|
-
});
|
|
2794
|
-
return { id: target.id, name: target.name, ok: false, status: res.status, error };
|
|
2795
|
-
}
|
|
2796
|
-
await (0, import_crm4.recordDeliveryAttempt)(crmDeps3, {
|
|
2797
|
-
recordId: record.id,
|
|
2798
|
-
targetId: target.id,
|
|
2799
|
-
status: "delivered",
|
|
2800
|
-
payloadVersion: 2,
|
|
2801
|
-
...responseBody.recordId ? { remoteRecordId: responseBody.recordId } : {}
|
|
2802
|
-
});
|
|
2803
|
-
return { id: target.id, name: target.name, ok: true, status: res.status, recordId: responseBody.recordId };
|
|
2804
|
-
} catch (err) {
|
|
2805
|
-
const error = err instanceof Error ? err.message : "delivery failed";
|
|
2806
|
-
await (0, import_crm4.recordDeliveryAttempt)(crmDeps3, {
|
|
2807
|
-
recordId: record.id,
|
|
2808
|
-
targetId: target.id,
|
|
2809
|
-
status: "failed",
|
|
2810
|
-
payloadVersion: 2,
|
|
2811
|
-
error
|
|
2812
|
-
});
|
|
2813
|
-
return { id: target.id, name: target.name, ok: false, error };
|
|
2814
|
-
}
|
|
2815
|
-
}
|
|
2816
2862
|
var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
2817
2863
|
if (req.method !== "POST" || url.pathname !== "/api/admin/network/push") return null;
|
|
2818
2864
|
const got = await gate4(req, env, ctx);
|
|
@@ -2830,16 +2876,418 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
2830
2876
|
if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
|
|
2831
2877
|
const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
|
|
2832
2878
|
if (targets.length !== requested.size) return json({ error: "one or more targetIds are not configured" }, 400);
|
|
2833
|
-
const record = await (0,
|
|
2879
|
+
const record = await (0, import_crm5.getRecord)({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
|
|
2834
2880
|
if (!record) return json({ error: "record not found" }, 404);
|
|
2835
2881
|
const results = await Promise.all(
|
|
2836
|
-
targets.map((target) =>
|
|
2882
|
+
targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record))
|
|
2837
2883
|
);
|
|
2838
2884
|
return json({ ok: results.every((result) => result.ok), results });
|
|
2839
2885
|
};
|
|
2840
2886
|
|
|
2887
|
+
// src/worker-routes-network-records.ts
|
|
2888
|
+
var import_crm6 = require("@odla-ai/crm");
|
|
2889
|
+
var import_db5 = require("@odla-ai/db");
|
|
2890
|
+
var IDENTIFIER = /^[a-z][a-zA-Z0-9_]*$/;
|
|
2891
|
+
var MAX_PAGE = 50;
|
|
2892
|
+
var MAX_OFFSET = 1e4;
|
|
2893
|
+
var MAX_SEARCH = 200;
|
|
2894
|
+
async function gate5(req, env, ctx) {
|
|
2895
|
+
const db = ctx.makeDb(env);
|
|
2896
|
+
const user = await ctx.verifyUser(req, env);
|
|
2897
|
+
if (!user) return { response: json({ error: "unauthorized" }, 401) };
|
|
2898
|
+
if (!await ctx.isAdmin(db, user)) return { response: json({ error: "forbidden" }, 403) };
|
|
2899
|
+
return { db, user };
|
|
2900
|
+
}
|
|
2901
|
+
function integer(value, fallback, min, max) {
|
|
2902
|
+
if (value == null || value === "") return fallback;
|
|
2903
|
+
if (!/^\d+$/.test(value)) return null;
|
|
2904
|
+
const parsed = Number(value);
|
|
2905
|
+
return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : null;
|
|
2906
|
+
}
|
|
2907
|
+
function projectRecord(record, allowed) {
|
|
2908
|
+
const fields = Object.fromEntries(
|
|
2909
|
+
allowed.flatMap((name) => Object.prototype.hasOwnProperty.call(record.fields, name) ? [[name, record.fields[name]]] : [])
|
|
2910
|
+
);
|
|
2911
|
+
return {
|
|
2912
|
+
id: record.id,
|
|
2913
|
+
type: record.type,
|
|
2914
|
+
name: record.name,
|
|
2915
|
+
...record.stage ? { stage: record.stage } : {},
|
|
2916
|
+
fields,
|
|
2917
|
+
createdAt: record.createdAt,
|
|
2918
|
+
updatedAt: record.updatedAt
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2921
|
+
var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
2922
|
+
if (req.method !== "GET" || url.pathname !== "/api/network/records") return null;
|
|
2923
|
+
const db = ctx.makeDb(env);
|
|
2924
|
+
const secret = await getVaultSecret(db, "network_share_secret");
|
|
2925
|
+
const readers = ctx.chapter.network.readers;
|
|
2926
|
+
if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
|
|
2927
|
+
const verified = await (0, import_db5.verifyFederatedRequest)(req, {
|
|
2928
|
+
secret,
|
|
2929
|
+
senders: readers.map((reader2) => reader2.id)
|
|
2930
|
+
});
|
|
2931
|
+
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
2932
|
+
const reader = readers.find((candidate) => candidate.id === verified.sender);
|
|
2933
|
+
const type = url.searchParams.get("type") ?? "";
|
|
2934
|
+
const allowed = reader?.fields[type];
|
|
2935
|
+
if (!IDENTIFIER.test(type) || !allowed) return json({ error: "record type is not shared with this reader" }, 403);
|
|
2936
|
+
const limit = integer(url.searchParams.get("limit"), 25, 1, MAX_PAGE);
|
|
2937
|
+
const offset = integer(url.searchParams.get("offset"), 0, 0, MAX_OFFSET);
|
|
2938
|
+
const search = (url.searchParams.get("search") ?? "").trim();
|
|
2939
|
+
if (limit == null || offset == null || search.length > MAX_SEARCH) {
|
|
2940
|
+
return json({ error: "invalid pagination or search parameters" }, 400);
|
|
2941
|
+
}
|
|
2942
|
+
const page = await (0, import_crm6.listRecords)(
|
|
2943
|
+
{ crm: ctx.chapter.crm, db },
|
|
2944
|
+
{
|
|
2945
|
+
type,
|
|
2946
|
+
limit,
|
|
2947
|
+
offset,
|
|
2948
|
+
...search ? { search } : {},
|
|
2949
|
+
sort: { field: "updatedAt", dir: "desc" }
|
|
2950
|
+
}
|
|
2951
|
+
);
|
|
2952
|
+
const response = {
|
|
2953
|
+
version: 1,
|
|
2954
|
+
site: { id: ctx.chapter.id, name: ctx.chapter.name },
|
|
2955
|
+
type,
|
|
2956
|
+
records: page.records.map((record) => projectRecord(record, allowed)),
|
|
2957
|
+
total: page.total,
|
|
2958
|
+
limit: page.limit,
|
|
2959
|
+
offset: page.offset
|
|
2960
|
+
};
|
|
2961
|
+
return json(response);
|
|
2962
|
+
};
|
|
2963
|
+
function targetFor(ctx, id) {
|
|
2964
|
+
return ctx.chapter.network.targets.find((target) => target.id === id);
|
|
2965
|
+
}
|
|
2966
|
+
async function readTargetRecords(db, env, ctx, target, query) {
|
|
2967
|
+
const secret = await getVaultSecret(db, target.secretName);
|
|
2968
|
+
if (!secret) return { ok: false, error: "edge secret is missing" };
|
|
2969
|
+
const destination = new URL("/api/network/records", target.url);
|
|
2970
|
+
destination.search = query.toString();
|
|
2971
|
+
try {
|
|
2972
|
+
const headers = await (0, import_db5.signFederatedRequest)({
|
|
2973
|
+
secret,
|
|
2974
|
+
sender: ctx.chapter.id,
|
|
2975
|
+
method: "GET",
|
|
2976
|
+
url: destination
|
|
2977
|
+
});
|
|
2978
|
+
const response = await fetchNetworkTarget(env, target, destination, {
|
|
2979
|
+
headers,
|
|
2980
|
+
signal: AbortSignal.timeout(1e4)
|
|
2981
|
+
});
|
|
2982
|
+
const body = await response.json().catch(() => null);
|
|
2983
|
+
if (!response.ok || !body || !("version" in body) || body.version !== 1 || !("site" in body) || body.site.id !== target.id || !Array.isArray(body.records)) {
|
|
2984
|
+
const upstream = body && "error" in body && typeof body.error === "string" ? body.error : void 0;
|
|
2985
|
+
return {
|
|
2986
|
+
ok: false,
|
|
2987
|
+
error: upstream ?? (response.ok ? "invalid follower record response" : `follower returned ${response.status}`)
|
|
2988
|
+
};
|
|
2989
|
+
}
|
|
2990
|
+
return { ok: true, page: body };
|
|
2991
|
+
} catch (error) {
|
|
2992
|
+
return { ok: false, error: error instanceof Error ? error.message : "record read failed" };
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
var handleAdminNetworkRecords = async (req, url, env, ctx) => {
|
|
2996
|
+
if (req.method !== "GET" || url.pathname !== "/api/admin/network/records") return null;
|
|
2997
|
+
const got = await gate5(req, env, ctx);
|
|
2998
|
+
if ("response" in got) return got.response;
|
|
2999
|
+
const targetId = url.searchParams.get("targetId") ?? "";
|
|
3000
|
+
const type = url.searchParams.get("type") ?? "";
|
|
3001
|
+
const target = targetFor(ctx, targetId);
|
|
3002
|
+
if (!target || !IDENTIFIER.test(type)) return json({ error: "configured targetId and record type are required" }, 400);
|
|
3003
|
+
const limit = integer(url.searchParams.get("limit"), 25, 1, MAX_PAGE);
|
|
3004
|
+
const offset = integer(url.searchParams.get("offset"), 0, 0, MAX_OFFSET);
|
|
3005
|
+
const search = (url.searchParams.get("search") ?? "").trim();
|
|
3006
|
+
if (limit == null || offset == null || search.length > MAX_SEARCH) {
|
|
3007
|
+
return json({ error: "invalid pagination or search parameters" }, 400);
|
|
3008
|
+
}
|
|
3009
|
+
const query = new URLSearchParams({ type, limit: String(limit), offset: String(offset) });
|
|
3010
|
+
if (search) query.set("search", search);
|
|
3011
|
+
const result = await readTargetRecords(
|
|
3012
|
+
got.db,
|
|
3013
|
+
env,
|
|
3014
|
+
ctx,
|
|
3015
|
+
target,
|
|
3016
|
+
query
|
|
3017
|
+
);
|
|
3018
|
+
return result.ok ? json(result.page) : json({ error: result.error }, 502);
|
|
3019
|
+
};
|
|
3020
|
+
|
|
3021
|
+
// src/worker-routes-network-notes.ts
|
|
3022
|
+
var IDENTIFIER2 = /^[a-z][a-zA-Z0-9_]*$/;
|
|
3023
|
+
var RECORD_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
|
|
3024
|
+
var MAX_NOTE = 4e3;
|
|
3025
|
+
async function gate6(req, env, ctx) {
|
|
3026
|
+
const db = ctx.makeDb(env);
|
|
3027
|
+
const user = await ctx.verifyUser(req, env);
|
|
3028
|
+
if (!user) return { response: json({ error: "unauthorized" }, 401) };
|
|
3029
|
+
if (!await ctx.isAdmin(db, user)) return { response: json({ error: "forbidden" }, 403) };
|
|
3030
|
+
return { db, user };
|
|
3031
|
+
}
|
|
3032
|
+
function noteIdentity(url, ctx) {
|
|
3033
|
+
const targetId = url.searchParams.get("targetId") ?? "";
|
|
3034
|
+
const recordType = url.searchParams.get("type") ?? "";
|
|
3035
|
+
const recordId = url.searchParams.get("recordId") ?? "";
|
|
3036
|
+
const configured = ctx.chapter.network.targets.some((target) => target.id === targetId);
|
|
3037
|
+
if (!configured || !IDENTIFIER2.test(recordType) || !RECORD_ID.test(recordId)) return null;
|
|
3038
|
+
return {
|
|
3039
|
+
targetId,
|
|
3040
|
+
recordType,
|
|
3041
|
+
recordId,
|
|
3042
|
+
recordKey: `${targetId}:${recordType}:${recordId}`
|
|
3043
|
+
};
|
|
3044
|
+
}
|
|
3045
|
+
function asNote(row) {
|
|
3046
|
+
return {
|
|
3047
|
+
id: String(row.id),
|
|
3048
|
+
targetId: String(row.targetId),
|
|
3049
|
+
recordType: String(row.recordType),
|
|
3050
|
+
recordId: String(row.recordId),
|
|
3051
|
+
body: String(row.body),
|
|
3052
|
+
authorId: String(row.authorId),
|
|
3053
|
+
...typeof row.authorEmail === "string" ? { authorEmail: row.authorEmail } : {},
|
|
3054
|
+
createdAt: Number(row.createdAt)
|
|
3055
|
+
};
|
|
3056
|
+
}
|
|
3057
|
+
var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
3058
|
+
if (url.pathname !== "/api/admin/network/notes" || req.method !== "GET" && req.method !== "POST") return null;
|
|
3059
|
+
const got = await gate6(req, env, ctx);
|
|
3060
|
+
if ("response" in got) return got.response;
|
|
3061
|
+
const identity = noteIdentity(url, ctx);
|
|
3062
|
+
if (!identity) return json({ error: "valid targetId, type, and recordId are required" }, 400);
|
|
3063
|
+
if (req.method === "GET") {
|
|
3064
|
+
const result = await got.db.query({
|
|
3065
|
+
networkNotes: {
|
|
3066
|
+
$: {
|
|
3067
|
+
where: { recordKey: identity.recordKey },
|
|
3068
|
+
order: { createdAt: "desc" },
|
|
3069
|
+
limit: 100
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
});
|
|
3073
|
+
return json({ notes: (result.networkNotes ?? []).map((row) => asNote(row)) });
|
|
3074
|
+
}
|
|
3075
|
+
const contentLength = Number(req.headers.get("content-length") ?? 0);
|
|
3076
|
+
if (contentLength > MAX_NOTE * 2) return json({ error: "note is too large" }, 413);
|
|
3077
|
+
let input;
|
|
3078
|
+
try {
|
|
3079
|
+
input = await req.json();
|
|
3080
|
+
} catch {
|
|
3081
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
3082
|
+
}
|
|
3083
|
+
const body = typeof input.body === "string" ? input.body.trim() : "";
|
|
3084
|
+
if (!body || body.length > MAX_NOTE) return json({ error: `note must be 1\u2013${MAX_NOTE} characters` }, 400);
|
|
3085
|
+
const id = crypto.randomUUID();
|
|
3086
|
+
const note = {
|
|
3087
|
+
id,
|
|
3088
|
+
targetId: identity.targetId,
|
|
3089
|
+
recordType: identity.recordType,
|
|
3090
|
+
recordId: identity.recordId,
|
|
3091
|
+
body,
|
|
3092
|
+
authorId: got.user.userId,
|
|
3093
|
+
...got.user.email ? { authorEmail: got.user.email } : {},
|
|
3094
|
+
createdAt: Date.now()
|
|
3095
|
+
};
|
|
3096
|
+
await got.db.transact([{
|
|
3097
|
+
t: "update",
|
|
3098
|
+
ns: "networkNotes",
|
|
3099
|
+
id,
|
|
3100
|
+
attrs: { ...note, recordKey: identity.recordKey }
|
|
3101
|
+
}]);
|
|
3102
|
+
return json({ note }, 201);
|
|
3103
|
+
};
|
|
3104
|
+
|
|
3105
|
+
// src/worker-routes-network-shared-notes.ts
|
|
3106
|
+
var import_crm7 = require("@odla-ai/crm");
|
|
3107
|
+
var import_db6 = require("@odla-ai/db");
|
|
3108
|
+
var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
|
|
3109
|
+
var RECORD_ID2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
|
|
3110
|
+
var MUTATION_ID = /^[A-Za-z0-9_-]{16,128}$/;
|
|
3111
|
+
var MAX_NOTE2 = 4e3;
|
|
3112
|
+
var NETWORK_META = "chapterNetwork";
|
|
3113
|
+
async function gate7(req, env, ctx) {
|
|
3114
|
+
const db = ctx.makeDb(env);
|
|
3115
|
+
const user = await ctx.verifyUser(req, env);
|
|
3116
|
+
if (!user) return { response: json({ error: "unauthorized" }, 401) };
|
|
3117
|
+
if (!await ctx.isAdmin(db, user)) return { response: json({ error: "forbidden" }, 403) };
|
|
3118
|
+
return { db, user };
|
|
3119
|
+
}
|
|
3120
|
+
function sharedNote(activity, sourceId) {
|
|
3121
|
+
const meta = activity.meta?.[NETWORK_META];
|
|
3122
|
+
if (!meta || typeof meta !== "object" || Array.isArray(meta)) return null;
|
|
3123
|
+
const value = meta;
|
|
3124
|
+
if (value.version !== 1 || value.sourceId !== sourceId || typeof value.body !== "string") return null;
|
|
3125
|
+
return {
|
|
3126
|
+
id: activity.id,
|
|
3127
|
+
sourceId,
|
|
3128
|
+
recordType: String(value.recordType ?? ""),
|
|
3129
|
+
recordId: activity.recordId,
|
|
3130
|
+
body: value.body,
|
|
3131
|
+
createdAt: activity.createdAt
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
async function listSharedNotes(db, sourceId, recordId) {
|
|
3135
|
+
const result = await db.query({
|
|
3136
|
+
[import_crm7.CRM_NS.activity]: {
|
|
3137
|
+
$: {
|
|
3138
|
+
where: {
|
|
3139
|
+
and: [
|
|
3140
|
+
{ recordId },
|
|
3141
|
+
{ authorId: `network:${sourceId}` }
|
|
3142
|
+
]
|
|
3143
|
+
},
|
|
3144
|
+
order: { createdAt: "desc" },
|
|
3145
|
+
limit: 100
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
});
|
|
3149
|
+
return (result[import_crm7.CRM_NS.activity] ?? []).flatMap((activity) => {
|
|
3150
|
+
const note = sharedNote(activity, sourceId);
|
|
3151
|
+
return note ? [note] : [];
|
|
3152
|
+
});
|
|
3153
|
+
}
|
|
3154
|
+
function sourceLabel(sourceId) {
|
|
3155
|
+
return sourceId.split("-").map((word) => word ? `${word[0]?.toUpperCase()}${word.slice(1)}` : word).join(" ");
|
|
3156
|
+
}
|
|
3157
|
+
var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
3158
|
+
if (url.pathname !== "/api/network/shared-notes" || req.method !== "GET" && req.method !== "POST") return null;
|
|
3159
|
+
if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_NOTE2 * 2) {
|
|
3160
|
+
return json({ error: "note is too large" }, 413);
|
|
3161
|
+
}
|
|
3162
|
+
const db = ctx.makeDb(env);
|
|
3163
|
+
const secret = await getVaultSecret(db, "network_share_secret");
|
|
3164
|
+
const readers = ctx.chapter.network.readers;
|
|
3165
|
+
if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
|
|
3166
|
+
const verified = await (0, import_db6.verifyFederatedRequest)(req, {
|
|
3167
|
+
secret,
|
|
3168
|
+
senders: readers.map((reader2) => reader2.id)
|
|
3169
|
+
});
|
|
3170
|
+
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
3171
|
+
const reader = readers.find((candidate) => candidate.id === verified.sender);
|
|
3172
|
+
const recordType = url.searchParams.get("type") ?? "";
|
|
3173
|
+
const recordId = url.searchParams.get("recordId") ?? "";
|
|
3174
|
+
if (!IDENTIFIER3.test(recordType) || !RECORD_ID2.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
|
|
3175
|
+
return json({ error: "shared notes are not enabled for this reader and record type" }, 403);
|
|
3176
|
+
}
|
|
3177
|
+
const record = await (0, import_crm7.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
|
|
3178
|
+
if (!record || record.type !== recordType) return json({ error: "record not found" }, 404);
|
|
3179
|
+
if (req.method === "GET") {
|
|
3180
|
+
return json({
|
|
3181
|
+
version: 1,
|
|
3182
|
+
site: { id: ctx.chapter.id, name: ctx.chapter.name },
|
|
3183
|
+
notes: await listSharedNotes(db, verified.sender, recordId)
|
|
3184
|
+
});
|
|
3185
|
+
}
|
|
3186
|
+
let input;
|
|
3187
|
+
try {
|
|
3188
|
+
input = JSON.parse(verified.body);
|
|
3189
|
+
} catch {
|
|
3190
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
3191
|
+
}
|
|
3192
|
+
const body = typeof input.body === "string" ? input.body.trim() : "";
|
|
3193
|
+
const mutationId = typeof input.mutationId === "string" ? input.mutationId : "";
|
|
3194
|
+
if (!body || body.length > MAX_NOTE2 || !MUTATION_ID.test(mutationId)) {
|
|
3195
|
+
return json({ error: `body must be 1\u2013${MAX_NOTE2} characters and mutationId must be valid` }, 400);
|
|
3196
|
+
}
|
|
3197
|
+
const createdAt = Date.now();
|
|
3198
|
+
const id = `network:${verified.sender}:${mutationId}`;
|
|
3199
|
+
const result = await (0, import_crm7.addActivity)(
|
|
3200
|
+
{
|
|
3201
|
+
crm: ctx.chapter.crm,
|
|
3202
|
+
db,
|
|
3203
|
+
now: () => createdAt,
|
|
3204
|
+
newId: () => id
|
|
3205
|
+
},
|
|
3206
|
+
{
|
|
3207
|
+
recordId,
|
|
3208
|
+
kind: "note",
|
|
3209
|
+
body: `Shared by ${sourceLabel(verified.sender)}: ${body}`,
|
|
3210
|
+
authorId: `network:${verified.sender}`,
|
|
3211
|
+
meta: {
|
|
3212
|
+
[NETWORK_META]: {
|
|
3213
|
+
version: 1,
|
|
3214
|
+
sourceId: verified.sender,
|
|
3215
|
+
recordType,
|
|
3216
|
+
body
|
|
3217
|
+
}
|
|
3218
|
+
},
|
|
3219
|
+
mutationId: id
|
|
3220
|
+
}
|
|
3221
|
+
);
|
|
3222
|
+
const note = {
|
|
3223
|
+
id,
|
|
3224
|
+
sourceId: verified.sender,
|
|
3225
|
+
recordType,
|
|
3226
|
+
recordId,
|
|
3227
|
+
body,
|
|
3228
|
+
createdAt
|
|
3229
|
+
};
|
|
3230
|
+
return json({ version: 1, site: { id: ctx.chapter.id, name: ctx.chapter.name }, note, duplicate: result.duplicate }, 201);
|
|
3231
|
+
};
|
|
3232
|
+
function targetFor2(ctx, id) {
|
|
3233
|
+
return ctx.chapter.network.targets.find((target) => target.id === id);
|
|
3234
|
+
}
|
|
3235
|
+
var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
3236
|
+
if (url.pathname !== "/api/admin/network/shared-notes" || req.method !== "GET" && req.method !== "POST") return null;
|
|
3237
|
+
const got = await gate7(req, env, ctx);
|
|
3238
|
+
if ("response" in got) return got.response;
|
|
3239
|
+
const targetId = url.searchParams.get("targetId") ?? "";
|
|
3240
|
+
const recordType = url.searchParams.get("type") ?? "";
|
|
3241
|
+
const recordId = url.searchParams.get("recordId") ?? "";
|
|
3242
|
+
const target = targetFor2(ctx, targetId);
|
|
3243
|
+
if (!target || !target.sharedNotes.includes(recordType) || !IDENTIFIER3.test(recordType) || !RECORD_ID2.test(recordId)) {
|
|
3244
|
+
return json({ error: "configured targetId, type, and recordId are required" }, 400);
|
|
3245
|
+
}
|
|
3246
|
+
let body = "";
|
|
3247
|
+
if (req.method === "POST") {
|
|
3248
|
+
let input;
|
|
3249
|
+
try {
|
|
3250
|
+
input = await req.json();
|
|
3251
|
+
} catch {
|
|
3252
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
3253
|
+
}
|
|
3254
|
+
const note = typeof input.body === "string" ? input.body.trim() : "";
|
|
3255
|
+
if (!note || note.length > MAX_NOTE2) return json({ error: `note must be 1\u2013${MAX_NOTE2} characters` }, 400);
|
|
3256
|
+
body = JSON.stringify({ body: note, mutationId: crypto.randomUUID() });
|
|
3257
|
+
}
|
|
3258
|
+
const secret = await getVaultSecret(got.db, target.secretName);
|
|
3259
|
+
if (!secret) return json({ error: "edge secret is missing" }, 502);
|
|
3260
|
+
const destination = new URL("/api/network/shared-notes", target.url);
|
|
3261
|
+
destination.search = new URLSearchParams({ type: recordType, recordId }).toString();
|
|
3262
|
+
try {
|
|
3263
|
+
const headers = await (0, import_db6.signFederatedRequest)({
|
|
3264
|
+
secret,
|
|
3265
|
+
sender: ctx.chapter.id,
|
|
3266
|
+
method: req.method,
|
|
3267
|
+
url: destination,
|
|
3268
|
+
...body ? { body } : {}
|
|
3269
|
+
});
|
|
3270
|
+
const response = await fetchNetworkTarget(env, target, destination, {
|
|
3271
|
+
method: req.method,
|
|
3272
|
+
headers: { ...headers, ...body ? { "content-type": "application/json" } : {} },
|
|
3273
|
+
...body ? { body } : {},
|
|
3274
|
+
signal: AbortSignal.timeout(1e4)
|
|
3275
|
+
});
|
|
3276
|
+
const payload = await response.json().catch(() => null);
|
|
3277
|
+
const site = payload?.site;
|
|
3278
|
+
if (!response.ok || !payload || site?.id !== target.id) {
|
|
3279
|
+
return json({
|
|
3280
|
+
error: typeof payload?.error === "string" ? payload.error : `follower returned ${response.status}`
|
|
3281
|
+
}, 502);
|
|
3282
|
+
}
|
|
3283
|
+
return json(payload, req.method === "POST" ? 201 : 200);
|
|
3284
|
+
} catch (error) {
|
|
3285
|
+
return json({ error: error instanceof Error ? error.message : "shared note request failed" }, 502);
|
|
3286
|
+
}
|
|
3287
|
+
};
|
|
3288
|
+
|
|
2841
3289
|
// src/worker-routes-formation.ts
|
|
2842
|
-
var
|
|
3290
|
+
var import_crm8 = require("@odla-ai/crm");
|
|
2843
3291
|
|
|
2844
3292
|
// src/formation.ts
|
|
2845
3293
|
function formationFields(crm, formation) {
|
|
@@ -2905,7 +3353,7 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
2905
3353
|
const db = ctx.makeDb(env);
|
|
2906
3354
|
const id = submissionId ? await applicationIdForSubmission(`formation:${ctx.chapter.id}:${submissionId}`) : crypto.randomUUID();
|
|
2907
3355
|
try {
|
|
2908
|
-
const created = await (0,
|
|
3356
|
+
const created = await (0, import_crm8.createRecord)(
|
|
2909
3357
|
{
|
|
2910
3358
|
crm: ctx.chapter.crm,
|
|
2911
3359
|
db,
|
|
@@ -2940,6 +3388,8 @@ var BUILTIN_ROUTES = [
|
|
|
2940
3388
|
handleCrm,
|
|
2941
3389
|
handleNetworkShared,
|
|
2942
3390
|
handleNetworkSnapshot,
|
|
3391
|
+
handleNetworkRecords,
|
|
3392
|
+
handleNetworkSharedNotes,
|
|
2943
3393
|
handleFormation,
|
|
2944
3394
|
handleMember,
|
|
2945
3395
|
handleSchedule,
|
|
@@ -2970,6 +3420,9 @@ var BUILTIN_ROUTES = [
|
|
|
2970
3420
|
handleAdminNetworkTargets,
|
|
2971
3421
|
handleAdminNetworkRollup,
|
|
2972
3422
|
handleAdminNetworkPush,
|
|
3423
|
+
handleAdminNetworkRecords,
|
|
3424
|
+
handleAdminNetworkNotes,
|
|
3425
|
+
handleAdminNetworkSharedNotes,
|
|
2973
3426
|
// API requests must never fall through to an SPA asset response. Hosts still
|
|
2974
3427
|
// get first refusal through options.routes, then this terminates unknown API
|
|
2975
3428
|
// paths with an explicit machine-readable 404.
|