@odla-ai/chapter 0.23.0 → 0.25.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.
@@ -1,8 +1,8 @@
1
1
  import * as preact from 'preact';
2
2
  import { ComponentChildren, JSX } from 'preact';
3
3
  import { CrmClient, Crm, CrmRecord } from '@odla-ai/crm';
4
- import { a as Chapter, b as ChapterCopy } from '../../copy-context-pDogTR4x.js';
5
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-pDogTR4x.js';
4
+ import { a as Chapter, b as ChapterCopy } from '../../copy-context-DI21CYQ3.js';
5
+ export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-DI21CYQ3.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. */
@@ -24,11 +24,11 @@ import {
24
24
  meetingsSection,
25
25
  peopleSection,
26
26
  useAdminResource
27
- } from "../../chunk-VCC4S6JU.js";
27
+ } from "../../chunk-ZPAJL7KG.js";
28
28
  import {
29
29
  ChapterCopyProvider,
30
30
  useChapterCopy
31
- } from "../../chunk-NTZSXAFT.js";
31
+ } from "../../chunk-WGQO4WDJ.js";
32
32
  export {
33
33
  AdminNote,
34
34
  AdminPage,
@@ -1,5 +1,5 @@
1
1
  export { AdminAccountMenuProps, AdminChrome, AdminNote, AdminPage, AdminResource, AdminRouteState, AdminRouteTarget, AdminRouting, AdminSection, AdminSectionContext, AdminWorkspace, AdminWorkspaceNavProps, AvailabilitySectionOptions, BillingSectionOptions, ChapterAdmin, ChapterAdminAuthAdapter, ChapterAdminAuthConfig, ChapterAdminAuthLoadContext, ChapterAdminProps, ChapterAdminUser, CollectionSectionOptions, DashboardSectionOptions, EmailSectionOptions, MeetingsSectionOptions, NetworkShareActions, NetworkShareActionsProps, Panel, PeopleSectionOptions, RecordActions, RecordActionsProps, ThemeWarning, adminFetch, adminRouteFromUrl, adminRouteHref, adminSectionFromUrl, adminSectionHref, applicationLifecycleAdapter, availabilitySection, billingSection, collectionSection, dashboardSection, defaultAdminSections, defaultAdminWorkspaces, emailSection, loadChapterAdminConfig, loadChapterAdminUser, meetingsSection, peopleSection, useAdminResource } from './admin/index.js';
2
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../copy-context-pDogTR4x.js';
2
+ export { C as ChapterCopyProvider, u as useChapterCopy } from '../copy-context-DI21CYQ3.js';
3
3
  export { ApiFn, BrandStyle, BrandStyleProps, JoinConfig, JoinFlowState, JoinIsland, JoinIslandProps, JoinStepRenderContext, JoinSubmitRenderContext, MemberProvisionalRenderContext, MembersArea, MembersAreaProps, PaymentPriceLines, PaymentStep, PaymentStepProps, RescheduleProps, Rescheduler, Slot, SlotPicker, SlotPickerClasses, SlotPickerProps, dayKey, dayLabel, fmtDate, fmtMoney, fullLabel, groupSlotsByDay, loadJoinResume, timeLabel, tzShort } from './member/index.js';
4
4
  import 'preact';
5
5
  import '@odla-ai/crm';
package/dist/ui/index.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  loadJoinResume,
14
14
  timeLabel,
15
15
  tzShort
16
- } from "../chunk-OYXNI6VR.js";
16
+ } from "../chunk-GMGTJIY3.js";
17
17
  import {
18
18
  AdminNote,
19
19
  AdminPage,
@@ -40,12 +40,12 @@ import {
40
40
  meetingsSection,
41
41
  peopleSection,
42
42
  useAdminResource
43
- } from "../chunk-VCC4S6JU.js";
43
+ } from "../chunk-ZPAJL7KG.js";
44
44
  import {
45
45
  BrandStyle,
46
46
  ChapterCopyProvider,
47
47
  useChapterCopy
48
- } from "../chunk-NTZSXAFT.js";
48
+ } from "../chunk-WGQO4WDJ.js";
49
49
  export {
50
50
  AdminNote,
51
51
  AdminPage,
@@ -1,7 +1,7 @@
1
1
  import * as preact from 'preact';
2
2
  import { ComponentChildren, JSX } from 'preact';
3
- import { b as ChapterCopy, c as ChapterBrand } from '../../copy-context-pDogTR4x.js';
4
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-pDogTR4x.js';
3
+ import { b as ChapterCopy, c as ChapterBrand } from '../../copy-context-DI21CYQ3.js';
4
+ export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-DI21CYQ3.js';
5
5
  import '@odla-ai/crm';
6
6
 
7
7
  /** A bookable slot: a start instant in epoch milliseconds. */
@@ -13,12 +13,12 @@ import {
13
13
  loadJoinResume,
14
14
  timeLabel,
15
15
  tzShort
16
- } from "../../chunk-OYXNI6VR.js";
16
+ } from "../../chunk-GMGTJIY3.js";
17
17
  import {
18
18
  BrandStyle,
19
19
  ChapterCopyProvider,
20
20
  useChapterCopy
21
- } from "../../chunk-NTZSXAFT.js";
21
+ } from "../../chunk-WGQO4WDJ.js";
22
22
  export {
23
23
  BrandStyle,
24
24
  ChapterCopyProvider,
@@ -546,23 +546,30 @@ function clerkUserRequest(input) {
546
546
  skip_password_requirement: true,
547
547
  ...input.firstName ? { first_name: input.firstName } : {},
548
548
  ...input.lastName ? { last_name: input.lastName } : {},
549
- ...input.publicMetadata ? { public_metadata: input.publicMetadata } : {}
549
+ ...input.publicMetadata !== void 0 ? { public_metadata: input.publicMetadata } : {},
550
+ ...input.privateMetadata !== void 0 ? { private_metadata: input.privateMetadata } : {}
550
551
  }
551
552
  };
552
553
  }
553
- async function refreshUserMetadata(secretKey, email, publicMetadata, fetchImpl) {
554
+ async function updateClerkUserMetadata(secretKey, userId, input, fetchImpl = fetch) {
555
+ const res = await fetchImpl(`https://api.clerk.com/v1/users/${encodeURIComponent(userId)}/metadata`, {
556
+ method: "PATCH",
557
+ headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
558
+ body: JSON.stringify({
559
+ ...input.publicMetadata !== void 0 ? { public_metadata: input.publicMetadata } : {},
560
+ ...input.privateMetadata !== void 0 ? { private_metadata: input.privateMetadata } : {}
561
+ })
562
+ });
563
+ return res.ok;
564
+ }
565
+ async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl = fetch) {
554
566
  const auth = { authorization: `Bearer ${secretKey}` };
555
567
  const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
556
568
  if (!found.ok) return false;
557
569
  const users = await found.json().catch(() => null);
558
570
  const id = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
559
571
  if (!id) return false;
560
- const patched = await fetchImpl(`https://api.clerk.com/v1/users/${id}/metadata`, {
561
- method: "PATCH",
562
- headers: { ...auth, "content-type": "application/json" },
563
- body: JSON.stringify({ public_metadata: publicMetadata })
564
- });
565
- return patched.ok;
572
+ return updateClerkUserMetadata(secretKey, id, input, fetchImpl);
566
573
  }
567
574
  async function createClerkUser(secretKey, input, fetchImpl = fetch) {
568
575
  const { path, body } = clerkUserRequest(input);
@@ -573,8 +580,15 @@ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
573
580
  });
574
581
  if (res.ok) return { ok: true, status: res.status };
575
582
  const healed = heal(res.status);
576
- if (!healed.existed || !input.publicMetadata) return healed;
577
- const refreshed = await refreshUserMetadata(secretKey, input.email, input.publicMetadata, fetchImpl).catch(() => false);
583
+ const repair = input.repairMetadata ?? {
584
+ ...input.publicMetadata !== void 0 ? { publicMetadata: input.publicMetadata } : {},
585
+ ...input.privateMetadata !== void 0 ? { privateMetadata: input.privateMetadata } : {}
586
+ };
587
+ if (!healed.existed || repair.publicMetadata === void 0 && repair.privateMetadata === void 0) return healed;
588
+ const refreshed = await updateClerkUserMetadataByEmail(secretKey, input.email, {
589
+ ...repair.publicMetadata !== void 0 ? { publicMetadata: repair.publicMetadata } : {},
590
+ ...repair.privateMetadata !== void 0 ? { privateMetadata: repair.privateMetadata } : {}
591
+ }, fetchImpl).catch(() => false);
578
592
  return { ...healed, refreshed };
579
593
  }
580
594
 
@@ -682,6 +696,41 @@ function emailGroupFrom(row) {
682
696
  }
683
697
 
684
698
  // src/worker-provisioning.ts
699
+ var applicantPrivateMetadata = (chapter, applicationId, fields, removeMissingProfile = false) => {
700
+ const profile = applicantProfile(chapter, fields);
701
+ return {
702
+ applicationId,
703
+ ...profile ? { profile } : removeMissingProfile ? { profile: null } : {}
704
+ };
705
+ };
706
+ var publicProfileRemoval = { applicationId: null, profile: null };
707
+ async function markPrivateMetadataSynced(db, applicationId, clerkUserId) {
708
+ await db.transact([{
709
+ t: "update",
710
+ ns: "applications",
711
+ id: applicationId,
712
+ attrs: {
713
+ clerkPrivateMetadataSyncedAt: Date.now(),
714
+ ...clerkUserId ? { clerkUserId } : {}
715
+ }
716
+ }]);
717
+ }
718
+ async function syncApplicantPrivateMetadata(db, chapter, applicationId, fields, clerkUserId) {
719
+ if (chapter.account === "none") return false;
720
+ try {
721
+ const secret = await getVaultSecret(db, "clerk_secret_key");
722
+ if (!secret) return false;
723
+ const ok = await updateClerkUserMetadata(secret, clerkUserId, {
724
+ publicMetadata: publicProfileRemoval,
725
+ privateMetadata: applicantPrivateMetadata(chapter, applicationId, fields, true)
726
+ });
727
+ if (!ok) return false;
728
+ await markPrivateMetadataSynced(db, applicationId, clerkUserId);
729
+ return true;
730
+ } catch {
731
+ return false;
732
+ }
733
+ }
685
734
  async function provisionApplicant(db, chapter, applicationId, fields) {
686
735
  const email = typeof fields.email === "string" ? fields.email : "";
687
736
  if (!email) return;
@@ -709,17 +758,30 @@ async function provisionApplicant(db, chapter, applicationId, fields) {
709
758
  try {
710
759
  const secret = await getVaultSecret(db, "clerk_secret_key");
711
760
  if (!secret) return;
712
- const profile = applicantProfile(chapter, fields);
713
- const publicMetadata = { applicationId, ...profile ? { profile } : {} };
761
+ const privateMetadata = applicantPrivateMetadata(chapter, applicationId, fields);
714
762
  if (chapter.account === "create") {
715
- await createClerkUser(secret, {
763
+ const result = await createClerkUser(secret, {
716
764
  email,
717
765
  firstName: stringValue(fields.firstName),
718
766
  lastName: stringValue(fields.lastName),
719
- publicMetadata
767
+ privateMetadata,
768
+ repairMetadata: {
769
+ publicMetadata: publicProfileRemoval,
770
+ privateMetadata: applicantPrivateMetadata(chapter, applicationId, fields, true)
771
+ }
720
772
  });
773
+ if (result.ok && (!result.existed || result.refreshed)) {
774
+ await markPrivateMetadataSynced(db, applicationId);
775
+ }
721
776
  } else {
722
- await createClerkInvitation(secret, { email, publicMetadata });
777
+ const result = await createClerkInvitation(secret, { email });
778
+ if (result.existed) {
779
+ const refreshed = await updateClerkUserMetadataByEmail(secret, email, {
780
+ publicMetadata: publicProfileRemoval,
781
+ privateMetadata: applicantPrivateMetadata(chapter, applicationId, fields, true)
782
+ });
783
+ if (refreshed) await markPrivateMetadataSynced(db, applicationId);
784
+ }
723
785
  }
724
786
  } catch {
725
787
  }
@@ -768,7 +830,10 @@ async function memberSessionApplication(db, chapterId, email) {
768
830
  const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;
769
831
  const group = Array.isArray(groups) ? groups[0] : void 0;
770
832
  const timezone = resolveScheduling(group?.schedulingJson).timezone;
771
- return memberApplication(app, meeting, timezone);
833
+ return {
834
+ application: memberApplication(app, meeting, timezone),
835
+ source: app
836
+ };
772
837
  }
773
838
  var handleHealth = async (_req, url) => url.pathname === "/api/health" ? json({ ok: true }) : null;
774
839
  var handleConfig = async (_req, url, env, ctx) => {
@@ -789,8 +854,26 @@ var handleMe = async (req, url, env, ctx) => {
789
854
  const superAdmin = await ctx.isSuperAdminEmail(db, u.email);
790
855
  const base = { authorized: isAdminRole(role, ctx.auth), role, superAdmin, email: u.email ?? null };
791
856
  if (ctx.chapter.mode !== "chapter" || !u.email) return json(base);
792
- const application = await memberSessionApplication(db, ctx.chapter.id, u.email);
793
- return json({ ...base, application });
857
+ const member = await memberSessionApplication(db, ctx.chapter.id, u.email);
858
+ let privateMetadataSynced = false;
859
+ if (member && ctx.chapter.account !== "none" && typeof member.source.clerkPrivateMetadataSyncedAt !== "number") {
860
+ privateMetadataSynced = await syncApplicantPrivateMetadata(
861
+ db,
862
+ ctx.chapter,
863
+ String(member.source.id),
864
+ member.source,
865
+ u.userId
866
+ );
867
+ }
868
+ if (member && !privateMetadataSynced && member.source.clerkUserId !== u.userId) {
869
+ await db.transact([{
870
+ t: "update",
871
+ ns: "applications",
872
+ id: String(member.source.id),
873
+ attrs: { clerkUserId: u.userId }
874
+ }]).catch(() => void 0);
875
+ }
876
+ return json({ ...base, application: member?.application ?? null });
794
877
  };
795
878
  var handleCrm = async (req, url, env, ctx) => {
796
879
  const crmBase = ctx.crmBase;
@@ -948,6 +1031,93 @@ var handleMember = async (req, url, env, ctx) => {
948
1031
  return null;
949
1032
  };
950
1033
 
1034
+ // src/crm-sync.ts
1035
+ var import_crm3 = require("@odla-ai/crm");
1036
+ var str = (v) => typeof v === "string" ? v : v == null ? "" : String(v);
1037
+ var crmDeps = (db, ctx) => ({
1038
+ crm: ctx.chapter.crm,
1039
+ db,
1040
+ now: () => Date.now(),
1041
+ newId: () => crypto.randomUUID(),
1042
+ chapter: ctx.chapter
1043
+ });
1044
+ function personInputFromApp(chapter, app) {
1045
+ const input = sharedPersonInput({
1046
+ email: str(app.email),
1047
+ firstName: str(app.firstName) || void 0,
1048
+ lastName: str(app.lastName) || void 0,
1049
+ phone: str(app.phone) || void 0,
1050
+ linkedin: str(app.linkedin) || void 0,
1051
+ hubRecordId: str(app.id)
1052
+ });
1053
+ for (const f of chapter.application.crmFields) {
1054
+ if (app[f] !== void 0) input[f] = app[f];
1055
+ }
1056
+ if (app.id !== void 0) input.applicationId = str(app.id);
1057
+ return input;
1058
+ }
1059
+ function billingColumns(app) {
1060
+ const status = str(app.status);
1061
+ const paid = Boolean(app.stripeSubscriptionId) && status !== "refunded";
1062
+ const billingStatus = status === "refunded" ? "refunded" : app.canceled === true ? "canceled" : paid ? "active" : "none";
1063
+ const cols = { billingStatus };
1064
+ if (app.stripeCustomerId) cols.stripeCustomerId = str(app.stripeCustomerId);
1065
+ if (app.stripeSubscriptionId) cols.subscriptionId = str(app.stripeSubscriptionId);
1066
+ if (typeof app.renewalAt === "number") cols.renewalAt = app.renewalAt;
1067
+ return cols;
1068
+ }
1069
+ async function syncApplicationToCrm(deps, opts) {
1070
+ const emailKey = str(opts.app.email).toLowerCase();
1071
+ if (!emailKey) return null;
1072
+ const recordDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
1073
+ const input = personInputFromApp(deps.chapter, opts.app);
1074
+ const { crm_record } = await deps.db.query({
1075
+ crm_record: { $: { where: { type: "person", primaryEmail: emailKey }, limit: 1 } }
1076
+ });
1077
+ const existing = crm_record?.[0] ?? null;
1078
+ const stage = opts.stage || void 0;
1079
+ let recordId;
1080
+ if (existing && typeof existing.id === "string") {
1081
+ recordId = existing.id;
1082
+ await (0, import_crm3.updateRecord)(recordDeps, { id: recordId, input });
1083
+ } else {
1084
+ const created = await (0, import_crm3.createRecord)(recordDeps, { type: "person", input, ...stage ? { stage } : {} });
1085
+ recordId = created.id;
1086
+ }
1087
+ if (existing && stage && existing.stage !== stage) {
1088
+ await (0, import_crm3.setStage)(recordDeps, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${stage}` }).catch(() => void 0);
1089
+ }
1090
+ await deps.db.transact([{ t: "update", ns: "crm_record", id: recordId, attrs: billingColumns(opts.app) }]);
1091
+ await (0, import_crm3.linkIdentity)(recordDeps, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
1092
+ return recordId;
1093
+ }
1094
+ async function backfillCrm(deps) {
1095
+ const [appsRes, usersRes] = await Promise.all([
1096
+ deps.db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } } }),
1097
+ deps.db.query({ $users: { $: { limit: 1e3 } } })
1098
+ ]);
1099
+ const seen = /* @__PURE__ */ new Set();
1100
+ let synced = 0;
1101
+ const errors = [];
1102
+ const run = async (app, stage) => {
1103
+ const key = str(app.email).toLowerCase();
1104
+ if (!key || seen.has(key)) return;
1105
+ seen.add(key);
1106
+ try {
1107
+ await syncApplicationToCrm(deps, { app, stage });
1108
+ synced += 1;
1109
+ } catch (err) {
1110
+ errors.push({ email: key, error: err instanceof Error ? err.message : String(err) });
1111
+ }
1112
+ };
1113
+ for (const a of appsRes.applications ?? []) await run(a, str(a.status));
1114
+ for (const u of usersRes.$users ?? []) {
1115
+ if (u.deleted === true) continue;
1116
+ await run({ email: u.email, firstName: str(u.name) });
1117
+ }
1118
+ return { synced, errors };
1119
+ }
1120
+
951
1121
  // src/worker-routes-schedule.ts
952
1122
  var import_calendar = require("@odla-ai/calendar");
953
1123
  function errCode(err) {
@@ -1051,8 +1221,11 @@ async function bookSlot(req, env, ctx) {
1051
1221
  if (code === "calendar_slot_unavailable") return json({ error: "slot no longer available", code }, 409);
1052
1222
  return json({ error: "booking failed", code }, 502);
1053
1223
  }
1054
- const appOp = { t: "update", ns: "applications", id: applicationId, attrs: applicationBookingUpdate(status, startAt, htmlLink) };
1224
+ const bookingAttrs = applicationBookingUpdate(status, startAt, htmlLink);
1225
+ const appOp = { t: "update", ns: "applications", id: applicationId, attrs: bookingAttrs };
1055
1226
  await db.transact([meetingOp, appOp]);
1227
+ const booked = { ...app, ...bookingAttrs };
1228
+ await syncApplicationToCrm(crmDeps(db, ctx), { app: booked, stage: String(booked.status ?? "") }).catch(() => void 0);
1056
1229
  if (typeof app.email === "string" && app.email) {
1057
1230
  await sendTemplated(
1058
1231
  { db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },
@@ -1402,6 +1575,8 @@ async function ingestWebhook(req, env, ctx) {
1402
1575
  const patch = webhookPatch(event, String(app.status ?? ""));
1403
1576
  if (Object.keys(patch).length) {
1404
1577
  await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: patch }], { mutationId: webhookMutationId(eventId) });
1578
+ const next = { ...app, ...patch };
1579
+ await syncApplicationToCrm(crmDeps(db, ctx), { app: next, stage: String(next.status ?? "") }).catch(() => void 0);
1405
1580
  }
1406
1581
  if (event.kind === "first_payment") {
1407
1582
  await notifyPaymentConfirmed(db, env, eventId, app);
@@ -1553,9 +1728,21 @@ var PAGE = 100;
1553
1728
  function toRecord(u) {
1554
1729
  if (typeof u.id !== "string") return null;
1555
1730
  const pm = u.public_metadata ?? {};
1731
+ const privateMetadata = u.private_metadata ?? {};
1732
+ const rawProfile = privateMetadata.profile;
1733
+ const profile = rawProfile && typeof rawProfile === "object" && !Array.isArray(rawProfile) ? rawProfile : {};
1734
+ const applicationId = typeof privateMetadata.applicationId === "string" ? privateMetadata.applicationId : void 0;
1556
1735
  const role = typeof pm.role === "string" && pm.role ? pm.role : DEFAULT_ROLE;
1557
1736
  const email = u.email_addresses?.[0]?.email_address;
1558
- return { id: u.id, email: typeof email === "string" ? email : void 0, role, publicMetadata: pm };
1737
+ return {
1738
+ id: u.id,
1739
+ email: typeof email === "string" ? email : void 0,
1740
+ role,
1741
+ publicMetadata: pm,
1742
+ privateMetadata,
1743
+ profile,
1744
+ ...applicationId ? { applicationId } : {}
1745
+ };
1559
1746
  }
1560
1747
  async function clerkGet(path, secretKey, fetchImpl) {
1561
1748
  const res = await fetchImpl(`${CLERK_API}${path}`, { headers: { authorization: `Bearer ${secretKey}` } });
@@ -1593,86 +1780,6 @@ async function clerkSetRole(secretKey, id, role, fetchImpl = fetch) {
1593
1780
  return res.ok;
1594
1781
  }
1595
1782
 
1596
- // src/crm-sync.ts
1597
- var import_crm3 = require("@odla-ai/crm");
1598
- var str = (v) => typeof v === "string" ? v : v == null ? "" : String(v);
1599
- function personInputFromApp(chapter, app) {
1600
- const input = sharedPersonInput({
1601
- email: str(app.email),
1602
- firstName: str(app.firstName) || void 0,
1603
- lastName: str(app.lastName) || void 0,
1604
- phone: str(app.phone) || void 0,
1605
- linkedin: str(app.linkedin) || void 0,
1606
- hubRecordId: str(app.id)
1607
- });
1608
- for (const f of chapter.application.crmFields) {
1609
- if (app[f] !== void 0) input[f] = app[f];
1610
- }
1611
- if (app.id !== void 0) input.applicationId = str(app.id);
1612
- return input;
1613
- }
1614
- function billingColumns(app) {
1615
- const status = str(app.status);
1616
- const paid = Boolean(app.stripeSubscriptionId) && status !== "refunded";
1617
- const billingStatus = status === "refunded" ? "refunded" : app.canceled === true ? "canceled" : paid ? "active" : "none";
1618
- const cols = { billingStatus };
1619
- if (app.stripeCustomerId) cols.stripeCustomerId = str(app.stripeCustomerId);
1620
- if (app.stripeSubscriptionId) cols.subscriptionId = str(app.stripeSubscriptionId);
1621
- if (typeof app.renewalAt === "number") cols.renewalAt = app.renewalAt;
1622
- return cols;
1623
- }
1624
- async function syncApplicationToCrm(deps, opts) {
1625
- const emailKey = str(opts.app.email).toLowerCase();
1626
- if (!emailKey) return null;
1627
- const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
1628
- const input = personInputFromApp(deps.chapter, opts.app);
1629
- const { crm_record } = await deps.db.query({
1630
- crm_record: { $: { where: { type: "person", primaryEmail: emailKey }, limit: 1 } }
1631
- });
1632
- const existing = crm_record?.[0] ?? null;
1633
- const stage = opts.stage || void 0;
1634
- let recordId;
1635
- if (existing && typeof existing.id === "string") {
1636
- recordId = existing.id;
1637
- await (0, import_crm3.updateRecord)(crmDeps3, { id: recordId, input });
1638
- } else {
1639
- const created = await (0, import_crm3.createRecord)(crmDeps3, { type: "person", input, ...stage ? { stage } : {} });
1640
- recordId = created.id;
1641
- }
1642
- if (existing && stage && existing.stage !== stage) {
1643
- await (0, import_crm3.setStage)(crmDeps3, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${stage}` }).catch(() => void 0);
1644
- }
1645
- await deps.db.transact([{ t: "update", ns: "crm_record", id: recordId, attrs: billingColumns(opts.app) }]);
1646
- await (0, import_crm3.linkIdentity)(crmDeps3, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
1647
- return recordId;
1648
- }
1649
- async function backfillCrm(deps) {
1650
- const [appsRes, usersRes] = await Promise.all([
1651
- deps.db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } } }),
1652
- deps.db.query({ $users: { $: { limit: 1e3 } } })
1653
- ]);
1654
- const seen = /* @__PURE__ */ new Set();
1655
- let synced = 0;
1656
- const errors = [];
1657
- const run = async (app, stage) => {
1658
- const key = str(app.email).toLowerCase();
1659
- if (!key || seen.has(key)) return;
1660
- seen.add(key);
1661
- try {
1662
- await syncApplicationToCrm(deps, { app, stage });
1663
- synced += 1;
1664
- } catch (err) {
1665
- errors.push({ email: key, error: err instanceof Error ? err.message : String(err) });
1666
- }
1667
- };
1668
- for (const a of appsRes.applications ?? []) await run(a, str(a.status));
1669
- for (const u of usersRes.$users ?? []) {
1670
- if (u.deleted === true) continue;
1671
- await run({ email: u.email, firstName: str(u.name) });
1672
- }
1673
- return { synced, errors };
1674
- }
1675
-
1676
1783
  // src/worker-routes-admin-people.ts
1677
1784
  async function adminGate(req, env, ctx) {
1678
1785
  const rawDb = ctx.makeDb(env);
@@ -1681,7 +1788,7 @@ async function adminGate(req, env, ctx) {
1681
1788
  if (!await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
1682
1789
  return { db: rawDb, actor: { userId: u.userId, email: u.email ?? void 0 } };
1683
1790
  }
1684
- var crmDeps = (db, ctx) => ({
1791
+ var crmDeps2 = (db, ctx) => ({
1685
1792
  crm: ctx.chapter.crm,
1686
1793
  db,
1687
1794
  now: () => Date.now(),
@@ -1692,7 +1799,7 @@ var handleAdminCrmSync = async (req, url, env, ctx) => {
1692
1799
  if (req.method !== "POST" || url.pathname !== "/api/admin/crm/sync") return null;
1693
1800
  const gate5 = await adminGate(req, env, ctx);
1694
1801
  if (gate5 instanceof Response) return gate5;
1695
- const result = await backfillCrm(crmDeps(gate5.db, ctx));
1802
+ const result = await backfillCrm(crmDeps2(gate5.db, ctx));
1696
1803
  return json({ ok: true, ...result });
1697
1804
  };
1698
1805
  var handleAdminPeople = async (req, url, env, ctx) => {
@@ -1706,17 +1813,20 @@ var handleAdminPeople = async (req, url, env, ctx) => {
1706
1813
  db.query({ $users: { $: { limit: 200 } } }),
1707
1814
  sk ? clerkListUsers(sk).catch(() => []) : Promise.resolve([])
1708
1815
  ]);
1709
- const roleByUserId = new Map(roleList.map((u) => [u.id, u.role]));
1816
+ const clerkByUserId = new Map(roleList.map((u) => [u.id, u]));
1710
1817
  const people = /* @__PURE__ */ new Map();
1711
1818
  for (const u of usersRes.$users ?? []) {
1712
1819
  if (u.deleted === true) continue;
1713
1820
  const email = typeof u.email === "string" ? u.email : "";
1714
1821
  if (!email) continue;
1822
+ const clerk = clerkByUserId.get(String(u.id));
1715
1823
  people.set(email.toLowerCase(), {
1716
1824
  email,
1717
1825
  name: typeof u.name === "string" ? u.name : "",
1718
1826
  userId: typeof u.id === "string" ? u.id : null,
1719
- role: roleByUserId.get(String(u.id)) ?? "provisional",
1827
+ role: clerk?.role ?? "provisional",
1828
+ profile: clerk ? clerk.profile : null,
1829
+ clerkApplicationId: clerk?.applicationId ?? null,
1720
1830
  application: null
1721
1831
  });
1722
1832
  }
@@ -1729,7 +1839,15 @@ var handleAdminPeople = async (req, url, env, ctx) => {
1729
1839
  if (!row.application) row.application = applicationSummary(a);
1730
1840
  if (!row.name) row.name = name;
1731
1841
  } else {
1732
- people.set(key, { email: String(a.email), name, userId: null, role: null, application: applicationSummary(a) });
1842
+ people.set(key, {
1843
+ email: String(a.email),
1844
+ name,
1845
+ userId: null,
1846
+ role: null,
1847
+ profile: null,
1848
+ clerkApplicationId: null,
1849
+ application: applicationSummary(a)
1850
+ });
1733
1851
  }
1734
1852
  }
1735
1853
  const rows2 = [...people.values()].sort(
@@ -1781,6 +1899,88 @@ var handleAdminPeopleRole = async (req, url, env, ctx) => {
1781
1899
  return json({ ok: true });
1782
1900
  };
1783
1901
 
1902
+ // src/worker-routes-admin-clerk.ts
1903
+ var own = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
1904
+ function canonical(value) {
1905
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
1906
+ if (value && typeof value === "object") {
1907
+ return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`).join(",")}}`;
1908
+ }
1909
+ return JSON.stringify(value) ?? "undefined";
1910
+ }
1911
+ function positiveInt(raw, fallback, max) {
1912
+ const parsed = Number(raw);
1913
+ return Number.isSafeInteger(parsed) && parsed >= 0 ? Math.min(parsed, max) : fallback;
1914
+ }
1915
+ var handleAdminClerkPrivateProfileSync = async (req, url, env, ctx) => {
1916
+ if (req.method !== "POST" || url.pathname !== "/api/admin/clerk/private-profiles/sync") return null;
1917
+ const rawDb = ctx.makeDb(env);
1918
+ const actor = await ctx.verifyUser(req, env);
1919
+ if (!actor) return json({ error: "unauthorized" }, 401);
1920
+ if (!await ctx.isAdmin(rawDb, actor)) return json({ error: "forbidden" }, 403);
1921
+ if (ctx.chapter.mode !== "chapter" || ctx.chapter.account === "none") {
1922
+ return json({ error: "private profile sync requires a side-effecting chapter account model" }, 409);
1923
+ }
1924
+ const db = rawDb;
1925
+ const secret = await getVaultSecret(db, "clerk_secret_key");
1926
+ if (!secret) return json({ error: "clerk_secret_key missing from vault" }, 503);
1927
+ let all;
1928
+ try {
1929
+ all = await clerkListUsers(secret);
1930
+ } catch {
1931
+ return json({ error: "Clerk user listing failed" }, 502);
1932
+ }
1933
+ const offset = positiveInt(url.searchParams.get("offset"), 0, Number.MAX_SAFE_INTEGER);
1934
+ const limit = positiveInt(url.searchParams.get("limit"), 50, 100) || 50;
1935
+ const users = all.slice(offset, offset + limit);
1936
+ const result = { updated: 0, alreadyCurrent: 0, noApplication: 0, failed: 0 };
1937
+ for (const user of users) {
1938
+ if (!user.email) {
1939
+ result.noApplication += 1;
1940
+ continue;
1941
+ }
1942
+ const apps = (await db.query({
1943
+ applications: { $: { where: { email: user.email }, order: { createdAt: "desc" }, limit: 1 } }
1944
+ })).applications;
1945
+ const app = Array.isArray(apps) ? apps[0] : void 0;
1946
+ if (!app || typeof app.id !== "string") {
1947
+ result.noApplication += 1;
1948
+ continue;
1949
+ }
1950
+ const desired = applicantPrivateMetadata(ctx.chapter, app.id, app);
1951
+ const desiredProfile = desired.profile ?? {};
1952
+ const legacyPublic = own(user.publicMetadata, "applicationId") || own(user.publicMetadata, "profile");
1953
+ const current = user.applicationId === app.id && canonical(user.profile) === canonical(desiredProfile);
1954
+ try {
1955
+ let updated = false;
1956
+ if (!current || legacyPublic) {
1957
+ const ok = await updateClerkUserMetadata(secret, user.id, {
1958
+ publicMetadata: publicProfileRemoval,
1959
+ privateMetadata: applicantPrivateMetadata(ctx.chapter, app.id, app, true)
1960
+ });
1961
+ if (!ok) {
1962
+ result.failed += 1;
1963
+ continue;
1964
+ }
1965
+ updated = true;
1966
+ }
1967
+ await markPrivateMetadataSynced(db, app.id, user.id);
1968
+ if (updated) result.updated += 1;
1969
+ else result.alreadyCurrent += 1;
1970
+ } catch {
1971
+ result.failed += 1;
1972
+ }
1973
+ }
1974
+ const nextOffset = offset + users.length < all.length ? offset + users.length : null;
1975
+ return json({
1976
+ totalUsers: all.length,
1977
+ offset,
1978
+ processed: users.length,
1979
+ nextOffset,
1980
+ ...result
1981
+ });
1982
+ };
1983
+
1784
1984
  // src/series.ts
1785
1985
  function periodSeries(points, now, count, periodMs) {
1786
1986
  const start = now - count * periodMs;
@@ -1972,7 +2172,6 @@ async function gate2(req, env, ctx) {
1972
2172
  return rawDb;
1973
2173
  }
1974
2174
  var calFor = (env) => (0, import_calendar3.initCalendar)({ appId: env.ODLA_APP_ID, env: env.ODLA_ENV, adminToken: env.ODLA_API_KEY, endpoint: env.ODLA_PLATFORM });
1975
- var crmDeps2 = (db, ctx) => ({ crm: ctx.chapter.crm, db, now: () => Date.now(), newId: () => crypto.randomUUID(), chapter: ctx.chapter });
1976
2175
  var readJson = async (req) => {
1977
2176
  try {
1978
2177
  return await req.json();
@@ -2058,7 +2257,7 @@ var handleAdminApprove = async (req, url, env, ctx) => {
2058
2257
  if (!canApprove(String(app.status), ctx.chapter.pipeline)) return json({ error: `cannot approve from status "${String(app.status)}"` }, 409);
2059
2258
  const target = "approved";
2060
2259
  await db.transact([{ t: "update", ns: "applications", id, attrs: { status: target } }]);
2061
- await syncApplicationToCrm(crmDeps2(db, ctx), { app: { ...app, status: target }, stage: target }).catch(() => void 0);
2260
+ await syncApplicationToCrm(crmDeps(db, ctx), { app: { ...app, status: target }, stage: target }).catch(() => void 0);
2062
2261
  const { promoteTo, send } = ctx.chapter.operations.onApprove;
2063
2262
  let rolePromoted = false;
2064
2263
  const sk = await getVaultSecret(db, "clerk_secret_key");
@@ -2139,7 +2338,7 @@ var handleAdminApplicationPatch = async (req, url, env, ctx) => {
2139
2338
  if (Object.keys(attrs).length === 0) return json({ error: "nothing to update" }, 400);
2140
2339
  await db.transact([{ t: "update", ns: "applications", id, attrs }]);
2141
2340
  if (attrs.status !== void 0) {
2142
- await syncApplicationToCrm(crmDeps2(db, ctx), { app: { ...app, ...attrs }, stage: String(attrs.status) }).catch(() => void 0);
2341
+ await syncApplicationToCrm(crmDeps(db, ctx), { app: { ...app, ...attrs }, stage: String(attrs.status) }).catch(() => void 0);
2143
2342
  }
2144
2343
  return json({ ok: true });
2145
2344
  };
@@ -2398,6 +2597,7 @@ var BUILTIN_ROUTES = [
2398
2597
  handleAdminPeopleAccess,
2399
2598
  handleAdminPeopleRole,
2400
2599
  handleAdminCrmSync,
2600
+ handleAdminClerkPrivateProfileSync,
2401
2601
  // Aggregation
2402
2602
  handleAdminDashboard,
2403
2603
  handleAdminBilling,