@odla-ai/chapter 0.25.8 → 0.26.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 +148 -15
- package/dist/{chunk-4FW35E3P.js → chunk-4QZEAWQL.js} +170 -45
- package/dist/chunk-4QZEAWQL.js.map +1 -0
- package/dist/{chunk-WGQO4WDJ.js → chunk-OIWCKF2G.js} +20 -3
- package/dist/chunk-OIWCKF2G.js.map +1 -0
- package/dist/{chunk-WHA3MUDQ.js → chunk-QBTFQUDL.js} +2 -2
- package/dist/{copy-context-DI21CYQ3.d.ts → copy-context-Vof2eTMt.d.ts} +78 -42
- package/dist/index.cjs +335 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +175 -58
- package/dist/index.d.ts +175 -58
- package/dist/index.js +329 -20
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +8 -3
- package/dist/ui/admin/index.js +4 -2
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +5 -3
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/ui/member/index.js +2 -2
- package/dist/worker/index.cjs +552 -216
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +78 -42
- package/dist/worker/index.d.ts +78 -42
- package/dist/worker/index.js +557 -216
- package/dist/worker/index.js.map +1 -1
- package/package.json +3 -3
- package/runbooks/adopt-existing.md +12 -1
- package/runbooks/greenfield.md +27 -6
- package/dist/chunk-4FW35E3P.js.map +0 -1
- package/dist/chunk-WGQO4WDJ.js.map +0 -1
- /package/dist/{chunk-WHA3MUDQ.js.map → chunk-QBTFQUDL.js.map} +0 -0
package/dist/worker/index.cjs
CHANGED
|
@@ -263,160 +263,6 @@ function joinConfig(group, paymentsReady) {
|
|
|
263
263
|
};
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
// src/network.ts
|
|
267
|
-
var import_crm = require("@odla-ai/crm");
|
|
268
|
-
var DEFAULT_SHARE_FIELDS = {
|
|
269
|
-
person: ["name", "email", "firstName", "lastName", "phone", "linkedin"],
|
|
270
|
-
company: ["name", "domain", "industry", "location", "linkedin", "notes"]
|
|
271
|
-
};
|
|
272
|
-
function sharedPersonInput(person) {
|
|
273
|
-
const email = person.email.toLowerCase();
|
|
274
|
-
const fullName = [person.firstName, person.lastName].filter(Boolean).join(" ").trim();
|
|
275
|
-
const input = { name: person.name ?? fullName ?? email, email };
|
|
276
|
-
if (input.name === "") input.name = email;
|
|
277
|
-
if (person.firstName) input.firstName = person.firstName;
|
|
278
|
-
if (person.lastName) input.lastName = person.lastName;
|
|
279
|
-
if (person.phone) input.phone = person.phone;
|
|
280
|
-
if (person.linkedin) input.linkedin = person.linkedin;
|
|
281
|
-
return input;
|
|
282
|
-
}
|
|
283
|
-
function shortHash(value) {
|
|
284
|
-
let a = 2166136261;
|
|
285
|
-
let b = 2654435769;
|
|
286
|
-
for (let i = 0; i < value.length; i += 1) {
|
|
287
|
-
const n = value.charCodeAt(i);
|
|
288
|
-
a = Math.imul(a ^ n, 16777619);
|
|
289
|
-
b = Math.imul(b ^ n, 2246822507);
|
|
290
|
-
}
|
|
291
|
-
return `${(a >>> 0).toString(36)}${(b >>> 0).toString(36)}`;
|
|
292
|
-
}
|
|
293
|
-
function networkSourceTag(type, hubRecordId) {
|
|
294
|
-
const typeKey = type.toLowerCase();
|
|
295
|
-
const readable = /^[a-z0-9_-]+$/.test(hubRecordId);
|
|
296
|
-
const raw = `network:${typeKey}:${hubRecordId}`;
|
|
297
|
-
if (readable && raw.length <= 64) return raw;
|
|
298
|
-
return `network:${typeKey.slice(0, 20)}:${shortHash(`${type}\0${hubRecordId}`)}`;
|
|
299
|
-
}
|
|
300
|
-
function normalizeSharedRecord(record) {
|
|
301
|
-
if ("input" in record) return { version: 1, type: record.type, hubRecordId: record.hubRecordId, input: record.input };
|
|
302
|
-
if ("type" in record && record.type === "company") {
|
|
303
|
-
const input = { name: record.name };
|
|
304
|
-
for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
|
|
305
|
-
if (record[key]) input[key] = record[key];
|
|
306
|
-
}
|
|
307
|
-
return { version: 1, type: "company", hubRecordId: record.hubRecordId, input };
|
|
308
|
-
}
|
|
309
|
-
return { version: 1, type: "person", hubRecordId: record.hubRecordId, input: sharedPersonInput(record) };
|
|
310
|
-
}
|
|
311
|
-
function sharedRecordFromCrm(crm, record, target) {
|
|
312
|
-
if (target.fields && !target.fields[record.type]) {
|
|
313
|
-
throw new Error(`${target.name} does not accept "${record.type}" records`);
|
|
314
|
-
}
|
|
315
|
-
const def = crm.type(record.type);
|
|
316
|
-
const fields = target.fields?.[record.type] ?? DEFAULT_SHARE_FIELDS[record.type];
|
|
317
|
-
if (!fields) {
|
|
318
|
-
throw new Error(`${target.name} requires an explicit field allowlist for "${record.type}" records`);
|
|
319
|
-
}
|
|
320
|
-
const nameField = def.nameField ?? "name";
|
|
321
|
-
const input = {};
|
|
322
|
-
for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
|
|
323
|
-
const value = record.fields?.[field];
|
|
324
|
-
if (value !== void 0) input[field] = value;
|
|
325
|
-
}
|
|
326
|
-
if (input[nameField] === void 0) input[nameField] = record.name;
|
|
327
|
-
return { version: 1, type: record.type, hubRecordId: record.id, input };
|
|
328
|
-
}
|
|
329
|
-
async function upsertPerson(deps, opts) {
|
|
330
|
-
const email = opts.email.toLowerCase();
|
|
331
|
-
const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
332
|
-
const { crm_record } = await deps.db.query({ crm_record: { $: { where: { type: "person", primaryEmail: email }, limit: 1 } } });
|
|
333
|
-
const existing = crm_record?.[0];
|
|
334
|
-
if (existing && typeof existing.id === "string") {
|
|
335
|
-
await (0, import_crm.updateRecord)(crmDeps3, { id: existing.id, input: opts.input });
|
|
336
|
-
return { recordId: existing.id };
|
|
337
|
-
}
|
|
338
|
-
const created = await (0, import_crm.createRecord)(crmDeps3, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
339
|
-
return { recordId: created.id };
|
|
340
|
-
}
|
|
341
|
-
async function findSharedRecord(deps, record, tag) {
|
|
342
|
-
const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
343
|
-
const mappedId = mapped.crm_tag?.[0]?.recordId;
|
|
344
|
-
if (typeof mappedId === "string") {
|
|
345
|
-
const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
|
|
346
|
-
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
347
|
-
}
|
|
348
|
-
const def = deps.crm.type(record.type);
|
|
349
|
-
const emailField = def.emailField;
|
|
350
|
-
if (emailField && typeof record.input[emailField] === "string") {
|
|
351
|
-
const primaryEmail = record.input[emailField].toLowerCase();
|
|
352
|
-
const found = await deps.db.query({
|
|
353
|
-
crm_record: { $: { where: { type: record.type, primaryEmail }, limit: 1 } }
|
|
354
|
-
});
|
|
355
|
-
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
356
|
-
}
|
|
357
|
-
const domain = record.input.domain;
|
|
358
|
-
const domainSlot = def.fields.domain?.slot;
|
|
359
|
-
if (typeof domain === "string" && domainSlot) {
|
|
360
|
-
const found = await deps.db.query({
|
|
361
|
-
crm_record: { $: { where: { type: record.type, [domainSlot]: domain }, limit: 1 } }
|
|
362
|
-
});
|
|
363
|
-
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
364
|
-
}
|
|
365
|
-
const nameField = def.nameField ?? "name";
|
|
366
|
-
const name = record.input[nameField];
|
|
367
|
-
if (record.type === "company" && typeof name === "string" && name.trim()) {
|
|
368
|
-
const found = await deps.db.query({
|
|
369
|
-
crm_record: { $: { where: { type: record.type, name: name.trim() }, limit: 1 } }
|
|
370
|
-
});
|
|
371
|
-
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
372
|
-
}
|
|
373
|
-
return void 0;
|
|
374
|
-
}
|
|
375
|
-
async function projectSharedRecord(deps, shared) {
|
|
376
|
-
const record = normalizeSharedRecord(shared);
|
|
377
|
-
if (!record.type.trim() || !record.hubRecordId.trim()) {
|
|
378
|
-
throw new Error("type and hubRecordId must be non-empty");
|
|
379
|
-
}
|
|
380
|
-
const tag = networkSourceTag(record.type, record.hubRecordId);
|
|
381
|
-
const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
382
|
-
const existing = await findSharedRecord(deps, record, tag);
|
|
383
|
-
let recordId;
|
|
384
|
-
if (existing && typeof existing.id === "string") {
|
|
385
|
-
await (0, import_crm.updateRecord)(crmDeps3, { id: existing.id, input: record.input });
|
|
386
|
-
recordId = existing.id;
|
|
387
|
-
} else {
|
|
388
|
-
recordId = `network_${shortHash(`${record.type}\0${record.hubRecordId}`)}`;
|
|
389
|
-
await (0, import_crm.createRecord)({ ...crmDeps3, newId: () => recordId }, {
|
|
390
|
-
type: record.type,
|
|
391
|
-
input: record.input,
|
|
392
|
-
mutationId: `share-create:${tag}`
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
await deps.db.transact(
|
|
396
|
-
[{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt: deps.now() } }],
|
|
397
|
-
{ mutationId: `share-map:${tag}:${recordId}` }
|
|
398
|
-
);
|
|
399
|
-
return { recordId };
|
|
400
|
-
}
|
|
401
|
-
async function projectApplicant(deps, applicant) {
|
|
402
|
-
const base = sharedPersonInput({
|
|
403
|
-
email: applicant.email,
|
|
404
|
-
firstName: applicant.firstName,
|
|
405
|
-
lastName: applicant.lastName,
|
|
406
|
-
phone: applicant.phone,
|
|
407
|
-
linkedin: applicant.linkedin,
|
|
408
|
-
hubRecordId: applicant.applicationId
|
|
409
|
-
});
|
|
410
|
-
const mutationId = `apply:${applicant.applicationId}`;
|
|
411
|
-
const extra = applicant.extra ?? {};
|
|
412
|
-
if (Object.keys(extra).length === 0) return upsertPerson(deps, { email: applicant.email, input: base, mutationId });
|
|
413
|
-
try {
|
|
414
|
-
return await upsertPerson(deps, { email: applicant.email, input: { ...base, ...extra }, mutationId });
|
|
415
|
-
} catch {
|
|
416
|
-
return upsertPerson(deps, { email: applicant.email, input: base, mutationId });
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
266
|
// src/scheduling.ts
|
|
421
267
|
var SCHEDULING_DEFAULTS = {
|
|
422
268
|
slotMinutes: 45,
|
|
@@ -645,6 +491,210 @@ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
|
645
491
|
return { ...healed, refreshed };
|
|
646
492
|
}
|
|
647
493
|
|
|
494
|
+
// src/network.ts
|
|
495
|
+
var import_crm = require("@odla-ai/crm");
|
|
496
|
+
|
|
497
|
+
// src/network-contract.ts
|
|
498
|
+
var DEFAULT_SHARE_FIELDS = {
|
|
499
|
+
person: ["name", "email", "firstName", "lastName", "phone", "linkedin"],
|
|
500
|
+
company: ["name", "domain", "industry", "location", "linkedin", "notes"]
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
// src/network.ts
|
|
504
|
+
function sharedPersonInput(person) {
|
|
505
|
+
const email = person.email.toLowerCase();
|
|
506
|
+
const fullName = [person.firstName, person.lastName].filter(Boolean).join(" ").trim();
|
|
507
|
+
const input = { name: person.name ?? fullName ?? email, email };
|
|
508
|
+
if (input.name === "") input.name = email;
|
|
509
|
+
if (person.firstName) input.firstName = person.firstName;
|
|
510
|
+
if (person.lastName) input.lastName = person.lastName;
|
|
511
|
+
if (person.phone) input.phone = person.phone;
|
|
512
|
+
if (person.linkedin) input.linkedin = person.linkedin;
|
|
513
|
+
return input;
|
|
514
|
+
}
|
|
515
|
+
function shortHash(value) {
|
|
516
|
+
let a = 2166136261;
|
|
517
|
+
let b = 2654435769;
|
|
518
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
519
|
+
const n = value.charCodeAt(i);
|
|
520
|
+
a = Math.imul(a ^ n, 16777619);
|
|
521
|
+
b = Math.imul(b ^ n, 2246822507);
|
|
522
|
+
}
|
|
523
|
+
return `${(a >>> 0).toString(36)}${(b >>> 0).toString(36)}`;
|
|
524
|
+
}
|
|
525
|
+
function networkSourceTag(type, hubRecordId, sourceId) {
|
|
526
|
+
const typeKey = type.toLowerCase();
|
|
527
|
+
const readable = /^[a-z0-9_-]+$/.test(hubRecordId);
|
|
528
|
+
const prefix = sourceId ? `network:${sourceId.toLowerCase()}:${typeKey}` : `network:${typeKey}`;
|
|
529
|
+
const raw = `${prefix}:${hubRecordId}`;
|
|
530
|
+
if (readable && raw.length <= 64) return raw;
|
|
531
|
+
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
532
|
+
}
|
|
533
|
+
function normalizeSharedRecord(record, fallbackSourceId = "legacy-source") {
|
|
534
|
+
if ("input" in record && record.version === 2) {
|
|
535
|
+
return {
|
|
536
|
+
version: 2,
|
|
537
|
+
sourceId: record.source.siteId,
|
|
538
|
+
sourceRecordId: record.source.recordId,
|
|
539
|
+
type: record.type,
|
|
540
|
+
input: record.input
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
if ("input" in record) {
|
|
544
|
+
return {
|
|
545
|
+
version: 1,
|
|
546
|
+
sourceId: fallbackSourceId,
|
|
547
|
+
sourceRecordId: record.hubRecordId,
|
|
548
|
+
type: record.type,
|
|
549
|
+
input: record.input
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
if ("type" in record && record.type === "company") {
|
|
553
|
+
const input = { name: record.name };
|
|
554
|
+
for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
|
|
555
|
+
if (record[key]) input[key] = record[key];
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
version: 1,
|
|
559
|
+
sourceId: fallbackSourceId,
|
|
560
|
+
sourceRecordId: record.hubRecordId,
|
|
561
|
+
type: "company",
|
|
562
|
+
input
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
return {
|
|
566
|
+
version: 1,
|
|
567
|
+
sourceId: fallbackSourceId,
|
|
568
|
+
sourceRecordId: record.hubRecordId,
|
|
569
|
+
type: "person",
|
|
570
|
+
input: sharedPersonInput(record)
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function sharedRecordFromCrm(crm, record, target, sourceId) {
|
|
574
|
+
if (target.fields && !target.fields[record.type]) {
|
|
575
|
+
throw new Error(`${target.name} does not accept "${record.type}" records`);
|
|
576
|
+
}
|
|
577
|
+
const def = crm.type(record.type);
|
|
578
|
+
const fields = target.fields?.[record.type] ?? DEFAULT_SHARE_FIELDS[record.type];
|
|
579
|
+
if (!fields) {
|
|
580
|
+
throw new Error(`${target.name} requires an explicit field allowlist for "${record.type}" records`);
|
|
581
|
+
}
|
|
582
|
+
const nameField = def.nameField ?? "name";
|
|
583
|
+
const input = {};
|
|
584
|
+
for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
|
|
585
|
+
const value = record.fields?.[field];
|
|
586
|
+
if (value !== void 0) input[field] = value;
|
|
587
|
+
}
|
|
588
|
+
if (input[nameField] === void 0) input[nameField] = record.name;
|
|
589
|
+
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record.id }, type: record.type, input } : { version: 1, type: record.type, hubRecordId: record.id, input };
|
|
590
|
+
}
|
|
591
|
+
async function upsertPerson(deps, opts) {
|
|
592
|
+
const email = opts.email.toLowerCase();
|
|
593
|
+
const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
594
|
+
const { crm_record } = await deps.db.query({ crm_record: { $: { where: { type: "person", primaryEmail: email }, limit: 1 } } });
|
|
595
|
+
const existing = crm_record?.[0];
|
|
596
|
+
if (existing && typeof existing.id === "string") {
|
|
597
|
+
await (0, import_crm.updateRecord)(crmDeps3, { id: existing.id, input: opts.input });
|
|
598
|
+
return { recordId: existing.id };
|
|
599
|
+
}
|
|
600
|
+
const created = await (0, import_crm.createRecord)(crmDeps3, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
601
|
+
return { recordId: created.id };
|
|
602
|
+
}
|
|
603
|
+
async function findSharedRecord(deps, record, tag) {
|
|
604
|
+
const structured = await (0, import_crm.getRecordByOrigin)(
|
|
605
|
+
{ crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
|
|
606
|
+
record.sourceId,
|
|
607
|
+
record.sourceRecordId
|
|
608
|
+
);
|
|
609
|
+
if (structured) return structured;
|
|
610
|
+
const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
611
|
+
const mappedId = mapped.crm_tag?.[0]?.recordId;
|
|
612
|
+
if (typeof mappedId === "string") {
|
|
613
|
+
const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
|
|
614
|
+
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
615
|
+
}
|
|
616
|
+
const def = deps.crm.type(record.type);
|
|
617
|
+
const emailField = def.emailField;
|
|
618
|
+
if (emailField && typeof record.input[emailField] === "string") {
|
|
619
|
+
const primaryEmail = record.input[emailField].toLowerCase();
|
|
620
|
+
const found = await deps.db.query({
|
|
621
|
+
crm_record: { $: { where: { type: record.type, primaryEmail }, limit: 1 } }
|
|
622
|
+
});
|
|
623
|
+
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
624
|
+
}
|
|
625
|
+
const domain = record.input.domain;
|
|
626
|
+
const domainSlot = def.fields.domain?.slot;
|
|
627
|
+
if (typeof domain === "string" && domainSlot) {
|
|
628
|
+
const found = await deps.db.query({
|
|
629
|
+
crm_record: { $: { where: { type: record.type, [domainSlot]: domain }, limit: 1 } }
|
|
630
|
+
});
|
|
631
|
+
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
632
|
+
}
|
|
633
|
+
const nameField = def.nameField ?? "name";
|
|
634
|
+
const name = record.input[nameField];
|
|
635
|
+
if (record.type === "company" && typeof name === "string" && name.trim()) {
|
|
636
|
+
const found = await deps.db.query({
|
|
637
|
+
crm_record: { $: { where: { type: record.type, name: name.trim() }, limit: 1 } }
|
|
638
|
+
});
|
|
639
|
+
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
640
|
+
}
|
|
641
|
+
return void 0;
|
|
642
|
+
}
|
|
643
|
+
async function projectSharedRecord(deps, shared, options = {}) {
|
|
644
|
+
const record = normalizeSharedRecord(shared, options.sourceId);
|
|
645
|
+
if (!record.type.trim() || !record.sourceId.trim() || !record.sourceRecordId.trim()) {
|
|
646
|
+
throw new Error("type, source site id, and source record id must be non-empty");
|
|
647
|
+
}
|
|
648
|
+
if (options.sourceId && options.sourceId !== record.sourceId) {
|
|
649
|
+
throw new Error("signed sender does not match payload source");
|
|
650
|
+
}
|
|
651
|
+
const tag = networkSourceTag(record.type, record.sourceRecordId, record.version === 2 ? record.sourceId : void 0);
|
|
652
|
+
const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
653
|
+
const existing = await findSharedRecord(deps, record, tag);
|
|
654
|
+
let recordId;
|
|
655
|
+
if (existing && typeof existing.id === "string") {
|
|
656
|
+
await (0, import_crm.updateRecord)(crmDeps3, { id: existing.id, input: record.input });
|
|
657
|
+
recordId = existing.id;
|
|
658
|
+
} else {
|
|
659
|
+
recordId = `network_${shortHash(`${record.sourceId}\0${record.type}\0${record.sourceRecordId}`)}`;
|
|
660
|
+
await (0, import_crm.createRecord)({ ...crmDeps3, newId: () => recordId }, {
|
|
661
|
+
type: record.type,
|
|
662
|
+
input: record.input,
|
|
663
|
+
mutationId: `share-create:${tag}`
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
await deps.db.transact(
|
|
667
|
+
[{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt: deps.now() } }],
|
|
668
|
+
{ mutationId: `share-map:${tag}:${recordId}` }
|
|
669
|
+
);
|
|
670
|
+
await (0, import_crm.upsertRecordOrigin)(crmDeps3, {
|
|
671
|
+
recordId,
|
|
672
|
+
sourceId: record.sourceId,
|
|
673
|
+
sourceRecordId: record.sourceRecordId,
|
|
674
|
+
...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
|
|
675
|
+
payloadVersion: record.version
|
|
676
|
+
});
|
|
677
|
+
return { recordId };
|
|
678
|
+
}
|
|
679
|
+
async function projectApplicant(deps, applicant) {
|
|
680
|
+
const base = sharedPersonInput({
|
|
681
|
+
email: applicant.email,
|
|
682
|
+
firstName: applicant.firstName,
|
|
683
|
+
lastName: applicant.lastName,
|
|
684
|
+
phone: applicant.phone,
|
|
685
|
+
linkedin: applicant.linkedin,
|
|
686
|
+
hubRecordId: applicant.applicationId
|
|
687
|
+
});
|
|
688
|
+
const mutationId = `apply:${applicant.applicationId}`;
|
|
689
|
+
const extra = applicant.extra ?? {};
|
|
690
|
+
if (Object.keys(extra).length === 0) return upsertPerson(deps, { email: applicant.email, input: base, mutationId });
|
|
691
|
+
try {
|
|
692
|
+
return await upsertPerson(deps, { email: applicant.email, input: { ...base, ...extra }, mutationId });
|
|
693
|
+
} catch {
|
|
694
|
+
return upsertPerson(deps, { email: applicant.email, input: base, mutationId });
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
648
698
|
// src/email.ts
|
|
649
699
|
function render(template, vars) {
|
|
650
700
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
|
|
@@ -951,44 +1001,6 @@ var handleCrm = async (req, url, env, ctx) => {
|
|
|
951
1001
|
if (res) return res;
|
|
952
1002
|
return json({ error: "not found" }, 404);
|
|
953
1003
|
};
|
|
954
|
-
var handleNetworkShared = async (req, url, env, ctx) => {
|
|
955
|
-
if (req.method !== "POST" || url.pathname !== "/api/network/shared") return null;
|
|
956
|
-
const db = ctx.makeDb(env);
|
|
957
|
-
const secret = await getVaultSecret(db, "network_share_secret");
|
|
958
|
-
const provided = (req.headers.get("authorization") ?? "").replace(/^Bearer /, "");
|
|
959
|
-
if (!secret || provided.length !== secret.length || provided !== secret) {
|
|
960
|
-
return json({ error: "unauthorized" }, 401);
|
|
961
|
-
}
|
|
962
|
-
let payload;
|
|
963
|
-
try {
|
|
964
|
-
payload = JSON.parse(await req.text());
|
|
965
|
-
} catch {
|
|
966
|
-
return json({ error: "invalid JSON body" }, 400);
|
|
967
|
-
}
|
|
968
|
-
if (typeof payload.hubRecordId !== "string" || !payload.hubRecordId.trim()) {
|
|
969
|
-
return json({ error: "hubRecordId is required" }, 400);
|
|
970
|
-
}
|
|
971
|
-
if ("input" in payload) {
|
|
972
|
-
if (payload.version !== 1 || typeof payload.type !== "string" || !payload.type.trim() || !payload.input || typeof payload.input !== "object" || Array.isArray(payload.input)) {
|
|
973
|
-
return json({ error: "version 1, type, and input are required" }, 400);
|
|
974
|
-
}
|
|
975
|
-
} else if (payload.type !== "company" && typeof payload.email !== "string") {
|
|
976
|
-
return json({ error: "legacy person shares require email" }, 400);
|
|
977
|
-
} else if (payload.type === "company" && typeof payload.name !== "string") {
|
|
978
|
-
return json({ error: "business shares require name" }, 400);
|
|
979
|
-
}
|
|
980
|
-
try {
|
|
981
|
-
const record = normalizeSharedRecord(payload);
|
|
982
|
-
const { recordId } = await projectSharedRecord(
|
|
983
|
-
{ crm: ctx.chapter.crm, db, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
984
|
-
record
|
|
985
|
-
);
|
|
986
|
-
return json({ recordId, type: record.type });
|
|
987
|
-
} catch (err) {
|
|
988
|
-
const message = err instanceof Error ? err.message : "invalid shared record";
|
|
989
|
-
return json({ error: message }, 400);
|
|
990
|
-
}
|
|
991
|
-
};
|
|
992
1004
|
var handleMember = async (req, url, env, ctx) => {
|
|
993
1005
|
const chapter = ctx.chapter;
|
|
994
1006
|
if (chapter.mode !== "chapter") return null;
|
|
@@ -1085,6 +1097,62 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
1085
1097
|
return null;
|
|
1086
1098
|
};
|
|
1087
1099
|
|
|
1100
|
+
// src/worker-routes-network-receive.ts
|
|
1101
|
+
var import_db2 = require("@odla-ai/db");
|
|
1102
|
+
var handleNetworkShared = async (req, url, env, ctx) => {
|
|
1103
|
+
if (req.method !== "POST" || url.pathname !== "/api/network/shared") return null;
|
|
1104
|
+
const db = ctx.makeDb(env);
|
|
1105
|
+
const secret = await getVaultSecret(db, "network_share_secret");
|
|
1106
|
+
if (!secret) return json({ error: "unauthorized" }, 401);
|
|
1107
|
+
let raw;
|
|
1108
|
+
let sourceId;
|
|
1109
|
+
if (req.headers.has(import_db2.FEDERATION_HEADERS.version)) {
|
|
1110
|
+
const verified = await (0, import_db2.verifyFederatedRequest)(req, { secret });
|
|
1111
|
+
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
1112
|
+
raw = verified.body;
|
|
1113
|
+
sourceId = verified.sender;
|
|
1114
|
+
} else {
|
|
1115
|
+
const provided = (req.headers.get("authorization") ?? "").replace(/^Bearer /, "");
|
|
1116
|
+
if (provided.length !== secret.length || provided !== secret) {
|
|
1117
|
+
return json({ error: "unauthorized" }, 401);
|
|
1118
|
+
}
|
|
1119
|
+
raw = await req.text();
|
|
1120
|
+
}
|
|
1121
|
+
let payload;
|
|
1122
|
+
try {
|
|
1123
|
+
payload = JSON.parse(raw);
|
|
1124
|
+
} catch {
|
|
1125
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
1126
|
+
}
|
|
1127
|
+
if ("input" in payload) {
|
|
1128
|
+
const v1 = payload.version === 1 && typeof payload.hubRecordId === "string" && payload.hubRecordId.trim();
|
|
1129
|
+
const source = payload.source;
|
|
1130
|
+
const v2 = payload.version === 2 && source && typeof source.siteId === "string" && source.siteId.trim() && typeof source.recordId === "string" && source.recordId.trim();
|
|
1131
|
+
if (!v1 && !v2 || typeof payload.type !== "string" || !payload.type.trim() || !payload.input || typeof payload.input !== "object" || Array.isArray(payload.input)) {
|
|
1132
|
+
return json({ error: "a supported version, source record, type, and input are required" }, 400);
|
|
1133
|
+
}
|
|
1134
|
+
} else if (typeof payload.hubRecordId !== "string" || !payload.hubRecordId.trim()) {
|
|
1135
|
+
return json({ error: "hubRecordId is required" }, 400);
|
|
1136
|
+
} else if (payload.type !== "company" && typeof payload.email !== "string") {
|
|
1137
|
+
return json({ error: "legacy person shares require email" }, 400);
|
|
1138
|
+
} else if (payload.type === "company" && typeof payload.name !== "string") {
|
|
1139
|
+
return json({ error: "business shares require name" }, 400);
|
|
1140
|
+
}
|
|
1141
|
+
try {
|
|
1142
|
+
const { recordId } = await projectSharedRecord(
|
|
1143
|
+
{ crm: ctx.chapter.crm, db, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
1144
|
+
payload,
|
|
1145
|
+
sourceId ? { sourceId } : {}
|
|
1146
|
+
);
|
|
1147
|
+
const record = normalizeSharedRecord(payload, sourceId);
|
|
1148
|
+
return json({ recordId, type: record.type });
|
|
1149
|
+
} catch (error) {
|
|
1150
|
+
return json({
|
|
1151
|
+
error: error instanceof Error ? error.message : "invalid shared record"
|
|
1152
|
+
}, 400);
|
|
1153
|
+
}
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1088
1156
|
// src/crm-sync.ts
|
|
1089
1157
|
var import_crm3 = require("@odla-ai/crm");
|
|
1090
1158
|
var str = (v) => typeof v === "string" ? v : v == null ? "" : String(v);
|
|
@@ -2569,7 +2637,118 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
2569
2637
|
};
|
|
2570
2638
|
|
|
2571
2639
|
// src/worker-routes-network.ts
|
|
2640
|
+
var import_crm5 = require("@odla-ai/crm");
|
|
2641
|
+
var import_db4 = require("@odla-ai/db");
|
|
2642
|
+
|
|
2643
|
+
// src/worker-network-fetch.ts
|
|
2644
|
+
function targetFetcher(env, target) {
|
|
2645
|
+
if (!target.binding) return null;
|
|
2646
|
+
const candidate = env[target.binding];
|
|
2647
|
+
if (typeof candidate !== "object" || candidate === null || !("fetch" in candidate) || typeof candidate.fetch !== "function") {
|
|
2648
|
+
throw new Error(`service binding "${target.binding}" is unavailable`);
|
|
2649
|
+
}
|
|
2650
|
+
return candidate;
|
|
2651
|
+
}
|
|
2652
|
+
function fetchNetworkTarget(env, target, destination, init) {
|
|
2653
|
+
const binding = targetFetcher(env, target);
|
|
2654
|
+
return binding ? binding.fetch(new Request(destination, init)) : globalThis.fetch(destination, init);
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
// src/worker-network-push.ts
|
|
2572
2658
|
var import_crm4 = require("@odla-ai/crm");
|
|
2659
|
+
var import_db3 = require("@odla-ai/db");
|
|
2660
|
+
async function pushNetworkRecord(db, env, ctx, target, record) {
|
|
2661
|
+
const secret = await getVaultSecret(db, target.secretName);
|
|
2662
|
+
const crmDeps3 = {
|
|
2663
|
+
crm: ctx.chapter.crm,
|
|
2664
|
+
db,
|
|
2665
|
+
now: () => Date.now(),
|
|
2666
|
+
newId: () => crypto.randomUUID()
|
|
2667
|
+
};
|
|
2668
|
+
if (!secret) {
|
|
2669
|
+
const error = `vault secret "${target.secretName}" is missing`;
|
|
2670
|
+
await (0, import_crm4.recordDeliveryAttempt)(crmDeps3, {
|
|
2671
|
+
recordId: record.id,
|
|
2672
|
+
targetId: target.id,
|
|
2673
|
+
status: "failed",
|
|
2674
|
+
payloadVersion: 2,
|
|
2675
|
+
error
|
|
2676
|
+
});
|
|
2677
|
+
return { id: target.id, name: target.name, ok: false, error };
|
|
2678
|
+
}
|
|
2679
|
+
let payload;
|
|
2680
|
+
try {
|
|
2681
|
+
payload = sharedRecordFromCrm(ctx.chapter.crm, record, target, ctx.chapter.id);
|
|
2682
|
+
} catch (err) {
|
|
2683
|
+
return {
|
|
2684
|
+
id: target.id,
|
|
2685
|
+
name: target.name,
|
|
2686
|
+
ok: false,
|
|
2687
|
+
error: err instanceof Error ? err.message : "record is not shareable"
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
const payloadBody = JSON.stringify(payload);
|
|
2691
|
+
const destination = new URL("/api/network/shared", target.url);
|
|
2692
|
+
try {
|
|
2693
|
+
const signed = await (0, import_db3.signFederatedRequest)({
|
|
2694
|
+
secret,
|
|
2695
|
+
sender: ctx.chapter.id,
|
|
2696
|
+
method: "POST",
|
|
2697
|
+
url: destination,
|
|
2698
|
+
body: payloadBody
|
|
2699
|
+
});
|
|
2700
|
+
const res = await fetchNetworkTarget(env, target, destination, {
|
|
2701
|
+
method: "POST",
|
|
2702
|
+
headers: { ...signed, "content-type": "application/json" },
|
|
2703
|
+
body: payloadBody,
|
|
2704
|
+
signal: AbortSignal.timeout(1e4)
|
|
2705
|
+
});
|
|
2706
|
+
const responseBody = await res.json().catch(() => ({}));
|
|
2707
|
+
if (!res.ok) {
|
|
2708
|
+
const error = responseBody.error ?? "follower rejected the record";
|
|
2709
|
+
await (0, import_crm4.recordDeliveryAttempt)(crmDeps3, {
|
|
2710
|
+
recordId: record.id,
|
|
2711
|
+
targetId: target.id,
|
|
2712
|
+
status: "failed",
|
|
2713
|
+
payloadVersion: 2,
|
|
2714
|
+
error
|
|
2715
|
+
});
|
|
2716
|
+
return {
|
|
2717
|
+
id: target.id,
|
|
2718
|
+
name: target.name,
|
|
2719
|
+
ok: false,
|
|
2720
|
+
status: res.status,
|
|
2721
|
+
error
|
|
2722
|
+
};
|
|
2723
|
+
}
|
|
2724
|
+
await (0, import_crm4.recordDeliveryAttempt)(crmDeps3, {
|
|
2725
|
+
recordId: record.id,
|
|
2726
|
+
targetId: target.id,
|
|
2727
|
+
status: "delivered",
|
|
2728
|
+
payloadVersion: 2,
|
|
2729
|
+
...responseBody.recordId ? { remoteRecordId: responseBody.recordId } : {}
|
|
2730
|
+
});
|
|
2731
|
+
return {
|
|
2732
|
+
id: target.id,
|
|
2733
|
+
name: target.name,
|
|
2734
|
+
ok: true,
|
|
2735
|
+
status: res.status,
|
|
2736
|
+
recordId: responseBody.recordId
|
|
2737
|
+
};
|
|
2738
|
+
} catch (err) {
|
|
2739
|
+
const error = err instanceof Error ? err.message : "delivery failed";
|
|
2740
|
+
await (0, import_crm4.recordDeliveryAttempt)(crmDeps3, {
|
|
2741
|
+
recordId: record.id,
|
|
2742
|
+
targetId: target.id,
|
|
2743
|
+
status: "failed",
|
|
2744
|
+
payloadVersion: 2,
|
|
2745
|
+
error
|
|
2746
|
+
});
|
|
2747
|
+
return { id: target.id, name: target.name, ok: false, error };
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
// src/worker-routes-network.ts
|
|
2573
2752
|
async function gate4(req, env, ctx) {
|
|
2574
2753
|
const db = ctx.makeDb(env);
|
|
2575
2754
|
const user = await ctx.verifyUser(req, env);
|
|
@@ -2590,35 +2769,95 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
|
2590
2769
|
}))
|
|
2591
2770
|
});
|
|
2592
2771
|
};
|
|
2593
|
-
async
|
|
2772
|
+
var handleNetworkSnapshot = async (req, url, env, ctx) => {
|
|
2773
|
+
if (req.method !== "GET" || url.pathname !== "/api/network/snapshot") return null;
|
|
2774
|
+
const db = ctx.makeDb(env);
|
|
2775
|
+
const secret = await getVaultSecret(db, "network_share_secret");
|
|
2776
|
+
if (!secret) return json({ error: "unauthorized" }, 401);
|
|
2777
|
+
const verified = await (0, import_db4.verifyFederatedRequest)(req, { secret });
|
|
2778
|
+
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
2779
|
+
const types = await Promise.all(
|
|
2780
|
+
Object.keys(ctx.chapter.crm.config.types).map((type) => (0, import_crm5.typeSummary)({ crm: ctx.chapter.crm, db }, type))
|
|
2781
|
+
);
|
|
2782
|
+
const snapshot = {
|
|
2783
|
+
version: 1,
|
|
2784
|
+
site: { id: ctx.chapter.id, name: ctx.chapter.name, mode: ctx.chapter.mode },
|
|
2785
|
+
generatedAt: Date.now(),
|
|
2786
|
+
types
|
|
2787
|
+
};
|
|
2788
|
+
return json(snapshot);
|
|
2789
|
+
};
|
|
2790
|
+
async function snapshotOne(db, env, ctx, target) {
|
|
2594
2791
|
const secret = await getVaultSecret(db, target.secretName);
|
|
2595
|
-
if (!secret) return { id: target.id, name: target.name,
|
|
2596
|
-
|
|
2792
|
+
if (!secret) return { id: target.id, name: target.name, url: target.url, available: false, error: "edge secret is missing" };
|
|
2793
|
+
const destination = new URL("/api/network/snapshot", target.url);
|
|
2597
2794
|
try {
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
headers
|
|
2606
|
-
body: JSON.stringify(payload),
|
|
2795
|
+
const headers = await (0, import_db4.signFederatedRequest)({
|
|
2796
|
+
secret,
|
|
2797
|
+
sender: ctx.chapter.id,
|
|
2798
|
+
method: "GET",
|
|
2799
|
+
url: destination
|
|
2800
|
+
});
|
|
2801
|
+
const response = await fetchNetworkTarget(env, target, destination, {
|
|
2802
|
+
headers,
|
|
2607
2803
|
signal: AbortSignal.timeout(1e4)
|
|
2608
2804
|
});
|
|
2609
|
-
const body = await
|
|
2610
|
-
if (!
|
|
2611
|
-
|
|
2805
|
+
const body = await response.json().catch(() => null);
|
|
2806
|
+
if (!response.ok || !body || !("version" in body) || body.version !== 1 || !("site" in body) || body.site.id !== target.id || !Array.isArray(body.types)) {
|
|
2807
|
+
const upstream = body && "error" in body && typeof body.error === "string" ? body.error : void 0;
|
|
2808
|
+
return {
|
|
2809
|
+
id: target.id,
|
|
2810
|
+
name: target.name,
|
|
2811
|
+
url: target.url,
|
|
2812
|
+
available: false,
|
|
2813
|
+
error: upstream ?? (response.ok ? "invalid follower snapshot" : `follower returned ${response.status}`)
|
|
2814
|
+
};
|
|
2612
2815
|
}
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2816
|
+
return {
|
|
2817
|
+
id: target.id,
|
|
2818
|
+
name: target.name,
|
|
2819
|
+
url: target.url,
|
|
2820
|
+
available: true,
|
|
2821
|
+
generatedAt: body.generatedAt,
|
|
2822
|
+
types: body.types
|
|
2823
|
+
};
|
|
2824
|
+
} catch (error) {
|
|
2825
|
+
return {
|
|
2826
|
+
id: target.id,
|
|
2827
|
+
name: target.name,
|
|
2828
|
+
url: target.url,
|
|
2829
|
+
available: false,
|
|
2830
|
+
error: error instanceof Error ? error.message : "snapshot failed"
|
|
2831
|
+
};
|
|
2620
2832
|
}
|
|
2621
2833
|
}
|
|
2834
|
+
var handleAdminNetworkRollup = async (req, url, env, ctx) => {
|
|
2835
|
+
if (req.method !== "GET" || url.pathname !== "/api/admin/network/rollup") return null;
|
|
2836
|
+
const got = await gate4(req, env, ctx);
|
|
2837
|
+
if ("response" in got) return got.response;
|
|
2838
|
+
const targets = await Promise.all(
|
|
2839
|
+
ctx.chapter.network.targets.map((target) => snapshotOne(got.db, env, ctx, target))
|
|
2840
|
+
);
|
|
2841
|
+
const byType = /* @__PURE__ */ new Map();
|
|
2842
|
+
for (const target of targets) {
|
|
2843
|
+
for (const type of target.types ?? []) {
|
|
2844
|
+
const aggregate = byType.get(type.type) ?? { type: type.type, total: 0, stages: {} };
|
|
2845
|
+
aggregate.total += type.total;
|
|
2846
|
+
for (const [stage, count] of Object.entries(type.stages)) {
|
|
2847
|
+
aggregate.stages[stage] = (aggregate.stages[stage] ?? 0) + count;
|
|
2848
|
+
}
|
|
2849
|
+
byType.set(type.type, aggregate);
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
const rollup = {
|
|
2853
|
+
configured: targets.length,
|
|
2854
|
+
available: targets.filter((target) => target.available).length,
|
|
2855
|
+
totalRecords: [...byType.values()].reduce((sum, type) => sum + type.total, 0),
|
|
2856
|
+
types: [...byType.values()],
|
|
2857
|
+
targets
|
|
2858
|
+
};
|
|
2859
|
+
return json(rollup);
|
|
2860
|
+
};
|
|
2622
2861
|
var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
2623
2862
|
if (req.method !== "POST" || url.pathname !== "/api/admin/network/push") return null;
|
|
2624
2863
|
const got = await gate4(req, env, ctx);
|
|
@@ -2636,14 +2875,108 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
2636
2875
|
if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
|
|
2637
2876
|
const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
|
|
2638
2877
|
if (targets.length !== requested.size) return json({ error: "one or more targetIds are not configured" }, 400);
|
|
2639
|
-
const record = await (0,
|
|
2878
|
+
const record = await (0, import_crm5.getRecord)({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
|
|
2640
2879
|
if (!record) return json({ error: "record not found" }, 404);
|
|
2641
2880
|
const results = await Promise.all(
|
|
2642
|
-
targets.map((target) =>
|
|
2881
|
+
targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record))
|
|
2643
2882
|
);
|
|
2644
2883
|
return json({ ok: results.every((result) => result.ok), results });
|
|
2645
2884
|
};
|
|
2646
2885
|
|
|
2886
|
+
// src/worker-routes-formation.ts
|
|
2887
|
+
var import_crm6 = require("@odla-ai/crm");
|
|
2888
|
+
|
|
2889
|
+
// src/formation.ts
|
|
2890
|
+
function formationFields(crm, formation) {
|
|
2891
|
+
const def = crm.type(formation.type);
|
|
2892
|
+
return [...formation.required, ...formation.optional].map((id) => {
|
|
2893
|
+
const field = def.fields[id];
|
|
2894
|
+
return {
|
|
2895
|
+
id,
|
|
2896
|
+
label: field.label ?? id,
|
|
2897
|
+
type: field.type,
|
|
2898
|
+
required: formation.required.includes(id),
|
|
2899
|
+
...field.options ? { options: field.options } : {}
|
|
2900
|
+
};
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
// src/worker-routes-formation.ts
|
|
2905
|
+
var SUBMISSION = /^[A-Za-z0-9_-]{8,128}$/;
|
|
2906
|
+
var handleFormation = async (req, url, env, ctx) => {
|
|
2907
|
+
const formation = ctx.chapter.formation;
|
|
2908
|
+
if (!formation.enabled) return null;
|
|
2909
|
+
if (req.method === "GET" && url.pathname === "/api/formation/config") {
|
|
2910
|
+
return json({
|
|
2911
|
+
type: formation.type,
|
|
2912
|
+
fields: formationFields(ctx.chapter.crm, formation)
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2915
|
+
if (req.method !== "POST" || url.pathname !== "/api/formation/applications") return null;
|
|
2916
|
+
const raw = await req.text();
|
|
2917
|
+
if (raw.length > formation.bodyCap) return json({ error: "request body too large" }, 413);
|
|
2918
|
+
let body;
|
|
2919
|
+
try {
|
|
2920
|
+
body = JSON.parse(raw);
|
|
2921
|
+
} catch {
|
|
2922
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
2923
|
+
}
|
|
2924
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return json({ error: "JSON body must be an object" }, 400);
|
|
2925
|
+
const submissionId = typeof body.submissionId === "string" ? body.submissionId : void 0;
|
|
2926
|
+
if (submissionId && !SUBMISSION.test(submissionId)) {
|
|
2927
|
+
return json({ error: "submissionId must be an 8\u2013128 character base64url value" }, 400);
|
|
2928
|
+
}
|
|
2929
|
+
const allowed = /* @__PURE__ */ new Set([...formation.required, ...formation.optional]);
|
|
2930
|
+
const unknown = Object.keys(body).filter((field) => field !== "submissionId" && !allowed.has(field));
|
|
2931
|
+
if (unknown.length) return json({ error: `field "${unknown[0]}" is not accepted` }, 400);
|
|
2932
|
+
for (const field of formation.required) {
|
|
2933
|
+
const value = body[field];
|
|
2934
|
+
if (typeof value !== "string" || value.trim() === "") return json({ error: `${field} is required` }, 400);
|
|
2935
|
+
}
|
|
2936
|
+
const input = {};
|
|
2937
|
+
for (const field of allowed) {
|
|
2938
|
+
const value = body[field];
|
|
2939
|
+
if (value === void 0) continue;
|
|
2940
|
+
const cap = formation.maxLen[field] ?? formation.defaultMaxLen;
|
|
2941
|
+
if (typeof value === "string") {
|
|
2942
|
+
if (value.length > cap) return json({ error: `${field} exceeds ${cap} characters` }, 400);
|
|
2943
|
+
input[field] = value.trim();
|
|
2944
|
+
} else if (Array.isArray(value)) {
|
|
2945
|
+
input[field] = value.slice(0, 100);
|
|
2946
|
+
} else {
|
|
2947
|
+
input[field] = value;
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
const db = ctx.makeDb(env);
|
|
2951
|
+
const id = submissionId ? await applicationIdForSubmission(`formation:${ctx.chapter.id}:${submissionId}`) : crypto.randomUUID();
|
|
2952
|
+
try {
|
|
2953
|
+
const created = await (0, import_crm6.createRecord)(
|
|
2954
|
+
{
|
|
2955
|
+
crm: ctx.chapter.crm,
|
|
2956
|
+
db,
|
|
2957
|
+
now: () => Date.now(),
|
|
2958
|
+
newId: () => id
|
|
2959
|
+
},
|
|
2960
|
+
{
|
|
2961
|
+
type: formation.type,
|
|
2962
|
+
input,
|
|
2963
|
+
...submissionId ? { mutationId: `formation:${ctx.chapter.id}:${submissionId}` } : {}
|
|
2964
|
+
}
|
|
2965
|
+
);
|
|
2966
|
+
return json({
|
|
2967
|
+
id,
|
|
2968
|
+
duplicate: created.duplicate,
|
|
2969
|
+
status: created.record.stage ?? null
|
|
2970
|
+
}, created.duplicate ? 200 : 201);
|
|
2971
|
+
} catch (error) {
|
|
2972
|
+
const detail = error && typeof error === "object" && "fields" in error ? error.fields : void 0;
|
|
2973
|
+
return json({
|
|
2974
|
+
error: error instanceof Error ? error.message : "invalid formation application",
|
|
2975
|
+
...detail ? { fields: detail } : {}
|
|
2976
|
+
}, 400);
|
|
2977
|
+
}
|
|
2978
|
+
};
|
|
2979
|
+
|
|
2647
2980
|
// src/worker.ts
|
|
2648
2981
|
var BUILTIN_ROUTES = [
|
|
2649
2982
|
handleHealth,
|
|
@@ -2651,6 +2984,8 @@ var BUILTIN_ROUTES = [
|
|
|
2651
2984
|
handleMe,
|
|
2652
2985
|
handleCrm,
|
|
2653
2986
|
handleNetworkShared,
|
|
2987
|
+
handleNetworkSnapshot,
|
|
2988
|
+
handleFormation,
|
|
2654
2989
|
handleMember,
|
|
2655
2990
|
handleSchedule,
|
|
2656
2991
|
handlePayments,
|
|
@@ -2678,6 +3013,7 @@ var BUILTIN_ROUTES = [
|
|
|
2678
3013
|
handleAdminComms,
|
|
2679
3014
|
// Leader → follower record delivery
|
|
2680
3015
|
handleAdminNetworkTargets,
|
|
3016
|
+
handleAdminNetworkRollup,
|
|
2681
3017
|
handleAdminNetworkPush,
|
|
2682
3018
|
// API requests must never fall through to an SPA asset response. Hosts still
|
|
2683
3019
|
// get first refusal through options.routes, then this terminates unknown API
|