@odla-ai/chapter 0.24.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-Dp5hvj5W.js';
5
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-Dp5hvj5W.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. */
@@ -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-Dp5hvj5W.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';
@@ -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-Dp5hvj5W.js';
4
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-Dp5hvj5W.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. */
@@ -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;
@@ -1645,9 +1728,21 @@ var PAGE = 100;
1645
1728
  function toRecord(u) {
1646
1729
  if (typeof u.id !== "string") return null;
1647
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;
1648
1735
  const role = typeof pm.role === "string" && pm.role ? pm.role : DEFAULT_ROLE;
1649
1736
  const email = u.email_addresses?.[0]?.email_address;
1650
- 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
+ };
1651
1746
  }
1652
1747
  async function clerkGet(path, secretKey, fetchImpl) {
1653
1748
  const res = await fetchImpl(`${CLERK_API}${path}`, { headers: { authorization: `Bearer ${secretKey}` } });
@@ -1718,17 +1813,20 @@ var handleAdminPeople = async (req, url, env, ctx) => {
1718
1813
  db.query({ $users: { $: { limit: 200 } } }),
1719
1814
  sk ? clerkListUsers(sk).catch(() => []) : Promise.resolve([])
1720
1815
  ]);
1721
- const roleByUserId = new Map(roleList.map((u) => [u.id, u.role]));
1816
+ const clerkByUserId = new Map(roleList.map((u) => [u.id, u]));
1722
1817
  const people = /* @__PURE__ */ new Map();
1723
1818
  for (const u of usersRes.$users ?? []) {
1724
1819
  if (u.deleted === true) continue;
1725
1820
  const email = typeof u.email === "string" ? u.email : "";
1726
1821
  if (!email) continue;
1822
+ const clerk = clerkByUserId.get(String(u.id));
1727
1823
  people.set(email.toLowerCase(), {
1728
1824
  email,
1729
1825
  name: typeof u.name === "string" ? u.name : "",
1730
1826
  userId: typeof u.id === "string" ? u.id : null,
1731
- role: roleByUserId.get(String(u.id)) ?? "provisional",
1827
+ role: clerk?.role ?? "provisional",
1828
+ profile: clerk ? clerk.profile : null,
1829
+ clerkApplicationId: clerk?.applicationId ?? null,
1732
1830
  application: null
1733
1831
  });
1734
1832
  }
@@ -1741,7 +1839,15 @@ var handleAdminPeople = async (req, url, env, ctx) => {
1741
1839
  if (!row.application) row.application = applicationSummary(a);
1742
1840
  if (!row.name) row.name = name;
1743
1841
  } else {
1744
- 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
+ });
1745
1851
  }
1746
1852
  }
1747
1853
  const rows2 = [...people.values()].sort(
@@ -1793,6 +1899,88 @@ var handleAdminPeopleRole = async (req, url, env, ctx) => {
1793
1899
  return json({ ok: true });
1794
1900
  };
1795
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
+
1796
1984
  // src/series.ts
1797
1985
  function periodSeries(points, now, count, periodMs) {
1798
1986
  const start = now - count * periodMs;
@@ -2409,6 +2597,7 @@ var BUILTIN_ROUTES = [
2409
2597
  handleAdminPeopleAccess,
2410
2598
  handleAdminPeopleRole,
2411
2599
  handleAdminCrmSync,
2600
+ handleAdminClerkPrivateProfileSync,
2412
2601
  // Aggregation
2413
2602
  handleAdminDashboard,
2414
2603
  handleAdminBilling,