@odla-ai/chapter 0.42.0 → 0.42.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 +20 -4
- package/dist/{copy-context-DuJkH4U9.d.ts → copy-context-BNUzbL5g.d.ts} +18 -0
- package/dist/index.cjs +60 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -1
- package/dist/index.d.ts +65 -1
- package/dist/index.js +60 -10
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/worker/index.cjs +299 -50
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +18 -0
- package/dist/worker/index.d.ts +18 -0
- package/dist/worker/index.js +292 -43
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -2
- package/runbooks/greenfield.md +3 -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, M as MemberContentDocument, E as EffectiveMemberContent, c as MemberContentTierAuthority } from '../../copy-context-
|
|
5
|
-
export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-
|
|
4
|
+
import { a as Chapter, b as ChapterCopy, M as MemberContentDocument, E as EffectiveMemberContent, c as MemberContentTierAuthority } from '../../copy-context-BNUzbL5g.js';
|
|
5
|
+
export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-BNUzbL5g.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. */
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { AdminAccountMenuProps, AdminBuilderLibraryData, AdminChrome, AdminMemberContentData, AdminNote, AdminPage, AdminResource, AdminRouteState, AdminRouteTarget, AdminRouting, AdminSection, AdminSectionContext, AdminWorkspace, AdminWorkspaceNavProps, AdmissionGrantsWorkspace, AvailabilitySectionOptions, BillingSectionOptions, BuilderLibraryAdmin, ChapterAdmin, ChapterAdminAuthAdapter, ChapterAdminAuthConfig, ChapterAdminAuthLoadContext, ChapterAdminProps, ChapterAdminUser, CollectionSectionOptions, DashboardSectionOptions, EmailSectionOptions, LeaderNetworkOverview, MeetingsSectionOptions, MemberContentAdmin, NetworkRecordsWorkspace, NetworkShareActions, NetworkShareActionsProps, Panel, PeopleSectionOptions, RecordActions, RecordActionsProps, ThemeWarning, adminFetch, adminRouteFromUrl, adminRouteHref, adminSectionFromUrl, adminSectionHref, admissionGrantsWorkspace, applicationLifecycleAdapter, availabilitySection, billingSection, builderLibrarySection, collectionSection, dashboardSection, defaultAdminSections, defaultAdminWorkspaces, emailSection, loadChapterAdminConfig, loadChapterAdminUser, meetingsSection, memberContentSection, networkRecordsWorkspace, peopleSection, useAdminResource } from './admin/index.js';
|
|
2
|
-
export { C as ChapterCopyProvider, u as useChapterCopy } from '../copy-context-
|
|
2
|
+
export { C as ChapterCopyProvider, u as useChapterCopy } from '../copy-context-BNUzbL5g.js';
|
|
3
3
|
export { ApiFn, BrandStyle, BrandStyleProps, GiftCodeRedeemResult, GiftCodeRedeemer, GiftCodeRedeemerCopy, GiftCodeRedeemerProps, GiftOfferCards, GiftOfferCardsMode, GiftOfferCardsProps, GiftPurchases, GiftPurchasesProps, JoinBookingSimulation, JoinConfig, JoinFlowState, JoinIsland, JoinIslandProps, JoinSimulation, JoinStepRenderContext, JoinSubmitRenderContext, JoinTier, JoinTierRenderContext, MEMBERSHIP_CARD_TOKEN_DEFAULTS, MemberContentArea, MemberContentAreaProps, MemberGifts, MemberProvisionalRenderContext, MembersArea, MembersAreaProps, MembershipCardTokenName, MembershipCards, MembershipCardsMode, MembershipCardsProps, PaymentPriceLines, PaymentStep, PaymentStepProps, PaymentStepSimulation, RescheduleProps, Rescheduler, Slot, SlotPicker, SlotPickerClasses, SlotPickerProps, SubResponse, TierPlaceholder, dayKey, dayLabel, fmtDate, fmtMoney, fullLabel, groupSlotsByDay, loadJoinResume, membershipApplicationHref, tierNeedsPayment, timeLabel, tzShort, useTierSelection } from './member/index.js';
|
|
4
4
|
import 'preact';
|
|
5
5
|
import '@odla-ai/crm';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as preact from 'preact';
|
|
2
2
|
import { ComponentChildren, JSX } from 'preact';
|
|
3
|
-
import { d as MemberContentProjection, e as MemberContentPageProjection, b as ChapterCopy, f as ChapterBrand } from '../../copy-context-
|
|
4
|
-
export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-
|
|
3
|
+
import { d as MemberContentProjection, e as MemberContentPageProjection, b as ChapterCopy, f as ChapterBrand } from '../../copy-context-BNUzbL5g.js';
|
|
4
|
+
export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-BNUzbL5g.js';
|
|
5
5
|
import { FieldType, FieldConditions } from '@odla-ai/crm';
|
|
6
6
|
|
|
7
7
|
/** A bookable slot: a start instant in epoch milliseconds. */
|
package/dist/worker/index.cjs
CHANGED
|
@@ -5376,12 +5376,22 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
|
5376
5376
|
const got = await gate5(req, env, ctx);
|
|
5377
5377
|
if ("response" in got) return got.response;
|
|
5378
5378
|
return json({
|
|
5379
|
-
targets: ctx.chapter.network.targets.map(({
|
|
5379
|
+
targets: ctx.chapter.network.targets.map(({
|
|
5380
|
+
id,
|
|
5381
|
+
name,
|
|
5382
|
+
url: targetUrl,
|
|
5383
|
+
fields,
|
|
5384
|
+
sharedNotes,
|
|
5385
|
+
editableFields,
|
|
5386
|
+
stageTransitions
|
|
5387
|
+
}) => ({
|
|
5380
5388
|
id,
|
|
5381
5389
|
name,
|
|
5382
5390
|
url: targetUrl,
|
|
5383
5391
|
types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS),
|
|
5384
|
-
sharedNotes
|
|
5392
|
+
sharedNotes,
|
|
5393
|
+
editableFields,
|
|
5394
|
+
stageTransitions
|
|
5385
5395
|
}))
|
|
5386
5396
|
});
|
|
5387
5397
|
};
|
|
@@ -5499,19 +5509,20 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
5499
5509
|
return json({ ok: results.every((result) => result.ok), results });
|
|
5500
5510
|
};
|
|
5501
5511
|
|
|
5502
|
-
// src/worker-routes-network-records.ts
|
|
5512
|
+
// src/worker-routes-network-records-follower.ts
|
|
5503
5513
|
var import_crm7 = require("@odla-ai/crm");
|
|
5504
5514
|
var import_db8 = require("@odla-ai/db");
|
|
5515
|
+
|
|
5516
|
+
// src/worker-routes-network-records-common.ts
|
|
5505
5517
|
var IDENTIFIER = /^[a-z][a-zA-Z0-9_]*$/;
|
|
5506
5518
|
var MAX_PAGE = 50;
|
|
5507
5519
|
var MAX_OFFSET = 1e4;
|
|
5508
5520
|
var MAX_SEARCH = 200;
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
return { db, user };
|
|
5521
|
+
var MAX_BODY = 32768;
|
|
5522
|
+
var RECORD_ID = /^[a-zA-Z0-9][a-zA-Z0-9:_-]{0,191}$/;
|
|
5523
|
+
var MUTATION_ID = /^[a-zA-Z0-9][a-zA-Z0-9:._-]{7,127}$/;
|
|
5524
|
+
function bodyTooLarge(body) {
|
|
5525
|
+
return new TextEncoder().encode(body).byteLength > MAX_BODY;
|
|
5515
5526
|
}
|
|
5516
5527
|
function integer(value, fallback, min, max) {
|
|
5517
5528
|
if (value == null || value === "") return fallback;
|
|
@@ -5519,10 +5530,22 @@ function integer(value, fallback, min, max) {
|
|
|
5519
5530
|
const parsed = Number(value);
|
|
5520
5531
|
return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : null;
|
|
5521
5532
|
}
|
|
5522
|
-
function projectRecord(record4, allowed2) {
|
|
5533
|
+
function projectRecord(record4, allowed2, editable, stagesEnabled, crm) {
|
|
5523
5534
|
const fields = Object.fromEntries(
|
|
5524
5535
|
allowed2.flatMap((name) => Object.prototype.hasOwnProperty.call(record4.fields, name) ? [[name, record4.fields[name]]] : [])
|
|
5525
5536
|
);
|
|
5537
|
+
const def = crm.type(record4.type);
|
|
5538
|
+
const editFields = editable.map((id) => {
|
|
5539
|
+
const field = def.fields[id];
|
|
5540
|
+
return {
|
|
5541
|
+
id,
|
|
5542
|
+
label: field.label ?? id,
|
|
5543
|
+
type: field.type,
|
|
5544
|
+
required: field.required ?? false,
|
|
5545
|
+
...field.options ? { options: field.options } : {}
|
|
5546
|
+
};
|
|
5547
|
+
});
|
|
5548
|
+
const allowedStages = stagesEnabled ? (def.pipeline?.stages ?? []).flatMap((stage) => crm.canTransition(record4.type, record4, stage.id) ? [] : [{ id: stage.id, label: stage.label ?? stage.id }]) : [];
|
|
5526
5549
|
return {
|
|
5527
5550
|
id: record4.id,
|
|
5528
5551
|
type: record4.type,
|
|
@@ -5530,11 +5553,66 @@ function projectRecord(record4, allowed2) {
|
|
|
5530
5553
|
...record4.stage ? { stage: record4.stage } : {},
|
|
5531
5554
|
fields,
|
|
5532
5555
|
createdAt: record4.createdAt,
|
|
5533
|
-
updatedAt: record4.updatedAt
|
|
5556
|
+
updatedAt: record4.updatedAt,
|
|
5557
|
+
revision: record4.updatedAt,
|
|
5558
|
+
...editFields.length || stagesEnabled ? { edit: { fields: editFields, allowedStages } } : {}
|
|
5534
5559
|
};
|
|
5535
5560
|
}
|
|
5561
|
+
function readerRecord(record4, reader, crm) {
|
|
5562
|
+
return projectRecord(
|
|
5563
|
+
record4,
|
|
5564
|
+
reader.fields[record4.type] ?? [],
|
|
5565
|
+
reader.editableFields[record4.type] ?? [],
|
|
5566
|
+
reader.stageTransitions.includes(record4.type),
|
|
5567
|
+
crm
|
|
5568
|
+
);
|
|
5569
|
+
}
|
|
5570
|
+
function restrictedRecord(record4, target) {
|
|
5571
|
+
if (!record4.edit) return record4;
|
|
5572
|
+
const fields = record4.edit.fields.filter((field) => target.editableFields[record4.type]?.includes(field.id));
|
|
5573
|
+
const allowedStages = target.stageTransitions.includes(record4.type) ? record4.edit.allowedStages : [];
|
|
5574
|
+
const edit = fields.length || target.stageTransitions.includes(record4.type) ? { fields, allowedStages } : void 0;
|
|
5575
|
+
return { ...record4, ...edit ? { edit } : { edit: void 0 } };
|
|
5576
|
+
}
|
|
5577
|
+
|
|
5578
|
+
// src/worker-routes-network-records-follower.ts
|
|
5579
|
+
var NETWORK_MUTATION_META = "chapterNetworkMutation";
|
|
5580
|
+
function mutationFingerprint(input) {
|
|
5581
|
+
const fields = input.fields ? Object.fromEntries(Object.entries(input.fields).sort(([a], [b]) => a.localeCompare(b))) : void 0;
|
|
5582
|
+
return JSON.stringify({
|
|
5583
|
+
recordId: input.recordId,
|
|
5584
|
+
type: input.type,
|
|
5585
|
+
expectedRevision: input.expectedRevision,
|
|
5586
|
+
expectedStage: input.expectedStage,
|
|
5587
|
+
mutationId: input.mutationId,
|
|
5588
|
+
fields,
|
|
5589
|
+
stage: input.stage,
|
|
5590
|
+
actor: input.actor
|
|
5591
|
+
});
|
|
5592
|
+
}
|
|
5593
|
+
function mutationError(error) {
|
|
5594
|
+
if (error?.code === "transact_guard_failed") {
|
|
5595
|
+
return json({ error: "record changed since it was reviewed", code: "stale_revision" }, 409);
|
|
5596
|
+
}
|
|
5597
|
+
if (error instanceof import_crm7.CrmInputError) {
|
|
5598
|
+
return json({ error: error.message, code: "invalid_transition_or_fields", ...error.fields ? { fields: error.fields } : {} }, 422);
|
|
5599
|
+
}
|
|
5600
|
+
return json({ error: "follower mutation failed", code: "mutation_failed" }, 500);
|
|
5601
|
+
}
|
|
5602
|
+
async function priorMutation(db, id) {
|
|
5603
|
+
const result = await db.query({
|
|
5604
|
+
[import_crm7.CRM_NS.activity]: { $: { where: { id }, limit: 1 } }
|
|
5605
|
+
});
|
|
5606
|
+
const row = (result[import_crm7.CRM_NS.activity] ?? [])[0];
|
|
5607
|
+
const meta = row?.meta;
|
|
5608
|
+
const receipt = meta?.[NETWORK_MUTATION_META];
|
|
5609
|
+
return typeof receipt?.fingerprint === "string" ? { fingerprint: receipt.fingerprint } : null;
|
|
5610
|
+
}
|
|
5536
5611
|
var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
5537
|
-
if (req.method !== "GET"
|
|
5612
|
+
if (url.pathname !== "/api/network/records" || req.method !== "GET" && req.method !== "PATCH") return null;
|
|
5613
|
+
if (req.method === "PATCH" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY) {
|
|
5614
|
+
return json({ error: "mutation body is too large", code: "body_too_large" }, 413);
|
|
5615
|
+
}
|
|
5538
5616
|
const db = ctx.makeDb(env);
|
|
5539
5617
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
5540
5618
|
const readers = ctx.chapter.network.readers;
|
|
@@ -5545,8 +5623,105 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
5545
5623
|
});
|
|
5546
5624
|
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
5547
5625
|
const reader = readers.find((candidate) => candidate.id === verified.sender);
|
|
5626
|
+
if (!reader) return json({ error: "unauthorized" }, 401);
|
|
5627
|
+
if (req.method === "PATCH") {
|
|
5628
|
+
if (bodyTooLarge(verified.body)) {
|
|
5629
|
+
return json({ error: "mutation body is too large", code: "body_too_large" }, 413);
|
|
5630
|
+
}
|
|
5631
|
+
let input;
|
|
5632
|
+
try {
|
|
5633
|
+
input = JSON.parse(verified.body);
|
|
5634
|
+
} catch {
|
|
5635
|
+
return json({ error: "invalid JSON body", code: "invalid_json" }, 422);
|
|
5636
|
+
}
|
|
5637
|
+
const fields = input?.fields;
|
|
5638
|
+
const hasFields = Boolean(fields && typeof fields === "object" && !Array.isArray(fields));
|
|
5639
|
+
const hasStage = typeof input?.stage === "string";
|
|
5640
|
+
const actor = input?.actor;
|
|
5641
|
+
if (!input || !IDENTIFIER.test(input.type) || !RECORD_ID.test(input.recordId) || !Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0 || !MUTATION_ID.test(input.mutationId) || !actor || typeof actor.id !== "string" || !RECORD_ID.test(actor.id) || hasFields === hasStage) {
|
|
5642
|
+
return json({ error: "invalid network mutation", code: "invalid_mutation" }, 422);
|
|
5643
|
+
}
|
|
5644
|
+
const editable = reader.editableFields[input.type];
|
|
5645
|
+
if (hasFields) {
|
|
5646
|
+
const names = Object.keys(fields);
|
|
5647
|
+
if (!editable || names.length === 0 || names.length > 32 || names.some((name) => !editable.includes(name))) {
|
|
5648
|
+
return json({ error: "profile fields are not delegated for this reader", code: "field_forbidden" }, 403);
|
|
5649
|
+
}
|
|
5650
|
+
} else if (!reader.stageTransitions.includes(input.type)) {
|
|
5651
|
+
return json({ error: "stage transitions are not delegated for this reader", code: "stage_forbidden" }, 403);
|
|
5652
|
+
}
|
|
5653
|
+
const activityId = `network-edit:${verified.sender}:${input.mutationId}`;
|
|
5654
|
+
const fingerprint = mutationFingerprint(input);
|
|
5655
|
+
const prior = await priorMutation(db, activityId);
|
|
5656
|
+
if (prior) {
|
|
5657
|
+
if (prior.fingerprint !== fingerprint) {
|
|
5658
|
+
return json({ error: "mutationId was already used for different input", code: "mutation_conflict" }, 409);
|
|
5659
|
+
}
|
|
5660
|
+
const replayed = await (0, import_crm7.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
|
|
5661
|
+
if (!replayed || replayed.type !== input.type) return json({ error: "record not found", code: "not_found" }, 404);
|
|
5662
|
+
const result = {
|
|
5663
|
+
version: 1,
|
|
5664
|
+
site: { id: ctx.chapter.id, name: ctx.chapter.name },
|
|
5665
|
+
record: readerRecord(replayed, reader, ctx.chapter.crm),
|
|
5666
|
+
duplicate: true
|
|
5667
|
+
};
|
|
5668
|
+
return json(result);
|
|
5669
|
+
}
|
|
5670
|
+
const record4 = await (0, import_crm7.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
|
|
5671
|
+
if (!record4 || record4.type !== input.type) return json({ error: "record not found", code: "not_found" }, 404);
|
|
5672
|
+
if (record4.updatedAt !== input.expectedRevision) {
|
|
5673
|
+
return json({ error: "record changed since it was reviewed", code: "stale_revision" }, 409);
|
|
5674
|
+
}
|
|
5675
|
+
if (hasStage && (input.expectedStage ?? null) !== (record4.stage ?? null)) {
|
|
5676
|
+
return json({ error: "stage changed since it was reviewed", code: "stale_stage" }, 409);
|
|
5677
|
+
}
|
|
5678
|
+
const actorEmail = typeof actor.email === "string" && actor.email.length <= 320 ? actor.email : void 0;
|
|
5679
|
+
const auditMeta = {
|
|
5680
|
+
[NETWORK_MUTATION_META]: {
|
|
5681
|
+
version: 1,
|
|
5682
|
+
sourceId: verified.sender,
|
|
5683
|
+
actorId: actor.id,
|
|
5684
|
+
...actorEmail ? { actorEmail } : {},
|
|
5685
|
+
mutationId: input.mutationId,
|
|
5686
|
+
fingerprint
|
|
5687
|
+
}
|
|
5688
|
+
};
|
|
5689
|
+
try {
|
|
5690
|
+
const deps = { crm: ctx.chapter.crm, db };
|
|
5691
|
+
const updated = hasFields ? await (0, import_crm7.updateRecord)(deps, {
|
|
5692
|
+
id: record4.id,
|
|
5693
|
+
input: fields,
|
|
5694
|
+
mutationId: activityId,
|
|
5695
|
+
expectedUpdatedAt: input.expectedRevision,
|
|
5696
|
+
audit: {
|
|
5697
|
+
id: activityId,
|
|
5698
|
+
body: `Profile edited remotely by ${verified.sender}`,
|
|
5699
|
+
authorId: `network:${verified.sender}`,
|
|
5700
|
+
meta: auditMeta
|
|
5701
|
+
}
|
|
5702
|
+
}) : await (0, import_crm7.setStage)(deps, {
|
|
5703
|
+
id: record4.id,
|
|
5704
|
+
to: input.stage,
|
|
5705
|
+
authorId: `network:${verified.sender}`,
|
|
5706
|
+
mutationId: activityId,
|
|
5707
|
+
expectedUpdatedAt: input.expectedRevision,
|
|
5708
|
+
expectedStage: input.expectedStage ?? null,
|
|
5709
|
+
activityId,
|
|
5710
|
+
activityMeta: auditMeta
|
|
5711
|
+
});
|
|
5712
|
+
const result = {
|
|
5713
|
+
version: 1,
|
|
5714
|
+
site: { id: ctx.chapter.id, name: ctx.chapter.name },
|
|
5715
|
+
record: readerRecord(updated.record, reader, ctx.chapter.crm),
|
|
5716
|
+
duplicate: updated.duplicate
|
|
5717
|
+
};
|
|
5718
|
+
return json(result);
|
|
5719
|
+
} catch (error) {
|
|
5720
|
+
return mutationError(error);
|
|
5721
|
+
}
|
|
5722
|
+
}
|
|
5548
5723
|
const type = url.searchParams.get("type") ?? "";
|
|
5549
|
-
const allowed2 = reader
|
|
5724
|
+
const allowed2 = reader.fields[type];
|
|
5550
5725
|
if (!IDENTIFIER.test(type) || !allowed2) return json({ error: "record type is not shared with this reader" }, 403);
|
|
5551
5726
|
const limit = integer(url.searchParams.get("limit"), 25, 1, MAX_PAGE);
|
|
5552
5727
|
const offset = integer(url.searchParams.get("offset"), 0, 0, MAX_OFFSET);
|
|
@@ -5568,13 +5743,23 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
5568
5743
|
version: 1,
|
|
5569
5744
|
site: { id: ctx.chapter.id, name: ctx.chapter.name },
|
|
5570
5745
|
type,
|
|
5571
|
-
records: page.records.map((record4) =>
|
|
5746
|
+
records: page.records.map((record4) => readerRecord(record4, reader, ctx.chapter.crm)),
|
|
5572
5747
|
total: page.total,
|
|
5573
5748
|
limit: page.limit,
|
|
5574
5749
|
offset: page.offset
|
|
5575
5750
|
};
|
|
5576
5751
|
return json(response);
|
|
5577
5752
|
};
|
|
5753
|
+
|
|
5754
|
+
// src/worker-routes-network-records-admin.ts
|
|
5755
|
+
var import_db9 = require("@odla-ai/db");
|
|
5756
|
+
async function gate6(req, env, ctx) {
|
|
5757
|
+
const db = ctx.makeDb(env);
|
|
5758
|
+
const user = await ctx.verifyUser(req, env);
|
|
5759
|
+
if (!user) return { response: json({ error: "unauthorized" }, 401) };
|
|
5760
|
+
if (!await ctx.isAdmin(db, user)) return { response: json({ error: "forbidden" }, 403) };
|
|
5761
|
+
return { db, user };
|
|
5762
|
+
}
|
|
5578
5763
|
function targetFor(ctx, id) {
|
|
5579
5764
|
return ctx.chapter.network.targets.find((target) => target.id === id);
|
|
5580
5765
|
}
|
|
@@ -5584,12 +5769,7 @@ async function readTargetRecords(db, env, ctx, target, query) {
|
|
|
5584
5769
|
const destination = new URL("/api/network/records", target.url);
|
|
5585
5770
|
destination.search = query.toString();
|
|
5586
5771
|
try {
|
|
5587
|
-
const headers = await (0,
|
|
5588
|
-
secret,
|
|
5589
|
-
sender: ctx.chapter.id,
|
|
5590
|
-
method: "GET",
|
|
5591
|
-
url: destination
|
|
5592
|
-
});
|
|
5772
|
+
const headers = await (0, import_db9.signFederatedRequest)({ secret, sender: ctx.chapter.id, method: "GET", url: destination });
|
|
5593
5773
|
const response = await fetchNetworkTarget(env, target, destination, {
|
|
5594
5774
|
headers,
|
|
5595
5775
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -5602,15 +5782,90 @@ async function readTargetRecords(db, env, ctx, target, query) {
|
|
|
5602
5782
|
error: upstream ?? (response.ok ? "invalid follower record response" : `follower returned ${response.status}`)
|
|
5603
5783
|
};
|
|
5604
5784
|
}
|
|
5605
|
-
return {
|
|
5785
|
+
return {
|
|
5786
|
+
ok: true,
|
|
5787
|
+
page: { ...body, records: body.records.map((record4) => restrictedRecord(record4, target)) }
|
|
5788
|
+
};
|
|
5606
5789
|
} catch (error) {
|
|
5607
5790
|
return { ok: false, error: error instanceof Error ? error.message : "record read failed" };
|
|
5608
5791
|
}
|
|
5609
5792
|
}
|
|
5793
|
+
async function mutateTargetRecord(db, env, ctx, target, input) {
|
|
5794
|
+
const fields = input.fields;
|
|
5795
|
+
if (fields) {
|
|
5796
|
+
const allowed2 = target.editableFields[input.type];
|
|
5797
|
+
if (!allowed2 || Object.keys(fields).some((name) => !allowed2.includes(name))) {
|
|
5798
|
+
return json({ error: "profile fields are not enabled for this target", code: "field_forbidden" }, 403);
|
|
5799
|
+
}
|
|
5800
|
+
} else if (!target.stageTransitions.includes(input.type)) {
|
|
5801
|
+
return json({ error: "stage transitions are not enabled for this target", code: "stage_forbidden" }, 403);
|
|
5802
|
+
}
|
|
5803
|
+
const secret = await getVaultSecret(db, target.secretName);
|
|
5804
|
+
if (!secret) return json({ error: "edge secret is missing", code: "edge_unavailable" }, 502);
|
|
5805
|
+
const destination = new URL("/api/network/records", target.url);
|
|
5806
|
+
const body = JSON.stringify(input);
|
|
5807
|
+
try {
|
|
5808
|
+
const headers = await (0, import_db9.signFederatedRequest)({
|
|
5809
|
+
secret,
|
|
5810
|
+
sender: ctx.chapter.id,
|
|
5811
|
+
method: "PATCH",
|
|
5812
|
+
url: destination,
|
|
5813
|
+
body
|
|
5814
|
+
});
|
|
5815
|
+
const response = await fetchNetworkTarget(env, target, destination, {
|
|
5816
|
+
method: "PATCH",
|
|
5817
|
+
headers: { ...headers, "content-type": "application/json" },
|
|
5818
|
+
body,
|
|
5819
|
+
signal: AbortSignal.timeout(1e4)
|
|
5820
|
+
});
|
|
5821
|
+
const payload = await response.json().catch(() => null);
|
|
5822
|
+
const site = payload && "site" in payload ? payload.site : void 0;
|
|
5823
|
+
if (!response.ok || !payload || !("record" in payload) || site && site.id !== target.id) {
|
|
5824
|
+
const status = [401, 403, 404, 409, 422].includes(response.status) ? response.status : 502;
|
|
5825
|
+
return json({
|
|
5826
|
+
error: payload && "error" in payload && typeof payload.error === "string" ? payload.error : `follower returned ${response.status}`,
|
|
5827
|
+
...payload && "code" in payload && typeof payload.code === "string" ? { code: payload.code } : {}
|
|
5828
|
+
}, status);
|
|
5829
|
+
}
|
|
5830
|
+
return json({ ...payload, record: restrictedRecord(payload.record, target) });
|
|
5831
|
+
} catch {
|
|
5832
|
+
return json({ error: "follower mutation request failed", code: "follower_unavailable" }, 502);
|
|
5833
|
+
}
|
|
5834
|
+
}
|
|
5610
5835
|
var handleAdminNetworkRecords = async (req, url, env, ctx) => {
|
|
5611
|
-
if (req.method !== "GET"
|
|
5836
|
+
if (url.pathname !== "/api/admin/network/records" || req.method !== "GET" && req.method !== "PATCH") return null;
|
|
5837
|
+
if (req.method === "PATCH" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY) {
|
|
5838
|
+
return json({ error: "mutation body is too large", code: "body_too_large" }, 413);
|
|
5839
|
+
}
|
|
5612
5840
|
const got = await gate6(req, env, ctx);
|
|
5613
5841
|
if ("response" in got) return got.response;
|
|
5842
|
+
if (req.method === "PATCH") {
|
|
5843
|
+
let input;
|
|
5844
|
+
try {
|
|
5845
|
+
const body = await req.text();
|
|
5846
|
+
if (bodyTooLarge(body)) {
|
|
5847
|
+
return json({ error: "mutation body is too large", code: "body_too_large" }, 413);
|
|
5848
|
+
}
|
|
5849
|
+
input = JSON.parse(body);
|
|
5850
|
+
} catch {
|
|
5851
|
+
return json({ error: "invalid JSON body", code: "invalid_json" }, 422);
|
|
5852
|
+
}
|
|
5853
|
+
const target2 = targetFor(ctx, input?.targetId);
|
|
5854
|
+
const hasFields = Boolean(input?.fields && typeof input.fields === "object" && !Array.isArray(input.fields));
|
|
5855
|
+
const hasStage = typeof input?.stage === "string";
|
|
5856
|
+
if (!target2 || !IDENTIFIER.test(input.type) || !RECORD_ID.test(input.recordId) || !Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0 || !MUTATION_ID.test(input.mutationId) || hasFields === hasStage) {
|
|
5857
|
+
return json({ error: "invalid network mutation", code: "invalid_mutation" }, 422);
|
|
5858
|
+
}
|
|
5859
|
+
return mutateTargetRecord(got.db, env, ctx, target2, {
|
|
5860
|
+
type: input.type,
|
|
5861
|
+
recordId: input.recordId,
|
|
5862
|
+
expectedRevision: input.expectedRevision,
|
|
5863
|
+
mutationId: input.mutationId,
|
|
5864
|
+
...input.expectedStage !== void 0 ? { expectedStage: input.expectedStage } : {},
|
|
5865
|
+
...hasFields ? { fields: input.fields } : { stage: input.stage },
|
|
5866
|
+
actor: { id: got.user.userId, ...got.user.email ? { email: got.user.email } : {} }
|
|
5867
|
+
});
|
|
5868
|
+
}
|
|
5614
5869
|
const targetId = url.searchParams.get("targetId") ?? "";
|
|
5615
5870
|
const type = url.searchParams.get("type") ?? "";
|
|
5616
5871
|
const target = targetFor(ctx, targetId);
|
|
@@ -5623,19 +5878,13 @@ var handleAdminNetworkRecords = async (req, url, env, ctx) => {
|
|
|
5623
5878
|
}
|
|
5624
5879
|
const query = new URLSearchParams({ type, limit: String(limit), offset: String(offset) });
|
|
5625
5880
|
if (search) query.set("search", search);
|
|
5626
|
-
const result = await readTargetRecords(
|
|
5627
|
-
got.db,
|
|
5628
|
-
env,
|
|
5629
|
-
ctx,
|
|
5630
|
-
target,
|
|
5631
|
-
query
|
|
5632
|
-
);
|
|
5881
|
+
const result = await readTargetRecords(got.db, env, ctx, target, query);
|
|
5633
5882
|
return result.ok ? json(result.page) : json({ error: result.error }, 502);
|
|
5634
5883
|
};
|
|
5635
5884
|
|
|
5636
5885
|
// src/worker-routes-network-notes.ts
|
|
5637
5886
|
var IDENTIFIER2 = /^[a-z][a-zA-Z0-9_]*$/;
|
|
5638
|
-
var
|
|
5887
|
+
var RECORD_ID2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
|
|
5639
5888
|
var MAX_NOTE2 = 4e3;
|
|
5640
5889
|
async function gate7(req, env, ctx) {
|
|
5641
5890
|
const db = ctx.makeDb(env);
|
|
@@ -5649,7 +5898,7 @@ function noteIdentity(url, ctx) {
|
|
|
5649
5898
|
const recordType = url.searchParams.get("type") ?? "";
|
|
5650
5899
|
const recordId = url.searchParams.get("recordId") ?? "";
|
|
5651
5900
|
const configured = ctx.chapter.network.targets.some((target) => target.id === targetId);
|
|
5652
|
-
if (!configured || !IDENTIFIER2.test(recordType) || !
|
|
5901
|
+
if (!configured || !IDENTIFIER2.test(recordType) || !RECORD_ID2.test(recordId)) return null;
|
|
5653
5902
|
return {
|
|
5654
5903
|
targetId,
|
|
5655
5904
|
recordType,
|
|
@@ -5719,10 +5968,10 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
5719
5968
|
|
|
5720
5969
|
// src/worker-routes-network-shared-notes.ts
|
|
5721
5970
|
var import_crm8 = require("@odla-ai/crm");
|
|
5722
|
-
var
|
|
5971
|
+
var import_db10 = require("@odla-ai/db");
|
|
5723
5972
|
var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
|
|
5724
|
-
var
|
|
5725
|
-
var
|
|
5973
|
+
var RECORD_ID3 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
|
|
5974
|
+
var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
5726
5975
|
var MAX_NOTE3 = 4e3;
|
|
5727
5976
|
var NETWORK_META = "chapterNetwork";
|
|
5728
5977
|
async function gate8(req, env, ctx) {
|
|
@@ -5778,7 +6027,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
5778
6027
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
5779
6028
|
const readers = ctx.chapter.network.readers;
|
|
5780
6029
|
if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
|
|
5781
|
-
const verified = await (0,
|
|
6030
|
+
const verified = await (0, import_db10.verifyFederatedRequest)(req, {
|
|
5782
6031
|
secret,
|
|
5783
6032
|
senders: readers.map((reader2) => reader2.id)
|
|
5784
6033
|
});
|
|
@@ -5786,7 +6035,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
5786
6035
|
const reader = readers.find((candidate) => candidate.id === verified.sender);
|
|
5787
6036
|
const recordType = url.searchParams.get("type") ?? "";
|
|
5788
6037
|
const recordId = url.searchParams.get("recordId") ?? "";
|
|
5789
|
-
if (!IDENTIFIER3.test(recordType) || !
|
|
6038
|
+
if (!IDENTIFIER3.test(recordType) || !RECORD_ID3.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
|
|
5790
6039
|
return json({ error: "shared notes are not enabled for this reader and record type" }, 403);
|
|
5791
6040
|
}
|
|
5792
6041
|
const record4 = await (0, import_crm8.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
|
|
@@ -5806,7 +6055,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
5806
6055
|
}
|
|
5807
6056
|
const body = typeof input.body === "string" ? input.body.trim() : "";
|
|
5808
6057
|
const mutationId = typeof input.mutationId === "string" ? input.mutationId : "";
|
|
5809
|
-
if (!body || body.length > MAX_NOTE3 || !
|
|
6058
|
+
if (!body || body.length > MAX_NOTE3 || !MUTATION_ID2.test(mutationId)) {
|
|
5810
6059
|
return json({ error: `body must be 1\u2013${MAX_NOTE3} characters and mutationId must be valid` }, 400);
|
|
5811
6060
|
}
|
|
5812
6061
|
const createdAt = Date.now();
|
|
@@ -5855,7 +6104,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
5855
6104
|
const recordType = url.searchParams.get("type") ?? "";
|
|
5856
6105
|
const recordId = url.searchParams.get("recordId") ?? "";
|
|
5857
6106
|
const target = targetFor2(ctx, targetId);
|
|
5858
|
-
if (!target || !target.sharedNotes.includes(recordType) || !IDENTIFIER3.test(recordType) || !
|
|
6107
|
+
if (!target || !target.sharedNotes.includes(recordType) || !IDENTIFIER3.test(recordType) || !RECORD_ID3.test(recordId)) {
|
|
5859
6108
|
return json({ error: "configured targetId, type, and recordId are required" }, 400);
|
|
5860
6109
|
}
|
|
5861
6110
|
let body = "";
|
|
@@ -5875,7 +6124,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
5875
6124
|
const destination = new URL("/api/network/shared-notes", target.url);
|
|
5876
6125
|
destination.search = new URLSearchParams({ type: recordType, recordId }).toString();
|
|
5877
6126
|
try {
|
|
5878
|
-
const headers = await (0,
|
|
6127
|
+
const headers = await (0, import_db10.signFederatedRequest)({
|
|
5879
6128
|
secret,
|
|
5880
6129
|
sender: ctx.chapter.id,
|
|
5881
6130
|
method: req.method,
|
|
@@ -5982,7 +6231,7 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
5982
6231
|
};
|
|
5983
6232
|
|
|
5984
6233
|
// src/worker-routes-builder-library.ts
|
|
5985
|
-
var
|
|
6234
|
+
var import_db11 = require("@odla-ai/db");
|
|
5986
6235
|
|
|
5987
6236
|
// src/builder-library-validation.ts
|
|
5988
6237
|
var ID4 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
@@ -6265,7 +6514,7 @@ async function verifySource(req, db, ctx) {
|
|
|
6265
6514
|
if (ctx.chapter.mode === "chapter") {
|
|
6266
6515
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
6267
6516
|
if (!secret) return null;
|
|
6268
|
-
const verified = await (0,
|
|
6517
|
+
const verified = await (0, import_db11.verifyFederatedRequest)(req, {
|
|
6269
6518
|
secret,
|
|
6270
6519
|
senders: sources.map((source2) => source2.sourceId)
|
|
6271
6520
|
});
|
|
@@ -6278,7 +6527,7 @@ async function verifySource(req, db, ctx) {
|
|
|
6278
6527
|
if (!target) continue;
|
|
6279
6528
|
const secret = await getVaultSecret(db, target.secretName);
|
|
6280
6529
|
if (!secret) continue;
|
|
6281
|
-
const verified = await (0,
|
|
6530
|
+
const verified = await (0, import_db11.verifyFederatedRequest)(req.clone(), { secret, senders: [source.sourceId] });
|
|
6282
6531
|
if (verified.ok) return { source, body: verified.body };
|
|
6283
6532
|
}
|
|
6284
6533
|
return null;
|
|
@@ -6467,11 +6716,11 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
6467
6716
|
};
|
|
6468
6717
|
|
|
6469
6718
|
// src/signup-control-delivery.ts
|
|
6470
|
-
var
|
|
6719
|
+
var import_db12 = require("@odla-ai/db");
|
|
6471
6720
|
async function deliverSignupControl(options) {
|
|
6472
6721
|
const destination = new URL(SIGNUP_CONTROL_PATH, options.targetUrl);
|
|
6473
6722
|
const body = JSON.stringify(options.envelope);
|
|
6474
|
-
const signed = await (0,
|
|
6723
|
+
const signed = await (0, import_db12.signFederatedRequest)({
|
|
6475
6724
|
secret: options.secret,
|
|
6476
6725
|
sender: options.sender,
|
|
6477
6726
|
method: "POST",
|
|
@@ -6492,11 +6741,11 @@ async function deliverSignupControl(options) {
|
|
|
6492
6741
|
}
|
|
6493
6742
|
|
|
6494
6743
|
// src/member-content-delivery.ts
|
|
6495
|
-
var
|
|
6744
|
+
var import_db13 = require("@odla-ai/db");
|
|
6496
6745
|
async function deliverMemberContent(options) {
|
|
6497
6746
|
const destination = new URL(MEMBER_CONTENT_CONTROL_PATH, options.targetUrl);
|
|
6498
6747
|
const body = JSON.stringify(options.envelope);
|
|
6499
|
-
const signed = await (0,
|
|
6748
|
+
const signed = await (0, import_db13.signFederatedRequest)({
|
|
6500
6749
|
secret: options.secret,
|
|
6501
6750
|
sender: options.sender,
|
|
6502
6751
|
method: "POST",
|
|
@@ -6517,11 +6766,11 @@ async function deliverMemberContent(options) {
|
|
|
6517
6766
|
}
|
|
6518
6767
|
|
|
6519
6768
|
// src/builder-library-delivery.ts
|
|
6520
|
-
var
|
|
6769
|
+
var import_db14 = require("@odla-ai/db");
|
|
6521
6770
|
async function deliverBuilderLibrary(options) {
|
|
6522
6771
|
const destination = new URL(BUILDER_LIBRARY_CONTROL_PATH, options.targetUrl);
|
|
6523
6772
|
const body = JSON.stringify(options.envelope);
|
|
6524
|
-
const signed = await (0,
|
|
6773
|
+
const signed = await (0, import_db14.signFederatedRequest)({ secret: options.secret, sender: options.sender, method: "POST", url: destination, body });
|
|
6525
6774
|
const response = await (options.fetch ?? globalThis.fetch)(destination, { method: "POST", headers: { ...signed, "content-type": "application/json" }, body });
|
|
6526
6775
|
const result = await response.json();
|
|
6527
6776
|
if (!response.ok || !("ok" in result) || result.ok !== true) {
|
|
@@ -6532,11 +6781,11 @@ async function deliverBuilderLibrary(options) {
|
|
|
6532
6781
|
}
|
|
6533
6782
|
|
|
6534
6783
|
// src/admission-grants-delivery.ts
|
|
6535
|
-
var
|
|
6784
|
+
var import_db15 = require("@odla-ai/db");
|
|
6536
6785
|
async function deliverAdmissionGrants(options) {
|
|
6537
6786
|
const destination = new URL(ADMISSION_GRANTS_PATH, options.targetUrl);
|
|
6538
6787
|
const body = JSON.stringify(options.envelope);
|
|
6539
|
-
const signed = await (0,
|
|
6788
|
+
const signed = await (0, import_db15.signFederatedRequest)({
|
|
6540
6789
|
secret: options.secret,
|
|
6541
6790
|
sender: options.sender,
|
|
6542
6791
|
method: "POST",
|