@odla-ai/chapter 0.51.0 → 0.52.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.
@@ -26,7 +26,7 @@ type TextFields$1<Key extends string> = {
26
26
  [Field in Key]: string;
27
27
  };
28
28
  /** User-visible language for sharing, delivery state, and leader rollups. */
29
- type AdminNetworkCopy = TextFields$1<"title" | "allowlistDescription" | "sharing" | "shareWith" | "shared" | "shareFailed" | "deliveryFailed" | "loadingRollup" | "loadRollupFailed" | "overview" | "configuredFollowers" | "reachableFollowers" | "recordsAcrossFollowers" | "noFollowers" | "followers" | "available" | "unavailable" | "transportReachable" | "transportUnavailable" | "transportNotChecked" | "bindingTransport" | "urlTransport" | "activeConfiguration" | "noActiveConfiguration" | "publicSiteAvailable" | "publicSiteUnavailable" | "publicSiteNotChecked" | "recordTotals" | "noRecordTotals">;
29
+ type AdminNetworkCopy = TextFields$1<"title" | "allowlistDescription" | "sharing" | "shareWith" | "shared" | "shareFailed" | "deliveryFailed" | "loadingRollup" | "loadRollupFailed" | "overview" | "configuredFollowers" | "reachableFollowers" | "recordsAcrossFollowers" | "noFollowers" | "followers" | "available" | "unavailable" | "transportReachable" | "transportUnavailable" | "transportNotChecked" | "edgeUnpaired" | "edgeUnpairedHint" | "bindingTransport" | "urlTransport" | "activeConfiguration" | "noActiveConfiguration" | "publicSiteAvailable" | "publicSiteUnavailable" | "publicSiteNotChecked" | "recordTotals" | "noRecordTotals">;
30
30
 
31
31
  /** Copy for the person record panels: lifecycle actions, comms, scheduling, billing and meetings. */
32
32
  type AdminRecordsCopy = {
@@ -57,7 +57,7 @@ interface ChapterCopy {
57
57
  * agree rather than merely restating the policy. Set it to an empty string
58
58
  * to render the checkbox with the policy as its only label.
59
59
  */
60
- payment: TextFields<"setupFailed" | "preparing" | "pending" | "processing" | "payAndContinue" | "incomplete" | "consent" | "foundingMember" | "foundingDiscount">;
60
+ payment: TextFields<"setupFailed" | "preparing" | "pending" | "processing" | "payAndContinue" | "incomplete" | "consent" | "foundingMember" | "foundingDiscount" | "standardRate" | "yourRate">;
61
61
  done: TextFields<"label" | "calendarInvite" | "memberArea">;
62
62
  accepted: TextFields<"label" | "body">;
63
63
  gift: TextFields<"chooseSelf" | "optionsLabel" | "label" | "body" | "save" | "rotate" | "rotateFailed" | "claimTitle" | "claimBody" | "code" | "redeem" | "redeemed" | "redeemFailed">;
@@ -79,7 +79,7 @@ interface ChapterCopy {
79
79
  availability: TextFields<"loading" | "loadFailed" | "saved" | "title" | "days" | "startHour" | "endHour" | "slotMinutes" | "minNoticeHours" | "windowDays" | "timezone" | "summaryTemplate" | "save"> & {
80
80
  dayLabels: readonly string[];
81
81
  };
82
- email: TextFields<"loading" | "loadFailed" | "saved" | "sent" | "testFailed" | "delivery" | "notificationAddress" | "replyTo" | "debugInbox" | "debugInboxHint" | "sendTest" | "enabled" | "subject" | "body" | "save" | "sendLog" | "sentColumn" | "templateColumn" | "statusColumn" | "toColumn" | "failed" | "redirected" | "delivered">;
82
+ email: TextFields<"loading" | "loadFailed" | "saved" | "sent" | "testFailed" | "delivery" | "notificationAddress" | "replyTo" | "fromName" | "fromNameHint" | "debugInbox" | "debugInboxHint" | "sendTest" | "enabled" | "subject" | "body" | "save" | "sendLog" | "sentColumn" | "templateColumn" | "statusColumn" | "toColumn" | "failed" | "redirected" | "delivered">;
83
83
  meetings: TextFields<"loading" | "loadFailed" | "cancelConfirm" | "newStartPrompt" | "parseFailed" | "agenda" | "empty" | "unknown" | "drift" | "meet" | "reschedule" | "cancel">;
84
84
  admission: TextFields<"title" | "description" | "email" | "decision" | "expires" | "note" | "waive" | "require" | "add" | "ledger" | "loading" | "consumed" | "revoked" | "expired" | "active" | "local" | "parent" | "revoke">;
85
85
  network: AdminNetworkCopy;
@@ -215,7 +215,10 @@ type DbRules = Record<string, Rule>;
215
215
 
216
216
  /** One follower site a leader can push CRM records to. The secret VALUE never
217
217
  * lives in config: `secretName` names the leader tenant's vaulted copy of the
218
- * follower's `network_share_secret`. */
218
+ * follower's `network_share_secret`. Both slots are declared by
219
+ * `createChapterIntegration` and filled together by
220
+ * `odla-ai chapter edge pair --leader <leader> --follower <id>`; until then
221
+ * every leader-side surface reports the edge as `edge_unpaired`. */
219
222
  interface ChapterNetworkTarget {
220
223
  /** Stable lowercase slug used by the UI, audit tags, and default secret name. */
221
224
  id: string;
@@ -460,9 +463,12 @@ interface EmailTemplate {
460
463
  interface ChapterEmails {
461
464
  notificationEmail: string;
462
465
  replyTo?: string;
466
+ /** Display name on outbound lifecycle mail (`Tori Horton <from@…>`); the
467
+ * address itself stays the Worker's `EMAIL_FROM`. Absent ⇒ bare address. */
468
+ fromName?: string;
463
469
  debugEmail?: string;
464
- /** Operational lifecycle templates: adminNotification / paymentConfirmation /
465
- * prepEmail / onboardingInvite, each `{ subject, text, enabled? }`. */
470
+ /** Operational lifecycle templates: adminNotification / submitConfirmation /
471
+ * paymentConfirmation / prepEmail / onboardingInvite, each `{ subject, text, enabled? }`. */
466
472
  templates?: Record<string, EmailTemplate>;
467
473
  }
468
474
  /** Booking rules for the intro-call scheduler (stored as schedulingJson). */
package/dist/index.cjs CHANGED
@@ -31,6 +31,7 @@ __export(src_exports, {
31
31
  CHAPTER_PROMOTION_ADAPTER: () => CHAPTER_PROMOTION_ADAPTER,
32
32
  DEFAULT_CHAPTER_COPY: () => DEFAULT_CHAPTER_COPY,
33
33
  DEFAULT_SHARE_FIELDS: () => DEFAULT_SHARE_FIELDS,
34
+ EDGE_UNPAIRED: () => EDGE_UNPAIRED,
34
35
  GIFT_OFFER_CATALOG_VERSION: () => GIFT_OFFER_CATALOG_VERSION,
35
36
  JOURNEY_ENDINGS: () => JOURNEY_ENDINGS,
36
37
  JOURNEY_STAGES: () => JOURNEY_STAGES,
@@ -111,6 +112,7 @@ __export(src_exports, {
111
112
  defaultCrm: () => defaultCrm,
112
113
  defineChapter: () => defineChapter,
113
114
  drainMembershipEffects: () => drainMembershipEffects,
115
+ edgeUnpaired: () => edgeUnpaired,
114
116
  emailGroupFrom: () => emailGroupFrom,
115
117
  endForSlot: () => endForSlot,
116
118
  exportChapterPromotion: () => exportChapterPromotion,
@@ -170,6 +172,7 @@ __export(src_exports, {
170
172
  normalizeSharedRecord: () => normalizeSharedRecord,
171
173
  normalizeWebhookEvent: () => normalizeWebhookEvent,
172
174
  offerableTiers: () => offerableTiers,
175
+ pairingCommand: () => pairingCommand,
173
176
  paymentsReady: () => paymentsReady,
174
177
  personInputFromApp: () => personInputFromApp,
175
178
  planDelivery: () => planDelivery,
@@ -777,7 +780,7 @@ var giftRedemptionAttemptsEntity = { attrs: {
777
780
  } };
778
781
 
779
782
  // src/payment-recovery-schema.ts
780
- var field = (type, unique = false) => ({ type, unique, indexed: type !== "json", optional: false });
783
+ var field = (type, unique = false, optional = false) => ({ type, unique, indexed: type !== "json", optional });
781
784
  var paymentRecoveryReviewsEntity = { attrs: {
782
785
  id: field("string", true),
783
786
  eventId: field("string"),
@@ -786,7 +789,9 @@ var paymentRecoveryReviewsEntity = { attrs: {
786
789
  previousDigest: field("string"),
787
790
  providerDigest: field("string"),
788
791
  scope: field("string"),
789
- previousState: field("json")
792
+ previousState: field("json"),
793
+ action: field("string", false, true),
794
+ reason: field("string", false, true)
790
795
  } };
791
796
 
792
797
  // src/schema-stripe.ts
@@ -982,7 +987,7 @@ var boolean = (flags = {}) => ({
982
987
  indexed: flags.indexed ?? false,
983
988
  optional: false
984
989
  });
985
- var json2 = () => ({ type: "json", unique: false, indexed: false, optional: false });
990
+ var json2 = (flags = {}) => ({ type: "json", unique: false, indexed: false, optional: flags.optional ?? false });
986
991
  var id4 = () => string2({ unique: true, indexed: true });
987
992
  var events = { attrs: {
988
993
  id: id4(),
@@ -1011,6 +1016,10 @@ var exceptions = { attrs: {
1011
1016
  status: string2({ indexed: true }),
1012
1017
  resolvedDigest: string2({ optional: true }),
1013
1018
  incomingDigest: string2({ optional: true }),
1019
+ incomingFact: json2({ optional: true }),
1020
+ reviewedDigest: string2({ optional: true }),
1021
+ resolvedBy: string2({ optional: true }),
1022
+ reviewId: string2({ optional: true }),
1014
1023
  detail: json2(),
1015
1024
  updatedAt: number2({ indexed: true })
1016
1025
  } };
@@ -1149,6 +1158,7 @@ var groups = {
1149
1158
  stripePublishableKey: attr7("string", { optional: true }),
1150
1159
  notificationEmail: attr7("string"),
1151
1160
  replyTo: attr7("string"),
1161
+ fromName: attr7("string", { optional: true }),
1152
1162
  debugEmail: attr7("string", { optional: true }),
1153
1163
  calendarLink: attr7("string", { optional: true }),
1154
1164
  disclaimerText: attr7("string"),
@@ -1345,6 +1355,13 @@ function defaultCrm(mode) {
1345
1355
  };
1346
1356
  }
1347
1357
 
1358
+ // src/named-seat-accepted-template.ts
1359
+ var SEAT_ACCEPTED_TEMPLATE = {
1360
+ enabled: true,
1361
+ subject: "{{recipientName}} accepted your {{chapterName}} seat",
1362
+ text: "Hi {{firstName}},\n\n{{recipientName}} has accepted the {{tierName}} seat you purchased at {{chapterName}}. They will complete their own application and any membership requirements from here; nothing more is needed from you.\n\nWarmly,\n{{chapterName}}"
1363
+ };
1364
+
1348
1365
  // src/group.ts
1349
1366
  var DEFAULT_SCHEDULING = {
1350
1367
  slotMinutes: 45,
@@ -1366,7 +1383,16 @@ ${name}`;
1366
1383
  text: `A new application came in for ${name}.
1367
1384
 
1368
1385
  Name: {{firstName}} {{lastName}}
1369
- Email: {{email}}`
1386
+ Email: {{email}}
1387
+ Tier: {{tier}}
1388
+
1389
+ Review it: {{adminUrl}}`
1390
+ },
1391
+ submitConfirmation: {
1392
+ subject: `We received your application to ${name}`,
1393
+ text: `Hi {{firstName}},
1394
+
1395
+ Thanks for applying to ${name}. {{submitNextStep}}${sign}`
1370
1396
  },
1371
1397
  paymentConfirmation: {
1372
1398
  subject: `Welcome to ${name}`,
@@ -1378,14 +1404,17 @@ Your membership payment{{paymentAmountSummary}} is confirmed. {{paymentNextStep}
1378
1404
  subject: `Your ${name} intro call`,
1379
1405
  text: `Hi {{firstName}},
1380
1406
 
1381
- Looking forward to our call at {{meetingTime}}. {{meetingLink}}${sign}`
1407
+ You are all set{{paymentAmountSummary}}. Looking forward to our call on {{startAt}}.
1408
+
1409
+ Join here: {{meetingLink}}${sign}`
1382
1410
  },
1383
1411
  onboardingInvite: {
1384
1412
  subject: `You're in, ${name}`,
1385
1413
  text: `Hi {{firstName}},
1386
1414
 
1387
1415
  Welcome to ${name}. Your member area is here: {{membersUrl}}${sign}`
1388
- }
1416
+ },
1417
+ seatAccepted: SEAT_ACCEPTED_TEMPLATE
1389
1418
  };
1390
1419
  }
1391
1420
  function buildGroupSeed(config) {
@@ -1414,6 +1443,7 @@ function buildGroupSeed(config) {
1414
1443
  schedulingJson: scheduling
1415
1444
  };
1416
1445
  if (emails?.debugEmail) row2.debugEmail = emails.debugEmail;
1446
+ if (emails?.fromName?.trim()) row2.fromName = emails.fromName.trim();
1417
1447
  if (policy.commitmentText) row2.commitmentText = policy.commitmentText;
1418
1448
  if (policy.normsText) row2.normsText = policy.normsText;
1419
1449
  return row2;
@@ -1634,6 +1664,8 @@ var DEFAULT_ADMIN_COPY = {
1634
1664
  delivery: "Delivery",
1635
1665
  notificationAddress: "Notification address",
1636
1666
  replyTo: "Reply-to",
1667
+ fromName: "From name",
1668
+ fromNameHint: "Shown as the sender; the address stays the Worker's configured one.",
1637
1669
  debugInbox: "Debug inbox",
1638
1670
  debugInboxHint: "Non-production sends redirect here.",
1639
1671
  sendTest: "Send test",
@@ -1705,6 +1737,8 @@ var DEFAULT_ADMIN_COPY = {
1705
1737
  transportReachable: "transport reachable",
1706
1738
  transportUnavailable: "transport unavailable",
1707
1739
  transportNotChecked: "transport not checked",
1740
+ edgeUnpaired: "edge unpaired",
1741
+ edgeUnpairedHint: "This follower's shared secret is not in the leader vault, so nothing can be signed. Pair both vaults from an enrolled machine:",
1708
1742
  bindingTransport: "service binding {binding}",
1709
1743
  urlTransport: "public URL transport",
1710
1744
  activeConfiguration: "active configuration r{revision}",
@@ -1804,7 +1838,9 @@ var DEFAULT_JOIN_COPY = {
1804
1838
  incomplete: "The payment did not complete. Please try again.",
1805
1839
  consent: "I have read and agree to the pricing and refund policy above.",
1806
1840
  foundingMember: "Founding member rate",
1807
- foundingDiscount: "Founding discount"
1841
+ foundingDiscount: "Founding discount",
1842
+ standardRate: "Standard rate",
1843
+ yourRate: "Your rate"
1808
1844
  },
1809
1845
  done: {
1810
1846
  label: "You're booked",
@@ -2433,6 +2469,10 @@ function defineChapter(config) {
2433
2469
  if (!config.emails || typeof config.emails.notificationEmail !== "string" || config.emails.notificationEmail === "") {
2434
2470
  throw new Error("defineChapter.emails.notificationEmail: required in chapter mode");
2435
2471
  }
2472
+ const fromName = config.emails.fromName;
2473
+ if (fromName !== void 0 && (typeof fromName !== "string" || /[\r\n<>]/.test(fromName) || fromName.trim().length > 100)) {
2474
+ throw new Error("defineChapter.emails.fromName: must be one line of at most 100 characters with no angle brackets");
2475
+ }
2436
2476
  if (!config.prices || typeof config.prices.standardCents !== "number") {
2437
2477
  throw new Error("defineChapter.prices.standardCents: required in chapter mode");
2438
2478
  }
@@ -3428,6 +3468,7 @@ function createChapterIntegration(chapter, options = {}) {
3428
3468
  description: "Signing secret for the read-only network commercial Stripe webhook endpoint.",
3429
3469
  required: true
3430
3470
  });
3471
+ secrets.push(...federationEdgeSecrets(chapter));
3431
3472
  return {
3432
3473
  id: "chapter",
3433
3474
  title: `Chapter: ${chapter.name}`,
@@ -3471,6 +3512,41 @@ function createChapterIntegration(chapter, options = {}) {
3471
3512
  runbooks
3472
3513
  };
3473
3514
  }
3515
+ function federationEdgeSecrets(chapter) {
3516
+ const secrets = chapter.network.targets.map((target) => ({
3517
+ name: target.secretName,
3518
+ description: `Shared secret for the signed federation edge to follower "${target.id}"; pair both vaults with "odla-ai chapter edge pair --leader ${chapter.id} --follower ${target.id}".`,
3519
+ required: true,
3520
+ edge: { role: "leader", peers: [target.id] }
3521
+ }));
3522
+ const peers = [.../* @__PURE__ */ new Set([
3523
+ ...chapter.network.readers.map((reader) => reader.id),
3524
+ ...(chapter.builderLibrary?.sources ?? []).map((source) => source.sourceId)
3525
+ ])];
3526
+ if (peers.length > 0) secrets.push({
3527
+ name: "network_share_secret",
3528
+ description: `Shared secret every declared leader (${peers.join(", ")}) signs federation requests with; pair it with "odla-ai chapter edge pair --leader <leader> --follower ${chapter.id}".`,
3529
+ required: true,
3530
+ edge: { role: "follower", peers }
3531
+ });
3532
+ return secrets;
3533
+ }
3534
+
3535
+ // src/network-edge.ts
3536
+ var EDGE_UNPAIRED = "edge_unpaired";
3537
+ function pairingCommand(leaderId, targetId, env) {
3538
+ const scope2 = env ? ` --env ${env}` : "";
3539
+ return `odla-ai chapter edge pair --leader ${leaderId} --follower ${targetId}${scope2}`;
3540
+ }
3541
+ function edgeUnpaired(leaderId, target, env) {
3542
+ return {
3543
+ code: EDGE_UNPAIRED,
3544
+ target: target.id,
3545
+ secretName: target.secretName,
3546
+ message: `edge to "${target.id}" is unpaired: leader vault slot "${target.secretName}" is empty`,
3547
+ hint: pairingCommand(leaderId, target.id, env)
3548
+ };
3549
+ }
3474
3550
 
3475
3551
  // src/membership-policy.ts
3476
3552
  function seatPurchaseDeadline(offering, member) {
@@ -6840,6 +6916,12 @@ function planDelivery(input) {
6840
6916
  }
6841
6917
 
6842
6918
  // src/notify.ts
6919
+ function fromAddress(configured, name) {
6920
+ const display = name?.trim();
6921
+ if (!display) return configured;
6922
+ const angled = configured.match(/<([^>]+)>/);
6923
+ return { name: display, email: (angled ? angled[1] : configured).trim() };
6924
+ }
6843
6925
  async function sendTemplated(deps, input) {
6844
6926
  const { emailLog: emailLog2 } = await deps.db.query({ emailLog: { $: { where: { dedupeKey: input.dedupeKey } } } });
6845
6927
  const prior = Array.isArray(emailLog2) ? emailLog2 : [];
@@ -6902,7 +6984,7 @@ async function sendTemplated(deps, input) {
6902
6984
  let messageId;
6903
6985
  try {
6904
6986
  const response = await deps.sender.send({
6905
- from: deps.from,
6987
+ from: fromAddress(deps.from, input.group.fromName),
6906
6988
  to: [decision.to],
6907
6989
  subject: String(row2.subject),
6908
6990
  text: String(row2.body),
@@ -6935,6 +7017,7 @@ function emailGroupFrom(row2) {
6935
7017
  name: String(row2.name ?? ""),
6936
7018
  replyTo: str2(row2.replyTo) ?? "",
6937
7019
  debugEmail: str2(row2.debugEmail),
7020
+ fromName: str2(row2.fromName),
6938
7021
  refundPolicyText: str2(row2.refundPolicyText),
6939
7022
  commitmentText: str2(row2.commitmentText),
6940
7023
  normsText: str2(row2.normsText),