@odla-ai/chapter 0.40.1 → 0.40.3

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.
@@ -407,16 +407,20 @@ interface AdminBuilderLibraryData {
407
407
  receivedAt: number;
408
408
  }[];
409
409
  localAuthoring: boolean;
410
+ /** Closed set of Chapter destinations the server will accept. */
411
+ chapterTargets: {
412
+ chapterId: string;
413
+ }[];
410
414
  visibility: {
411
415
  builtNotFoundSeesChapterSources: boolean;
412
416
  chaptersSeeSiblingSources: false;
413
417
  };
414
418
  }
415
- /** Private source-material editor, visibly separate from member publishing. */
419
+ /** CMS-style private source-material list and editor. */
416
420
  declare function BuilderLibraryAdmin({ ctx, initialData }: {
417
421
  ctx: AdminSectionContext;
418
422
  initialData?: AdminBuilderLibraryData;
419
- }): preact.JSX.Element;
423
+ }): JSX.Element;
420
424
  /** Reusable private Builder Library section for the standard Settings workspace. */
421
425
  declare function builderLibrarySection(): AdminSection;
422
426
 
@@ -33,7 +33,7 @@ import {
33
33
  networkRecordsWorkspace,
34
34
  peopleSection,
35
35
  useAdminResource
36
- } from "../../chunk-4K2Q5JBC.js";
36
+ } from "../../chunk-O22RRPBT.js";
37
37
  import {
38
38
  ChapterCopyProvider,
39
39
  useChapterCopy
package/dist/ui/index.js CHANGED
@@ -58,7 +58,7 @@ import {
58
58
  networkRecordsWorkspace,
59
59
  peopleSection,
60
60
  useAdminResource
61
- } from "../chunk-4K2Q5JBC.js";
61
+ } from "../chunk-O22RRPBT.js";
62
62
  import {
63
63
  BrandStyle,
64
64
  ChapterCopyProvider,
@@ -1676,7 +1676,14 @@ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
1676
1676
  headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
1677
1677
  body: JSON.stringify(body)
1678
1678
  });
1679
- if (res.ok) return { ok: true, status: res.status };
1679
+ if (res.ok) {
1680
+ const body2 = typeof res.json === "function" ? await res.json().catch(() => null) : null;
1681
+ return {
1682
+ ok: true,
1683
+ status: res.status,
1684
+ ...typeof body2?.id === "string" && body2.id ? { userId: body2.id } : {}
1685
+ };
1686
+ }
1680
1687
  const healed = heal(res.status);
1681
1688
  const repair = input.repairMetadata ?? {
1682
1689
  ...input.publicMetadata !== void 0 ? { publicMetadata: input.publicMetadata } : {},
@@ -1726,6 +1733,34 @@ async function syncApplicantPrivateMetadata(db, chapter, applicationId, fields,
1726
1733
  return false;
1727
1734
  }
1728
1735
  }
1736
+ var AccountProvisioningError = class extends Error {
1737
+ constructor(code, status) {
1738
+ super(code);
1739
+ this.code = code;
1740
+ this.status = status;
1741
+ }
1742
+ code;
1743
+ status;
1744
+ name = "AccountProvisioningError";
1745
+ };
1746
+ async function provisionApplicantForRoute(db, chapter, applicationId, fields) {
1747
+ try {
1748
+ await provisionApplicant(db, chapter, applicationId, fields);
1749
+ return null;
1750
+ } catch (error) {
1751
+ if (!(error instanceof AccountProvisioningError)) throw error;
1752
+ return {
1753
+ status: error.status,
1754
+ body: {
1755
+ error: {
1756
+ code: error.code,
1757
+ message: error.code === "clerk_secret_unavailable" ? "account provisioning credential is unavailable" : "account provisioning failed"
1758
+ },
1759
+ applicationId
1760
+ }
1761
+ };
1762
+ }
1763
+ }
1729
1764
  async function provisionApplicant(db, chapter, applicationId, fields) {
1730
1765
  const email = typeof fields.email === "string" ? fields.email : "";
1731
1766
  if (!email) return;
@@ -1750,9 +1785,9 @@ async function provisionApplicant(db, chapter, applicationId, fields) {
1750
1785
  } catch {
1751
1786
  }
1752
1787
  if (chapter.account === "none") return;
1788
+ const secret = await getVaultSecret(db, "clerk_secret_key");
1789
+ if (!secret) throw new AccountProvisioningError("clerk_secret_unavailable", 503);
1753
1790
  try {
1754
- const secret = await getVaultSecret(db, "clerk_secret_key");
1755
- if (!secret) return;
1756
1791
  const privateMetadata = applicantPrivateMetadata(chapter, applicationId, fields);
1757
1792
  if (chapter.account === "create") {
1758
1793
  const result = await createClerkUser(secret, {
@@ -1765,11 +1800,13 @@ async function provisionApplicant(db, chapter, applicationId, fields) {
1765
1800
  privateMetadata: applicantPrivateMetadata(chapter, applicationId, fields, true)
1766
1801
  }
1767
1802
  });
1768
- if (result.ok && (!result.existed || result.refreshed)) {
1769
- await markPrivateMetadataSynced(db, applicationId);
1803
+ if (!result.ok || result.existed && !result.refreshed) {
1804
+ throw new AccountProvisioningError("clerk_account_provisioning_failed", 502);
1770
1805
  }
1806
+ await markPrivateMetadataSynced(db, applicationId, result.userId);
1771
1807
  } else {
1772
1808
  const result = await createClerkInvitation(secret, { email });
1809
+ if (!result.ok) throw new AccountProvisioningError("clerk_account_provisioning_failed", 502);
1773
1810
  if (result.existed) {
1774
1811
  const refreshed = await updateClerkUserMetadataByEmail(secret, email, {
1775
1812
  publicMetadata: publicProfileRemoval,
@@ -1778,7 +1815,9 @@ async function provisionApplicant(db, chapter, applicationId, fields) {
1778
1815
  if (refreshed) await markPrivateMetadataSynced(db, applicationId);
1779
1816
  }
1780
1817
  }
1781
- } catch {
1818
+ } catch (error) {
1819
+ if (error instanceof AccountProvisioningError) throw error;
1820
+ throw new AccountProvisioningError("clerk_account_provisioning_failed", 502);
1782
1821
  }
1783
1822
  }
1784
1823
  async function notifyAdminOfApplication(db, env, chapterId, applicationId, fields) {
@@ -1816,12 +1855,16 @@ async function notifyAdminOfApplication(db, env, chapterId, applicationId, field
1816
1855
  }
1817
1856
 
1818
1857
  // src/worker-routes.ts
1819
- async function memberSessionApplication(db, chapterId, email) {
1858
+ async function memberSessionApplication(db, chapterId, identity) {
1820
1859
  let apps = (await db.query({ applications: { $: {
1821
- where: { emailNormalized: normalizeChapterEmail(email) },
1860
+ where: { clerkUserId: identity.userId },
1822
1861
  limit: 20
1823
1862
  } } })).applications;
1824
- if (!apps?.length) apps = (await db.query({ applications: { $: { where: { email }, limit: 20 } } })).applications;
1863
+ if (!apps?.length && identity.email) apps = (await db.query({ applications: { $: {
1864
+ where: { emailNormalized: normalizeChapterEmail(identity.email) },
1865
+ limit: 20
1866
+ } } })).applications;
1867
+ if (!apps?.length && identity.email) apps = (await db.query({ applications: { $: { where: { email: identity.email }, limit: 20 } } })).applications;
1825
1868
  const app = Array.isArray(apps) ? [...apps].sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0] : void 0;
1826
1869
  if (!app) return null;
1827
1870
  const meetings = (await db.query({ meetings: { $: { where: { applicationId: app.id, status: "scheduled" }, order: { createdAt: "desc" }, limit: 1 } } })).meetings;
@@ -1852,8 +1895,8 @@ var handleMe = async (req, url, env, ctx) => {
1852
1895
  const role = await ctx.roleFor(db, u);
1853
1896
  const superAdmin = await ctx.isSuperAdminEmail(db, u.email);
1854
1897
  const base = { authorized: isAdminRole(role, ctx.auth), role, superAdmin, email: u.email ?? null };
1855
- if (ctx.chapter.mode !== "chapter" || !u.email) return json(base);
1856
- const member = await memberSessionApplication(db, ctx.chapter.id, u.email);
1898
+ if (ctx.chapter.mode !== "chapter") return json(base);
1899
+ const member = await memberSessionApplication(db, ctx.chapter.id, { userId: u.userId, email: u.email });
1857
1900
  let privateMetadataSynced = false;
1858
1901
  if (member && ctx.chapter.account !== "none" && typeof member.source.clerkPrivateMetadataSyncedAt !== "number") {
1859
1902
  privateMetadataSynced = await syncApplicantPrivateMetadata(
@@ -2002,8 +2045,9 @@ var handleMember = async (req, url, env, ctx) => {
2002
2045
  if (effectiveChapter.sends.adminNotification === "submit") {
2003
2046
  await notifyAdminOfApplication(db, env, chapter.id, result.id, parsed);
2004
2047
  }
2005
- await provisionApplicant(db, effectiveChapter, result.id, parsed);
2006
2048
  }
2049
+ const provisioningFailure = await provisionApplicantForRoute(db, effectiveChapter, result.id, parsed);
2050
+ if (provisioningFailure) return json(provisioningFailure.body, provisioningFailure.status);
2007
2051
  const apps = (await db.query({ applications: { $: { where: { id: result.id }, limit: 1 } } })).applications;
2008
2052
  const canonical2 = Array.isArray(apps) ? apps[0] : void 0;
2009
2053
  const groups = (await db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } })).groups;
@@ -6039,8 +6083,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
6039
6083
  if (sourceType === "chapter" && (resource.scope.kind !== "chapter" || resource.scope.chapterId !== sourceAppId)) {
6040
6084
  errors.push(`document.resources.${resource.id}: Chapter sources may author only their own chapter scope`);
6041
6085
  }
6042
- if (sourceType === "built_not_found" && resource.scope.kind === "chapter") {
6043
- errors.push(`document.resources.${resource.id}: Built Not Found sources may author only built_not_found or global scope`);
6086
+ if (sourceType === "built_not_found" && resource.scope.kind === "chapter" && !(options.allowedChapterIds ?? []).includes(resource.scope.chapterId)) {
6087
+ errors.push(`document.resources.${resource.id}: Built Not Found may target only configured Chapters`);
6044
6088
  }
6045
6089
  }
6046
6090
  if (errors.length) return { ok: false, errors };
@@ -6200,12 +6244,13 @@ var handleBuilderLibraryControl = async (req, url, env, ctx) => {
6200
6244
  const checked = validateBuilderLibraryDocument(input.document, {
6201
6245
  sourceType,
6202
6246
  sourceAppId: verified.source.sourceId,
6203
- targetAppId: ctx.chapter.id
6247
+ targetAppId: ctx.chapter.id,
6248
+ ...ctx.chapter.mode === "chapter" ? { allowedChapterIds: [ctx.chapter.id] } : {}
6204
6249
  });
6205
6250
  if (!checked.ok) return json({ ok: false, reason: "invalid_document", errors: checked.errors }, 400);
6206
6251
  const document = checked.value;
6207
- if (ctx.chapter.mode === "chapter" && document.resources.some((resource) => resource.scope.kind !== "global")) {
6208
- return json({ ok: false, reason: "invalid_document", errors: ["Built Not Found may deliver only global resources to a Chapter"] }, 400);
6252
+ if (ctx.chapter.mode === "chapter" && document.resources.some((resource) => resource.scope.kind !== "global" && (resource.scope.kind !== "chapter" || resource.scope.chapterId !== ctx.chapter.id))) {
6253
+ return json({ ok: false, reason: "invalid_document", errors: ["Built Not Found may deliver only global or this Chapter's resources"] }, 400);
6209
6254
  }
6210
6255
  if (!document.target || document.target.environment !== env.ODLA_ENV) return json({ ok: false, reason: "wrong_environment" }, 409);
6211
6256
  if (!digest || await builderLibraryDigest(document) !== digest) return json({ ok: false, reason: "invalid_digest" }, 400);
@@ -6311,7 +6356,11 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
6311
6356
  source: { type: "built_not_found", appId: ctx.chapter.id },
6312
6357
  resources: [...local3?.document.resources ?? [], promoted]
6313
6358
  };
6314
- const checked2 = validateBuilderLibraryDocument(document, { sourceType: "built_not_found", sourceAppId: ctx.chapter.id });
6359
+ const checked2 = validateBuilderLibraryDocument(document, {
6360
+ sourceType: "built_not_found",
6361
+ sourceAppId: ctx.chapter.id,
6362
+ allowedChapterIds: ctx.chapter.builderLibrary.sources.map((source2) => source2.sourceId)
6363
+ });
6315
6364
  if (!checked2.ok) return json({ error: "promoted resource is invalid", errors: checked2.errors }, 400);
6316
6365
  try {
6317
6366
  const saved = await saveLocalBuilderLibrary(db, checked2.value, env.ODLA_ENV, expectedRevision2, now2, actorFor(user));
@@ -6326,6 +6375,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
6326
6375
  local: local3 ? { revision: local3.revision, document: local3.document } : null,
6327
6376
  sources: controls.map(({ sourceId, revision, digest, document, receivedAt }) => ({ sourceId, revision, digest, document, receivedAt })),
6328
6377
  localAuthoring: ctx.chapter.builderLibrary.localAuthoring,
6378
+ chapterTargets: ctx.chapter.mode === "hub" ? ctx.chapter.builderLibrary.sources.map((source) => ({ chapterId: source.sourceId })) : [{ chapterId: ctx.chapter.id }],
6329
6379
  visibility: { builtNotFoundSeesChapterSources: ctx.chapter.mode === "hub", chaptersSeeSiblingSources: false }
6330
6380
  });
6331
6381
  }
@@ -6344,7 +6394,11 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
6344
6394
  const now = Date.now();
6345
6395
  const actor = actorFor(user);
6346
6396
  const authoritative = withLocalAuthority2(input.document, ctx.chapter.mode, ctx.chapter.id, expectedRevision + 1, now, actor, local3?.document);
6347
- const checked = validateBuilderLibraryDocument(authoritative, { sourceType: ctx.chapter.mode === "hub" ? "built_not_found" : "chapter", sourceAppId: ctx.chapter.id });
6397
+ const checked = validateBuilderLibraryDocument(authoritative, {
6398
+ sourceType: ctx.chapter.mode === "hub" ? "built_not_found" : "chapter",
6399
+ sourceAppId: ctx.chapter.id,
6400
+ ...ctx.chapter.mode === "hub" ? { allowedChapterIds: ctx.chapter.builderLibrary.sources.map((source) => source.sourceId) } : {}
6401
+ });
6348
6402
  if (!checked.ok) return json({ error: "invalid Builder Library", errors: checked.errors }, 400);
6349
6403
  try {
6350
6404
  const saved = await saveLocalBuilderLibrary(db, checked.value, env.ODLA_ENV, expectedRevision, now, actor);