@odla-ai/chapter 0.26.1 → 0.27.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 +51 -3
- 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-Vof2eTMt.d.ts → copy-context-efEMF_aD.d.ts} +33 -5
- package/dist/index.cjs +90 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +84 -11
- package/dist/index.d.ts +84 -11
- package/dist/index.js +90 -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 +424 -16
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +35 -7
- package/dist/worker/index.d.ts +35 -7
- package/dist/worker/index.js +426 -14
- 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/ui/admin/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as preact from 'preact';
|
|
2
2
|
import { ComponentChildren, JSX } from 'preact';
|
|
3
3
|
import { CrmClient, Crm, CrmRecord } from '@odla-ai/crm';
|
|
4
|
-
import { a as Chapter, b as ChapterCopy } from '../../copy-context-
|
|
5
|
-
export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-
|
|
4
|
+
import { a as Chapter, b as ChapterCopy } from '../../copy-context-efEMF_aD.js';
|
|
5
|
+
export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-efEMF_aD.js';
|
|
6
6
|
import { CrmLifecycleAdapter, CrmWorkspaceMasterContext, CrmWorkspaceRecordContext, RecordPanelTab } from '@odla-ai/crm/ui';
|
|
7
7
|
|
|
8
8
|
/** Normalized public auth configuration consumed by the Clerk gate. */
|
|
@@ -267,6 +267,15 @@ interface NetworkShareActionsProps {
|
|
|
267
267
|
* targets. Targets with an explicit type allowlist hide incompatible records. */
|
|
268
268
|
declare function NetworkShareActions(props: NetworkShareActionsProps): preact.JSX.Element | null;
|
|
269
269
|
|
|
270
|
+
/** Leader workspace for browsing follower-owned records and attaching local,
|
|
271
|
+
* leader-private notes without importing or mutating the follower record. */
|
|
272
|
+
declare function NetworkRecordsWorkspace(props: {
|
|
273
|
+
chapter: Chapter;
|
|
274
|
+
ctx: AdminSectionContext;
|
|
275
|
+
}): preact.JSX.Element;
|
|
276
|
+
/** Reusable default-workspace factory for a leader ChapterAdmin. */
|
|
277
|
+
declare function networkRecordsWorkspace(chapter: Chapter): AdminWorkspace;
|
|
278
|
+
|
|
270
279
|
/** Aggregate-only leader overview over signed follower snapshots. */
|
|
271
280
|
declare function LeaderNetworkOverview({ ctx }: {
|
|
272
281
|
ctx: AdminSectionContext;
|
|
@@ -304,4 +313,4 @@ declare function AdminNote({ children }: {
|
|
|
304
313
|
children: ComponentChildren;
|
|
305
314
|
}): JSX.Element;
|
|
306
315
|
|
|
307
|
-
export { type AdminAccountMenuProps, type AdminChrome, AdminNote, AdminPage, type AdminResource, type AdminRouteState, type AdminRouteTarget, type AdminRouting, type AdminSection, type AdminSectionContext, type AdminWorkspace, type AdminWorkspaceNavProps, type AvailabilitySectionOptions, type BillingSectionOptions, ChapterAdmin, type ChapterAdminAuthAdapter, type ChapterAdminAuthConfig, type ChapterAdminAuthLoadContext, type ChapterAdminProps, type ChapterAdminUser, type CollectionSectionOptions, type DashboardSectionOptions, type EmailSectionOptions, LeaderNetworkOverview, type MeetingsSectionOptions, NetworkShareActions, type NetworkShareActionsProps, Panel, type PeopleSectionOptions, RecordActions, type RecordActionsProps, ThemeWarning, adminFetch, adminRouteFromUrl, adminRouteHref, adminSectionFromUrl, adminSectionHref, applicationLifecycleAdapter, availabilitySection, billingSection, collectionSection, dashboardSection, defaultAdminSections, defaultAdminWorkspaces, emailSection, loadChapterAdminConfig, loadChapterAdminUser, meetingsSection, peopleSection, useAdminResource };
|
|
316
|
+
export { type AdminAccountMenuProps, type AdminChrome, AdminNote, AdminPage, type AdminResource, type AdminRouteState, type AdminRouteTarget, type AdminRouting, type AdminSection, type AdminSectionContext, type AdminWorkspace, type AdminWorkspaceNavProps, type AvailabilitySectionOptions, type BillingSectionOptions, ChapterAdmin, type ChapterAdminAuthAdapter, type ChapterAdminAuthConfig, type ChapterAdminAuthLoadContext, type ChapterAdminProps, type ChapterAdminUser, type CollectionSectionOptions, type DashboardSectionOptions, type EmailSectionOptions, LeaderNetworkOverview, type MeetingsSectionOptions, NetworkRecordsWorkspace, NetworkShareActions, type NetworkShareActionsProps, Panel, type PeopleSectionOptions, RecordActions, type RecordActionsProps, ThemeWarning, adminFetch, adminRouteFromUrl, adminRouteHref, adminSectionFromUrl, adminSectionHref, applicationLifecycleAdapter, availabilitySection, billingSection, collectionSection, dashboardSection, defaultAdminSections, defaultAdminWorkspaces, emailSection, loadChapterAdminConfig, loadChapterAdminUser, meetingsSection, networkRecordsWorkspace, peopleSection, useAdminResource };
|
package/dist/ui/admin/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
AdminPage,
|
|
4
4
|
ChapterAdmin,
|
|
5
5
|
LeaderNetworkOverview,
|
|
6
|
+
NetworkRecordsWorkspace,
|
|
6
7
|
NetworkShareActions,
|
|
7
8
|
Panel,
|
|
8
9
|
RecordActions,
|
|
@@ -23,19 +24,21 @@ import {
|
|
|
23
24
|
loadChapterAdminConfig,
|
|
24
25
|
loadChapterAdminUser,
|
|
25
26
|
meetingsSection,
|
|
27
|
+
networkRecordsWorkspace,
|
|
26
28
|
peopleSection,
|
|
27
29
|
useAdminResource
|
|
28
|
-
} from "../../chunk-
|
|
30
|
+
} from "../../chunk-L2SA47IO.js";
|
|
29
31
|
import {
|
|
30
32
|
ChapterCopyProvider,
|
|
31
33
|
useChapterCopy
|
|
32
|
-
} from "../../chunk-
|
|
34
|
+
} from "../../chunk-JAWTIROV.js";
|
|
33
35
|
export {
|
|
34
36
|
AdminNote,
|
|
35
37
|
AdminPage,
|
|
36
38
|
ChapterAdmin,
|
|
37
39
|
ChapterCopyProvider,
|
|
38
40
|
LeaderNetworkOverview,
|
|
41
|
+
NetworkRecordsWorkspace,
|
|
39
42
|
NetworkShareActions,
|
|
40
43
|
Panel,
|
|
41
44
|
RecordActions,
|
|
@@ -56,6 +59,7 @@ export {
|
|
|
56
59
|
loadChapterAdminConfig,
|
|
57
60
|
loadChapterAdminUser,
|
|
58
61
|
meetingsSection,
|
|
62
|
+
networkRecordsWorkspace,
|
|
59
63
|
peopleSection,
|
|
60
64
|
useAdminResource,
|
|
61
65
|
useChapterCopy
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { AdminAccountMenuProps, AdminChrome, AdminNote, AdminPage, AdminResource, AdminRouteState, AdminRouteTarget, AdminRouting, AdminSection, AdminSectionContext, AdminWorkspace, AdminWorkspaceNavProps, AvailabilitySectionOptions, BillingSectionOptions, ChapterAdmin, ChapterAdminAuthAdapter, ChapterAdminAuthConfig, ChapterAdminAuthLoadContext, ChapterAdminProps, ChapterAdminUser, CollectionSectionOptions, DashboardSectionOptions, EmailSectionOptions, LeaderNetworkOverview, MeetingsSectionOptions, NetworkShareActions, NetworkShareActionsProps, Panel, PeopleSectionOptions, RecordActions, RecordActionsProps, ThemeWarning, adminFetch, adminRouteFromUrl, adminRouteHref, adminSectionFromUrl, adminSectionHref, applicationLifecycleAdapter, availabilitySection, billingSection, collectionSection, dashboardSection, defaultAdminSections, defaultAdminWorkspaces, emailSection, loadChapterAdminConfig, loadChapterAdminUser, meetingsSection, peopleSection, useAdminResource } from './admin/index.js';
|
|
2
|
-
export { C as ChapterCopyProvider, u as useChapterCopy } from '../copy-context-
|
|
1
|
+
export { AdminAccountMenuProps, AdminChrome, AdminNote, AdminPage, AdminResource, AdminRouteState, AdminRouteTarget, AdminRouting, AdminSection, AdminSectionContext, AdminWorkspace, AdminWorkspaceNavProps, AvailabilitySectionOptions, BillingSectionOptions, ChapterAdmin, ChapterAdminAuthAdapter, ChapterAdminAuthConfig, ChapterAdminAuthLoadContext, ChapterAdminProps, ChapterAdminUser, CollectionSectionOptions, DashboardSectionOptions, EmailSectionOptions, LeaderNetworkOverview, MeetingsSectionOptions, NetworkRecordsWorkspace, NetworkShareActions, NetworkShareActionsProps, Panel, PeopleSectionOptions, RecordActions, RecordActionsProps, ThemeWarning, adminFetch, adminRouteFromUrl, adminRouteHref, adminSectionFromUrl, adminSectionHref, applicationLifecycleAdapter, availabilitySection, billingSection, collectionSection, dashboardSection, defaultAdminSections, defaultAdminWorkspaces, emailSection, loadChapterAdminConfig, loadChapterAdminUser, meetingsSection, networkRecordsWorkspace, peopleSection, useAdminResource } from './admin/index.js';
|
|
2
|
+
export { C as ChapterCopyProvider, u as useChapterCopy } from '../copy-context-efEMF_aD.js';
|
|
3
3
|
export { ApiFn, BrandStyle, BrandStyleProps, JoinConfig, JoinFlowState, JoinIsland, JoinIslandProps, JoinStepRenderContext, JoinSubmitRenderContext, MemberProvisionalRenderContext, MembersArea, MembersAreaProps, PaymentPriceLines, PaymentStep, PaymentStepProps, RescheduleProps, Rescheduler, Slot, SlotPicker, SlotPickerClasses, SlotPickerProps, dayKey, dayLabel, fmtDate, fmtMoney, fullLabel, groupSlotsByDay, loadJoinResume, timeLabel, tzShort } from './member/index.js';
|
|
4
4
|
import 'preact';
|
|
5
5
|
import '@odla-ai/crm';
|
package/dist/ui/index.js
CHANGED
|
@@ -13,12 +13,13 @@ import {
|
|
|
13
13
|
loadJoinResume,
|
|
14
14
|
timeLabel,
|
|
15
15
|
tzShort
|
|
16
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-IOHVFBXH.js";
|
|
17
17
|
import {
|
|
18
18
|
AdminNote,
|
|
19
19
|
AdminPage,
|
|
20
20
|
ChapterAdmin,
|
|
21
21
|
LeaderNetworkOverview,
|
|
22
|
+
NetworkRecordsWorkspace,
|
|
22
23
|
NetworkShareActions,
|
|
23
24
|
Panel,
|
|
24
25
|
RecordActions,
|
|
@@ -39,14 +40,15 @@ import {
|
|
|
39
40
|
loadChapterAdminConfig,
|
|
40
41
|
loadChapterAdminUser,
|
|
41
42
|
meetingsSection,
|
|
43
|
+
networkRecordsWorkspace,
|
|
42
44
|
peopleSection,
|
|
43
45
|
useAdminResource
|
|
44
|
-
} from "../chunk-
|
|
46
|
+
} from "../chunk-L2SA47IO.js";
|
|
45
47
|
import {
|
|
46
48
|
BrandStyle,
|
|
47
49
|
ChapterCopyProvider,
|
|
48
50
|
useChapterCopy
|
|
49
|
-
} from "../chunk-
|
|
51
|
+
} from "../chunk-JAWTIROV.js";
|
|
50
52
|
export {
|
|
51
53
|
AdminNote,
|
|
52
54
|
AdminPage,
|
|
@@ -56,6 +58,7 @@ export {
|
|
|
56
58
|
JoinIsland,
|
|
57
59
|
LeaderNetworkOverview,
|
|
58
60
|
MembersArea,
|
|
61
|
+
NetworkRecordsWorkspace,
|
|
59
62
|
NetworkShareActions,
|
|
60
63
|
Panel,
|
|
61
64
|
PaymentStep,
|
|
@@ -86,6 +89,7 @@ export {
|
|
|
86
89
|
loadChapterAdminUser,
|
|
87
90
|
loadJoinResume,
|
|
88
91
|
meetingsSection,
|
|
92
|
+
networkRecordsWorkspace,
|
|
89
93
|
peopleSection,
|
|
90
94
|
timeLabel,
|
|
91
95
|
tzShort,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as preact from 'preact';
|
|
2
2
|
import { ComponentChildren, JSX } from 'preact';
|
|
3
|
-
import { b as ChapterCopy, c as ChapterBrand } from '../../copy-context-
|
|
4
|
-
export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-
|
|
3
|
+
import { b as ChapterCopy, c as ChapterBrand } from '../../copy-context-efEMF_aD.js';
|
|
4
|
+
export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-efEMF_aD.js';
|
|
5
5
|
import '@odla-ai/crm';
|
|
6
6
|
|
|
7
7
|
/** A bookable slot: a start instant in epoch milliseconds. */
|
package/dist/ui/member/index.js
CHANGED
|
@@ -13,12 +13,12 @@ import {
|
|
|
13
13
|
loadJoinResume,
|
|
14
14
|
timeLabel,
|
|
15
15
|
tzShort
|
|
16
|
-
} from "../../chunk-
|
|
16
|
+
} from "../../chunk-IOHVFBXH.js";
|
|
17
17
|
import {
|
|
18
18
|
BrandStyle,
|
|
19
19
|
ChapterCopyProvider,
|
|
20
20
|
useChapterCopy
|
|
21
|
-
} from "../../chunk-
|
|
21
|
+
} from "../../chunk-JAWTIROV.js";
|
|
22
22
|
export {
|
|
23
23
|
BrandStyle,
|
|
24
24
|
ChapterCopyProvider,
|
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();
|
|
@@ -2761,11 +2761,12 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
|
2761
2761
|
const got = await gate4(req, env, ctx);
|
|
2762
2762
|
if ("response" in got) return got.response;
|
|
2763
2763
|
return json({
|
|
2764
|
-
targets: ctx.chapter.network.targets.map(({ id, name, url: targetUrl, fields }) => ({
|
|
2764
|
+
targets: ctx.chapter.network.targets.map(({ id, name, url: targetUrl, fields, sharedNotes }) => ({
|
|
2765
2765
|
id,
|
|
2766
2766
|
name,
|
|
2767
2767
|
url: targetUrl,
|
|
2768
|
-
types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS)
|
|
2768
|
+
types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS),
|
|
2769
|
+
sharedNotes
|
|
2769
2770
|
}))
|
|
2770
2771
|
});
|
|
2771
2772
|
};
|
|
@@ -2883,8 +2884,410 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
2883
2884
|
return json({ ok: results.every((result) => result.ok), results });
|
|
2884
2885
|
};
|
|
2885
2886
|
|
|
2886
|
-
// src/worker-routes-
|
|
2887
|
+
// src/worker-routes-network-records.ts
|
|
2887
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
|
+
|
|
3289
|
+
// src/worker-routes-formation.ts
|
|
3290
|
+
var import_crm8 = require("@odla-ai/crm");
|
|
2888
3291
|
|
|
2889
3292
|
// src/formation.ts
|
|
2890
3293
|
function formationFields(crm, formation) {
|
|
@@ -2950,7 +3353,7 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
2950
3353
|
const db = ctx.makeDb(env);
|
|
2951
3354
|
const id = submissionId ? await applicationIdForSubmission(`formation:${ctx.chapter.id}:${submissionId}`) : crypto.randomUUID();
|
|
2952
3355
|
try {
|
|
2953
|
-
const created = await (0,
|
|
3356
|
+
const created = await (0, import_crm8.createRecord)(
|
|
2954
3357
|
{
|
|
2955
3358
|
crm: ctx.chapter.crm,
|
|
2956
3359
|
db,
|
|
@@ -2985,6 +3388,8 @@ var BUILTIN_ROUTES = [
|
|
|
2985
3388
|
handleCrm,
|
|
2986
3389
|
handleNetworkShared,
|
|
2987
3390
|
handleNetworkSnapshot,
|
|
3391
|
+
handleNetworkRecords,
|
|
3392
|
+
handleNetworkSharedNotes,
|
|
2988
3393
|
handleFormation,
|
|
2989
3394
|
handleMember,
|
|
2990
3395
|
handleSchedule,
|
|
@@ -3015,6 +3420,9 @@ var BUILTIN_ROUTES = [
|
|
|
3015
3420
|
handleAdminNetworkTargets,
|
|
3016
3421
|
handleAdminNetworkRollup,
|
|
3017
3422
|
handleAdminNetworkPush,
|
|
3423
|
+
handleAdminNetworkRecords,
|
|
3424
|
+
handleAdminNetworkNotes,
|
|
3425
|
+
handleAdminNetworkSharedNotes,
|
|
3018
3426
|
// API requests must never fall through to an SPA asset response. Hosts still
|
|
3019
3427
|
// get first refusal through options.routes, then this terminates unknown API
|
|
3020
3428
|
// paths with an explicit machine-readable 404.
|