@odla-ai/chapter 0.50.0 → 0.52.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.
package/dist/index.js CHANGED
@@ -502,7 +502,7 @@ var giftRedemptionAttemptsEntity = { attrs: {
502
502
  } };
503
503
 
504
504
  // src/payment-recovery-schema.ts
505
- var field = (type, unique = false) => ({ type, unique, indexed: type !== "json", optional: false });
505
+ var field = (type, unique = false, optional = false) => ({ type, unique, indexed: type !== "json", optional });
506
506
  var paymentRecoveryReviewsEntity = { attrs: {
507
507
  id: field("string", true),
508
508
  eventId: field("string"),
@@ -511,7 +511,9 @@ var paymentRecoveryReviewsEntity = { attrs: {
511
511
  previousDigest: field("string"),
512
512
  providerDigest: field("string"),
513
513
  scope: field("string"),
514
- previousState: field("json")
514
+ previousState: field("json"),
515
+ action: field("string", false, true),
516
+ reason: field("string", false, true)
515
517
  } };
516
518
 
517
519
  // src/schema-stripe.ts
@@ -707,7 +709,7 @@ var boolean = (flags = {}) => ({
707
709
  indexed: flags.indexed ?? false,
708
710
  optional: false
709
711
  });
710
- var json2 = () => ({ type: "json", unique: false, indexed: false, optional: false });
712
+ var json2 = (flags = {}) => ({ type: "json", unique: false, indexed: false, optional: flags.optional ?? false });
711
713
  var id4 = () => string2({ unique: true, indexed: true });
712
714
  var events = { attrs: {
713
715
  id: id4(),
@@ -736,6 +738,10 @@ var exceptions = { attrs: {
736
738
  status: string2({ indexed: true }),
737
739
  resolvedDigest: string2({ optional: true }),
738
740
  incomingDigest: string2({ optional: true }),
741
+ incomingFact: json2({ optional: true }),
742
+ reviewedDigest: string2({ optional: true }),
743
+ resolvedBy: string2({ optional: true }),
744
+ reviewId: string2({ optional: true }),
739
745
  detail: json2(),
740
746
  updatedAt: number2({ indexed: true })
741
747
  } };
@@ -874,6 +880,7 @@ var groups = {
874
880
  stripePublishableKey: attr7("string", { optional: true }),
875
881
  notificationEmail: attr7("string"),
876
882
  replyTo: attr7("string"),
883
+ fromName: attr7("string", { optional: true }),
877
884
  debugEmail: attr7("string", { optional: true }),
878
885
  calendarLink: attr7("string", { optional: true }),
879
886
  disclaimerText: attr7("string"),
@@ -1070,6 +1077,13 @@ function defaultCrm(mode) {
1070
1077
  };
1071
1078
  }
1072
1079
 
1080
+ // src/named-seat-accepted-template.ts
1081
+ var SEAT_ACCEPTED_TEMPLATE = {
1082
+ enabled: true,
1083
+ subject: "{{recipientName}} accepted your {{chapterName}} seat",
1084
+ 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}}"
1085
+ };
1086
+
1073
1087
  // src/group.ts
1074
1088
  var DEFAULT_SCHEDULING = {
1075
1089
  slotMinutes: 45,
@@ -1091,7 +1105,16 @@ ${name}`;
1091
1105
  text: `A new application came in for ${name}.
1092
1106
 
1093
1107
  Name: {{firstName}} {{lastName}}
1094
- Email: {{email}}`
1108
+ Email: {{email}}
1109
+ Tier: {{tier}}
1110
+
1111
+ Review it: {{adminUrl}}`
1112
+ },
1113
+ submitConfirmation: {
1114
+ subject: `We received your application to ${name}`,
1115
+ text: `Hi {{firstName}},
1116
+
1117
+ Thanks for applying to ${name}. {{submitNextStep}}${sign}`
1095
1118
  },
1096
1119
  paymentConfirmation: {
1097
1120
  subject: `Welcome to ${name}`,
@@ -1103,14 +1126,17 @@ Your membership payment{{paymentAmountSummary}} is confirmed. {{paymentNextStep}
1103
1126
  subject: `Your ${name} intro call`,
1104
1127
  text: `Hi {{firstName}},
1105
1128
 
1106
- Looking forward to our call at {{meetingTime}}. {{meetingLink}}${sign}`
1129
+ You are all set{{paymentAmountSummary}}. Looking forward to our call on {{startAt}}.
1130
+
1131
+ Join here: {{meetingLink}}${sign}`
1107
1132
  },
1108
1133
  onboardingInvite: {
1109
1134
  subject: `You're in, ${name}`,
1110
1135
  text: `Hi {{firstName}},
1111
1136
 
1112
1137
  Welcome to ${name}. Your member area is here: {{membersUrl}}${sign}`
1113
- }
1138
+ },
1139
+ seatAccepted: SEAT_ACCEPTED_TEMPLATE
1114
1140
  };
1115
1141
  }
1116
1142
  function buildGroupSeed(config) {
@@ -1139,6 +1165,7 @@ function buildGroupSeed(config) {
1139
1165
  schedulingJson: scheduling
1140
1166
  };
1141
1167
  if (emails?.debugEmail) row2.debugEmail = emails.debugEmail;
1168
+ if (emails?.fromName?.trim()) row2.fromName = emails.fromName.trim();
1142
1169
  if (policy.commitmentText) row2.commitmentText = policy.commitmentText;
1143
1170
  if (policy.normsText) row2.normsText = policy.normsText;
1144
1171
  return row2;
@@ -1359,6 +1386,8 @@ var DEFAULT_ADMIN_COPY = {
1359
1386
  delivery: "Delivery",
1360
1387
  notificationAddress: "Notification address",
1361
1388
  replyTo: "Reply-to",
1389
+ fromName: "From name",
1390
+ fromNameHint: "Shown as the sender; the address stays the Worker's configured one.",
1362
1391
  debugInbox: "Debug inbox",
1363
1392
  debugInboxHint: "Non-production sends redirect here.",
1364
1393
  sendTest: "Send test",
@@ -1430,6 +1459,8 @@ var DEFAULT_ADMIN_COPY = {
1430
1459
  transportReachable: "transport reachable",
1431
1460
  transportUnavailable: "transport unavailable",
1432
1461
  transportNotChecked: "transport not checked",
1462
+ edgeUnpaired: "edge unpaired",
1463
+ edgeUnpairedHint: "This follower's shared secret is not in the leader vault, so nothing can be signed. Pair both vaults from an enrolled machine:",
1433
1464
  bindingTransport: "service binding {binding}",
1434
1465
  urlTransport: "public URL transport",
1435
1466
  activeConfiguration: "active configuration r{revision}",
@@ -2158,6 +2189,10 @@ function defineChapter(config) {
2158
2189
  if (!config.emails || typeof config.emails.notificationEmail !== "string" || config.emails.notificationEmail === "") {
2159
2190
  throw new Error("defineChapter.emails.notificationEmail: required in chapter mode");
2160
2191
  }
2192
+ const fromName = config.emails.fromName;
2193
+ if (fromName !== void 0 && (typeof fromName !== "string" || /[\r\n<>]/.test(fromName) || fromName.trim().length > 100)) {
2194
+ throw new Error("defineChapter.emails.fromName: must be one line of at most 100 characters with no angle brackets");
2195
+ }
2161
2196
  if (!config.prices || typeof config.prices.standardCents !== "number") {
2162
2197
  throw new Error("defineChapter.prices.standardCents: required in chapter mode");
2163
2198
  }
@@ -3153,6 +3188,7 @@ function createChapterIntegration(chapter, options = {}) {
3153
3188
  description: "Signing secret for the read-only network commercial Stripe webhook endpoint.",
3154
3189
  required: true
3155
3190
  });
3191
+ secrets.push(...federationEdgeSecrets(chapter));
3156
3192
  return {
3157
3193
  id: "chapter",
3158
3194
  title: `Chapter: ${chapter.name}`,
@@ -3196,8 +3232,48 @@ function createChapterIntegration(chapter, options = {}) {
3196
3232
  runbooks
3197
3233
  };
3198
3234
  }
3235
+ function federationEdgeSecrets(chapter) {
3236
+ const secrets = chapter.network.targets.map((target) => ({
3237
+ name: target.secretName,
3238
+ 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}".`,
3239
+ required: true,
3240
+ edge: { role: "leader", peers: [target.id] }
3241
+ }));
3242
+ const peers = [.../* @__PURE__ */ new Set([
3243
+ ...chapter.network.readers.map((reader) => reader.id),
3244
+ ...(chapter.builderLibrary?.sources ?? []).map((source) => source.sourceId)
3245
+ ])];
3246
+ if (peers.length > 0) secrets.push({
3247
+ name: "network_share_secret",
3248
+ 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}".`,
3249
+ required: true,
3250
+ edge: { role: "follower", peers }
3251
+ });
3252
+ return secrets;
3253
+ }
3254
+
3255
+ // src/network-edge.ts
3256
+ var EDGE_UNPAIRED = "edge_unpaired";
3257
+ function pairingCommand(leaderId, targetId, env) {
3258
+ const scope2 = env ? ` --env ${env}` : "";
3259
+ return `odla-ai chapter edge pair --leader ${leaderId} --follower ${targetId}${scope2}`;
3260
+ }
3261
+ function edgeUnpaired(leaderId, target, env) {
3262
+ return {
3263
+ code: EDGE_UNPAIRED,
3264
+ target: target.id,
3265
+ secretName: target.secretName,
3266
+ message: `edge to "${target.id}" is unpaired: leader vault slot "${target.secretName}" is empty`,
3267
+ hint: pairingCommand(leaderId, target.id, env)
3268
+ };
3269
+ }
3199
3270
 
3200
3271
  // src/membership-policy.ts
3272
+ function seatPurchaseDeadline(offering, member) {
3273
+ if (offering.seatPurchaseWindowCalendarMonths === void 0) return Number(member.termEndsAt);
3274
+ if (Number.isSafeInteger(member.seatPurchaseEndsAt)) return Number(member.seatPurchaseEndsAt);
3275
+ return addMembershipCalendarMonths(Number(member.initialTermStartedAt ?? member.termStartedAt), offering.seatPurchaseWindowCalendarMonths);
3276
+ }
3201
3277
  var fail2 = (field2) => {
3202
3278
  throw new TypeError(`invalid membership policy: ${field2}`);
3203
3279
  };
@@ -3254,7 +3330,7 @@ function membershipOfferingVersion(value) {
3254
3330
  autoRenew: true,
3255
3331
  paymentGraceDays: integer2(input.paymentGraceDays, 0, 60),
3256
3332
  seatPriceCents: integer2(input.seatPriceCents, 1, Number.MAX_SAFE_INTEGER),
3257
- seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120),
3333
+ ...input.seatPurchaseWindowCalendarMonths === void 0 || input.seatPurchaseWindowCalendarMonths === null ? {} : { seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120) },
3258
3334
  requiredDisclosures: ids(input.requiredDisclosures)
3259
3335
  };
3260
3336
  }
@@ -3648,7 +3724,7 @@ async function confirmMembershipPayment(db, input, now) {
3648
3724
  initialTermEndsAt: endsAt,
3649
3725
  termStartedAt: paidAt,
3650
3726
  termEndsAt: endsAt,
3651
- seatPurchaseEndsAt: addMembershipCalendarMonths(Number(paidAt), offering.seatPurchaseWindowCalendarMonths),
3727
+ seatPurchaseEndsAt: seatPurchaseDeadline(offering, { initialTermStartedAt: paidAt, termEndsAt: endsAt }),
3652
3728
  autoRenew: true,
3653
3729
  refundedCents: 0,
3654
3730
  revision: 1,
@@ -4076,7 +4152,11 @@ async function reconcileMembershipRenewal(db, input, now) {
4076
4152
  membershipUpdate("networkMembership", id10, {
4077
4153
  status: status2,
4078
4154
  graceEndsAt,
4079
- ...paid ? { termStartedAt: start, termEndsAt: end } : {},
4155
+ ...paid ? {
4156
+ termStartedAt: start,
4157
+ termEndsAt: end,
4158
+ ...membershipOfferingVersion(member.offeringSnapshot).seatPurchaseWindowCalendarMonths === void 0 ? { seatPurchaseEndsAt: end } : {}
4159
+ } : {},
4080
4160
  revision: Number(member.revision) + 1,
4081
4161
  updatedAt: now
4082
4162
  }),
@@ -4378,10 +4458,11 @@ async function reserveNamedMembershipSeat(db, input, now) {
4378
4458
  const id10 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
4379
4459
  let seat = await membershipLoad(db, "networkSeat", id10);
4380
4460
  if (!seat) {
4381
- if (!["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || now < Number(member.initialTermStartedAt) || now >= Number(member.termEndsAt) || now > Number(member.seatPurchaseEndsAt) || !Number.isSafeInteger(member.seatPurchaseEndsAt) || member.namedSeatId !== void 0) {
4461
+ const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
4462
+ const purchaseEndsAt = seatPurchaseDeadline(offering, member);
4463
+ if (!["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || now < Number(member.initialTermStartedAt) || now >= Number(member.termEndsAt) || now > purchaseEndsAt || !Number.isSafeInteger(purchaseEndsAt) || member.namedSeatId !== void 0) {
4382
4464
  membershipFail("named_seat_purchase_unavailable");
4383
4465
  }
4384
- const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
4385
4466
  const attrs = {
4386
4467
  id: id10,
4387
4468
  ...membershipScope(input),
@@ -4400,7 +4481,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
4400
4481
  status: "payment_pending",
4401
4482
  preparedAt: now,
4402
4483
  termEndsAt: member.termEndsAt,
4403
- seatPurchaseEndsAt: member.seatPurchaseEndsAt,
4484
+ seatPurchaseEndsAt: purchaseEndsAt,
4404
4485
  offeringSnapshot: offering,
4405
4486
  revision: 1,
4406
4487
  createdAt: now,
@@ -4617,7 +4698,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
4617
4698
  }
4618
4699
  if (seat.purchasedAt === void 0) {
4619
4700
  if (!["payment_processing", "payment_canceling"].includes(String(seat.status))) membershipFail("named_seat_payment_not_expected");
4620
- const eligible = seat.status !== "payment_canceling" && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && Number(member.refundedCents) === 0 && paidAt <= Number(member.seatPurchaseEndsAt) && now < Number(member.termEndsAt);
4701
+ const eligible = seat.status !== "payment_canceling" && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && Number(member.refundedCents) === 0 && paidAt <= seatPurchaseDeadline(membershipOfferingVersion(member.offeringSnapshot), member) && now < Number(member.termEndsAt);
4621
4702
  const status2 = eligible ? "renewal_pending" : "refund_pending", effectId = `${seat.id}:${eligible ? "attach-renewal" : "refund"}`;
4622
4703
  try {
4623
4704
  await db.transact([
@@ -4969,15 +5050,16 @@ async function readNamedSeatOffer(db, input, now) {
4969
5050
  const offering = membershipOfferingVersion(member.offeringSnapshot);
4970
5051
  const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
4971
5052
  if (seat) requireMembershipScope(seat, input);
5053
+ const purchaseEndsAt = seatPurchaseDeadline(offering, member);
4972
5054
  const offer = {
4973
5055
  membershipId: id10,
4974
5056
  amountCents: offering.seatPriceCents,
4975
5057
  currency: offering.currency,
4976
5058
  interval: offering.interval,
4977
5059
  termEndsAt: Number(member.termEndsAt),
4978
- purchaseEndsAt: Number(member.seatPurchaseEndsAt),
5060
+ purchaseEndsAt,
4979
5061
  autoRenew: member.autoRenew === true,
4980
- eligible: ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && member.refundedCents === 0 && now >= Number(member.initialTermStartedAt) && now < Number(member.termEndsAt) && now <= Number(member.seatPurchaseEndsAt) && (!seat || ["payment_pending", "payment_processing"].includes(String(seat.status))),
5062
+ eligible: ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && member.refundedCents === 0 && now >= Number(member.initialTermStartedAt) && now < Number(member.termEndsAt) && now <= purchaseEndsAt && (!seat || ["payment_pending", "payment_processing"].includes(String(seat.status))),
4981
5063
  ...seat ? { seat: {
4982
5064
  seatId: String(seat.id),
4983
5065
  status: String(seat.status),
@@ -4985,7 +5067,7 @@ async function readNamedSeatOffer(db, input, now) {
4985
5067
  recipientEmail: String(seat.recipientEmailNormalized)
4986
5068
  } } : {}
4987
5069
  };
4988
- return { ...offer, digest: await namedSeatOfferDigest(offer) };
5070
+ return { ...offer, digest: await namedSeatOfferDigest(offer), purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0 };
4989
5071
  }
4990
5072
 
4991
5073
  // src/membership-seat-invitation.ts
@@ -5470,7 +5552,7 @@ async function requestNamedSeatReservation(options) {
5470
5552
  async function requestNamedSeatOffer(options) {
5471
5553
  const result = await requestAuthority(options, "seat-offer", { applicationId: options.applicationId, skuId: options.skuId });
5472
5554
  const offer = result.seat;
5473
- if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents <= 0 || !/^[a-z]{3}$/.test(offer.currency) || !["month", "year"].includes(offer.interval) || typeof offer.eligible !== "boolean" || typeof offer.autoRenew !== "boolean" || !Number.isSafeInteger(offer.termEndsAt) || !Number.isSafeInteger(offer.purchaseEndsAt) || offer.digest !== await namedSeatOfferDigest(offer)) throw new Error("invalid named seat offer");
5555
+ if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents <= 0 || !/^[a-z]{3}$/.test(offer.currency) || !["month", "year"].includes(offer.interval) || typeof offer.eligible !== "boolean" || typeof offer.autoRenew !== "boolean" || !Number.isSafeInteger(offer.termEndsAt) || !Number.isSafeInteger(offer.purchaseEndsAt) || offer.purchaseWindowed !== void 0 && typeof offer.purchaseWindowed !== "boolean" || offer.digest !== await namedSeatOfferDigest(offer)) throw new Error("invalid named seat offer");
5474
5556
  return offer;
5475
5557
  }
5476
5558
  async function requestNamedSeatInvitation(options) {
@@ -6554,6 +6636,12 @@ function planDelivery(input) {
6554
6636
  }
6555
6637
 
6556
6638
  // src/notify.ts
6639
+ function fromAddress(configured, name) {
6640
+ const display = name?.trim();
6641
+ if (!display) return configured;
6642
+ const angled = configured.match(/<([^>]+)>/);
6643
+ return { name: display, email: (angled ? angled[1] : configured).trim() };
6644
+ }
6557
6645
  async function sendTemplated(deps, input) {
6558
6646
  const { emailLog: emailLog2 } = await deps.db.query({ emailLog: { $: { where: { dedupeKey: input.dedupeKey } } } });
6559
6647
  const prior = Array.isArray(emailLog2) ? emailLog2 : [];
@@ -6616,7 +6704,7 @@ async function sendTemplated(deps, input) {
6616
6704
  let messageId;
6617
6705
  try {
6618
6706
  const response = await deps.sender.send({
6619
- from: deps.from,
6707
+ from: fromAddress(deps.from, input.group.fromName),
6620
6708
  to: [decision.to],
6621
6709
  subject: String(row2.subject),
6622
6710
  text: String(row2.body),
@@ -6649,6 +6737,7 @@ function emailGroupFrom(row2) {
6649
6737
  name: String(row2.name ?? ""),
6650
6738
  replyTo: str2(row2.replyTo) ?? "",
6651
6739
  debugEmail: str2(row2.debugEmail),
6740
+ fromName: str2(row2.fromName),
6652
6741
  refundPolicyText: str2(row2.refundPolicyText),
6653
6742
  commitmentText: str2(row2.commitmentText),
6654
6743
  normsText: str2(row2.normsText),
@@ -9415,6 +9504,7 @@ export {
9415
9504
  CHAPTER_PROMOTION_ADAPTER,
9416
9505
  DEFAULT_CHAPTER_COPY,
9417
9506
  DEFAULT_SHARE_FIELDS,
9507
+ EDGE_UNPAIRED,
9418
9508
  GIFT_OFFER_CATALOG_VERSION,
9419
9509
  JOURNEY_ENDINGS,
9420
9510
  JOURNEY_STAGES,
@@ -9495,6 +9585,7 @@ export {
9495
9585
  defaultCrm,
9496
9586
  defineChapter,
9497
9587
  drainMembershipEffects,
9588
+ edgeUnpaired,
9498
9589
  emailGroupFrom,
9499
9590
  endForSlot,
9500
9591
  exportChapterPromotion,
@@ -9554,6 +9645,7 @@ export {
9554
9645
  normalizeSharedRecord,
9555
9646
  normalizeWebhookEvent,
9556
9647
  offerableTiers,
9648
+ pairingCommand,
9557
9649
  paymentsReady,
9558
9650
  personInputFromApp,
9559
9651
  planDelivery,
@@ -9623,6 +9715,7 @@ export {
9623
9715
  resolveSignupControl,
9624
9716
  resolveTiers,
9625
9717
  roleFromClaim,
9718
+ seatPurchaseDeadline,
9626
9719
  sendTemplated,
9627
9720
  sharedPersonInput,
9628
9721
  sharedRecordFromCrm,