@odla-ai/chapter 0.19.0 → 0.20.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 +204 -21
- package/dist/{chunk-GWY2ZTN7.js → chunk-LME2IHY5.js} +2 -50
- 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-D3QMK3ED.js → chunk-UIWFEESI.js} +611 -470
- package/dist/chunk-UIWFEESI.js.map +1 -0
- package/dist/index.cjs +176 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +83 -9
- package/dist/index.d.ts +83 -9
- package/dist/index.js +176 -11
- package/dist/index.js.map +1 -1
- package/dist/types-D2fGiNG8.d.ts +366 -0
- package/dist/ui/admin/index.d.ts +37 -3
- package/dist/ui/admin/index.js +10 -1
- package/dist/ui/index.d.ts +2 -1
- package/dist/ui/index.js +13 -3
- package/dist/ui/member/index.d.ts +2 -25
- 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 +45 -0
- package/dist/worker/index.d.ts +45 -0
- package/dist/worker/index.js +225 -24
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-D3QMK3ED.js.map +0 -1
- package/dist/chunk-GWY2ZTN7.js.map +0 -1
package/dist/worker/index.cjs
CHANGED
|
@@ -274,6 +274,10 @@ function joinConfig(group, paymentsReady) {
|
|
|
274
274
|
|
|
275
275
|
// src/network.ts
|
|
276
276
|
var import_crm = require("@odla-ai/crm");
|
|
277
|
+
var DEFAULT_SHARE_FIELDS = {
|
|
278
|
+
person: ["name", "email", "firstName", "lastName", "phone", "linkedin"],
|
|
279
|
+
company: ["name", "domain", "industry", "location", "linkedin", "notes"]
|
|
280
|
+
};
|
|
277
281
|
function sharedPersonInput(person) {
|
|
278
282
|
const email = person.email.toLowerCase();
|
|
279
283
|
const fullName = [person.firstName, person.lastName].filter(Boolean).join(" ").trim();
|
|
@@ -285,6 +289,52 @@ function sharedPersonInput(person) {
|
|
|
285
289
|
if (person.linkedin) input.linkedin = person.linkedin;
|
|
286
290
|
return input;
|
|
287
291
|
}
|
|
292
|
+
function shortHash(value) {
|
|
293
|
+
let a = 2166136261;
|
|
294
|
+
let b = 2654435769;
|
|
295
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
296
|
+
const n = value.charCodeAt(i);
|
|
297
|
+
a = Math.imul(a ^ n, 16777619);
|
|
298
|
+
b = Math.imul(b ^ n, 2246822507);
|
|
299
|
+
}
|
|
300
|
+
return `${(a >>> 0).toString(36)}${(b >>> 0).toString(36)}`;
|
|
301
|
+
}
|
|
302
|
+
function networkSourceTag(type, hubRecordId) {
|
|
303
|
+
const typeKey = type.toLowerCase();
|
|
304
|
+
const readable = /^[a-z0-9_-]+$/.test(hubRecordId);
|
|
305
|
+
const raw = `network:${typeKey}:${hubRecordId}`;
|
|
306
|
+
if (readable && raw.length <= 64) return raw;
|
|
307
|
+
return `network:${typeKey.slice(0, 20)}:${shortHash(`${type}\0${hubRecordId}`)}`;
|
|
308
|
+
}
|
|
309
|
+
function normalizeSharedRecord(record) {
|
|
310
|
+
if ("input" in record) return { version: 1, type: record.type, hubRecordId: record.hubRecordId, input: record.input };
|
|
311
|
+
if ("type" in record && record.type === "company") {
|
|
312
|
+
const input = { name: record.name };
|
|
313
|
+
for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
|
|
314
|
+
if (record[key]) input[key] = record[key];
|
|
315
|
+
}
|
|
316
|
+
return { version: 1, type: "company", hubRecordId: record.hubRecordId, input };
|
|
317
|
+
}
|
|
318
|
+
return { version: 1, type: "person", hubRecordId: record.hubRecordId, input: sharedPersonInput(record) };
|
|
319
|
+
}
|
|
320
|
+
function sharedRecordFromCrm(crm, record, target) {
|
|
321
|
+
if (target.fields && !target.fields[record.type]) {
|
|
322
|
+
throw new Error(`${target.name} does not accept "${record.type}" records`);
|
|
323
|
+
}
|
|
324
|
+
const def = crm.type(record.type);
|
|
325
|
+
const fields = target.fields?.[record.type] ?? DEFAULT_SHARE_FIELDS[record.type];
|
|
326
|
+
if (!fields) {
|
|
327
|
+
throw new Error(`${target.name} requires an explicit field allowlist for "${record.type}" records`);
|
|
328
|
+
}
|
|
329
|
+
const nameField = def.nameField ?? "name";
|
|
330
|
+
const input = {};
|
|
331
|
+
for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
|
|
332
|
+
const value = record.fields?.[field];
|
|
333
|
+
if (value !== void 0) input[field] = value;
|
|
334
|
+
}
|
|
335
|
+
if (input[nameField] === void 0) input[nameField] = record.name;
|
|
336
|
+
return { version: 1, type: record.type, hubRecordId: record.id, input };
|
|
337
|
+
}
|
|
288
338
|
async function upsertPerson(deps, opts) {
|
|
289
339
|
const email = opts.email.toLowerCase();
|
|
290
340
|
const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
@@ -297,8 +347,65 @@ async function upsertPerson(deps, opts) {
|
|
|
297
347
|
const created = await (0, import_crm.createRecord)(crmDeps3, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
298
348
|
return { recordId: created.id };
|
|
299
349
|
}
|
|
300
|
-
async function
|
|
301
|
-
|
|
350
|
+
async function findSharedRecord(deps, record, tag) {
|
|
351
|
+
const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
352
|
+
const mappedId = mapped.crm_tag?.[0]?.recordId;
|
|
353
|
+
if (typeof mappedId === "string") {
|
|
354
|
+
const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
|
|
355
|
+
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
356
|
+
}
|
|
357
|
+
const def = deps.crm.type(record.type);
|
|
358
|
+
const emailField = def.emailField;
|
|
359
|
+
if (emailField && typeof record.input[emailField] === "string") {
|
|
360
|
+
const primaryEmail = record.input[emailField].toLowerCase();
|
|
361
|
+
const found = await deps.db.query({
|
|
362
|
+
crm_record: { $: { where: { type: record.type, primaryEmail }, limit: 1 } }
|
|
363
|
+
});
|
|
364
|
+
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
365
|
+
}
|
|
366
|
+
const domain = record.input.domain;
|
|
367
|
+
const domainSlot = def.fields.domain?.slot;
|
|
368
|
+
if (typeof domain === "string" && domainSlot) {
|
|
369
|
+
const found = await deps.db.query({
|
|
370
|
+
crm_record: { $: { where: { type: record.type, [domainSlot]: domain }, limit: 1 } }
|
|
371
|
+
});
|
|
372
|
+
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
373
|
+
}
|
|
374
|
+
const nameField = def.nameField ?? "name";
|
|
375
|
+
const name = record.input[nameField];
|
|
376
|
+
if (record.type === "company" && typeof name === "string" && name.trim()) {
|
|
377
|
+
const found = await deps.db.query({
|
|
378
|
+
crm_record: { $: { where: { type: record.type, name: name.trim() }, limit: 1 } }
|
|
379
|
+
});
|
|
380
|
+
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
381
|
+
}
|
|
382
|
+
return void 0;
|
|
383
|
+
}
|
|
384
|
+
async function projectSharedRecord(deps, shared) {
|
|
385
|
+
const record = normalizeSharedRecord(shared);
|
|
386
|
+
if (!record.type.trim() || !record.hubRecordId.trim()) {
|
|
387
|
+
throw new Error("type and hubRecordId must be non-empty");
|
|
388
|
+
}
|
|
389
|
+
const tag = networkSourceTag(record.type, record.hubRecordId);
|
|
390
|
+
const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
391
|
+
const existing = await findSharedRecord(deps, record, tag);
|
|
392
|
+
let recordId;
|
|
393
|
+
if (existing && typeof existing.id === "string") {
|
|
394
|
+
await (0, import_crm.updateRecord)(crmDeps3, { id: existing.id, input: record.input });
|
|
395
|
+
recordId = existing.id;
|
|
396
|
+
} else {
|
|
397
|
+
recordId = `network_${shortHash(`${record.type}\0${record.hubRecordId}`)}`;
|
|
398
|
+
await (0, import_crm.createRecord)({ ...crmDeps3, newId: () => recordId }, {
|
|
399
|
+
type: record.type,
|
|
400
|
+
input: record.input,
|
|
401
|
+
mutationId: `share-create:${tag}`
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
await deps.db.transact(
|
|
405
|
+
[{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt: deps.now() } }],
|
|
406
|
+
{ mutationId: `share-map:${tag}:${recordId}` }
|
|
407
|
+
);
|
|
408
|
+
return { recordId };
|
|
302
409
|
}
|
|
303
410
|
async function projectApplicant(deps, applicant) {
|
|
304
411
|
const base = sharedPersonInput({
|
|
@@ -676,20 +783,35 @@ var handleNetworkShared = async (req, url, env, ctx) => {
|
|
|
676
783
|
if (!secret || provided.length !== secret.length || provided !== secret) {
|
|
677
784
|
return json({ error: "unauthorized" }, 401);
|
|
678
785
|
}
|
|
679
|
-
let
|
|
786
|
+
let payload;
|
|
680
787
|
try {
|
|
681
|
-
|
|
788
|
+
payload = JSON.parse(await req.text());
|
|
682
789
|
} catch {
|
|
683
790
|
return json({ error: "invalid JSON body" }, 400);
|
|
684
791
|
}
|
|
685
|
-
if (typeof
|
|
686
|
-
return json({ error: "
|
|
792
|
+
if (typeof payload.hubRecordId !== "string" || !payload.hubRecordId.trim()) {
|
|
793
|
+
return json({ error: "hubRecordId is required" }, 400);
|
|
794
|
+
}
|
|
795
|
+
if ("input" in payload) {
|
|
796
|
+
if (payload.version !== 1 || typeof payload.type !== "string" || !payload.type.trim() || !payload.input || typeof payload.input !== "object" || Array.isArray(payload.input)) {
|
|
797
|
+
return json({ error: "version 1, type, and input are required" }, 400);
|
|
798
|
+
}
|
|
799
|
+
} else if (payload.type !== "company" && typeof payload.email !== "string") {
|
|
800
|
+
return json({ error: "legacy person shares require email" }, 400);
|
|
801
|
+
} else if (payload.type === "company" && typeof payload.name !== "string") {
|
|
802
|
+
return json({ error: "business shares require name" }, 400);
|
|
803
|
+
}
|
|
804
|
+
try {
|
|
805
|
+
const record = normalizeSharedRecord(payload);
|
|
806
|
+
const { recordId } = await projectSharedRecord(
|
|
807
|
+
{ crm: ctx.chapter.crm, db, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
808
|
+
record
|
|
809
|
+
);
|
|
810
|
+
return json({ recordId, type: record.type });
|
|
811
|
+
} catch (err) {
|
|
812
|
+
const message = err instanceof Error ? err.message : "invalid shared record";
|
|
813
|
+
return json({ error: message }, 400);
|
|
687
814
|
}
|
|
688
|
-
const { recordId } = await projectSharedRecord(
|
|
689
|
-
{ crm: ctx.chapter.crm, db, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
690
|
-
person
|
|
691
|
-
);
|
|
692
|
-
return json({ recordId });
|
|
693
815
|
};
|
|
694
816
|
var handleMember = async (req, url, env, ctx) => {
|
|
695
817
|
const chapter = ctx.chapter;
|
|
@@ -1522,16 +1644,16 @@ var crmDeps = (db, ctx) => ({
|
|
|
1522
1644
|
});
|
|
1523
1645
|
var handleAdminCrmSync = async (req, url, env, ctx) => {
|
|
1524
1646
|
if (req.method !== "POST" || url.pathname !== "/api/admin/crm/sync") return null;
|
|
1525
|
-
const
|
|
1526
|
-
if (
|
|
1527
|
-
const result = await backfillCrm(crmDeps(
|
|
1647
|
+
const gate5 = await adminGate(req, env, ctx);
|
|
1648
|
+
if (gate5 instanceof Response) return gate5;
|
|
1649
|
+
const result = await backfillCrm(crmDeps(gate5.db, ctx));
|
|
1528
1650
|
return json({ ok: true, ...result });
|
|
1529
1651
|
};
|
|
1530
1652
|
var handleAdminPeople = async (req, url, env, ctx) => {
|
|
1531
1653
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people") return null;
|
|
1532
|
-
const
|
|
1533
|
-
if (
|
|
1534
|
-
const { db } =
|
|
1654
|
+
const gate5 = await adminGate(req, env, ctx);
|
|
1655
|
+
if (gate5 instanceof Response) return gate5;
|
|
1656
|
+
const { db } = gate5;
|
|
1535
1657
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
1536
1658
|
const [appsRes, usersRes, roleList] = await Promise.all([
|
|
1537
1659
|
db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 200 } } }),
|
|
@@ -1571,9 +1693,9 @@ var handleAdminPeople = async (req, url, env, ctx) => {
|
|
|
1571
1693
|
};
|
|
1572
1694
|
var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
1573
1695
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people/access") return null;
|
|
1574
|
-
const
|
|
1575
|
-
if (
|
|
1576
|
-
const { db } =
|
|
1696
|
+
const gate5 = await adminGate(req, env, ctx);
|
|
1697
|
+
if (gate5 instanceof Response) return gate5;
|
|
1698
|
+
const { db } = gate5;
|
|
1577
1699
|
const targetId = url.searchParams.get("userId") ?? "";
|
|
1578
1700
|
if (!targetId.startsWith("user_")) return json({ error: "invalid userId" }, 400);
|
|
1579
1701
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
@@ -1584,9 +1706,9 @@ var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
|
1584
1706
|
};
|
|
1585
1707
|
var handleAdminPeopleRole = async (req, url, env, ctx) => {
|
|
1586
1708
|
if (req.method !== "POST" || url.pathname !== "/api/admin/people/role") return null;
|
|
1587
|
-
const
|
|
1588
|
-
if (
|
|
1589
|
-
const { db, actor } =
|
|
1709
|
+
const gate5 = await adminGate(req, env, ctx);
|
|
1710
|
+
if (gate5 instanceof Response) return gate5;
|
|
1711
|
+
const { db, actor } = gate5;
|
|
1590
1712
|
let body;
|
|
1591
1713
|
try {
|
|
1592
1714
|
body = await req.json();
|
|
@@ -2125,6 +2247,82 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
2125
2247
|
return json({ items });
|
|
2126
2248
|
};
|
|
2127
2249
|
|
|
2250
|
+
// src/worker-routes-network.ts
|
|
2251
|
+
var import_crm4 = require("@odla-ai/crm");
|
|
2252
|
+
async function gate4(req, env, ctx) {
|
|
2253
|
+
const db = ctx.makeDb(env);
|
|
2254
|
+
const user = await ctx.verifyUser(req, env);
|
|
2255
|
+
if (!user) return { response: json({ error: "unauthorized" }, 401) };
|
|
2256
|
+
if (!await ctx.isAdmin(db, user)) return { response: json({ error: "forbidden" }, 403) };
|
|
2257
|
+
return { db, user };
|
|
2258
|
+
}
|
|
2259
|
+
var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
2260
|
+
if (req.method !== "GET" || url.pathname !== "/api/admin/network/targets") return null;
|
|
2261
|
+
const got = await gate4(req, env, ctx);
|
|
2262
|
+
if ("response" in got) return got.response;
|
|
2263
|
+
return json({
|
|
2264
|
+
targets: ctx.chapter.network.targets.map(({ id, name, url: targetUrl, fields }) => ({
|
|
2265
|
+
id,
|
|
2266
|
+
name,
|
|
2267
|
+
url: targetUrl,
|
|
2268
|
+
types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS)
|
|
2269
|
+
}))
|
|
2270
|
+
});
|
|
2271
|
+
};
|
|
2272
|
+
async function pushOne(db, ctx, target, record) {
|
|
2273
|
+
const secret = await getVaultSecret(db, target.secretName);
|
|
2274
|
+
if (!secret) return { id: target.id, name: target.name, ok: false, error: `vault secret "${target.secretName}" is missing` };
|
|
2275
|
+
let payload;
|
|
2276
|
+
try {
|
|
2277
|
+
payload = sharedRecordFromCrm(ctx.chapter.crm, record, target);
|
|
2278
|
+
} catch (err) {
|
|
2279
|
+
return { id: target.id, name: target.name, ok: false, error: err instanceof Error ? err.message : "record is not shareable" };
|
|
2280
|
+
}
|
|
2281
|
+
try {
|
|
2282
|
+
const res = await fetch(new URL("/api/network/shared", target.url), {
|
|
2283
|
+
method: "POST",
|
|
2284
|
+
headers: { authorization: `Bearer ${secret}`, "content-type": "application/json" },
|
|
2285
|
+
body: JSON.stringify(payload),
|
|
2286
|
+
signal: AbortSignal.timeout(1e4)
|
|
2287
|
+
});
|
|
2288
|
+
const body = await res.json().catch(() => ({}));
|
|
2289
|
+
if (!res.ok) {
|
|
2290
|
+
return { id: target.id, name: target.name, ok: false, status: res.status, error: body.error ?? "follower rejected the record" };
|
|
2291
|
+
}
|
|
2292
|
+
await (0, import_crm4.addTag)(
|
|
2293
|
+
{ crm: ctx.chapter.crm, db },
|
|
2294
|
+
{ recordId: record.id, tag: `shared:${target.id}`, mutationId: `network-delivered:${record.id}:${target.id}` }
|
|
2295
|
+
);
|
|
2296
|
+
return { id: target.id, name: target.name, ok: true, status: res.status, recordId: body.recordId };
|
|
2297
|
+
} catch (err) {
|
|
2298
|
+
return { id: target.id, name: target.name, ok: false, error: err instanceof Error ? err.message : "delivery failed" };
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
2302
|
+
if (req.method !== "POST" || url.pathname !== "/api/admin/network/push") return null;
|
|
2303
|
+
const got = await gate4(req, env, ctx);
|
|
2304
|
+
if ("response" in got) return got.response;
|
|
2305
|
+
let body;
|
|
2306
|
+
try {
|
|
2307
|
+
body = await req.json();
|
|
2308
|
+
} catch {
|
|
2309
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
2310
|
+
}
|
|
2311
|
+
if (typeof body.recordId !== "string" || !Array.isArray(body.targetIds) || body.targetIds.length === 0) {
|
|
2312
|
+
return json({ error: "recordId and a non-empty targetIds array are required" }, 400);
|
|
2313
|
+
}
|
|
2314
|
+
const requested = new Set(body.targetIds.filter((id) => typeof id === "string"));
|
|
2315
|
+
if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
|
|
2316
|
+
const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
|
|
2317
|
+
if (targets.length !== requested.size) return json({ error: "one or more targetIds are not configured" }, 400);
|
|
2318
|
+
const record = await (0, import_crm4.getRecord)({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
|
|
2319
|
+
if (!record) return json({ error: "record not found" }, 404);
|
|
2320
|
+
const results = await Promise.all(
|
|
2321
|
+
targets.map((target) => pushOne(got.db, ctx, target, record))
|
|
2322
|
+
);
|
|
2323
|
+
return json({ ok: results.every((result) => result.ok), results });
|
|
2324
|
+
};
|
|
2325
|
+
|
|
2128
2326
|
// src/worker.ts
|
|
2129
2327
|
var BUILTIN_ROUTES = [
|
|
2130
2328
|
handleHealth,
|
|
@@ -2155,7 +2353,10 @@ var BUILTIN_ROUTES = [
|
|
|
2155
2353
|
handleAdminGroupEmail,
|
|
2156
2354
|
handleAdminEmailLog,
|
|
2157
2355
|
handleAdminEmailTest,
|
|
2158
|
-
handleAdminComms
|
|
2356
|
+
handleAdminComms,
|
|
2357
|
+
// Leader → follower record delivery
|
|
2358
|
+
handleAdminNetworkTargets,
|
|
2359
|
+
handleAdminNetworkPush
|
|
2159
2360
|
];
|
|
2160
2361
|
function chapterWorker(options) {
|
|
2161
2362
|
const ctx = createWorkerContext(options);
|