@odla-ai/chapter 0.59.0 → 0.60.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/README.md +31 -2
- package/dist/{chunk-Y2H4VURI.js → chunk-D5UR5H6R.js} +4 -3
- package/dist/chunk-D5UR5H6R.js.map +1 -0
- package/dist/{chunk-WFTWT3V7.js → chunk-J7ES2OWN.js} +22 -6
- package/dist/{chunk-WFTWT3V7.js.map → chunk-J7ES2OWN.js.map} +1 -1
- package/dist/{chunk-GADLTNJF.js → chunk-QNP3UEIL.js} +2 -1
- package/dist/chunk-QNP3UEIL.js.map +1 -0
- package/dist/{copy-context-BuJBQafl.d.ts → copy-context-BP3G1iBd.d.ts} +1 -1
- package/dist/index.cjs +302 -98
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -3
- package/dist/index.d.ts +55 -3
- package/dist/index.js +302 -98
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +6 -2
- package/dist/ui/admin/index.js +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +3 -3
- package/dist/ui/member/index.d.ts +3 -3
- package/dist/ui/member/index.js +2 -2
- package/dist/worker/index.cjs +407 -186
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +123 -15
- package/dist/worker/index.d.ts +123 -15
- package/dist/worker/index.js +407 -186
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-GADLTNJF.js.map +0 -1
- package/dist/chunk-Y2H4VURI.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var src_exports = {};
|
|
22
22
|
__export(src_exports, {
|
|
23
|
+
ADMINISTRATOR_AUDIENCE: () => ADMINISTRATOR_AUDIENCE,
|
|
23
24
|
ADMISSION_GRANTS_PATH: () => ADMISSION_GRANTS_PATH,
|
|
24
25
|
ADMISSION_GRANTS_SCHEMA_VERSION: () => ADMISSION_GRANTS_SCHEMA_VERSION,
|
|
25
26
|
BUILDER_LIBRARY_ADMIN_PATH: () => BUILDER_LIBRARY_ADMIN_PATH,
|
|
@@ -113,9 +114,12 @@ __export(src_exports, {
|
|
|
113
114
|
dashboardMetricData: () => dashboardMetricData,
|
|
114
115
|
defaultCrm: () => defaultCrm,
|
|
115
116
|
defineChapter: () => defineChapter,
|
|
117
|
+
deliverabilityHeaders: () => deliverabilityHeaders,
|
|
116
118
|
drainMembershipEffects: () => drainMembershipEffects,
|
|
117
119
|
edgeUnpaired: () => edgeUnpaired,
|
|
118
120
|
emailGroupFrom: () => emailGroupFrom,
|
|
121
|
+
emailSenderWarnings: () => emailSenderWarnings,
|
|
122
|
+
emailSpamSignals: () => emailSpamSignals,
|
|
119
123
|
endForSlot: () => endForSlot,
|
|
120
124
|
exportChapterPromotion: () => exportChapterPromotion,
|
|
121
125
|
findAdmissionGrant: () => findAdmissionGrant,
|
|
@@ -134,6 +138,7 @@ __export(src_exports, {
|
|
|
134
138
|
introIdempotencyKey: () => introIdempotencyKey,
|
|
135
139
|
isAdminRole: () => isAdminRole,
|
|
136
140
|
isAlreadySent: () => isAlreadySent,
|
|
141
|
+
isRecipientFacing: () => isRecipientFacing,
|
|
137
142
|
isReconcilable: () => isReconcilable,
|
|
138
143
|
isSlotAvailable: () => isSlotAvailable,
|
|
139
144
|
isValidEmail: () => isValidEmail,
|
|
@@ -142,6 +147,7 @@ __export(src_exports, {
|
|
|
142
147
|
joinConfigTiers: () => joinConfigTiers,
|
|
143
148
|
leaderCrmConfig: () => leaderCrmConfig,
|
|
144
149
|
listStripeObjects: () => listStripeObjects,
|
|
150
|
+
listUnsubscribeHeader: () => listUnsubscribeHeader,
|
|
145
151
|
loadLatestAdmissionRevision: () => loadLatestAdmissionRevision,
|
|
146
152
|
loadOfferableTiers: () => loadOfferableTiers,
|
|
147
153
|
loadTiers: () => loadTiers,
|
|
@@ -259,6 +265,7 @@ __export(src_exports, {
|
|
|
259
265
|
submitApplication: () => submitApplication,
|
|
260
266
|
subscriptionIdempotencyKey: () => subscriptionIdempotencyKey,
|
|
261
267
|
syncApplicationToCrm: () => syncApplicationToCrm,
|
|
268
|
+
textToHtml: () => textToHtml,
|
|
262
269
|
tierIsFree: () => tierIsFree,
|
|
263
270
|
tierPayable: () => tierPayable,
|
|
264
271
|
updateClerkUserMetadata: () => updateClerkUserMetadata,
|
|
@@ -1680,6 +1687,7 @@ var DEFAULT_ADMIN_COPY = {
|
|
|
1680
1687
|
replyTo: "Reply-to",
|
|
1681
1688
|
fromName: "From name",
|
|
1682
1689
|
fromNameHint: "Shown as the sender; the address stays the Worker's configured one.",
|
|
1690
|
+
spamRisk: "Spam-filter risk",
|
|
1683
1691
|
debugInbox: "Debug inbox",
|
|
1684
1692
|
debugInboxHint: "Non-production sends redirect here.",
|
|
1685
1693
|
sendTest: "Send test",
|
|
@@ -1997,8 +2005,8 @@ var APPLICATION_LABELS = {
|
|
|
1997
2005
|
state: "State"
|
|
1998
2006
|
};
|
|
1999
2007
|
function humanize(id11) {
|
|
2000
|
-
const
|
|
2001
|
-
return
|
|
2008
|
+
const text8 = id11.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
2009
|
+
return text8 ? `${text8[0].toUpperCase()}${text8.slice(1)}` : id11;
|
|
2002
2010
|
}
|
|
2003
2011
|
function applicationDefs(chapter) {
|
|
2004
2012
|
try {
|
|
@@ -4665,6 +4673,9 @@ function namedSeatRecipientState(seat) {
|
|
|
4665
4673
|
return seat.refundReason === "declined" ? "declined" : "refunded";
|
|
4666
4674
|
case "canceled":
|
|
4667
4675
|
return "canceled";
|
|
4676
|
+
// A card-network dispute on the seat's own payment pauses the recipient until it is won or re-charged.
|
|
4677
|
+
case "payment_disputed":
|
|
4678
|
+
return "disputed";
|
|
4668
4679
|
default:
|
|
4669
4680
|
return "payment_pending";
|
|
4670
4681
|
}
|
|
@@ -4860,12 +4871,87 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4860
4871
|
|
|
4861
4872
|
// src/membership-dispute-authority.ts
|
|
4862
4873
|
var import_stripe4 = require("@odla-ai/stripe");
|
|
4874
|
+
|
|
4875
|
+
// src/membership-dispute-transition.ts
|
|
4863
4876
|
var MEMBERSHIP_DISPUTED_STATUS = "payment_disputed";
|
|
4864
4877
|
var DISPUTE_WON_STATUSES = ["won", "warning_closed"];
|
|
4865
4878
|
var CLOSED_STATUSES = [...DISPUTE_WON_STATUSES, "lost"];
|
|
4866
|
-
|
|
4879
|
+
function disputeTransition(row2, fact, eventId, now, terminal) {
|
|
4880
|
+
const status2 = String(fact.status ?? "unknown"), closed = CLOSED_STATUSES.includes(status2);
|
|
4881
|
+
const sameDispute = row2.disputeId === fact.objectId;
|
|
4882
|
+
if (sameDispute && Number(row2.disputeClosedAt ?? 0) > 0 && !closed) return null;
|
|
4883
|
+
const won = DISPUTE_WON_STATUSES.includes(status2);
|
|
4884
|
+
const suspended = row2.status === MEMBERSHIP_DISPUTED_STATUS, done = terminal.includes(String(row2.status));
|
|
4885
|
+
const restored = String(row2.disputePriorStatus ?? (row2.journeyApprovedAt ? "active" : "paid_pending_approval"));
|
|
4886
|
+
const next = done ? String(row2.status) : won ? suspended ? restored : String(row2.status) : MEMBERSHIP_DISPUTED_STATUS;
|
|
4887
|
+
return {
|
|
4888
|
+
status: next,
|
|
4889
|
+
disputeId: fact.objectId,
|
|
4890
|
+
disputeStatus: status2,
|
|
4891
|
+
disputeEventId: eventId,
|
|
4892
|
+
disputeClosedAt: closed ? now : 0,
|
|
4893
|
+
...fact.disputeReason ? { disputeReason: fact.disputeReason } : {},
|
|
4894
|
+
...fact.amountCents !== void 0 ? { disputeAmountCents: fact.amountCents } : {},
|
|
4895
|
+
...sameDispute ? {} : { disputedAt: now },
|
|
4896
|
+
...next === MEMBERSHIP_DISPUTED_STATUS && !suspended ? { disputePriorStatus: String(row2.status) } : {},
|
|
4897
|
+
revision: Number(row2.revision) + 1,
|
|
4898
|
+
updatedAt: now
|
|
4899
|
+
};
|
|
4900
|
+
}
|
|
4901
|
+
|
|
4902
|
+
// src/membership-seat-dispute.ts
|
|
4867
4903
|
var text3 = (value) => typeof value === "string" && value ? value : void 0;
|
|
4868
|
-
var
|
|
4904
|
+
var TERMINAL = ["payment_pending", "payment_processing", "payment_canceling", "refund_pending", "refunded", "canceled"];
|
|
4905
|
+
var namedSeatDisputeStanding = (seat, applied) => ({ seatId: String(seat.id), status: String(seat.status), ...text3(seat.disputeStatus) ? { disputeStatus: text3(seat.disputeStatus) } : {}, applied });
|
|
4906
|
+
function disputedChargeCoversSeat(charge, seat) {
|
|
4907
|
+
return charge.seatId === seat.id && charge.customerId === seat.providerCustomerId && charge.paymentIntentId !== void 0 && (charge.paymentIntentId === seat.providerPaymentIntentId || charge.paymentIntentId === seat.rechargePaymentIntentId);
|
|
4908
|
+
}
|
|
4909
|
+
async function reconcileNamedSeatDispute(db, input, now, charge, evidence) {
|
|
4910
|
+
if (!charge.seatId) membershipFail("membership_dispute_scope_mismatch");
|
|
4911
|
+
const membership = async (applied, seat) => {
|
|
4912
|
+
const id11 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id11);
|
|
4913
|
+
requireMembershipScope(member, input);
|
|
4914
|
+
return {
|
|
4915
|
+
membershipId: id11,
|
|
4916
|
+
status: String(member.status),
|
|
4917
|
+
termEndsAt: Number(member.termEndsAt),
|
|
4918
|
+
graceEndsAt: Number(member.graceEndsAt ?? 0),
|
|
4919
|
+
autoRenew: member.autoRenew === true,
|
|
4920
|
+
applied,
|
|
4921
|
+
...text3(member.disputeStatus) ? { disputeStatus: text3(member.disputeStatus) } : {},
|
|
4922
|
+
...seat ? { seat: namedSeatDisputeStanding(seat, applied) } : {}
|
|
4923
|
+
};
|
|
4924
|
+
};
|
|
4925
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
4926
|
+
const state = await loadMembershipAuthorityState(db, input), seat = await membershipLoad(db, "networkSeat", charge.seatId);
|
|
4927
|
+
if (!seat) return membership(false);
|
|
4928
|
+
requireMembershipScope(seat, input);
|
|
4929
|
+
if (charge.customerId !== seat.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
|
|
4930
|
+
if (!disputedChargeCoversSeat(charge, seat) || seat.disputeEventId === input.eventId) return membership(seat.disputeEventId === input.eventId, seat);
|
|
4931
|
+
const attrs = disputeTransition(seat, evidence.fact, input.eventId, now, TERMINAL);
|
|
4932
|
+
if (!attrs) return membership(false, seat);
|
|
4933
|
+
try {
|
|
4934
|
+
await db.transact([membershipUpdate("networkSeat", String(seat.id), attrs)], { mutationId: `seat-dispute:${seat.id}:${input.eventId}`, guards: [
|
|
4935
|
+
{ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } },
|
|
4936
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
4937
|
+
{ ns: "networkCommercialEvents", id: String(evidence.row.id), exists: true, equals: { digest: evidence.fact.digest } },
|
|
4938
|
+
evidence.guard
|
|
4939
|
+
] });
|
|
4940
|
+
} catch (error) {
|
|
4941
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
4942
|
+
continue;
|
|
4943
|
+
}
|
|
4944
|
+
const saved = await membershipLoad(db, "networkSeat", String(seat.id));
|
|
4945
|
+
requireMembershipScope(saved, input);
|
|
4946
|
+
return membership(true, saved);
|
|
4947
|
+
}
|
|
4948
|
+
return membershipFail("membership_dispute_retry_required");
|
|
4949
|
+
}
|
|
4950
|
+
|
|
4951
|
+
// src/membership-dispute-authority.ts
|
|
4952
|
+
var TERMINAL2 = ["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"];
|
|
4953
|
+
var text4 = (value) => typeof value === "string" && value ? value : void 0;
|
|
4954
|
+
var refOf = (value) => typeof value === "string" ? text4(value) : text4(value?.id);
|
|
4869
4955
|
async function readDisputedCharge(secretKey, chargeId, fetch2) {
|
|
4870
4956
|
if (!/^ch_[A-Za-z0-9]+$/.test(chargeId)) membershipFail("invalid_dispute_charge");
|
|
4871
4957
|
const config = { secretKey, ...fetch2 ? { fetch: fetch2 } : {} };
|
|
@@ -4879,7 +4965,7 @@ async function readDisputedCharge(secretKey, chargeId, fetch2) {
|
|
|
4879
4965
|
if (!invoice.ok || invoice.body.id !== invoiceId) membershipFail("membership_dispute_provider_unavailable");
|
|
4880
4966
|
subscriptionId = refOf(invoice.body.subscription) ?? refOf(invoice.body.parent?.subscription_details?.subscription);
|
|
4881
4967
|
}
|
|
4882
|
-
const seatId =
|
|
4968
|
+
const seatId = text4(charge.body.metadata?.seatId);
|
|
4883
4969
|
return {
|
|
4884
4970
|
chargeId,
|
|
4885
4971
|
customerId,
|
|
@@ -4905,37 +4991,23 @@ async function reconcileMembershipDispute(db, input, now, secretKey, resolved) {
|
|
|
4905
4991
|
termEndsAt: Number(value.termEndsAt),
|
|
4906
4992
|
graceEndsAt: Number(value.graceEndsAt ?? 0),
|
|
4907
4993
|
autoRenew: value.autoRenew === true,
|
|
4908
|
-
...
|
|
4994
|
+
...text4(value.disputeStatus) ? { disputeStatus: text4(value.disputeStatus) } : {},
|
|
4909
4995
|
applied
|
|
4910
4996
|
});
|
|
4911
4997
|
const known = await membershipLoad(db, "networkMembership", id11);
|
|
4912
4998
|
requireMembershipScope(known, input);
|
|
4913
4999
|
if (known.disputeEventId === input.eventId) return receipt(known);
|
|
5000
|
+
const seatKnown = known.namedSeatId ? await membershipLoad(db, "networkSeat", String(known.namedSeatId)) : void 0;
|
|
5001
|
+
if (seatKnown?.disputeEventId === input.eventId) return { ...receipt(known, true), seat: namedSeatDisputeStanding(seatKnown, true) };
|
|
4914
5002
|
const charge = resolved && resolved.chargeId === fact.chargeId ? resolved : await readDisputedCharge(secretKey, fact.chargeId);
|
|
5003
|
+
if (charge.customerId !== known.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
|
|
5004
|
+
if (charge.seatId) return reconcileNamedSeatDispute(db, input, now, charge, { row: row2, fact, guard: evidenceGuard });
|
|
4915
5005
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
4916
5006
|
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id11);
|
|
4917
5007
|
requireMembershipScope(member, input);
|
|
4918
|
-
if (charge.customerId !== member.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
|
|
4919
5008
|
if (!disputedChargeCoversMembership(charge, member) || member.disputeEventId === input.eventId) return receipt(member);
|
|
4920
|
-
const
|
|
4921
|
-
|
|
4922
|
-
if (sameDispute && Number(member.disputeClosedAt ?? 0) > 0 && !closed) return receipt(member, false);
|
|
4923
|
-
const won = DISPUTE_WON_STATUSES.includes(status2);
|
|
4924
|
-
const suspended = member.status === MEMBERSHIP_DISPUTED_STATUS, terminal = TERMINAL.includes(String(member.status));
|
|
4925
|
-
const next = terminal ? String(member.status) : won ? suspended ? String(member.disputePriorStatus ?? (member.journeyApprovedAt ? "active" : "paid_pending_approval")) : String(member.status) : MEMBERSHIP_DISPUTED_STATUS;
|
|
4926
|
-
const attrs = {
|
|
4927
|
-
status: next,
|
|
4928
|
-
disputeId: fact.objectId,
|
|
4929
|
-
disputeStatus: status2,
|
|
4930
|
-
disputeEventId: input.eventId,
|
|
4931
|
-
disputeClosedAt: closed ? now : 0,
|
|
4932
|
-
...fact.disputeReason ? { disputeReason: fact.disputeReason } : {},
|
|
4933
|
-
...fact.amountCents !== void 0 ? { disputeAmountCents: fact.amountCents } : {},
|
|
4934
|
-
...sameDispute ? {} : { disputedAt: now },
|
|
4935
|
-
...next === MEMBERSHIP_DISPUTED_STATUS && !suspended ? { disputePriorStatus: String(member.status) } : {},
|
|
4936
|
-
revision: Number(member.revision) + 1,
|
|
4937
|
-
updatedAt: now
|
|
4938
|
-
};
|
|
5009
|
+
const attrs = disputeTransition(member, fact, input.eventId, now, TERMINAL2);
|
|
5010
|
+
if (!attrs) return receipt(member, false);
|
|
4939
5011
|
try {
|
|
4940
5012
|
await db.transact([membershipUpdate("networkMembership", id11, attrs)], { mutationId: `membership-dispute:${id11}:${input.eventId}`, guards: [
|
|
4941
5013
|
{ ns: "networkMembership", id: id11, exists: true, equals: { revision: member.revision } },
|
|
@@ -6339,7 +6411,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
6339
6411
|
};
|
|
6340
6412
|
signal.addEventListener("abort", cancel, { once: true });
|
|
6341
6413
|
let size = 0;
|
|
6342
|
-
let
|
|
6414
|
+
let text8 = "";
|
|
6343
6415
|
const decoder = new TextDecoder();
|
|
6344
6416
|
try {
|
|
6345
6417
|
while (true) {
|
|
@@ -6349,10 +6421,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
6349
6421
|
if (done) break;
|
|
6350
6422
|
size += value.byteLength;
|
|
6351
6423
|
if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
|
|
6352
|
-
|
|
6424
|
+
text8 += decoder.decode(value, { stream: true });
|
|
6353
6425
|
}
|
|
6354
6426
|
try {
|
|
6355
|
-
return JSON.parse(
|
|
6427
|
+
return JSON.parse(text8 + decoder.decode());
|
|
6356
6428
|
} catch {
|
|
6357
6429
|
throw new TypeError(`invalid ${label}`);
|
|
6358
6430
|
}
|
|
@@ -6839,6 +6911,19 @@ var id10 = () => attr9("string", false, true, true);
|
|
|
6839
6911
|
var key2 = (optional = false) => attr9("string", optional, false, true);
|
|
6840
6912
|
var scope = () => ({ appId: key2(), chapterId: key2(), environment: key2(), runtime: key2(), applicationId: key2(), skuId: key2() });
|
|
6841
6913
|
var times = () => ({ revision: attr9("number"), createdAt: attr9("number"), updatedAt: attr9("number") });
|
|
6914
|
+
var dispute = () => ({
|
|
6915
|
+
disputeId: key2(true),
|
|
6916
|
+
disputeStatus: attr9("string", true),
|
|
6917
|
+
disputeReason: attr9("string", true),
|
|
6918
|
+
disputeAmountCents: attr9("number", true),
|
|
6919
|
+
disputedAt: attr9("number", true),
|
|
6920
|
+
disputeClosedAt: attr9("number", true),
|
|
6921
|
+
disputePriorStatus: attr9("string", true),
|
|
6922
|
+
disputeEventId: key2(true),
|
|
6923
|
+
rechargePaymentIntentId: key2(true),
|
|
6924
|
+
rechargedAt: attr9("number", true),
|
|
6925
|
+
rechargeAttempt: attr9("json", true)
|
|
6926
|
+
});
|
|
6842
6927
|
var membershipAuthorityEntities = {
|
|
6843
6928
|
networkMembership: { attrs: {
|
|
6844
6929
|
id: id10(),
|
|
@@ -6879,19 +6964,7 @@ var membershipAuthorityEntities = {
|
|
|
6879
6964
|
...times(),
|
|
6880
6965
|
refundRequestedAt: attr9("number", true),
|
|
6881
6966
|
restartId: key2(true),
|
|
6882
|
-
|
|
6883
|
-
// the status the membership held before it was suspended, and the re-issued charge once an operator collected again.
|
|
6884
|
-
disputeId: key2(true),
|
|
6885
|
-
disputeStatus: attr9("string", true),
|
|
6886
|
-
disputeReason: attr9("string", true),
|
|
6887
|
-
disputeAmountCents: attr9("number", true),
|
|
6888
|
-
disputedAt: attr9("number", true),
|
|
6889
|
-
disputeClosedAt: attr9("number", true),
|
|
6890
|
-
disputePriorStatus: attr9("string", true),
|
|
6891
|
-
disputeEventId: key2(true),
|
|
6892
|
-
rechargePaymentIntentId: key2(true),
|
|
6893
|
-
rechargedAt: attr9("number", true),
|
|
6894
|
-
rechargeAttempt: attr9("json", true)
|
|
6967
|
+
...dispute()
|
|
6895
6968
|
} },
|
|
6896
6969
|
membershipRestart: { attrs: {
|
|
6897
6970
|
id: id10(),
|
|
@@ -6967,7 +7040,8 @@ var membershipAuthorityEntities = {
|
|
|
6967
7040
|
refundedCents: attr9("number", true),
|
|
6968
7041
|
providerRefundIds: attr9("json", true),
|
|
6969
7042
|
regiftedAt: attr9("number", true),
|
|
6970
|
-
formerRecipients: attr9("json", true)
|
|
7043
|
+
formerRecipients: attr9("json", true),
|
|
7044
|
+
...dispute()
|
|
6971
7045
|
} },
|
|
6972
7046
|
networkSeatRecipient: { attrs: { id: id10(), appId: key2(), chapterId: key2(), environment: key2(), runtime: key2(), seatId: key2(), recipientEmailNormalized: key2(), createdAt: attr9("number") } },
|
|
6973
7047
|
membershipEffect: { attrs: {
|
|
@@ -7327,7 +7401,7 @@ var record2 = (value) => value !== null && typeof value === "object" && !Array.i
|
|
|
7327
7401
|
function unknownKeys2(value, allowed2, path, errors) {
|
|
7328
7402
|
for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
7329
7403
|
}
|
|
7330
|
-
function
|
|
7404
|
+
function text5(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
7331
7405
|
if (value === void 0 && !required) return void 0;
|
|
7332
7406
|
if (typeof value !== "string" || required && !value.trim()) {
|
|
7333
7407
|
errors.push(`${path} must be a non-empty string`);
|
|
@@ -7353,7 +7427,7 @@ function scopeValue(value, path, errors) {
|
|
|
7353
7427
|
return { kind: raw2.kind };
|
|
7354
7428
|
}
|
|
7355
7429
|
if (raw2.kind === "chapter") {
|
|
7356
|
-
const chapterId =
|
|
7430
|
+
const chapterId = text5(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
7357
7431
|
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
7358
7432
|
return { kind: "chapter", chapterId };
|
|
7359
7433
|
}
|
|
@@ -7367,22 +7441,22 @@ function resourceValue(value, path, errors) {
|
|
|
7367
7441
|
return null;
|
|
7368
7442
|
}
|
|
7369
7443
|
unknownKeys2(raw2, RESOURCE_KEYS, path, errors);
|
|
7370
|
-
const id11 =
|
|
7444
|
+
const id11 = text5(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
7371
7445
|
if (id11 && !ID2.test(id11)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
7372
|
-
const kind =
|
|
7446
|
+
const kind = text5(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
7373
7447
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
7374
|
-
const title =
|
|
7375
|
-
const summary =
|
|
7376
|
-
const status2 =
|
|
7448
|
+
const title = text5(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
7449
|
+
const summary = text5(raw2.summary, `${path}.summary`, errors, 1e3, false);
|
|
7450
|
+
const status2 = text5(raw2.status, `${path}.status`, errors) ?? "";
|
|
7377
7451
|
if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
7378
7452
|
const revision = integer3(raw2.revision, `${path}.revision`, errors);
|
|
7379
7453
|
const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
7380
|
-
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) =>
|
|
7454
|
+
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text5(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
|
|
7381
7455
|
if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
|
|
7382
7456
|
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
7383
7457
|
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
7384
|
-
const bodyMarkdown =
|
|
7385
|
-
const url =
|
|
7458
|
+
const bodyMarkdown = text5(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, false);
|
|
7459
|
+
const url = text5(raw2.url, `${path}.url`, errors, 2048, false);
|
|
7386
7460
|
if (url) {
|
|
7387
7461
|
try {
|
|
7388
7462
|
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
@@ -7394,9 +7468,9 @@ function resourceValue(value, path, errors) {
|
|
|
7394
7468
|
if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
7395
7469
|
if (asset) unknownKeys2(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
7396
7470
|
const brandAsset = asset ? {
|
|
7397
|
-
bookId:
|
|
7398
|
-
assetId:
|
|
7399
|
-
...asset.filename === void 0 ? {} : { filename:
|
|
7471
|
+
bookId: text5(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
7472
|
+
assetId: text5(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
7473
|
+
...asset.filename === void 0 ? {} : { filename: text5(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
7400
7474
|
} : void 0;
|
|
7401
7475
|
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
7402
7476
|
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
@@ -7411,17 +7485,17 @@ function resourceValue(value, path, errors) {
|
|
|
7411
7485
|
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
7412
7486
|
if (promotedRaw) unknownKeys2(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
7413
7487
|
const promotedFrom = promotedRaw ? {
|
|
7414
|
-
chapterId:
|
|
7415
|
-
resourceId:
|
|
7488
|
+
chapterId: text5(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
7489
|
+
resourceId: text5(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
7416
7490
|
resourceRevision: integer3(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
7417
7491
|
documentRevision: integer3(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
7418
|
-
promotedBy:
|
|
7492
|
+
promotedBy: text5(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
7419
7493
|
promotedAt: integer3(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
7420
7494
|
} : void 0;
|
|
7421
7495
|
const provenance = {
|
|
7422
|
-
createdBy:
|
|
7496
|
+
createdBy: text5(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
7423
7497
|
createdAt: integer3(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
7424
|
-
updatedBy:
|
|
7498
|
+
updatedBy: text5(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
7425
7499
|
updatedAt: integer3(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
7426
7500
|
...promotedFrom ? { promotedFrom } : {}
|
|
7427
7501
|
};
|
|
@@ -7440,7 +7514,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
7440
7514
|
if (sourceRaw) unknownKeys2(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
7441
7515
|
const sourceType = sourceRaw?.type;
|
|
7442
7516
|
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
7443
|
-
const sourceAppId =
|
|
7517
|
+
const sourceAppId = text5(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
7444
7518
|
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
7445
7519
|
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
7446
7520
|
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
@@ -7448,8 +7522,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
7448
7522
|
if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
7449
7523
|
if (targetRaw) unknownKeys2(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
7450
7524
|
const target = targetRaw ? {
|
|
7451
|
-
appId:
|
|
7452
|
-
environment:
|
|
7525
|
+
appId: text5(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
7526
|
+
environment: text5(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
7453
7527
|
} : void 0;
|
|
7454
7528
|
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
7455
7529
|
if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
|
|
@@ -7544,9 +7618,9 @@ async function createBuilderLibraryEnvelope(document) {
|
|
|
7544
7618
|
}
|
|
7545
7619
|
|
|
7546
7620
|
// src/email-retirement.ts
|
|
7547
|
-
function isRetiredEmailCopy(subject,
|
|
7621
|
+
function isRetiredEmailCopy(subject, text8) {
|
|
7548
7622
|
const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
|
|
7549
|
-
const bodyCopy =
|
|
7623
|
+
const bodyCopy = text8.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
|
|
7550
7624
|
return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
|
|
7551
7625
|
}
|
|
7552
7626
|
|
|
@@ -7624,14 +7698,92 @@ function planDelivery(input) {
|
|
|
7624
7698
|
if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
|
|
7625
7699
|
if ((0, import_crm5.validateEmailCopy)({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
|
|
7626
7700
|
const subject = (redirect ? "[dev] " : "") + renderedSubject;
|
|
7627
|
-
const
|
|
7701
|
+
const text8 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
7628
7702
|
|
|
7629
7703
|
` + renderedText : renderedText;
|
|
7630
|
-
return { deliver: true, transport, to, subject, text:
|
|
7704
|
+
return { deliver: true, transport, to, subject, text: text8, redirected: redirect };
|
|
7631
7705
|
}
|
|
7632
7706
|
|
|
7633
7707
|
// src/email-readiness.ts
|
|
7634
7708
|
var import_crm6 = require("@odla-ai/crm");
|
|
7709
|
+
|
|
7710
|
+
// src/email-spam-signals.ts
|
|
7711
|
+
var TRIGGER_PHRASES = [
|
|
7712
|
+
"act now",
|
|
7713
|
+
"click here",
|
|
7714
|
+
"limited time",
|
|
7715
|
+
"guaranteed",
|
|
7716
|
+
"risk-free",
|
|
7717
|
+
"risk free",
|
|
7718
|
+
"no obligation",
|
|
7719
|
+
"100% free",
|
|
7720
|
+
"winner",
|
|
7721
|
+
"congratulations",
|
|
7722
|
+
"urgent",
|
|
7723
|
+
"double your",
|
|
7724
|
+
"earn money",
|
|
7725
|
+
"make money",
|
|
7726
|
+
"cash bonus",
|
|
7727
|
+
"once in a lifetime",
|
|
7728
|
+
"exclusive deal",
|
|
7729
|
+
"order now",
|
|
7730
|
+
"buy now",
|
|
7731
|
+
"this is not spam",
|
|
7732
|
+
"dear friend",
|
|
7733
|
+
"dear customer"
|
|
7734
|
+
];
|
|
7735
|
+
var SUBJECT_TRIGGERS = ["free", ...TRIGGER_PHRASES];
|
|
7736
|
+
var DEV_HOSTS = /(?:^|\.)(?:workers\.dev|pages\.dev|localhost|ngrok(?:-free)?\.app|trycloudflare\.com)$/i;
|
|
7737
|
+
var SHORTENERS = /^(?:bit\.ly|tinyurl\.com|t\.co|goo\.gl|ow\.ly|is\.gd|buff\.ly|rebrand\.ly|cutt\.ly|tiny\.cc|shorturl\.at|rb\.gy)$/i;
|
|
7738
|
+
var URL_RE = /https?:\/\/[^\s<>"')\]]+/g;
|
|
7739
|
+
var strip = (s) => s.replace(/\{\{\w+\}\}/g, " ");
|
|
7740
|
+
var words = (s) => strip(s).split(/\s+/).filter(Boolean);
|
|
7741
|
+
var phraseIn = (phrases, haystack) => phrases.find((phrase) => new RegExp(`(?:^|[^a-z0-9])${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![a-z0-9])`, "i").test(haystack));
|
|
7742
|
+
function emailSpamSignals(copy) {
|
|
7743
|
+
const subject = typeof copy.subject === "string" ? copy.subject : "";
|
|
7744
|
+
const text8 = typeof copy.text === "string" ? copy.text : "";
|
|
7745
|
+
const out = [];
|
|
7746
|
+
const letters = strip(subject).replace(/[^A-Za-z]/g, "");
|
|
7747
|
+
const shouting = letters.length >= 6 && letters.replace(/[^A-Z]/g, "").length / letters.length > 0.5 || words(subject).some((word) => /^[A-Z]{4,}$/.test(word.replace(/[^A-Za-z]/g, "")));
|
|
7748
|
+
if (shouting) out.push({ code: "subject-caps", message: "The subject shouts in capital letters. Filters read that as a spam signal; use sentence case." });
|
|
7749
|
+
if (/[!?]{2,}|!\s*$/.test(subject)) out.push({ code: "subject-punctuation", message: "Repeated or trailing exclamation marks in the subject are a classic spam signal." });
|
|
7750
|
+
if (/^\s*(?:re|fw|fwd)\s*:/i.test(subject)) out.push({ code: "subject-fake-reply", message: "A subject that starts like a reply or forward reads as a spoof when the recipient never wrote first." });
|
|
7751
|
+
const subjectHit = phraseIn(SUBJECT_TRIGGERS, strip(subject));
|
|
7752
|
+
if (subjectHit) out.push({ code: "subject-trigger-words", message: `The subject uses "${subjectHit}", a word filters weight against the sender.` });
|
|
7753
|
+
const bodyHit = phraseIn(TRIGGER_PHRASES, strip(text8));
|
|
7754
|
+
if (bodyHit) out.push({ code: "body-trigger-words", message: `The body uses "${bodyHit}", a phrase filters weight against the sender.` });
|
|
7755
|
+
if (/(?:^|\s)[A-Z]{3,}(?:\s+[A-Z]{3,}){2,}(?=\s|$)/.test(strip(text8))) out.push({ code: "body-caps", message: "The body shouts a run of capitalised words; use sentence case." });
|
|
7756
|
+
if (/!{2,}/.test(text8)) out.push({ code: "body-punctuation", message: "Repeated exclamation marks in the body are a classic spam signal." });
|
|
7757
|
+
const urls = [...text8.matchAll(URL_RE)].map((match) => match[0]);
|
|
7758
|
+
const hosts = urls.map((url) => {
|
|
7759
|
+
try {
|
|
7760
|
+
return new URL(url).hostname;
|
|
7761
|
+
} catch {
|
|
7762
|
+
return "";
|
|
7763
|
+
}
|
|
7764
|
+
});
|
|
7765
|
+
if (urls.some((url) => /^http:\/\//i.test(url))) out.push({ code: "link-insecure", message: "A link uses http:// instead of https://. Filters flag insecure links." });
|
|
7766
|
+
if (hosts.some((host) => DEV_HOSTS.test(host) || /^\d+\.\d+\.\d+\.\d+$/.test(host))) out.push({ code: "link-origin", message: "A link points at a development host (workers.dev, pages.dev, localhost or a raw IP). Link to the site's own domain." });
|
|
7767
|
+
if (hosts.some((host) => SHORTENERS.test(host))) out.push({ code: "link-shortener", message: "A link uses a URL shortener, which hides the destination and is heavily abused. Link to the real address." });
|
|
7768
|
+
if (new Set(urls).size > 5) out.push({ code: "link-many", message: "The body has more than five links. Keep lifecycle mail to the one or two the reader needs." });
|
|
7769
|
+
if (urls.length && words(text8).length < 20) out.push({ code: "body-link-only", message: "The body is little more than a link. Say what the reader is being sent and why before the address." });
|
|
7770
|
+
return out;
|
|
7771
|
+
}
|
|
7772
|
+
function emailSenderWarnings(input) {
|
|
7773
|
+
const out = [];
|
|
7774
|
+
const from = (input.fromEmail ?? "").trim().toLowerCase().replace(/^.*<([^>]+)>.*$/, "$1");
|
|
7775
|
+
const reply = (input.replyTo ?? "").trim().toLowerCase();
|
|
7776
|
+
const domain = (address) => address.split("@")[1] ?? "";
|
|
7777
|
+
if (/^(?:no-?reply|do-?not-?reply|donotreply)(?:[+.-][^@]*)?@/.test(from)) {
|
|
7778
|
+
out.push({ code: "sender-no-reply", message: "Mail is sent from a no-reply address. Gmail's sender guidelines advise against it; send from a monitored address so replies and unsubscribe requests reach someone." });
|
|
7779
|
+
}
|
|
7780
|
+
if (domain(from) && domain(reply) && domain(from) !== domain(reply)) {
|
|
7781
|
+
out.push({ code: "reply-to-domain", message: `Replies go to ${domain(reply)} while mail is sent from ${domain(from)}. A Reply-To on another domain is a mild spam signal; prefer an address on the sending domain.` });
|
|
7782
|
+
}
|
|
7783
|
+
return out;
|
|
7784
|
+
}
|
|
7785
|
+
|
|
7786
|
+
// src/email-readiness.ts
|
|
7635
7787
|
function validateEmailReadiness(input) {
|
|
7636
7788
|
const issues = [];
|
|
7637
7789
|
for (const key3 of Object.keys(input.templates)) if (!input.events[key3]) {
|
|
@@ -7656,9 +7808,56 @@ ${template.text ?? ""}`.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1]);
|
|
|
7656
7808
|
}
|
|
7657
7809
|
if (event2.requiresTier && input.tiersUnavailable) problems.push({ code: "tiers-unavailable", message: "Tier names are unavailable; delivery checks will block unresolved memberships." });
|
|
7658
7810
|
else if (event2.requiresTier && noTierNames) problems.push({ code: "tiers-missing", message: "Configure the membership tier names needed by this event." });
|
|
7659
|
-
return {
|
|
7811
|
+
return {
|
|
7812
|
+
...event2,
|
|
7813
|
+
key: key3,
|
|
7814
|
+
status: template?.enabled === false ? "disabled" : problems.length ? "blocked" : "ready",
|
|
7815
|
+
issues: problems,
|
|
7816
|
+
warnings: template ? emailSpamSignals(template) : []
|
|
7817
|
+
};
|
|
7660
7818
|
});
|
|
7661
|
-
return { checkedAt: input.now ?? Date.now(), events: events2, issues };
|
|
7819
|
+
return { checkedAt: input.now ?? Date.now(), events: events2, issues, warnings: emailSenderWarnings(input.sender ?? {}) };
|
|
7820
|
+
}
|
|
7821
|
+
|
|
7822
|
+
// src/email-html.ts
|
|
7823
|
+
var URL_RE2 = /https?:\/\/[^\s<>"')\]]+[^\s<>"').,;:!?\]]/g;
|
|
7824
|
+
function escapeHtml(s) {
|
|
7825
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
7826
|
+
}
|
|
7827
|
+
function lineHtml(line) {
|
|
7828
|
+
let out = "", last = 0;
|
|
7829
|
+
for (const match of line.matchAll(URL_RE2)) {
|
|
7830
|
+
out += escapeHtml(line.slice(last, match.index));
|
|
7831
|
+
const url = escapeHtml(match[0]);
|
|
7832
|
+
out += `<a href="${url}">${url}</a>`;
|
|
7833
|
+
last = match.index + match[0].length;
|
|
7834
|
+
}
|
|
7835
|
+
return out + escapeHtml(line.slice(last));
|
|
7836
|
+
}
|
|
7837
|
+
function textToHtml(text8) {
|
|
7838
|
+
const paragraphs = text8.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
|
|
7839
|
+
const body = paragraphs.map((paragraph) => `<p>${paragraph.split("\n").map(lineHtml).join("<br>\n")}</p>`).join("\n");
|
|
7840
|
+
return `<!doctype html>
|
|
7841
|
+
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
|
|
7842
|
+
<body style="margin:0;padding:24px 16px;background:#ffffff;color:#1a1a1a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;font-size:16px;line-height:1.5">
|
|
7843
|
+
${body}
|
|
7844
|
+
</body></html>`;
|
|
7845
|
+
}
|
|
7846
|
+
|
|
7847
|
+
// src/email-deliverability.ts
|
|
7848
|
+
var ADMINISTRATOR_AUDIENCE = /^(?:administrator|admin|team|operator|owner|staff)s?$/i;
|
|
7849
|
+
function isRecipientFacing(event2) {
|
|
7850
|
+
return !event2 || !ADMINISTRATOR_AUDIENCE.test(event2.audience.trim());
|
|
7851
|
+
}
|
|
7852
|
+
function listUnsubscribeHeader(replyTo) {
|
|
7853
|
+
const address = replyTo.trim();
|
|
7854
|
+
if (!/^[^\s@,;<>?]+@[^\s@,;<>?]+\.[^\s@,;<>?]+$/.test(address)) return void 0;
|
|
7855
|
+
return `<mailto:${address}?subject=${encodeURIComponent("unsubscribe")}>`;
|
|
7856
|
+
}
|
|
7857
|
+
function deliverabilityHeaders(input) {
|
|
7858
|
+
if (!isRecipientFacing(input.event)) return void 0;
|
|
7859
|
+
const value = listUnsubscribeHeader(input.replyTo);
|
|
7860
|
+
return value ? { "List-Unsubscribe": value } : void 0;
|
|
7662
7861
|
}
|
|
7663
7862
|
|
|
7664
7863
|
// src/notify.ts
|
|
@@ -7826,11 +8025,16 @@ async function sendTemplated(deps, input) {
|
|
|
7826
8025
|
if (decision.transport === "log-only") return { sent: true };
|
|
7827
8026
|
let messageId;
|
|
7828
8027
|
try {
|
|
8028
|
+
const text8 = String(row2.body);
|
|
8029
|
+
const html = deps.renderHtml === false ? void 0 : (deps.renderHtml ?? textToHtml)(text8);
|
|
8030
|
+
const headers = deliverabilityHeaders({ event: event2, replyTo: input.group.replyTo });
|
|
7829
8031
|
const response = await deps.sender.send({
|
|
7830
8032
|
from: fromAddress(deps.from, input.group.fromName),
|
|
7831
8033
|
to: [decision.to],
|
|
7832
8034
|
subject: String(row2.subject),
|
|
7833
|
-
text:
|
|
8035
|
+
text: text8,
|
|
8036
|
+
...html ? { html } : {},
|
|
8037
|
+
...headers ? { headers } : {},
|
|
7834
8038
|
replyTo: input.group.replyTo
|
|
7835
8039
|
});
|
|
7836
8040
|
if (!response || typeof response.messageId !== "string" || !response.messageId) throw new Error("missing delivery receipt");
|
|
@@ -8283,7 +8487,7 @@ var ID4 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
|
8283
8487
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
8284
8488
|
var CURRENCY = /^[a-z]{3}$/;
|
|
8285
8489
|
var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
8286
|
-
var
|
|
8490
|
+
var text6 = (value, path, errors, max) => {
|
|
8287
8491
|
if (typeof value !== "string" || !value.trim()) {
|
|
8288
8492
|
errors.push(`${path} is required`);
|
|
8289
8493
|
return "";
|
|
@@ -8311,23 +8515,23 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
8311
8515
|
}
|
|
8312
8516
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
8313
8517
|
for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
8314
|
-
const id11 =
|
|
8518
|
+
const id11 = text6(item.id, `${path}.id`, errors, 80);
|
|
8315
8519
|
if (id11 && !ID4.test(id11)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
8316
8520
|
if (ids2.has(id11)) errors.push(`${path}.id duplicates "${id11}"`);
|
|
8317
8521
|
ids2.add(id11);
|
|
8318
8522
|
const targetRaw = record3(item.target);
|
|
8319
8523
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
8320
8524
|
if (targetRaw?.type === "tier") {
|
|
8321
|
-
const tierId =
|
|
8525
|
+
const tierId = text6(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
8322
8526
|
if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
|
|
8323
8527
|
target = { type: "tier", tierId };
|
|
8324
8528
|
} else if (targetRaw?.type === "entitlement") {
|
|
8325
|
-
const entitlementKey =
|
|
8529
|
+
const entitlementKey = text6(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
8326
8530
|
if (entitlementKey && !ID4.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
8327
8531
|
target = { type: "entitlement", entitlementKey };
|
|
8328
8532
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
8329
8533
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
8330
|
-
const stripePriceId =
|
|
8534
|
+
const stripePriceId = text6(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
|
|
8331
8535
|
if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
8332
8536
|
if (!Number.isSafeInteger(item.priceCents) || Number(item.priceCents) < 1 || Number(item.priceCents) > 1e8) errors.push(`${path}.priceCents must be an integer from 1 to 100,000,000`);
|
|
8333
8537
|
const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
|
|
@@ -8349,9 +8553,9 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
8349
8553
|
redemptionLifetimeHours: lifetime,
|
|
8350
8554
|
...activeFrom !== void 0 ? { activeFrom } : {},
|
|
8351
8555
|
...activeUntil !== void 0 ? { activeUntil } : {},
|
|
8352
|
-
title:
|
|
8353
|
-
description:
|
|
8354
|
-
ctaLabel:
|
|
8556
|
+
title: text6(item.title, `${path}.title`, errors, 160),
|
|
8557
|
+
description: text6(item.description, `${path}.description`, errors, 2e3),
|
|
8558
|
+
ctaLabel: text6(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
|
|
8355
8559
|
});
|
|
8356
8560
|
}
|
|
8357
8561
|
return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
|
|
@@ -9434,10 +9638,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
9434
9638
|
errors.push(`${path} must be a string`);
|
|
9435
9639
|
return void 0;
|
|
9436
9640
|
}
|
|
9437
|
-
const
|
|
9438
|
-
if (required && !
|
|
9439
|
-
if (
|
|
9440
|
-
return
|
|
9641
|
+
const text8 = value.trim();
|
|
9642
|
+
if (required && !text8) errors.push(`${path} is required`);
|
|
9643
|
+
if (text8.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
9644
|
+
return text8;
|
|
9441
9645
|
}
|
|
9442
9646
|
function validateMembershipPageDocument(input, options = {}) {
|
|
9443
9647
|
const errors = [];
|
|
@@ -9486,8 +9690,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
9486
9690
|
else {
|
|
9487
9691
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
9488
9692
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
9489
|
-
const
|
|
9490
|
-
return
|
|
9693
|
+
const text8 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
9694
|
+
return text8 ? [text8] : [];
|
|
9491
9695
|
});
|
|
9492
9696
|
}
|
|
9493
9697
|
}
|
|
@@ -9586,7 +9790,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
|
|
|
9586
9790
|
function unknownKeys3(value, allowed2, path, errors) {
|
|
9587
9791
|
for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
9588
9792
|
}
|
|
9589
|
-
function
|
|
9793
|
+
function text7(value, path, errors, max = 65536) {
|
|
9590
9794
|
if (typeof value !== "string" || !value.trim()) {
|
|
9591
9795
|
errors.push(`${path} is required`);
|
|
9592
9796
|
return "";
|
|
@@ -9609,11 +9813,11 @@ function tiersOf(value, errors) {
|
|
|
9609
9813
|
return [];
|
|
9610
9814
|
}
|
|
9611
9815
|
unknownKeys3(item, TIER_KEYS2, path, errors);
|
|
9612
|
-
const id11 =
|
|
9816
|
+
const id11 = text7(item.id, `${path}.id`, errors, 80);
|
|
9613
9817
|
if (id11 && !TIER.test(id11)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
9614
9818
|
if (seen.has(id11)) errors.push(`${path}.id duplicates "${id11}"`);
|
|
9615
9819
|
seen.add(id11);
|
|
9616
|
-
const name =
|
|
9820
|
+
const name = text7(item.name, `${path}.name`, errors, 160);
|
|
9617
9821
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
9618
9822
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
9619
9823
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
@@ -9631,7 +9835,7 @@ function tiersOf(value, errors) {
|
|
|
9631
9835
|
if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
|
|
9632
9836
|
errors.push(`${path}.interval must be year or month`);
|
|
9633
9837
|
}
|
|
9634
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
9838
|
+
const blurb = item.blurb === void 0 ? void 0 : text7(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
9635
9839
|
return [{
|
|
9636
9840
|
id: id11,
|
|
9637
9841
|
name,
|
|
@@ -9689,7 +9893,7 @@ function policyOf(value, errors, tiers2) {
|
|
|
9689
9893
|
const result = {};
|
|
9690
9894
|
for (const key3 of allowed2) {
|
|
9691
9895
|
if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
|
|
9692
|
-
result[key3] =
|
|
9896
|
+
result[key3] = text7(input[key3], `document.policy.${key3}`, errors);
|
|
9693
9897
|
}
|
|
9694
9898
|
const tierPolicies = {};
|
|
9695
9899
|
if (input.tierPolicies !== void 0) {
|
|
@@ -9704,8 +9908,8 @@ function policyOf(value, errors, tiers2) {
|
|
|
9704
9908
|
}
|
|
9705
9909
|
unknownKeys3(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
9706
9910
|
tierPolicies[id11] = {
|
|
9707
|
-
refundPolicyText:
|
|
9708
|
-
...policy2.trustCopy !== void 0 ? { trustCopy:
|
|
9911
|
+
refundPolicyText: text7(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
9912
|
+
...policy2.trustCopy !== void 0 ? { trustCopy: text7(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
9709
9913
|
};
|
|
9710
9914
|
}
|
|
9711
9915
|
}
|
|
@@ -9745,9 +9949,9 @@ function validateSignupControlDocument(input) {
|
|
|
9745
9949
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
9746
9950
|
const source = recordOf(root.source);
|
|
9747
9951
|
const target = recordOf(root.target);
|
|
9748
|
-
const appId =
|
|
9749
|
-
const chapterId =
|
|
9750
|
-
const environment =
|
|
9952
|
+
const appId = text7(source?.appId, "document.source.appId", errors, 63);
|
|
9953
|
+
const chapterId = text7(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
9954
|
+
const environment = text7(target?.environment, "document.target.environment", errors, 63);
|
|
9751
9955
|
if (appId && !SLUG9.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
9752
9956
|
if (chapterId && !SLUG9.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
9753
9957
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
@@ -9924,9 +10128,9 @@ function markdownBenefits(lines, path, errors) {
|
|
|
9924
10128
|
errors.push(`${path} line ${index2 + 1} must be a Markdown bullet`);
|
|
9925
10129
|
continue;
|
|
9926
10130
|
}
|
|
9927
|
-
const
|
|
9928
|
-
if (
|
|
9929
|
-
if (
|
|
10131
|
+
const text8 = (match[1] ?? "").trim();
|
|
10132
|
+
if (text8.length > 2e3) errors.push(`${path} line ${index2 + 1} must be at most 2,000 characters`);
|
|
10133
|
+
if (text8) benefits.push(text8);
|
|
9930
10134
|
}
|
|
9931
10135
|
if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
|
|
9932
10136
|
return benefits.slice(0, MAX_BENEFITS2);
|