@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.js
CHANGED
|
@@ -1397,6 +1397,7 @@ var DEFAULT_ADMIN_COPY = {
|
|
|
1397
1397
|
replyTo: "Reply-to",
|
|
1398
1398
|
fromName: "From name",
|
|
1399
1399
|
fromNameHint: "Shown as the sender; the address stays the Worker's configured one.",
|
|
1400
|
+
spamRisk: "Spam-filter risk",
|
|
1400
1401
|
debugInbox: "Debug inbox",
|
|
1401
1402
|
debugInboxHint: "Non-production sends redirect here.",
|
|
1402
1403
|
sendTest: "Send test",
|
|
@@ -1714,8 +1715,8 @@ var APPLICATION_LABELS = {
|
|
|
1714
1715
|
state: "State"
|
|
1715
1716
|
};
|
|
1716
1717
|
function humanize(id11) {
|
|
1717
|
-
const
|
|
1718
|
-
return
|
|
1718
|
+
const text8 = id11.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
1719
|
+
return text8 ? `${text8[0].toUpperCase()}${text8.slice(1)}` : id11;
|
|
1719
1720
|
}
|
|
1720
1721
|
function applicationDefs(chapter) {
|
|
1721
1722
|
try {
|
|
@@ -4382,6 +4383,9 @@ function namedSeatRecipientState(seat) {
|
|
|
4382
4383
|
return seat.refundReason === "declined" ? "declined" : "refunded";
|
|
4383
4384
|
case "canceled":
|
|
4384
4385
|
return "canceled";
|
|
4386
|
+
// A card-network dispute on the seat's own payment pauses the recipient until it is won or re-charged.
|
|
4387
|
+
case "payment_disputed":
|
|
4388
|
+
return "disputed";
|
|
4385
4389
|
default:
|
|
4386
4390
|
return "payment_pending";
|
|
4387
4391
|
}
|
|
@@ -4577,12 +4581,87 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4577
4581
|
|
|
4578
4582
|
// src/membership-dispute-authority.ts
|
|
4579
4583
|
import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
|
|
4584
|
+
|
|
4585
|
+
// src/membership-dispute-transition.ts
|
|
4580
4586
|
var MEMBERSHIP_DISPUTED_STATUS = "payment_disputed";
|
|
4581
4587
|
var DISPUTE_WON_STATUSES = ["won", "warning_closed"];
|
|
4582
4588
|
var CLOSED_STATUSES = [...DISPUTE_WON_STATUSES, "lost"];
|
|
4583
|
-
|
|
4589
|
+
function disputeTransition(row2, fact, eventId, now, terminal) {
|
|
4590
|
+
const status2 = String(fact.status ?? "unknown"), closed = CLOSED_STATUSES.includes(status2);
|
|
4591
|
+
const sameDispute = row2.disputeId === fact.objectId;
|
|
4592
|
+
if (sameDispute && Number(row2.disputeClosedAt ?? 0) > 0 && !closed) return null;
|
|
4593
|
+
const won = DISPUTE_WON_STATUSES.includes(status2);
|
|
4594
|
+
const suspended = row2.status === MEMBERSHIP_DISPUTED_STATUS, done = terminal.includes(String(row2.status));
|
|
4595
|
+
const restored = String(row2.disputePriorStatus ?? (row2.journeyApprovedAt ? "active" : "paid_pending_approval"));
|
|
4596
|
+
const next = done ? String(row2.status) : won ? suspended ? restored : String(row2.status) : MEMBERSHIP_DISPUTED_STATUS;
|
|
4597
|
+
return {
|
|
4598
|
+
status: next,
|
|
4599
|
+
disputeId: fact.objectId,
|
|
4600
|
+
disputeStatus: status2,
|
|
4601
|
+
disputeEventId: eventId,
|
|
4602
|
+
disputeClosedAt: closed ? now : 0,
|
|
4603
|
+
...fact.disputeReason ? { disputeReason: fact.disputeReason } : {},
|
|
4604
|
+
...fact.amountCents !== void 0 ? { disputeAmountCents: fact.amountCents } : {},
|
|
4605
|
+
...sameDispute ? {} : { disputedAt: now },
|
|
4606
|
+
...next === MEMBERSHIP_DISPUTED_STATUS && !suspended ? { disputePriorStatus: String(row2.status) } : {},
|
|
4607
|
+
revision: Number(row2.revision) + 1,
|
|
4608
|
+
updatedAt: now
|
|
4609
|
+
};
|
|
4610
|
+
}
|
|
4611
|
+
|
|
4612
|
+
// src/membership-seat-dispute.ts
|
|
4584
4613
|
var text3 = (value) => typeof value === "string" && value ? value : void 0;
|
|
4585
|
-
var
|
|
4614
|
+
var TERMINAL = ["payment_pending", "payment_processing", "payment_canceling", "refund_pending", "refunded", "canceled"];
|
|
4615
|
+
var namedSeatDisputeStanding = (seat, applied) => ({ seatId: String(seat.id), status: String(seat.status), ...text3(seat.disputeStatus) ? { disputeStatus: text3(seat.disputeStatus) } : {}, applied });
|
|
4616
|
+
function disputedChargeCoversSeat(charge, seat) {
|
|
4617
|
+
return charge.seatId === seat.id && charge.customerId === seat.providerCustomerId && charge.paymentIntentId !== void 0 && (charge.paymentIntentId === seat.providerPaymentIntentId || charge.paymentIntentId === seat.rechargePaymentIntentId);
|
|
4618
|
+
}
|
|
4619
|
+
async function reconcileNamedSeatDispute(db, input, now, charge, evidence) {
|
|
4620
|
+
if (!charge.seatId) membershipFail("membership_dispute_scope_mismatch");
|
|
4621
|
+
const membership = async (applied, seat) => {
|
|
4622
|
+
const id11 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id11);
|
|
4623
|
+
requireMembershipScope(member, input);
|
|
4624
|
+
return {
|
|
4625
|
+
membershipId: id11,
|
|
4626
|
+
status: String(member.status),
|
|
4627
|
+
termEndsAt: Number(member.termEndsAt),
|
|
4628
|
+
graceEndsAt: Number(member.graceEndsAt ?? 0),
|
|
4629
|
+
autoRenew: member.autoRenew === true,
|
|
4630
|
+
applied,
|
|
4631
|
+
...text3(member.disputeStatus) ? { disputeStatus: text3(member.disputeStatus) } : {},
|
|
4632
|
+
...seat ? { seat: namedSeatDisputeStanding(seat, applied) } : {}
|
|
4633
|
+
};
|
|
4634
|
+
};
|
|
4635
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
4636
|
+
const state = await loadMembershipAuthorityState(db, input), seat = await membershipLoad(db, "networkSeat", charge.seatId);
|
|
4637
|
+
if (!seat) return membership(false);
|
|
4638
|
+
requireMembershipScope(seat, input);
|
|
4639
|
+
if (charge.customerId !== seat.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
|
|
4640
|
+
if (!disputedChargeCoversSeat(charge, seat) || seat.disputeEventId === input.eventId) return membership(seat.disputeEventId === input.eventId, seat);
|
|
4641
|
+
const attrs = disputeTransition(seat, evidence.fact, input.eventId, now, TERMINAL);
|
|
4642
|
+
if (!attrs) return membership(false, seat);
|
|
4643
|
+
try {
|
|
4644
|
+
await db.transact([membershipUpdate("networkSeat", String(seat.id), attrs)], { mutationId: `seat-dispute:${seat.id}:${input.eventId}`, guards: [
|
|
4645
|
+
{ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } },
|
|
4646
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
4647
|
+
{ ns: "networkCommercialEvents", id: String(evidence.row.id), exists: true, equals: { digest: evidence.fact.digest } },
|
|
4648
|
+
evidence.guard
|
|
4649
|
+
] });
|
|
4650
|
+
} catch (error) {
|
|
4651
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
4652
|
+
continue;
|
|
4653
|
+
}
|
|
4654
|
+
const saved = await membershipLoad(db, "networkSeat", String(seat.id));
|
|
4655
|
+
requireMembershipScope(saved, input);
|
|
4656
|
+
return membership(true, saved);
|
|
4657
|
+
}
|
|
4658
|
+
return membershipFail("membership_dispute_retry_required");
|
|
4659
|
+
}
|
|
4660
|
+
|
|
4661
|
+
// src/membership-dispute-authority.ts
|
|
4662
|
+
var TERMINAL2 = ["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"];
|
|
4663
|
+
var text4 = (value) => typeof value === "string" && value ? value : void 0;
|
|
4664
|
+
var refOf = (value) => typeof value === "string" ? text4(value) : text4(value?.id);
|
|
4586
4665
|
async function readDisputedCharge(secretKey, chargeId, fetch2) {
|
|
4587
4666
|
if (!/^ch_[A-Za-z0-9]+$/.test(chargeId)) membershipFail("invalid_dispute_charge");
|
|
4588
4667
|
const config = { secretKey, ...fetch2 ? { fetch: fetch2 } : {} };
|
|
@@ -4596,7 +4675,7 @@ async function readDisputedCharge(secretKey, chargeId, fetch2) {
|
|
|
4596
4675
|
if (!invoice.ok || invoice.body.id !== invoiceId) membershipFail("membership_dispute_provider_unavailable");
|
|
4597
4676
|
subscriptionId = refOf(invoice.body.subscription) ?? refOf(invoice.body.parent?.subscription_details?.subscription);
|
|
4598
4677
|
}
|
|
4599
|
-
const seatId =
|
|
4678
|
+
const seatId = text4(charge.body.metadata?.seatId);
|
|
4600
4679
|
return {
|
|
4601
4680
|
chargeId,
|
|
4602
4681
|
customerId,
|
|
@@ -4622,37 +4701,23 @@ async function reconcileMembershipDispute(db, input, now, secretKey, resolved) {
|
|
|
4622
4701
|
termEndsAt: Number(value.termEndsAt),
|
|
4623
4702
|
graceEndsAt: Number(value.graceEndsAt ?? 0),
|
|
4624
4703
|
autoRenew: value.autoRenew === true,
|
|
4625
|
-
...
|
|
4704
|
+
...text4(value.disputeStatus) ? { disputeStatus: text4(value.disputeStatus) } : {},
|
|
4626
4705
|
applied
|
|
4627
4706
|
});
|
|
4628
4707
|
const known = await membershipLoad(db, "networkMembership", id11);
|
|
4629
4708
|
requireMembershipScope(known, input);
|
|
4630
4709
|
if (known.disputeEventId === input.eventId) return receipt(known);
|
|
4710
|
+
const seatKnown = known.namedSeatId ? await membershipLoad(db, "networkSeat", String(known.namedSeatId)) : void 0;
|
|
4711
|
+
if (seatKnown?.disputeEventId === input.eventId) return { ...receipt(known, true), seat: namedSeatDisputeStanding(seatKnown, true) };
|
|
4631
4712
|
const charge = resolved && resolved.chargeId === fact.chargeId ? resolved : await readDisputedCharge(secretKey, fact.chargeId);
|
|
4713
|
+
if (charge.customerId !== known.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
|
|
4714
|
+
if (charge.seatId) return reconcileNamedSeatDispute(db, input, now, charge, { row: row2, fact, guard: evidenceGuard });
|
|
4632
4715
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
4633
4716
|
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id11);
|
|
4634
4717
|
requireMembershipScope(member, input);
|
|
4635
|
-
if (charge.customerId !== member.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
|
|
4636
4718
|
if (!disputedChargeCoversMembership(charge, member) || member.disputeEventId === input.eventId) return receipt(member);
|
|
4637
|
-
const
|
|
4638
|
-
|
|
4639
|
-
if (sameDispute && Number(member.disputeClosedAt ?? 0) > 0 && !closed) return receipt(member, false);
|
|
4640
|
-
const won = DISPUTE_WON_STATUSES.includes(status2);
|
|
4641
|
-
const suspended = member.status === MEMBERSHIP_DISPUTED_STATUS, terminal = TERMINAL.includes(String(member.status));
|
|
4642
|
-
const next = terminal ? String(member.status) : won ? suspended ? String(member.disputePriorStatus ?? (member.journeyApprovedAt ? "active" : "paid_pending_approval")) : String(member.status) : MEMBERSHIP_DISPUTED_STATUS;
|
|
4643
|
-
const attrs = {
|
|
4644
|
-
status: next,
|
|
4645
|
-
disputeId: fact.objectId,
|
|
4646
|
-
disputeStatus: status2,
|
|
4647
|
-
disputeEventId: input.eventId,
|
|
4648
|
-
disputeClosedAt: closed ? now : 0,
|
|
4649
|
-
...fact.disputeReason ? { disputeReason: fact.disputeReason } : {},
|
|
4650
|
-
...fact.amountCents !== void 0 ? { disputeAmountCents: fact.amountCents } : {},
|
|
4651
|
-
...sameDispute ? {} : { disputedAt: now },
|
|
4652
|
-
...next === MEMBERSHIP_DISPUTED_STATUS && !suspended ? { disputePriorStatus: String(member.status) } : {},
|
|
4653
|
-
revision: Number(member.revision) + 1,
|
|
4654
|
-
updatedAt: now
|
|
4655
|
-
};
|
|
4719
|
+
const attrs = disputeTransition(member, fact, input.eventId, now, TERMINAL2);
|
|
4720
|
+
if (!attrs) return receipt(member, false);
|
|
4656
4721
|
try {
|
|
4657
4722
|
await db.transact([membershipUpdate("networkMembership", id11, attrs)], { mutationId: `membership-dispute:${id11}:${input.eventId}`, guards: [
|
|
4658
4723
|
{ ns: "networkMembership", id: id11, exists: true, equals: { revision: member.revision } },
|
|
@@ -6056,7 +6121,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
6056
6121
|
};
|
|
6057
6122
|
signal.addEventListener("abort", cancel, { once: true });
|
|
6058
6123
|
let size = 0;
|
|
6059
|
-
let
|
|
6124
|
+
let text8 = "";
|
|
6060
6125
|
const decoder = new TextDecoder();
|
|
6061
6126
|
try {
|
|
6062
6127
|
while (true) {
|
|
@@ -6066,10 +6131,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
6066
6131
|
if (done) break;
|
|
6067
6132
|
size += value.byteLength;
|
|
6068
6133
|
if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
|
|
6069
|
-
|
|
6134
|
+
text8 += decoder.decode(value, { stream: true });
|
|
6070
6135
|
}
|
|
6071
6136
|
try {
|
|
6072
|
-
return JSON.parse(
|
|
6137
|
+
return JSON.parse(text8 + decoder.decode());
|
|
6073
6138
|
} catch {
|
|
6074
6139
|
throw new TypeError(`invalid ${label}`);
|
|
6075
6140
|
}
|
|
@@ -6556,6 +6621,19 @@ var id10 = () => attr9("string", false, true, true);
|
|
|
6556
6621
|
var key2 = (optional = false) => attr9("string", optional, false, true);
|
|
6557
6622
|
var scope = () => ({ appId: key2(), chapterId: key2(), environment: key2(), runtime: key2(), applicationId: key2(), skuId: key2() });
|
|
6558
6623
|
var times = () => ({ revision: attr9("number"), createdAt: attr9("number"), updatedAt: attr9("number") });
|
|
6624
|
+
var dispute = () => ({
|
|
6625
|
+
disputeId: key2(true),
|
|
6626
|
+
disputeStatus: attr9("string", true),
|
|
6627
|
+
disputeReason: attr9("string", true),
|
|
6628
|
+
disputeAmountCents: attr9("number", true),
|
|
6629
|
+
disputedAt: attr9("number", true),
|
|
6630
|
+
disputeClosedAt: attr9("number", true),
|
|
6631
|
+
disputePriorStatus: attr9("string", true),
|
|
6632
|
+
disputeEventId: key2(true),
|
|
6633
|
+
rechargePaymentIntentId: key2(true),
|
|
6634
|
+
rechargedAt: attr9("number", true),
|
|
6635
|
+
rechargeAttempt: attr9("json", true)
|
|
6636
|
+
});
|
|
6559
6637
|
var membershipAuthorityEntities = {
|
|
6560
6638
|
networkMembership: { attrs: {
|
|
6561
6639
|
id: id10(),
|
|
@@ -6596,19 +6674,7 @@ var membershipAuthorityEntities = {
|
|
|
6596
6674
|
...times(),
|
|
6597
6675
|
refundRequestedAt: attr9("number", true),
|
|
6598
6676
|
restartId: key2(true),
|
|
6599
|
-
|
|
6600
|
-
// the status the membership held before it was suspended, and the re-issued charge once an operator collected again.
|
|
6601
|
-
disputeId: key2(true),
|
|
6602
|
-
disputeStatus: attr9("string", true),
|
|
6603
|
-
disputeReason: attr9("string", true),
|
|
6604
|
-
disputeAmountCents: attr9("number", true),
|
|
6605
|
-
disputedAt: attr9("number", true),
|
|
6606
|
-
disputeClosedAt: attr9("number", true),
|
|
6607
|
-
disputePriorStatus: attr9("string", true),
|
|
6608
|
-
disputeEventId: key2(true),
|
|
6609
|
-
rechargePaymentIntentId: key2(true),
|
|
6610
|
-
rechargedAt: attr9("number", true),
|
|
6611
|
-
rechargeAttempt: attr9("json", true)
|
|
6677
|
+
...dispute()
|
|
6612
6678
|
} },
|
|
6613
6679
|
membershipRestart: { attrs: {
|
|
6614
6680
|
id: id10(),
|
|
@@ -6684,7 +6750,8 @@ var membershipAuthorityEntities = {
|
|
|
6684
6750
|
refundedCents: attr9("number", true),
|
|
6685
6751
|
providerRefundIds: attr9("json", true),
|
|
6686
6752
|
regiftedAt: attr9("number", true),
|
|
6687
|
-
formerRecipients: attr9("json", true)
|
|
6753
|
+
formerRecipients: attr9("json", true),
|
|
6754
|
+
...dispute()
|
|
6688
6755
|
} },
|
|
6689
6756
|
networkSeatRecipient: { attrs: { id: id10(), appId: key2(), chapterId: key2(), environment: key2(), runtime: key2(), seatId: key2(), recipientEmailNormalized: key2(), createdAt: attr9("number") } },
|
|
6690
6757
|
membershipEffect: { attrs: {
|
|
@@ -7044,7 +7111,7 @@ var record2 = (value) => value !== null && typeof value === "object" && !Array.i
|
|
|
7044
7111
|
function unknownKeys2(value, allowed2, path, errors) {
|
|
7045
7112
|
for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
7046
7113
|
}
|
|
7047
|
-
function
|
|
7114
|
+
function text5(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
7048
7115
|
if (value === void 0 && !required) return void 0;
|
|
7049
7116
|
if (typeof value !== "string" || required && !value.trim()) {
|
|
7050
7117
|
errors.push(`${path} must be a non-empty string`);
|
|
@@ -7070,7 +7137,7 @@ function scopeValue(value, path, errors) {
|
|
|
7070
7137
|
return { kind: raw2.kind };
|
|
7071
7138
|
}
|
|
7072
7139
|
if (raw2.kind === "chapter") {
|
|
7073
|
-
const chapterId =
|
|
7140
|
+
const chapterId = text5(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
7074
7141
|
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
7075
7142
|
return { kind: "chapter", chapterId };
|
|
7076
7143
|
}
|
|
@@ -7084,22 +7151,22 @@ function resourceValue(value, path, errors) {
|
|
|
7084
7151
|
return null;
|
|
7085
7152
|
}
|
|
7086
7153
|
unknownKeys2(raw2, RESOURCE_KEYS, path, errors);
|
|
7087
|
-
const id11 =
|
|
7154
|
+
const id11 = text5(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
7088
7155
|
if (id11 && !ID2.test(id11)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
7089
|
-
const kind =
|
|
7156
|
+
const kind = text5(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
7090
7157
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
7091
|
-
const title =
|
|
7092
|
-
const summary =
|
|
7093
|
-
const status2 =
|
|
7158
|
+
const title = text5(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
7159
|
+
const summary = text5(raw2.summary, `${path}.summary`, errors, 1e3, false);
|
|
7160
|
+
const status2 = text5(raw2.status, `${path}.status`, errors) ?? "";
|
|
7094
7161
|
if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
7095
7162
|
const revision = integer3(raw2.revision, `${path}.revision`, errors);
|
|
7096
7163
|
const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
7097
|
-
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) =>
|
|
7164
|
+
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text5(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
|
|
7098
7165
|
if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
|
|
7099
7166
|
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
7100
7167
|
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
7101
|
-
const bodyMarkdown =
|
|
7102
|
-
const url =
|
|
7168
|
+
const bodyMarkdown = text5(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, false);
|
|
7169
|
+
const url = text5(raw2.url, `${path}.url`, errors, 2048, false);
|
|
7103
7170
|
if (url) {
|
|
7104
7171
|
try {
|
|
7105
7172
|
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
@@ -7111,9 +7178,9 @@ function resourceValue(value, path, errors) {
|
|
|
7111
7178
|
if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
7112
7179
|
if (asset) unknownKeys2(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
7113
7180
|
const brandAsset = asset ? {
|
|
7114
|
-
bookId:
|
|
7115
|
-
assetId:
|
|
7116
|
-
...asset.filename === void 0 ? {} : { filename:
|
|
7181
|
+
bookId: text5(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
7182
|
+
assetId: text5(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
7183
|
+
...asset.filename === void 0 ? {} : { filename: text5(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
7117
7184
|
} : void 0;
|
|
7118
7185
|
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
7119
7186
|
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
@@ -7128,17 +7195,17 @@ function resourceValue(value, path, errors) {
|
|
|
7128
7195
|
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
7129
7196
|
if (promotedRaw) unknownKeys2(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
7130
7197
|
const promotedFrom = promotedRaw ? {
|
|
7131
|
-
chapterId:
|
|
7132
|
-
resourceId:
|
|
7198
|
+
chapterId: text5(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
7199
|
+
resourceId: text5(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
7133
7200
|
resourceRevision: integer3(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
7134
7201
|
documentRevision: integer3(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
7135
|
-
promotedBy:
|
|
7202
|
+
promotedBy: text5(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
7136
7203
|
promotedAt: integer3(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
7137
7204
|
} : void 0;
|
|
7138
7205
|
const provenance = {
|
|
7139
|
-
createdBy:
|
|
7206
|
+
createdBy: text5(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
7140
7207
|
createdAt: integer3(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
7141
|
-
updatedBy:
|
|
7208
|
+
updatedBy: text5(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
7142
7209
|
updatedAt: integer3(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
7143
7210
|
...promotedFrom ? { promotedFrom } : {}
|
|
7144
7211
|
};
|
|
@@ -7157,7 +7224,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
7157
7224
|
if (sourceRaw) unknownKeys2(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
7158
7225
|
const sourceType = sourceRaw?.type;
|
|
7159
7226
|
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
7160
|
-
const sourceAppId =
|
|
7227
|
+
const sourceAppId = text5(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
7161
7228
|
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
7162
7229
|
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
7163
7230
|
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
@@ -7165,8 +7232,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
7165
7232
|
if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
7166
7233
|
if (targetRaw) unknownKeys2(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
7167
7234
|
const target = targetRaw ? {
|
|
7168
|
-
appId:
|
|
7169
|
-
environment:
|
|
7235
|
+
appId: text5(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
7236
|
+
environment: text5(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
7170
7237
|
} : void 0;
|
|
7171
7238
|
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
7172
7239
|
if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
|
|
@@ -7261,9 +7328,9 @@ async function createBuilderLibraryEnvelope(document) {
|
|
|
7261
7328
|
}
|
|
7262
7329
|
|
|
7263
7330
|
// src/email-retirement.ts
|
|
7264
|
-
function isRetiredEmailCopy(subject,
|
|
7331
|
+
function isRetiredEmailCopy(subject, text8) {
|
|
7265
7332
|
const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
|
|
7266
|
-
const bodyCopy =
|
|
7333
|
+
const bodyCopy = text8.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
|
|
7267
7334
|
return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
|
|
7268
7335
|
}
|
|
7269
7336
|
|
|
@@ -7341,14 +7408,92 @@ function planDelivery(input) {
|
|
|
7341
7408
|
if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
|
|
7342
7409
|
if (validateEmailCopy({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
|
|
7343
7410
|
const subject = (redirect ? "[dev] " : "") + renderedSubject;
|
|
7344
|
-
const
|
|
7411
|
+
const text8 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
7345
7412
|
|
|
7346
7413
|
` + renderedText : renderedText;
|
|
7347
|
-
return { deliver: true, transport, to, subject, text:
|
|
7414
|
+
return { deliver: true, transport, to, subject, text: text8, redirected: redirect };
|
|
7348
7415
|
}
|
|
7349
7416
|
|
|
7350
7417
|
// src/email-readiness.ts
|
|
7351
7418
|
import { isEmailTierName, validateEmailCopy as validateEmailCopy2 } from "@odla-ai/crm";
|
|
7419
|
+
|
|
7420
|
+
// src/email-spam-signals.ts
|
|
7421
|
+
var TRIGGER_PHRASES = [
|
|
7422
|
+
"act now",
|
|
7423
|
+
"click here",
|
|
7424
|
+
"limited time",
|
|
7425
|
+
"guaranteed",
|
|
7426
|
+
"risk-free",
|
|
7427
|
+
"risk free",
|
|
7428
|
+
"no obligation",
|
|
7429
|
+
"100% free",
|
|
7430
|
+
"winner",
|
|
7431
|
+
"congratulations",
|
|
7432
|
+
"urgent",
|
|
7433
|
+
"double your",
|
|
7434
|
+
"earn money",
|
|
7435
|
+
"make money",
|
|
7436
|
+
"cash bonus",
|
|
7437
|
+
"once in a lifetime",
|
|
7438
|
+
"exclusive deal",
|
|
7439
|
+
"order now",
|
|
7440
|
+
"buy now",
|
|
7441
|
+
"this is not spam",
|
|
7442
|
+
"dear friend",
|
|
7443
|
+
"dear customer"
|
|
7444
|
+
];
|
|
7445
|
+
var SUBJECT_TRIGGERS = ["free", ...TRIGGER_PHRASES];
|
|
7446
|
+
var DEV_HOSTS = /(?:^|\.)(?:workers\.dev|pages\.dev|localhost|ngrok(?:-free)?\.app|trycloudflare\.com)$/i;
|
|
7447
|
+
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;
|
|
7448
|
+
var URL_RE = /https?:\/\/[^\s<>"')\]]+/g;
|
|
7449
|
+
var strip = (s) => s.replace(/\{\{\w+\}\}/g, " ");
|
|
7450
|
+
var words = (s) => strip(s).split(/\s+/).filter(Boolean);
|
|
7451
|
+
var phraseIn = (phrases, haystack) => phrases.find((phrase) => new RegExp(`(?:^|[^a-z0-9])${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![a-z0-9])`, "i").test(haystack));
|
|
7452
|
+
function emailSpamSignals(copy) {
|
|
7453
|
+
const subject = typeof copy.subject === "string" ? copy.subject : "";
|
|
7454
|
+
const text8 = typeof copy.text === "string" ? copy.text : "";
|
|
7455
|
+
const out = [];
|
|
7456
|
+
const letters = strip(subject).replace(/[^A-Za-z]/g, "");
|
|
7457
|
+
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, "")));
|
|
7458
|
+
if (shouting) out.push({ code: "subject-caps", message: "The subject shouts in capital letters. Filters read that as a spam signal; use sentence case." });
|
|
7459
|
+
if (/[!?]{2,}|!\s*$/.test(subject)) out.push({ code: "subject-punctuation", message: "Repeated or trailing exclamation marks in the subject are a classic spam signal." });
|
|
7460
|
+
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." });
|
|
7461
|
+
const subjectHit = phraseIn(SUBJECT_TRIGGERS, strip(subject));
|
|
7462
|
+
if (subjectHit) out.push({ code: "subject-trigger-words", message: `The subject uses "${subjectHit}", a word filters weight against the sender.` });
|
|
7463
|
+
const bodyHit = phraseIn(TRIGGER_PHRASES, strip(text8));
|
|
7464
|
+
if (bodyHit) out.push({ code: "body-trigger-words", message: `The body uses "${bodyHit}", a phrase filters weight against the sender.` });
|
|
7465
|
+
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." });
|
|
7466
|
+
if (/!{2,}/.test(text8)) out.push({ code: "body-punctuation", message: "Repeated exclamation marks in the body are a classic spam signal." });
|
|
7467
|
+
const urls = [...text8.matchAll(URL_RE)].map((match) => match[0]);
|
|
7468
|
+
const hosts = urls.map((url) => {
|
|
7469
|
+
try {
|
|
7470
|
+
return new URL(url).hostname;
|
|
7471
|
+
} catch {
|
|
7472
|
+
return "";
|
|
7473
|
+
}
|
|
7474
|
+
});
|
|
7475
|
+
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." });
|
|
7476
|
+
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." });
|
|
7477
|
+
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." });
|
|
7478
|
+
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." });
|
|
7479
|
+
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." });
|
|
7480
|
+
return out;
|
|
7481
|
+
}
|
|
7482
|
+
function emailSenderWarnings(input) {
|
|
7483
|
+
const out = [];
|
|
7484
|
+
const from = (input.fromEmail ?? "").trim().toLowerCase().replace(/^.*<([^>]+)>.*$/, "$1");
|
|
7485
|
+
const reply = (input.replyTo ?? "").trim().toLowerCase();
|
|
7486
|
+
const domain = (address) => address.split("@")[1] ?? "";
|
|
7487
|
+
if (/^(?:no-?reply|do-?not-?reply|donotreply)(?:[+.-][^@]*)?@/.test(from)) {
|
|
7488
|
+
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." });
|
|
7489
|
+
}
|
|
7490
|
+
if (domain(from) && domain(reply) && domain(from) !== domain(reply)) {
|
|
7491
|
+
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.` });
|
|
7492
|
+
}
|
|
7493
|
+
return out;
|
|
7494
|
+
}
|
|
7495
|
+
|
|
7496
|
+
// src/email-readiness.ts
|
|
7352
7497
|
function validateEmailReadiness(input) {
|
|
7353
7498
|
const issues = [];
|
|
7354
7499
|
for (const key3 of Object.keys(input.templates)) if (!input.events[key3]) {
|
|
@@ -7373,9 +7518,56 @@ ${template.text ?? ""}`.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1]);
|
|
|
7373
7518
|
}
|
|
7374
7519
|
if (event2.requiresTier && input.tiersUnavailable) problems.push({ code: "tiers-unavailable", message: "Tier names are unavailable; delivery checks will block unresolved memberships." });
|
|
7375
7520
|
else if (event2.requiresTier && noTierNames) problems.push({ code: "tiers-missing", message: "Configure the membership tier names needed by this event." });
|
|
7376
|
-
return {
|
|
7521
|
+
return {
|
|
7522
|
+
...event2,
|
|
7523
|
+
key: key3,
|
|
7524
|
+
status: template?.enabled === false ? "disabled" : problems.length ? "blocked" : "ready",
|
|
7525
|
+
issues: problems,
|
|
7526
|
+
warnings: template ? emailSpamSignals(template) : []
|
|
7527
|
+
};
|
|
7377
7528
|
});
|
|
7378
|
-
return { checkedAt: input.now ?? Date.now(), events: events2, issues };
|
|
7529
|
+
return { checkedAt: input.now ?? Date.now(), events: events2, issues, warnings: emailSenderWarnings(input.sender ?? {}) };
|
|
7530
|
+
}
|
|
7531
|
+
|
|
7532
|
+
// src/email-html.ts
|
|
7533
|
+
var URL_RE2 = /https?:\/\/[^\s<>"')\]]+[^\s<>"').,;:!?\]]/g;
|
|
7534
|
+
function escapeHtml(s) {
|
|
7535
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
7536
|
+
}
|
|
7537
|
+
function lineHtml(line) {
|
|
7538
|
+
let out = "", last = 0;
|
|
7539
|
+
for (const match of line.matchAll(URL_RE2)) {
|
|
7540
|
+
out += escapeHtml(line.slice(last, match.index));
|
|
7541
|
+
const url = escapeHtml(match[0]);
|
|
7542
|
+
out += `<a href="${url}">${url}</a>`;
|
|
7543
|
+
last = match.index + match[0].length;
|
|
7544
|
+
}
|
|
7545
|
+
return out + escapeHtml(line.slice(last));
|
|
7546
|
+
}
|
|
7547
|
+
function textToHtml(text8) {
|
|
7548
|
+
const paragraphs = text8.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
|
|
7549
|
+
const body = paragraphs.map((paragraph) => `<p>${paragraph.split("\n").map(lineHtml).join("<br>\n")}</p>`).join("\n");
|
|
7550
|
+
return `<!doctype html>
|
|
7551
|
+
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
|
|
7552
|
+
<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">
|
|
7553
|
+
${body}
|
|
7554
|
+
</body></html>`;
|
|
7555
|
+
}
|
|
7556
|
+
|
|
7557
|
+
// src/email-deliverability.ts
|
|
7558
|
+
var ADMINISTRATOR_AUDIENCE = /^(?:administrator|admin|team|operator|owner|staff)s?$/i;
|
|
7559
|
+
function isRecipientFacing(event2) {
|
|
7560
|
+
return !event2 || !ADMINISTRATOR_AUDIENCE.test(event2.audience.trim());
|
|
7561
|
+
}
|
|
7562
|
+
function listUnsubscribeHeader(replyTo) {
|
|
7563
|
+
const address = replyTo.trim();
|
|
7564
|
+
if (!/^[^\s@,;<>?]+@[^\s@,;<>?]+\.[^\s@,;<>?]+$/.test(address)) return void 0;
|
|
7565
|
+
return `<mailto:${address}?subject=${encodeURIComponent("unsubscribe")}>`;
|
|
7566
|
+
}
|
|
7567
|
+
function deliverabilityHeaders(input) {
|
|
7568
|
+
if (!isRecipientFacing(input.event)) return void 0;
|
|
7569
|
+
const value = listUnsubscribeHeader(input.replyTo);
|
|
7570
|
+
return value ? { "List-Unsubscribe": value } : void 0;
|
|
7379
7571
|
}
|
|
7380
7572
|
|
|
7381
7573
|
// src/notify.ts
|
|
@@ -7543,11 +7735,16 @@ async function sendTemplated(deps, input) {
|
|
|
7543
7735
|
if (decision.transport === "log-only") return { sent: true };
|
|
7544
7736
|
let messageId;
|
|
7545
7737
|
try {
|
|
7738
|
+
const text8 = String(row2.body);
|
|
7739
|
+
const html = deps.renderHtml === false ? void 0 : (deps.renderHtml ?? textToHtml)(text8);
|
|
7740
|
+
const headers = deliverabilityHeaders({ event: event2, replyTo: input.group.replyTo });
|
|
7546
7741
|
const response = await deps.sender.send({
|
|
7547
7742
|
from: fromAddress(deps.from, input.group.fromName),
|
|
7548
7743
|
to: [decision.to],
|
|
7549
7744
|
subject: String(row2.subject),
|
|
7550
|
-
text:
|
|
7745
|
+
text: text8,
|
|
7746
|
+
...html ? { html } : {},
|
|
7747
|
+
...headers ? { headers } : {},
|
|
7551
7748
|
replyTo: input.group.replyTo
|
|
7552
7749
|
});
|
|
7553
7750
|
if (!response || typeof response.messageId !== "string" || !response.messageId) throw new Error("missing delivery receipt");
|
|
@@ -8000,7 +8197,7 @@ var ID4 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
|
8000
8197
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
8001
8198
|
var CURRENCY = /^[a-z]{3}$/;
|
|
8002
8199
|
var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
8003
|
-
var
|
|
8200
|
+
var text6 = (value, path, errors, max) => {
|
|
8004
8201
|
if (typeof value !== "string" || !value.trim()) {
|
|
8005
8202
|
errors.push(`${path} is required`);
|
|
8006
8203
|
return "";
|
|
@@ -8028,23 +8225,23 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
8028
8225
|
}
|
|
8029
8226
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
8030
8227
|
for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
8031
|
-
const id11 =
|
|
8228
|
+
const id11 = text6(item.id, `${path}.id`, errors, 80);
|
|
8032
8229
|
if (id11 && !ID4.test(id11)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
8033
8230
|
if (ids2.has(id11)) errors.push(`${path}.id duplicates "${id11}"`);
|
|
8034
8231
|
ids2.add(id11);
|
|
8035
8232
|
const targetRaw = record3(item.target);
|
|
8036
8233
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
8037
8234
|
if (targetRaw?.type === "tier") {
|
|
8038
|
-
const tierId =
|
|
8235
|
+
const tierId = text6(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
8039
8236
|
if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
|
|
8040
8237
|
target = { type: "tier", tierId };
|
|
8041
8238
|
} else if (targetRaw?.type === "entitlement") {
|
|
8042
|
-
const entitlementKey =
|
|
8239
|
+
const entitlementKey = text6(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
8043
8240
|
if (entitlementKey && !ID4.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
8044
8241
|
target = { type: "entitlement", entitlementKey };
|
|
8045
8242
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
8046
8243
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
8047
|
-
const stripePriceId =
|
|
8244
|
+
const stripePriceId = text6(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
|
|
8048
8245
|
if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
8049
8246
|
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`);
|
|
8050
8247
|
const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
|
|
@@ -8066,9 +8263,9 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
8066
8263
|
redemptionLifetimeHours: lifetime,
|
|
8067
8264
|
...activeFrom !== void 0 ? { activeFrom } : {},
|
|
8068
8265
|
...activeUntil !== void 0 ? { activeUntil } : {},
|
|
8069
|
-
title:
|
|
8070
|
-
description:
|
|
8071
|
-
ctaLabel:
|
|
8266
|
+
title: text6(item.title, `${path}.title`, errors, 160),
|
|
8267
|
+
description: text6(item.description, `${path}.description`, errors, 2e3),
|
|
8268
|
+
ctaLabel: text6(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
|
|
8072
8269
|
});
|
|
8073
8270
|
}
|
|
8074
8271
|
return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
|
|
@@ -9156,10 +9353,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
9156
9353
|
errors.push(`${path} must be a string`);
|
|
9157
9354
|
return void 0;
|
|
9158
9355
|
}
|
|
9159
|
-
const
|
|
9160
|
-
if (required && !
|
|
9161
|
-
if (
|
|
9162
|
-
return
|
|
9356
|
+
const text8 = value.trim();
|
|
9357
|
+
if (required && !text8) errors.push(`${path} is required`);
|
|
9358
|
+
if (text8.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
9359
|
+
return text8;
|
|
9163
9360
|
}
|
|
9164
9361
|
function validateMembershipPageDocument(input, options = {}) {
|
|
9165
9362
|
const errors = [];
|
|
@@ -9208,8 +9405,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
9208
9405
|
else {
|
|
9209
9406
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
9210
9407
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
9211
|
-
const
|
|
9212
|
-
return
|
|
9408
|
+
const text8 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
9409
|
+
return text8 ? [text8] : [];
|
|
9213
9410
|
});
|
|
9214
9411
|
}
|
|
9215
9412
|
}
|
|
@@ -9308,7 +9505,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
|
|
|
9308
9505
|
function unknownKeys3(value, allowed2, path, errors) {
|
|
9309
9506
|
for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
9310
9507
|
}
|
|
9311
|
-
function
|
|
9508
|
+
function text7(value, path, errors, max = 65536) {
|
|
9312
9509
|
if (typeof value !== "string" || !value.trim()) {
|
|
9313
9510
|
errors.push(`${path} is required`);
|
|
9314
9511
|
return "";
|
|
@@ -9331,11 +9528,11 @@ function tiersOf(value, errors) {
|
|
|
9331
9528
|
return [];
|
|
9332
9529
|
}
|
|
9333
9530
|
unknownKeys3(item, TIER_KEYS2, path, errors);
|
|
9334
|
-
const id11 =
|
|
9531
|
+
const id11 = text7(item.id, `${path}.id`, errors, 80);
|
|
9335
9532
|
if (id11 && !TIER.test(id11)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
9336
9533
|
if (seen.has(id11)) errors.push(`${path}.id duplicates "${id11}"`);
|
|
9337
9534
|
seen.add(id11);
|
|
9338
|
-
const name =
|
|
9535
|
+
const name = text7(item.name, `${path}.name`, errors, 160);
|
|
9339
9536
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
9340
9537
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
9341
9538
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
@@ -9353,7 +9550,7 @@ function tiersOf(value, errors) {
|
|
|
9353
9550
|
if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
|
|
9354
9551
|
errors.push(`${path}.interval must be year or month`);
|
|
9355
9552
|
}
|
|
9356
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
9553
|
+
const blurb = item.blurb === void 0 ? void 0 : text7(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
9357
9554
|
return [{
|
|
9358
9555
|
id: id11,
|
|
9359
9556
|
name,
|
|
@@ -9411,7 +9608,7 @@ function policyOf(value, errors, tiers2) {
|
|
|
9411
9608
|
const result = {};
|
|
9412
9609
|
for (const key3 of allowed2) {
|
|
9413
9610
|
if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
|
|
9414
|
-
result[key3] =
|
|
9611
|
+
result[key3] = text7(input[key3], `document.policy.${key3}`, errors);
|
|
9415
9612
|
}
|
|
9416
9613
|
const tierPolicies = {};
|
|
9417
9614
|
if (input.tierPolicies !== void 0) {
|
|
@@ -9426,8 +9623,8 @@ function policyOf(value, errors, tiers2) {
|
|
|
9426
9623
|
}
|
|
9427
9624
|
unknownKeys3(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
9428
9625
|
tierPolicies[id11] = {
|
|
9429
|
-
refundPolicyText:
|
|
9430
|
-
...policy2.trustCopy !== void 0 ? { trustCopy:
|
|
9626
|
+
refundPolicyText: text7(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
9627
|
+
...policy2.trustCopy !== void 0 ? { trustCopy: text7(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
9431
9628
|
};
|
|
9432
9629
|
}
|
|
9433
9630
|
}
|
|
@@ -9467,9 +9664,9 @@ function validateSignupControlDocument(input) {
|
|
|
9467
9664
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
9468
9665
|
const source = recordOf(root.source);
|
|
9469
9666
|
const target = recordOf(root.target);
|
|
9470
|
-
const appId =
|
|
9471
|
-
const chapterId =
|
|
9472
|
-
const environment =
|
|
9667
|
+
const appId = text7(source?.appId, "document.source.appId", errors, 63);
|
|
9668
|
+
const chapterId = text7(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
9669
|
+
const environment = text7(target?.environment, "document.target.environment", errors, 63);
|
|
9473
9670
|
if (appId && !SLUG9.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
9474
9671
|
if (chapterId && !SLUG9.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
9475
9672
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
@@ -9646,9 +9843,9 @@ function markdownBenefits(lines, path, errors) {
|
|
|
9646
9843
|
errors.push(`${path} line ${index2 + 1} must be a Markdown bullet`);
|
|
9647
9844
|
continue;
|
|
9648
9845
|
}
|
|
9649
|
-
const
|
|
9650
|
-
if (
|
|
9651
|
-
if (
|
|
9846
|
+
const text8 = (match[1] ?? "").trim();
|
|
9847
|
+
if (text8.length > 2e3) errors.push(`${path} line ${index2 + 1} must be at most 2,000 characters`);
|
|
9848
|
+
if (text8) benefits.push(text8);
|
|
9652
9849
|
}
|
|
9653
9850
|
if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
|
|
9654
9851
|
return benefits.slice(0, MAX_BENEFITS2);
|
|
@@ -10306,6 +10503,7 @@ function createMembershipAuthorityIntegration() {
|
|
|
10306
10503
|
};
|
|
10307
10504
|
}
|
|
10308
10505
|
export {
|
|
10506
|
+
ADMINISTRATOR_AUDIENCE,
|
|
10309
10507
|
ADMISSION_GRANTS_PATH,
|
|
10310
10508
|
ADMISSION_GRANTS_SCHEMA_VERSION,
|
|
10311
10509
|
BUILDER_LIBRARY_ADMIN_PATH,
|
|
@@ -10399,9 +10597,12 @@ export {
|
|
|
10399
10597
|
dashboardMetricData,
|
|
10400
10598
|
defaultCrm,
|
|
10401
10599
|
defineChapter,
|
|
10600
|
+
deliverabilityHeaders,
|
|
10402
10601
|
drainMembershipEffects,
|
|
10403
10602
|
edgeUnpaired,
|
|
10404
10603
|
emailGroupFrom,
|
|
10604
|
+
emailSenderWarnings,
|
|
10605
|
+
emailSpamSignals,
|
|
10405
10606
|
endForSlot,
|
|
10406
10607
|
exportChapterPromotion,
|
|
10407
10608
|
findAdmissionGrant,
|
|
@@ -10420,6 +10621,7 @@ export {
|
|
|
10420
10621
|
introIdempotencyKey,
|
|
10421
10622
|
isAdminRole,
|
|
10422
10623
|
isAlreadySent,
|
|
10624
|
+
isRecipientFacing,
|
|
10423
10625
|
isReconcilable,
|
|
10424
10626
|
isSlotAvailable,
|
|
10425
10627
|
isValidEmail,
|
|
@@ -10428,6 +10630,7 @@ export {
|
|
|
10428
10630
|
joinConfigTiers,
|
|
10429
10631
|
leaderCrmConfig,
|
|
10430
10632
|
listStripeObjects,
|
|
10633
|
+
listUnsubscribeHeader,
|
|
10431
10634
|
loadLatestAdmissionRevision,
|
|
10432
10635
|
loadOfferableTiers,
|
|
10433
10636
|
loadTiers,
|
|
@@ -10545,6 +10748,7 @@ export {
|
|
|
10545
10748
|
submitApplication,
|
|
10546
10749
|
subscriptionIdempotencyKey,
|
|
10547
10750
|
syncApplicationToCrm,
|
|
10751
|
+
textToHtml,
|
|
10548
10752
|
tierIsFree,
|
|
10549
10753
|
tierPayable,
|
|
10550
10754
|
updateClerkUserMetadata,
|