@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.
@@ -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-Vof2eTMt.js';
5
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-Vof2eTMt.js';
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 };
@@ -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-4QZEAWQL.js";
30
+ } from "../../chunk-L2SA47IO.js";
29
31
  import {
30
32
  ChapterCopyProvider,
31
33
  useChapterCopy
32
- } from "../../chunk-OIWCKF2G.js";
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
@@ -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-Vof2eTMt.js';
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-QBTFQUDL.js";
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-4QZEAWQL.js";
46
+ } from "../chunk-L2SA47IO.js";
45
47
  import {
46
48
  BrandStyle,
47
49
  ChapterCopyProvider,
48
50
  useChapterCopy
49
- } from "../chunk-OIWCKF2G.js";
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-Vof2eTMt.js';
4
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-Vof2eTMt.js';
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. */
@@ -13,12 +13,12 @@ import {
13
13
  loadJoinResume,
14
14
  timeLabel,
15
15
  tzShort
16
- } from "../../chunk-QBTFQUDL.js";
16
+ } from "../../chunk-IOHVFBXH.js";
17
17
  import {
18
18
  BrandStyle,
19
19
  ChapterCopyProvider,
20
20
  useChapterCopy
21
- } from "../../chunk-OIWCKF2G.js";
21
+ } from "../../chunk-JAWTIROV.js";
22
22
  export {
23
23
  BrandStyle,
24
24
  ChapterCopyProvider,
@@ -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 gate5 = await adminGate(req, env, ctx);
1927
- if (gate5 instanceof Response) return gate5;
1928
- const result = await backfillCrm(crmDeps2(gate5.db, ctx));
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 gate5 = await adminGate(req, env, ctx);
1934
- if (gate5 instanceof Response) return gate5;
1935
- const { db } = gate5;
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 gate5 = await adminGate(req, env, ctx);
1987
- if (gate5 instanceof Response) return gate5;
1988
- const { db } = gate5;
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 gate5 = await adminGate(req, env, ctx);
2000
- if (gate5 instanceof Response) return gate5;
2001
- const { db, actor } = gate5;
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-formation.ts
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, import_crm6.createRecord)(
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.