@odla-ai/chapter 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -11
- package/dist/{chunk-ZCZK6QLC.js → chunk-LME2IHY5.js} +2 -34
- package/dist/chunk-LME2IHY5.js.map +1 -0
- package/dist/chunk-MKXHZMAP.js +51 -0
- package/dist/chunk-MKXHZMAP.js.map +1 -0
- package/dist/chunk-UIWFEESI.js +922 -0
- package/dist/chunk-UIWFEESI.js.map +1 -0
- package/dist/index.cjs +200 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +97 -14
- package/dist/index.d.ts +97 -14
- package/dist/index.js +200 -19
- package/dist/index.js.map +1 -1
- package/dist/types-D2fGiNG8.d.ts +366 -0
- package/dist/ui/admin/index.d.ts +56 -5
- package/dist/ui/admin/index.js +18 -1
- package/dist/ui/index.d.ts +2 -1
- package/dist/ui/index.js +21 -3
- package/dist/ui/member/index.d.ts +2 -20
- package/dist/ui/member/index.js +4 -2
- package/dist/worker/index.cjs +225 -24
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +51 -1
- package/dist/worker/index.d.ts +51 -1
- package/dist/worker/index.js +225 -24
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-K6BDIFCJ.js +0 -801
- package/dist/chunk-K6BDIFCJ.js.map +0 -1
- package/dist/chunk-ZCZK6QLC.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -195,16 +195,10 @@ function companyType() {
|
|
|
195
195
|
facets: { rank: "manual" }
|
|
196
196
|
};
|
|
197
197
|
}
|
|
198
|
-
function defaultCrm(
|
|
199
|
-
if (mode === "hub") {
|
|
200
|
-
return {
|
|
201
|
-
types: { person: personType(), company: companyType() },
|
|
202
|
-
relations: { works_at: { from: "person", to: "company", label: "works at", reverseLabel: "team" } },
|
|
203
|
-
templates: TEMPLATES
|
|
204
|
-
};
|
|
205
|
-
}
|
|
198
|
+
function defaultCrm(_mode) {
|
|
206
199
|
return {
|
|
207
|
-
types: { person: personType() },
|
|
200
|
+
types: { person: personType(), company: companyType() },
|
|
201
|
+
relations: { works_at: { from: "person", to: "company", label: "works at", reverseLabel: "team" } },
|
|
208
202
|
templates: TEMPLATES
|
|
209
203
|
};
|
|
210
204
|
}
|
|
@@ -487,6 +481,62 @@ function joinConfig(group, paymentsReady2) {
|
|
|
487
481
|
|
|
488
482
|
// src/config.ts
|
|
489
483
|
var SLUG = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
484
|
+
var FIELD = /^[a-z][a-zA-Z0-9_]*$/;
|
|
485
|
+
function resolveNetwork(config, crm) {
|
|
486
|
+
const seen = /* @__PURE__ */ new Set();
|
|
487
|
+
const targets = [];
|
|
488
|
+
for (const target of config.network?.targets ?? []) {
|
|
489
|
+
if (!SLUG.test(target.id)) {
|
|
490
|
+
throw new Error(`defineChapter.network.targets.id: must be a lowercase slug \u2014 got ${JSON.stringify(target.id)}`);
|
|
491
|
+
}
|
|
492
|
+
if (seen.has(target.id)) throw new Error(`defineChapter.network.targets: duplicate target "${target.id}"`);
|
|
493
|
+
seen.add(target.id);
|
|
494
|
+
let url;
|
|
495
|
+
try {
|
|
496
|
+
url = new URL(target.url);
|
|
497
|
+
} catch {
|
|
498
|
+
throw new Error(`defineChapter.network.targets.${target.id}.url: must be an absolute URL`);
|
|
499
|
+
}
|
|
500
|
+
const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
|
|
501
|
+
if (url.protocol !== "https:" && !(local && url.protocol === "http:")) {
|
|
502
|
+
throw new Error(`defineChapter.network.targets.${target.id}.url: must use https (http is allowed only for localhost)`);
|
|
503
|
+
}
|
|
504
|
+
const fields = target.fields;
|
|
505
|
+
for (const [type, names] of Object.entries(fields ?? {})) {
|
|
506
|
+
if (!FIELD.test(type) || names.length === 0 || names.some((name) => !FIELD.test(name))) {
|
|
507
|
+
throw new Error(
|
|
508
|
+
`defineChapter.network.targets.${target.id}.fields.${type}: type and field names must be non-empty CRM identifiers`
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
const def = crm.config.types[type];
|
|
512
|
+
if (!def) throw new Error(`defineChapter.network.targets.${target.id}.fields: unknown leader CRM type "${type}"`);
|
|
513
|
+
const unknown = names.filter((field) => !def.fields[field]);
|
|
514
|
+
if (unknown.length > 0) {
|
|
515
|
+
throw new Error(
|
|
516
|
+
`defineChapter.network.targets.${target.id}.fields.${type}: ${unknown.map((field) => `"${field}"`).join(", ")} not declared on the leader CRM type`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
const nameField = def.nameField ?? "name";
|
|
520
|
+
if (!names.includes(nameField)) {
|
|
521
|
+
throw new Error(
|
|
522
|
+
`defineChapter.network.targets.${target.id}.fields.${type}: must include required name field "${nameField}"`
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const secretName = target.secretName?.trim() || `network_share_${target.id.replaceAll("-", "_")}`;
|
|
527
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_-]{1,127}$/.test(secretName)) {
|
|
528
|
+
throw new Error(`defineChapter.network.targets.${target.id}.secretName: must be a vault-key identifier`);
|
|
529
|
+
}
|
|
530
|
+
targets.push({
|
|
531
|
+
id: target.id,
|
|
532
|
+
name: target.name?.trim() || target.id,
|
|
533
|
+
url: url.origin,
|
|
534
|
+
secretName,
|
|
535
|
+
...fields ? { fields } : {}
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
return { targets };
|
|
539
|
+
}
|
|
490
540
|
function isResolvedCrm(x) {
|
|
491
541
|
return !!x && typeof x === "object" && "prepare" in x && typeof x.prepare === "function";
|
|
492
542
|
}
|
|
@@ -511,6 +561,8 @@ function defineChapter(config) {
|
|
|
511
561
|
const auth = resolveAuth(mode, config.auth);
|
|
512
562
|
const pipeline = resolvePipeline(config.pipeline);
|
|
513
563
|
const application = resolveApplication(config.application);
|
|
564
|
+
const brand = { ...config.brand, wordmark: config.brand?.wordmark ?? name };
|
|
565
|
+
const network = resolveNetwork(config, crm);
|
|
514
566
|
const { schema, rules } = chapterDb(mode, auth);
|
|
515
567
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
516
568
|
const account = config.account ?? "none";
|
|
@@ -563,6 +615,8 @@ function defineChapter(config) {
|
|
|
563
615
|
id: id2,
|
|
564
616
|
name,
|
|
565
617
|
mode,
|
|
618
|
+
brand,
|
|
619
|
+
network,
|
|
566
620
|
crm,
|
|
567
621
|
auth,
|
|
568
622
|
pipeline,
|
|
@@ -811,6 +865,10 @@ function canceledPatch() {
|
|
|
811
865
|
|
|
812
866
|
// src/network.ts
|
|
813
867
|
import { createRecord, updateRecord } from "@odla-ai/crm";
|
|
868
|
+
var DEFAULT_SHARE_FIELDS = {
|
|
869
|
+
person: ["name", "email", "firstName", "lastName", "phone", "linkedin"],
|
|
870
|
+
company: ["name", "domain", "industry", "location", "linkedin", "notes"]
|
|
871
|
+
};
|
|
814
872
|
function sharedPersonInput(person) {
|
|
815
873
|
const email = person.email.toLowerCase();
|
|
816
874
|
const fullName = [person.firstName, person.lastName].filter(Boolean).join(" ").trim();
|
|
@@ -822,6 +880,52 @@ function sharedPersonInput(person) {
|
|
|
822
880
|
if (person.linkedin) input.linkedin = person.linkedin;
|
|
823
881
|
return input;
|
|
824
882
|
}
|
|
883
|
+
function shortHash(value) {
|
|
884
|
+
let a = 2166136261;
|
|
885
|
+
let b = 2654435769;
|
|
886
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
887
|
+
const n = value.charCodeAt(i);
|
|
888
|
+
a = Math.imul(a ^ n, 16777619);
|
|
889
|
+
b = Math.imul(b ^ n, 2246822507);
|
|
890
|
+
}
|
|
891
|
+
return `${(a >>> 0).toString(36)}${(b >>> 0).toString(36)}`;
|
|
892
|
+
}
|
|
893
|
+
function networkSourceTag(type, hubRecordId) {
|
|
894
|
+
const typeKey = type.toLowerCase();
|
|
895
|
+
const readable = /^[a-z0-9_-]+$/.test(hubRecordId);
|
|
896
|
+
const raw = `network:${typeKey}:${hubRecordId}`;
|
|
897
|
+
if (readable && raw.length <= 64) return raw;
|
|
898
|
+
return `network:${typeKey.slice(0, 20)}:${shortHash(`${type}\0${hubRecordId}`)}`;
|
|
899
|
+
}
|
|
900
|
+
function normalizeSharedRecord(record) {
|
|
901
|
+
if ("input" in record) return { version: 1, type: record.type, hubRecordId: record.hubRecordId, input: record.input };
|
|
902
|
+
if ("type" in record && record.type === "company") {
|
|
903
|
+
const input = { name: record.name };
|
|
904
|
+
for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
|
|
905
|
+
if (record[key]) input[key] = record[key];
|
|
906
|
+
}
|
|
907
|
+
return { version: 1, type: "company", hubRecordId: record.hubRecordId, input };
|
|
908
|
+
}
|
|
909
|
+
return { version: 1, type: "person", hubRecordId: record.hubRecordId, input: sharedPersonInput(record) };
|
|
910
|
+
}
|
|
911
|
+
function sharedRecordFromCrm(crm, record, target) {
|
|
912
|
+
if (target.fields && !target.fields[record.type]) {
|
|
913
|
+
throw new Error(`${target.name} does not accept "${record.type}" records`);
|
|
914
|
+
}
|
|
915
|
+
const def = crm.type(record.type);
|
|
916
|
+
const fields = target.fields?.[record.type] ?? DEFAULT_SHARE_FIELDS[record.type];
|
|
917
|
+
if (!fields) {
|
|
918
|
+
throw new Error(`${target.name} requires an explicit field allowlist for "${record.type}" records`);
|
|
919
|
+
}
|
|
920
|
+
const nameField = def.nameField ?? "name";
|
|
921
|
+
const input = {};
|
|
922
|
+
for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
|
|
923
|
+
const value = record.fields?.[field];
|
|
924
|
+
if (value !== void 0) input[field] = value;
|
|
925
|
+
}
|
|
926
|
+
if (input[nameField] === void 0) input[nameField] = record.name;
|
|
927
|
+
return { version: 1, type: record.type, hubRecordId: record.id, input };
|
|
928
|
+
}
|
|
825
929
|
async function upsertPerson(deps, opts) {
|
|
826
930
|
const email = opts.email.toLowerCase();
|
|
827
931
|
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
@@ -834,8 +938,65 @@ async function upsertPerson(deps, opts) {
|
|
|
834
938
|
const created = await createRecord(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
835
939
|
return { recordId: created.id };
|
|
836
940
|
}
|
|
837
|
-
async function
|
|
838
|
-
|
|
941
|
+
async function findSharedRecord(deps, record, tag) {
|
|
942
|
+
const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
943
|
+
const mappedId = mapped.crm_tag?.[0]?.recordId;
|
|
944
|
+
if (typeof mappedId === "string") {
|
|
945
|
+
const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
|
|
946
|
+
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
947
|
+
}
|
|
948
|
+
const def = deps.crm.type(record.type);
|
|
949
|
+
const emailField = def.emailField;
|
|
950
|
+
if (emailField && typeof record.input[emailField] === "string") {
|
|
951
|
+
const primaryEmail = record.input[emailField].toLowerCase();
|
|
952
|
+
const found = await deps.db.query({
|
|
953
|
+
crm_record: { $: { where: { type: record.type, primaryEmail }, limit: 1 } }
|
|
954
|
+
});
|
|
955
|
+
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
956
|
+
}
|
|
957
|
+
const domain = record.input.domain;
|
|
958
|
+
const domainSlot = def.fields.domain?.slot;
|
|
959
|
+
if (typeof domain === "string" && domainSlot) {
|
|
960
|
+
const found = await deps.db.query({
|
|
961
|
+
crm_record: { $: { where: { type: record.type, [domainSlot]: domain }, limit: 1 } }
|
|
962
|
+
});
|
|
963
|
+
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
964
|
+
}
|
|
965
|
+
const nameField = def.nameField ?? "name";
|
|
966
|
+
const name = record.input[nameField];
|
|
967
|
+
if (record.type === "company" && typeof name === "string" && name.trim()) {
|
|
968
|
+
const found = await deps.db.query({
|
|
969
|
+
crm_record: { $: { where: { type: record.type, name: name.trim() }, limit: 1 } }
|
|
970
|
+
});
|
|
971
|
+
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
972
|
+
}
|
|
973
|
+
return void 0;
|
|
974
|
+
}
|
|
975
|
+
async function projectSharedRecord(deps, shared) {
|
|
976
|
+
const record = normalizeSharedRecord(shared);
|
|
977
|
+
if (!record.type.trim() || !record.hubRecordId.trim()) {
|
|
978
|
+
throw new Error("type and hubRecordId must be non-empty");
|
|
979
|
+
}
|
|
980
|
+
const tag = networkSourceTag(record.type, record.hubRecordId);
|
|
981
|
+
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
982
|
+
const existing = await findSharedRecord(deps, record, tag);
|
|
983
|
+
let recordId;
|
|
984
|
+
if (existing && typeof existing.id === "string") {
|
|
985
|
+
await updateRecord(crmDeps, { id: existing.id, input: record.input });
|
|
986
|
+
recordId = existing.id;
|
|
987
|
+
} else {
|
|
988
|
+
recordId = `network_${shortHash(`${record.type}\0${record.hubRecordId}`)}`;
|
|
989
|
+
await createRecord({ ...crmDeps, newId: () => recordId }, {
|
|
990
|
+
type: record.type,
|
|
991
|
+
input: record.input,
|
|
992
|
+
mutationId: `share-create:${tag}`
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
await deps.db.transact(
|
|
996
|
+
[{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt: deps.now() } }],
|
|
997
|
+
{ mutationId: `share-map:${tag}:${recordId}` }
|
|
998
|
+
);
|
|
999
|
+
return { recordId };
|
|
839
1000
|
}
|
|
840
1001
|
async function projectApplicant(deps, applicant) {
|
|
841
1002
|
const base = sharedPersonInput({
|
|
@@ -1199,20 +1360,36 @@ function paletteVar(key) {
|
|
|
1199
1360
|
function cleanValue(value) {
|
|
1200
1361
|
return value.replace(/[<>{};]/g, "").trim();
|
|
1201
1362
|
}
|
|
1202
|
-
function
|
|
1203
|
-
if (!brand) return "";
|
|
1363
|
+
function paletteDecls(palette) {
|
|
1204
1364
|
const decls = [];
|
|
1205
|
-
for (const [key, value] of Object.entries(
|
|
1365
|
+
for (const [key, value] of Object.entries(palette ?? {})) {
|
|
1206
1366
|
if (typeof value === "string" && value.trim()) decls.push(`${paletteVar(key)}: ${cleanValue(value)};`);
|
|
1207
1367
|
}
|
|
1368
|
+
return decls;
|
|
1369
|
+
}
|
|
1370
|
+
function brandTokens(brand) {
|
|
1371
|
+
if (!brand) return "";
|
|
1372
|
+
const light = paletteDecls(brand.palette);
|
|
1208
1373
|
const fonts = brand.fonts;
|
|
1209
|
-
if (fonts?.display)
|
|
1210
|
-
if (fonts?.body)
|
|
1211
|
-
if (fonts?.numeral)
|
|
1212
|
-
|
|
1213
|
-
|
|
1374
|
+
if (fonts?.display) light.push(`--ui-font-display: ${cleanValue(fonts.display)};`);
|
|
1375
|
+
if (fonts?.body) light.push(`--ui-font-sans: ${cleanValue(fonts.body)};`);
|
|
1376
|
+
if (fonts?.numeral) light.push(`--ui-font-numeral: ${cleanValue(fonts.numeral)};`);
|
|
1377
|
+
const dark = paletteDecls(brand.paletteDark);
|
|
1378
|
+
let css = light.length ? `:root {
|
|
1379
|
+
${light.join("\n ")}
|
|
1214
1380
|
}
|
|
1215
1381
|
` : "";
|
|
1382
|
+
if (dark.length) {
|
|
1383
|
+
const block = `{
|
|
1384
|
+
${dark.join("\n ")}
|
|
1385
|
+
}`;
|
|
1386
|
+
css += `:root[data-theme="dark"] ${block}
|
|
1387
|
+
@media (prefers-color-scheme: dark) {
|
|
1388
|
+
:root:not([data-theme="light"]) ${block}
|
|
1389
|
+
}
|
|
1390
|
+
`;
|
|
1391
|
+
}
|
|
1392
|
+
return css;
|
|
1216
1393
|
}
|
|
1217
1394
|
|
|
1218
1395
|
// src/reconcile.ts
|
|
@@ -1354,6 +1531,7 @@ function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
|
|
|
1354
1531
|
};
|
|
1355
1532
|
}
|
|
1356
1533
|
export {
|
|
1534
|
+
DEFAULT_SHARE_FIELDS,
|
|
1357
1535
|
SCHEDULING_DEFAULTS,
|
|
1358
1536
|
applicantProfile,
|
|
1359
1537
|
applicationBookingUpdate,
|
|
@@ -1399,6 +1577,8 @@ export {
|
|
|
1399
1577
|
meetingRescheduleUpdate,
|
|
1400
1578
|
memberApplication,
|
|
1401
1579
|
memberSession,
|
|
1580
|
+
networkSourceTag,
|
|
1581
|
+
normalizeSharedRecord,
|
|
1402
1582
|
normalizeWebhookEvent,
|
|
1403
1583
|
paymentsReady,
|
|
1404
1584
|
personInputFromApp,
|
|
@@ -1418,6 +1598,7 @@ export {
|
|
|
1418
1598
|
roleFromClaim,
|
|
1419
1599
|
sendTemplated,
|
|
1420
1600
|
sharedPersonInput,
|
|
1601
|
+
sharedRecordFromCrm,
|
|
1421
1602
|
slotWindow,
|
|
1422
1603
|
stageIndex,
|
|
1423
1604
|
stripeCall,
|