@aglyn/plugins-commerce 1.0.0-beta.290 → 1.0.0-beta.291
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 +4 -2
- package/package.json +13 -13
- package/src/lib/components/commerce-console-page.js +2 -1
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/console/host-orders-card.component.d.ts +6 -0
- package/src/lib/components/console/host-orders-card.component.js +3 -2
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/invoice-send.component.d.ts +44 -0
- package/src/lib/components/console/invoice-send.component.js +237 -0
- package/src/lib/components/console/invoice-send.component.js.map +1 -0
- package/src/lib/components/console/order-detail-dialog.component.d.ts +5 -0
- package/src/lib/components/console/order-detail-dialog.component.js +9 -2
- package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
- package/src/lib/components/console/order-invoice.component.d.ts +9 -0
- package/src/lib/components/console/order-invoice.component.js +27 -2
- package/src/lib/components/console/order-invoice.component.js.map +1 -1
- package/src/lib/components/console/quote-editor-dialog.component.d.ts +29 -0
- package/src/lib/components/console/quote-editor-dialog.component.js +102 -4
- package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/quote-proofs.component.d.ts +11 -0
- package/src/lib/components/console/quote-proofs.component.js +68 -0
- package/src/lib/components/console/quote-proofs.component.js.map +1 -1
- package/src/lib/components/console/quote-send.component.d.ts +42 -0
- package/src/lib/components/console/quote-send.component.js +307 -0
- package/src/lib/components/console/quote-send.component.js.map +1 -0
- package/src/lib/components/console/quote-start.component.d.ts +45 -0
- package/src/lib/components/console/quote-start.component.js +182 -0
- package/src/lib/components/console/quote-start.component.js.map +1 -0
- package/src/lib/components/product-options-field-body.d.ts +44 -0
- package/src/lib/components/product-options-field-body.js +260 -0
- package/src/lib/components/product-options-field-body.js.map +1 -0
- package/src/lib/components/product-options-field.d.ts +32 -0
- package/src/lib/components/product-options-field.js +128 -0
- package/src/lib/components/product-options-field.js.map +1 -0
- package/src/lib/components/quote-page-body.js +39 -20
- package/src/lib/components/quote-page-body.js.map +1 -1
- package/src/lib/model/commerce-invoices.d.ts +132 -0
- package/src/lib/model/commerce-invoices.js +332 -0
- package/src/lib/model/commerce-invoices.js.map +1 -0
- package/src/lib/model/commerce-orders.d.ts +29 -2
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/commerce-product-field.d.ts +23 -0
- package/src/lib/model/commerce-product-field.js +26 -0
- package/src/lib/model/commerce-product-field.js.map +1 -0
- package/src/lib/model/commerce-quote-email.d.ts +67 -0
- package/src/lib/model/commerce-quote-email.js +154 -0
- package/src/lib/model/commerce-quote-email.js.map +1 -0
- package/src/lib/model/commerce-quote-pay.d.ts +49 -6
- package/src/lib/model/commerce-quote-pay.js +69 -36
- package/src/lib/model/commerce-quote-pay.js.map +1 -1
- package/src/lib/model/commerce-quote-request.d.ts +93 -0
- package/src/lib/model/commerce-quote-request.js +207 -0
- package/src/lib/model/commerce-quote-request.js.map +1 -0
- package/src/lib/model/commerce-quotes.d.ts +81 -0
- package/src/lib/model/commerce-quotes.js.map +1 -1
- package/src/lib/model/index.d.ts +3 -0
- package/src/lib/model/index.js +3 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/plugin.js +19 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/billing-webhook.js +115 -70
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/commerce-pdf.d.ts +5 -1
- package/src/lib/server/commerce-pdf.js +37 -10
- package/src/lib/server/commerce-pdf.js.map +1 -1
- package/src/lib/server/draft-order.js +23 -3
- package/src/lib/server/draft-order.js.map +1 -1
- package/src/lib/server/invoice-email.d.ts +76 -0
- package/src/lib/server/invoice-email.js +183 -0
- package/src/lib/server/invoice-email.js.map +1 -0
- package/src/lib/server/order-invoice-send.d.ts +43 -0
- package/src/lib/server/order-invoice-send.js +218 -0
- package/src/lib/server/order-invoice-send.js.map +1 -0
- package/src/lib/server/order-status.d.ts +11 -0
- package/src/lib/server/order-status.js +44 -8
- package/src/lib/server/order-status.js.map +1 -1
- package/src/lib/server/person-eraser.d.ts +3 -2
- package/src/lib/server/person-eraser.js +25 -6
- package/src/lib/server/person-eraser.js.map +1 -1
- package/src/lib/server/quote-email.d.ts +85 -0
- package/src/lib/server/quote-email.js +211 -0
- package/src/lib/server/quote-email.js.map +1 -0
- package/src/lib/server/quote-link-token.d.ts +11 -0
- package/src/lib/server/quote-link-token.js +21 -0
- package/src/lib/server/quote-link-token.js.map +1 -1
- package/src/lib/server/quote-pay.d.ts +8 -3
- package/src/lib/server/quote-pay.js +105 -43
- package/src/lib/server/quote-pay.js.map +1 -1
- package/src/lib/server/quote.d.ts +11 -0
- package/src/lib/server/quote.js +309 -12
- package/src/lib/server/quote.js.map +1 -1
- package/src/lib/server/site-page-resolver.js +2 -1
- package/src/lib/server/site-page-resolver.js.map +1 -1
- package/src/lib/server.js +5 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/site.js +8 -0
- package/src/lib/site.js.map +1 -1
- package/src/lib/tenant-emails.js +243 -0
- package/src/lib/tenant-emails.js.map +1 -1
- package/src/lib/utils/commerce-document-pdf.d.ts +5 -0
- package/src/lib/utils/commerce-document-pdf.js +6 -1
- package/src/lib/utils/commerce-document-pdf.js.map +1 -1
- package/src/lib/utils/invoice-email-content.d.ts +45 -0
- package/src/lib/utils/invoice-email-content.js +126 -0
- package/src/lib/utils/invoice-email-content.js.map +1 -0
- package/src/lib/utils/order-invoice.d.ts +1 -1
- package/src/lib/utils/order-invoice.js +14 -6
- package/src/lib/utils/order-invoice.js.map +1 -1
- package/src/lib/utils/quote-email-content.d.ts +62 -0
- package/src/lib/utils/quote-email-content.js +154 -0
- package/src/lib/utils/quote-email-content.js.map +1 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type EmailTheme, type RenderedHostEmail, type SendEmailOptions, type SendEmailResult, type SendingIdentityVerdict } from '@aglyn/shared-util-email';
|
|
18
|
+
import * as CommerceModel from '../model';
|
|
19
|
+
import type { quotePdfFile } from './commerce-pdf';
|
|
20
|
+
import type { CommercePdfFile } from './pdf-response';
|
|
21
|
+
/** What the route hands over for one send. */
|
|
22
|
+
export interface QuoteDeliveryInput {
|
|
23
|
+
hostId: string;
|
|
24
|
+
quoteId: string;
|
|
25
|
+
/** The quote as it stands, after any pay link replacement this send made. */
|
|
26
|
+
order: CommerceModel.HostOrder;
|
|
27
|
+
host: Record<string, unknown>;
|
|
28
|
+
store: Record<string, unknown> | null;
|
|
29
|
+
owner: {
|
|
30
|
+
id?: string;
|
|
31
|
+
org?: unknown;
|
|
32
|
+
} | null;
|
|
33
|
+
to: string;
|
|
34
|
+
/** The subject as typed; empty takes the default. */
|
|
35
|
+
subject: string;
|
|
36
|
+
message: string;
|
|
37
|
+
payUrl: string;
|
|
38
|
+
proofUrl: (proofId: string) => string | null;
|
|
39
|
+
linkReplaced: boolean;
|
|
40
|
+
/** The sending member's email: the Reply-To when the site has no contact email. */
|
|
41
|
+
senderEmail: string | null;
|
|
42
|
+
atMs: number;
|
|
43
|
+
}
|
|
44
|
+
export type QuoteDeliveryResult = {
|
|
45
|
+
sent: true;
|
|
46
|
+
messageId: string;
|
|
47
|
+
providerId: string | null;
|
|
48
|
+
subject: string;
|
|
49
|
+
replyTo: string | null;
|
|
50
|
+
thread: CommerceModel.QuoteEmailThread;
|
|
51
|
+
} | {
|
|
52
|
+
sent: false;
|
|
53
|
+
status: number;
|
|
54
|
+
error: string;
|
|
55
|
+
};
|
|
56
|
+
/** The calls the send makes, injectable for specs. */
|
|
57
|
+
export interface QuoteMailerDeps {
|
|
58
|
+
configured(): boolean;
|
|
59
|
+
sendingIdentity(hostId: string): Promise<SendingIdentityVerdict>;
|
|
60
|
+
render(hostId: string, key: string, tokens: Record<string, string>): Promise<RenderedHostEmail | null>;
|
|
61
|
+
/** The site's email theme, for the colors the composed proof markup draws with. */
|
|
62
|
+
theme(hostId: string): Promise<EmailTheme | null>;
|
|
63
|
+
/** The quote's PDF; loads the PDF writer and sharp only here. */
|
|
64
|
+
quotePdf(input: Parameters<typeof quotePdfFile>[0]): Promise<CommercePdfFile>;
|
|
65
|
+
send(options: SendEmailOptions): Promise<SendEmailResult>;
|
|
66
|
+
meter(hostId: string): Promise<void>;
|
|
67
|
+
/** The bounce and complaint list only: a quote is mail the customer asked for. */
|
|
68
|
+
suppression(email: string): Promise<'suppressed-platform' | null>;
|
|
69
|
+
}
|
|
70
|
+
export declare function defaultQuoteMailerDeps(): QuoteMailerDeps;
|
|
71
|
+
/** What the console's Send quote reaches: whether mail can go, whether this address may be mailed, and the send. */
|
|
72
|
+
export interface QuoteMailer {
|
|
73
|
+
configured(): boolean;
|
|
74
|
+
suppression(email: string): Promise<'suppressed-platform' | null>;
|
|
75
|
+
deliver(input: QuoteDeliveryInput): Promise<QuoteDeliveryResult>;
|
|
76
|
+
}
|
|
77
|
+
/** The business's name as its documents print it. */
|
|
78
|
+
export declare function quoteBusinessName(host: Record<string, unknown>): string;
|
|
79
|
+
/**
|
|
80
|
+
* Who a reply reaches: the site's contact email, else the person sending, as
|
|
81
|
+
* an inbox reply answers to them. Null only when neither is an address.
|
|
82
|
+
*/
|
|
83
|
+
export declare function quoteReplyTo(host: Record<string, unknown>, senderEmail: string | null | undefined): string | null;
|
|
84
|
+
/** The quote email, said and sent. */
|
|
85
|
+
export declare function quoteMailer(deps?: QuoteMailerDeps): QuoteMailer;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { siteContactEmail, resolveHostToken } from "@aglyn/aglyn/app-utils/host-tokens";
|
|
18
|
+
import { resolveBrandingProfile } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
19
|
+
import { emailThreadAfterSend, isEmailConfigured, mintEmailMessageId, nextThreadHeaders, PLATFORM_EMAIL_PALETTE, readEmailThread, resolveEmailColor, sendEmail } from "@aglyn/shared-util-email";
|
|
20
|
+
import { firebaseAdmin, hostSendingIdentity, meterHostEmail, renderHostEmailWithTokens, siteEmailFrame, transactionalAddressSuppression } from "@aglyn/tenant-data-admin";
|
|
21
|
+
import * as CommerceModel from "../model/index.js";
|
|
22
|
+
import { QUOTE_EMAIL_KEY, quoteEmailContent } from "../utils/quote-email-content.js";
|
|
23
|
+
export function defaultQuoteMailerDeps() {
|
|
24
|
+
const firestore = ()=>firebaseAdmin.app().firestore();
|
|
25
|
+
return {
|
|
26
|
+
configured: ()=>isEmailConfigured(),
|
|
27
|
+
sendingIdentity: (hostId)=>hostSendingIdentity(hostId),
|
|
28
|
+
render: (hostId, key, tokens)=>renderHostEmailWithTokens(firestore(), hostId, key, tokens),
|
|
29
|
+
theme: async (hostId)=>(await siteEmailFrame(firestore(), hostId, QUOTE_EMAIL_KEY)).theme,
|
|
30
|
+
quotePdf: async (input)=>(await import("./commerce-pdf.js")).quotePdfFile(input),
|
|
31
|
+
send: (options)=>sendEmail(options),
|
|
32
|
+
meter: async (hostId)=>{
|
|
33
|
+
await meterHostEmail(hostId);
|
|
34
|
+
},
|
|
35
|
+
suppression: (email)=>transactionalAddressSuppression(email)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
39
|
+
/** The business's name as its documents print it. */ export function quoteBusinessName(host) {
|
|
40
|
+
var _host_name;
|
|
41
|
+
const named = resolveHostToken('businessName', host);
|
|
42
|
+
return String(named != null ? named : '').trim() || String((_host_name = host['name']) != null ? _host_name : '').trim();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Who a reply reaches: the site's contact email, else the person sending, as
|
|
46
|
+
* an inbox reply answers to them. Null only when neither is an address.
|
|
47
|
+
*/ export function quoteReplyTo(host, senderEmail) {
|
|
48
|
+
const contact = siteContactEmail(host);
|
|
49
|
+
if (contact && EMAIL.test(contact)) return contact;
|
|
50
|
+
const sender = String(senderEmail != null ? senderEmail : '').trim();
|
|
51
|
+
return EMAIL.test(sender) ? sender : null;
|
|
52
|
+
}
|
|
53
|
+
/** The theme's colors for the composed proof markup, the platform's where the site's cannot answer. */ function proofColors(theme) {
|
|
54
|
+
const read = (token)=>{
|
|
55
|
+
var _ref, _resolveEmailColor;
|
|
56
|
+
return (_ref = (_resolveEmailColor = resolveEmailColor(token, theme == null ? void 0 : theme.palette)) != null ? _resolveEmailColor : resolveEmailColor(token, PLATFORM_EMAIL_PALETTE)) != null ? _ref : 'inherit';
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
border: read('divider'),
|
|
60
|
+
muted: read('text.secondary')
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** A refused send in the words the console shows. */ function refusalOf(result) {
|
|
64
|
+
switch(result.reason){
|
|
65
|
+
case 'unconfigured':
|
|
66
|
+
return {
|
|
67
|
+
status: 503,
|
|
68
|
+
error: 'Email isn’t set up here, so the quote wasn’t sent.'
|
|
69
|
+
};
|
|
70
|
+
case 'rate-limited':
|
|
71
|
+
return {
|
|
72
|
+
status: 429,
|
|
73
|
+
error: 'Too many emails are going out right now. Try again in a minute.'
|
|
74
|
+
};
|
|
75
|
+
case 'suppressed':
|
|
76
|
+
case 'undeliverable':
|
|
77
|
+
return {
|
|
78
|
+
status: 409,
|
|
79
|
+
error: 'This address can’t receive email, so the quote wasn’t sent. Copy the pay link and send it another way.'
|
|
80
|
+
};
|
|
81
|
+
case 'unverified-domain':
|
|
82
|
+
return {
|
|
83
|
+
status: 409,
|
|
84
|
+
error: 'This site’s sending domain isn’t verified, so the quote wasn’t sent. Check Email settings.'
|
|
85
|
+
};
|
|
86
|
+
case 'suspended':
|
|
87
|
+
return {
|
|
88
|
+
status: 423,
|
|
89
|
+
error: 'This site can’t send email right now, so the quote wasn’t sent.'
|
|
90
|
+
};
|
|
91
|
+
case 'held-for-review':
|
|
92
|
+
return {
|
|
93
|
+
status: 409,
|
|
94
|
+
error: `The email was held for review, so it hasn’t gone out yet.${result.detail ? ` ${result.detail}` : ''}`
|
|
95
|
+
};
|
|
96
|
+
default:
|
|
97
|
+
return {
|
|
98
|
+
status: 502,
|
|
99
|
+
error: 'The quote email could not be sent. Try again.'
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** The quote email, said and sent. */ export function quoteMailer(deps = defaultQuoteMailerDeps()) {
|
|
104
|
+
return {
|
|
105
|
+
configured: ()=>deps.configured(),
|
|
106
|
+
suppression: (email)=>deps.suppression(email),
|
|
107
|
+
deliver: async (input)=>{
|
|
108
|
+
var _ref, _host_cname, _result_id;
|
|
109
|
+
var _input_store, _order_quote, _input_owner;
|
|
110
|
+
const { hostId, quoteId, order, host } = input;
|
|
111
|
+
const businessName = quoteBusinessName(host);
|
|
112
|
+
const subject = input.subject || CommerceModel.quoteEmailDefaultSubject(order, businessName, quoteId);
|
|
113
|
+
const [identity, theme] = await Promise.all([
|
|
114
|
+
deps.sendingIdentity(hostId),
|
|
115
|
+
deps.theme(hostId).catch(()=>null)
|
|
116
|
+
]);
|
|
117
|
+
const content = quoteEmailContent({
|
|
118
|
+
order,
|
|
119
|
+
quoteId,
|
|
120
|
+
businessName,
|
|
121
|
+
currency: String((_ref = (_input_store = input.store) == null ? void 0 : _input_store['currency']) != null ? _ref : 'USD'),
|
|
122
|
+
subject,
|
|
123
|
+
message: input.message,
|
|
124
|
+
payUrl: input.payUrl,
|
|
125
|
+
proofUrl: input.proofUrl,
|
|
126
|
+
linkReplaced: input.linkReplaced,
|
|
127
|
+
colors: proofColors(theme),
|
|
128
|
+
radius: theme == null ? void 0 : theme.buttonRadius
|
|
129
|
+
});
|
|
130
|
+
// The PDF is what the email says is attached; without it nothing goes.
|
|
131
|
+
let pdf;
|
|
132
|
+
try {
|
|
133
|
+
pdf = await deps.quotePdf({
|
|
134
|
+
hostId,
|
|
135
|
+
quoteId,
|
|
136
|
+
order,
|
|
137
|
+
host,
|
|
138
|
+
store: input.store,
|
|
139
|
+
owner: input.owner,
|
|
140
|
+
payUrl: input.payUrl
|
|
141
|
+
});
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error('[commerce/quote-email] the PDF could not be made', {
|
|
144
|
+
hostId,
|
|
145
|
+
quoteId,
|
|
146
|
+
error
|
|
147
|
+
});
|
|
148
|
+
return {
|
|
149
|
+
sent: false,
|
|
150
|
+
status: 500,
|
|
151
|
+
error: 'The quote’s PDF could not be made, so nothing was sent. Try again.'
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const rendered = await deps.render(hostId, QUOTE_EMAIL_KEY, content.tokens);
|
|
155
|
+
const thread = readEmailThread((_order_quote = order.quote) == null ? void 0 : _order_quote.emailThread);
|
|
156
|
+
const siteHost = String((_host_cname = host['cname']) != null ? _host_cname : '').trim() || null;
|
|
157
|
+
const messageId = mintEmailMessageId([
|
|
158
|
+
identity == null ? void 0 : identity.domain,
|
|
159
|
+
identity == null ? void 0 : identity.from,
|
|
160
|
+
siteHost
|
|
161
|
+
], {
|
|
162
|
+
label: 'quote',
|
|
163
|
+
atMs: input.atMs
|
|
164
|
+
});
|
|
165
|
+
const replyTo = quoteReplyTo(host, input.senderEmail);
|
|
166
|
+
const result = await deps.send(_extends({
|
|
167
|
+
to: input.to,
|
|
168
|
+
// The subject as sent, whatever a design's own says: it is what keeps
|
|
169
|
+
// the conversation together, and it was typed for this send.
|
|
170
|
+
subject,
|
|
171
|
+
text: (rendered == null ? void 0 : rendered.text) || content.text
|
|
172
|
+
}, (rendered == null ? void 0 : rendered.html) ? {
|
|
173
|
+
html: rendered.html
|
|
174
|
+
} : {}, {
|
|
175
|
+
headers: nextThreadHeaders(thread, messageId)
|
|
176
|
+
}, replyTo ? {
|
|
177
|
+
replyTo
|
|
178
|
+
} : {}, {
|
|
179
|
+
fromName: resolveBrandingProfile((_input_owner = input.owner) == null ? void 0 : _input_owner.org).fromName,
|
|
180
|
+
sendingIdentity: identity,
|
|
181
|
+
audience: 'tenant',
|
|
182
|
+
context: 'quote',
|
|
183
|
+
attachments: [
|
|
184
|
+
{
|
|
185
|
+
filename: pdf.filename,
|
|
186
|
+
contentType: 'application/pdf',
|
|
187
|
+
content: Buffer.from(pdf.bytes).toString('base64')
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
}));
|
|
191
|
+
if (!result.sent) return _extends({
|
|
192
|
+
sent: false
|
|
193
|
+
}, refusalOf(result));
|
|
194
|
+
// Cost meter (AGL-1438), as every transactional site email.
|
|
195
|
+
await deps.meter(hostId).catch((error)=>console.error('[commerce/quote-email] not metered', error));
|
|
196
|
+
return {
|
|
197
|
+
sent: true,
|
|
198
|
+
messageId,
|
|
199
|
+
providerId: (_result_id = result.id) != null ? _result_id : null,
|
|
200
|
+
subject,
|
|
201
|
+
replyTo,
|
|
202
|
+
thread: emailThreadAfterSend(thread, {
|
|
203
|
+
messageId,
|
|
204
|
+
subject
|
|
205
|
+
})
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
//# sourceMappingURL=quote-email.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/quote-email.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { siteContactEmail, resolveHostToken, type HostTokenSource } from '@aglyn/aglyn/app-utils/host-tokens'\nimport { resolveBrandingProfile } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n emailThreadAfterSend,\n isEmailConfigured,\n mintEmailMessageId,\n nextThreadHeaders,\n PLATFORM_EMAIL_PALETTE,\n readEmailThread,\n resolveEmailColor,\n sendEmail,\n type EmailTheme,\n type RenderedHostEmail,\n type SendEmailOptions,\n type SendEmailResult,\n type SendingIdentityVerdict,\n} from '@aglyn/shared-util-email'\nimport {\n firebaseAdmin,\n hostSendingIdentity,\n meterHostEmail,\n renderHostEmailWithTokens,\n siteEmailFrame,\n transactionalAddressSuppression,\n} from '@aglyn/tenant-data-admin'\nimport * as CommerceModel from '../model'\nimport { QUOTE_EMAIL_KEY, quoteEmailContent, type QuoteEmailColors } from '../utils/quote-email-content'\nimport type { quotePdfFile } from './commerce-pdf'\nimport type { CommercePdfFile } from './pdf-response'\n\n/*==========================================\n * SENDING THE QUOTE EMAIL (AGL-4097). Loaded only by the console's Send\n * quote (`POST commerce/quote { action: 'send' }`), so the renderer, the PDF\n * writer and sharp stay out of every other request.\n *\n * One email, the `quote-sent` catalog key in the site's theme and its header\n * and footer (or the site's own design of it), with the quote's PDF attached:\n *\n * - FROM the site's sending identity, under the site's name;\n * - REPLY-TO the site's contact email (its listing's, else its support\n * email), else the email of the person sending, so a reply reaches the\n * business either way, as an inbox reply's does;\n * - IN THE CUSTOMER'S CONVERSATION: a `Message-ID` minted on the sending\n * domain, and, when the quote already has a conversation (an earlier\n * send, or a request's confirmation), `In-Reply-To` and `References`\n * from it (`nextThreadHeaders`). The provider's own id is returned beside\n * ours, because whether a provider keeps a caller's `Message-ID` is its\n * own call, and a replaced one can then be traced;\n * - METERED as a site's transactional email;\n * - REFUSED only for an address that bounced for good or reported mail as\n * spam (`transactionalAddressSuppression`): the customer asked for the\n * quote, so an unsubscribe from the site's newsletter does not stop it,\n * as it does not stop a receipt (AGL-4098).\n *\n * The route decides everything about the quote (who may send, the link, the\n * stage); this module only says the email and sends it.\n *=========================================*/\n\n/** What the route hands over for one send. */\nexport interface QuoteDeliveryInput {\n hostId: string\n quoteId: string\n /** The quote as it stands, after any pay link replacement this send made. */\n order: CommerceModel.HostOrder\n host: Record<string, unknown>\n store: Record<string, unknown> | null\n owner: { id?: string; org?: unknown } | null\n to: string\n /** The subject as typed; empty takes the default. */\n subject: string\n message: string\n payUrl: string\n proofUrl: (proofId: string) => string | null\n linkReplaced: boolean\n /** The sending member's email: the Reply-To when the site has no contact email. */\n senderEmail: string | null\n atMs: number\n}\n\nexport type QuoteDeliveryResult =\n | {\n sent: true\n messageId: string\n providerId: string | null\n subject: string\n replyTo: string | null\n thread: CommerceModel.QuoteEmailThread\n }\n | { sent: false; status: number; error: string }\n\n/** The calls the send makes, injectable for specs. */\nexport interface QuoteMailerDeps {\n configured(): boolean\n sendingIdentity(hostId: string): Promise<SendingIdentityVerdict>\n render(hostId: string, key: string, tokens: Record<string, string>): Promise<RenderedHostEmail | null>\n /** The site's email theme, for the colors the composed proof markup draws with. */\n theme(hostId: string): Promise<EmailTheme | null>\n /** The quote's PDF; loads the PDF writer and sharp only here. */\n quotePdf(input: Parameters<typeof quotePdfFile>[0]): Promise<CommercePdfFile>\n send(options: SendEmailOptions): Promise<SendEmailResult>\n meter(hostId: string): Promise<void>\n /** The bounce and complaint list only: a quote is mail the customer asked for. */\n suppression(email: string): Promise<'suppressed-platform' | null>\n}\n\nexport function defaultQuoteMailerDeps(): QuoteMailerDeps {\n const firestore = () => firebaseAdmin.app().firestore()\n return {\n configured: () => isEmailConfigured(),\n sendingIdentity: (hostId) => hostSendingIdentity(hostId),\n render: (hostId, key, tokens) => renderHostEmailWithTokens(firestore(), hostId, key, tokens),\n theme: async (hostId) => (await siteEmailFrame(firestore(), hostId, QUOTE_EMAIL_KEY)).theme,\n quotePdf: async (input) => (await import('./commerce-pdf')).quotePdfFile(input),\n send: (options) => sendEmail(options),\n meter: async (hostId) => {\n await meterHostEmail(hostId)\n },\n suppression: (email) => transactionalAddressSuppression(email),\n }\n}\n\n/** What the console's Send quote reaches: whether mail can go, whether this address may be mailed, and the send. */\nexport interface QuoteMailer {\n configured(): boolean\n suppression(email: string): Promise<'suppressed-platform' | null>\n deliver(input: QuoteDeliveryInput): Promise<QuoteDeliveryResult>\n}\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/** The business's name as its documents print it. */\nexport function quoteBusinessName(host: Record<string, unknown>): string {\n const named = resolveHostToken('businessName', host as HostTokenSource)\n return String(named ?? '').trim() || String(host['name'] ?? '').trim()\n}\n\n/**\n * Who a reply reaches: the site's contact email, else the person sending, as\n * an inbox reply answers to them. Null only when neither is an address.\n */\nexport function quoteReplyTo(host: Record<string, unknown>, senderEmail: string | null | undefined): string | null {\n const contact = siteContactEmail(host as never)\n if (contact && EMAIL.test(contact)) return contact\n const sender = String(senderEmail ?? '').trim()\n return EMAIL.test(sender) ? sender : null\n}\n\n/** The theme's colors for the composed proof markup, the platform's where the site's cannot answer. */\nfunction proofColors(theme: EmailTheme | null): QuoteEmailColors {\n const read = (token: string) =>\n resolveEmailColor(token, theme?.palette) ?? resolveEmailColor(token, PLATFORM_EMAIL_PALETTE) ?? 'inherit'\n return { border: read('divider'), muted: read('text.secondary') }\n}\n\n/** A refused send in the words the console shows. */\nfunction refusalOf(result: Extract<SendEmailResult, { sent: false }>): { status: number; error: string } {\n switch (result.reason) {\n case 'unconfigured':\n return { status: 503, error: 'Email isn’t set up here, so the quote wasn’t sent.' }\n case 'rate-limited':\n return { status: 429, error: 'Too many emails are going out right now. Try again in a minute.' }\n case 'suppressed':\n case 'undeliverable':\n return { status: 409, error: 'This address can’t receive email, so the quote wasn’t sent. Copy the pay link and send it another way.' }\n case 'unverified-domain':\n return { status: 409, error: 'This site’s sending domain isn’t verified, so the quote wasn’t sent. Check Email settings.' }\n case 'suspended':\n return { status: 423, error: 'This site can’t send email right now, so the quote wasn’t sent.' }\n case 'held-for-review':\n return { status: 409, error: `The email was held for review, so it hasn’t gone out yet.${result.detail ? ` ${result.detail}` : ''}` }\n default:\n return { status: 502, error: 'The quote email could not be sent. Try again.' }\n }\n}\n\n/** The quote email, said and sent. */\nexport function quoteMailer(deps: QuoteMailerDeps = defaultQuoteMailerDeps()): QuoteMailer {\n return {\n configured: () => deps.configured(),\n suppression: (email) => deps.suppression(email),\n deliver: async (input) => {\n const { hostId, quoteId, order, host } = input\n const businessName = quoteBusinessName(host)\n const subject = input.subject || CommerceModel.quoteEmailDefaultSubject(order, businessName, quoteId)\n const [identity, theme] = await Promise.all([\n deps.sendingIdentity(hostId),\n deps.theme(hostId).catch(() => null),\n ])\n\n const content = quoteEmailContent({\n order,\n quoteId,\n businessName,\n currency: String(input.store?.['currency'] ?? 'USD'),\n subject,\n message: input.message,\n payUrl: input.payUrl,\n proofUrl: input.proofUrl,\n linkReplaced: input.linkReplaced,\n colors: proofColors(theme),\n radius: theme?.buttonRadius,\n })\n\n // The PDF is what the email says is attached; without it nothing goes.\n let pdf: CommercePdfFile\n try {\n pdf = await deps.quotePdf({\n hostId,\n quoteId,\n order,\n host,\n store: input.store,\n owner: input.owner,\n payUrl: input.payUrl,\n })\n } catch (error) {\n console.error('[commerce/quote-email] the PDF could not be made', { hostId, quoteId, error })\n return { sent: false, status: 500, error: 'The quote’s PDF could not be made, so nothing was sent. Try again.' }\n }\n\n const rendered = await deps.render(hostId, QUOTE_EMAIL_KEY, content.tokens)\n const thread = readEmailThread(order.quote?.emailThread)\n const siteHost = String(host['cname'] ?? '').trim() || null\n const messageId = mintEmailMessageId([identity?.domain, identity?.from, siteHost], {\n label: 'quote',\n atMs: input.atMs,\n })\n const replyTo = quoteReplyTo(host, input.senderEmail)\n\n const result = await deps.send({\n to: input.to,\n // The subject as sent, whatever a design's own says: it is what keeps\n // the conversation together, and it was typed for this send.\n subject,\n text: rendered?.text || content.text,\n ...(rendered?.html ? { html: rendered.html } : {}),\n headers: nextThreadHeaders(thread, messageId),\n ...(replyTo ? { replyTo } : {}),\n fromName: resolveBrandingProfile(input.owner?.org as never).fromName,\n sendingIdentity: identity,\n audience: 'tenant',\n context: 'quote',\n attachments: [\n {\n filename: pdf.filename,\n contentType: 'application/pdf',\n content: Buffer.from(pdf.bytes).toString('base64'),\n },\n ],\n })\n if (!result.sent) return { sent: false, ...refusalOf(result as Extract<SendEmailResult, { sent: false }>) }\n // Cost meter (AGL-1438), as every transactional site email.\n await deps.meter(hostId).catch((error) => console.error('[commerce/quote-email] not metered', error))\n return {\n sent: true,\n messageId,\n providerId: (result as Extract<SendEmailResult, { sent: true }>).id ?? null,\n subject,\n replyTo,\n thread: emailThreadAfterSend(thread, { messageId, subject }),\n }\n },\n }\n}\n"],"names":["siteContactEmail","resolveHostToken","resolveBrandingProfile","emailThreadAfterSend","isEmailConfigured","mintEmailMessageId","nextThreadHeaders","PLATFORM_EMAIL_PALETTE","readEmailThread","resolveEmailColor","sendEmail","firebaseAdmin","hostSendingIdentity","meterHostEmail","renderHostEmailWithTokens","siteEmailFrame","transactionalAddressSuppression","CommerceModel","QUOTE_EMAIL_KEY","quoteEmailContent","defaultQuoteMailerDeps","firestore","app","configured","sendingIdentity","hostId","render","key","tokens","theme","quotePdf","input","quotePdfFile","send","options","meter","suppression","email","EMAIL","quoteBusinessName","host","named","String","trim","quoteReplyTo","senderEmail","contact","test","sender","proofColors","read","token","palette","border","muted","refusalOf","result","reason","status","error","detail","quoteMailer","deps","deliver","order","quoteId","businessName","subject","quoteEmailDefaultSubject","identity","Promise","all","catch","content","currency","store","message","payUrl","proofUrl","linkReplaced","colors","radius","buttonRadius","pdf","owner","console","sent","rendered","thread","quote","emailThread","siteHost","messageId","domain","from","label","atMs","replyTo","to","text","html","headers","fromName","org","audience","context","attachments","filename","contentType","Buffer","bytes","toString","providerId","id"],"mappings":"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"}
|
|
@@ -37,3 +37,14 @@ export declare function quotePayLinkUrl(host: {
|
|
|
37
37
|
cname?: unknown;
|
|
38
38
|
subdomain?: unknown;
|
|
39
39
|
} | null | undefined, hostId: string, quoteId: string, version: number): string | null;
|
|
40
|
+
/**
|
|
41
|
+
* The absolute address of one of the current revision's proofs, behind the
|
|
42
|
+
* same signed link as the page (`GET /api/commerce/quote-proof`, AGL-4096),
|
|
43
|
+
* or null when the site has no public address or no signing secret. What the
|
|
44
|
+
* quote email draws a proof image from (AGL-4097); a link the business
|
|
45
|
+
* replaces stops serving it, with the page.
|
|
46
|
+
*/
|
|
47
|
+
export declare function quoteProofLinkUrl(host: {
|
|
48
|
+
cname?: unknown;
|
|
49
|
+
subdomain?: unknown;
|
|
50
|
+
} | null | undefined, hostId: string, quoteId: string, version: number, proofId: string): string | null;
|
|
@@ -93,5 +93,26 @@ function signQuoteLink(hostId, quoteId, version) {
|
|
|
93
93
|
if (!origin || !token) return null;
|
|
94
94
|
return `${origin}${quotePageHref(quoteId, token)}`;
|
|
95
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* The absolute address of one of the current revision's proofs, behind the
|
|
98
|
+
* same signed link as the page (`GET /api/commerce/quote-proof`, AGL-4096),
|
|
99
|
+
* or null when the site has no public address or no signing secret. What the
|
|
100
|
+
* quote email draws a proof image from (AGL-4097); a link the business
|
|
101
|
+
* replaces stops serving it, with the page.
|
|
102
|
+
*/ export function quoteProofLinkUrl(host, hostId, quoteId, version, proofId) {
|
|
103
|
+
const origin = hostPublicOrigin({
|
|
104
|
+
cname: typeof (host == null ? void 0 : host.cname) === 'string' ? host.cname : null,
|
|
105
|
+
subdomain: typeof (host == null ? void 0 : host.subdomain) === 'string' ? host.subdomain : null
|
|
106
|
+
});
|
|
107
|
+
const token = mintQuoteLinkToken(hostId, quoteId, version);
|
|
108
|
+
if (!origin || !token || !proofId) return null;
|
|
109
|
+
const query = new URLSearchParams({
|
|
110
|
+
hostId,
|
|
111
|
+
q: quoteId,
|
|
112
|
+
t: token,
|
|
113
|
+
file: proofId
|
|
114
|
+
});
|
|
115
|
+
return `${origin}/api/commerce/quote-proof?${query.toString()}`;
|
|
116
|
+
}
|
|
96
117
|
|
|
97
118
|
//# sourceMappingURL=quote-link-token.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/quote-link-token.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { hostPublicOrigin } from '@aglyn/aglyn/server'\nimport { createHmac, timingSafeEqual } from 'crypto'\nimport { quotePageHref } from '../constants/quote-page'\nimport { tokenSigningSecret } from './download'\n\n/**\n * A quote's pay link (AGL-4094): `/pay-quote?q=<quoteId>&t=<token>` on the\n * business's own site, where the customer sees the quote and pays it.\n *\n * The token is `<version>.<signature>`: the quote's link version\n * (`quote.linkVersion`, absent reading as 1) and the first 32 hex characters of\n * `HMAC(quote-link:hostId:quoteId:version)` under the commerce\n * `TOKEN_SIGNING_SECRET`. A pure function of what the quote already holds, so\n * nothing is stored per link and every message that carried the link carries\n * the same working one. The `quote-link:` prefix keeps it from being replayed\n * as an order-status, download or supplier token, which sign other payloads\n * under the same secret.\n *\n * NO EXPIRY. A quote is paid when the customer is ready, and a link that died\n * first would be a quote they could not pay. The version is what retires a\n * link: replacing it bumps the version on the quote, and a link signed for an\n * older version still verifies, so its page can say the quote was replaced,\n * without showing any of it.\n */\nconst SIGNATURE_LENGTH = 32\nconst MAX_VERSION = 1_000_000\n\nfunction signQuoteLink(hostId: string, quoteId: string, version: number): string {\n return createHmac('sha256', tokenSigningSecret())\n .update(`quote-link:${hostId}:${quoteId}:${version}`)\n .digest('hex')\n .slice(0, SIGNATURE_LENGTH)\n}\n\n/** The token's two halves, or null for a token not shaped like one. */\nexport function parseQuoteLinkToken(token: string): { version: number; signature: string } | null {\n const match = /^([1-9]\\d{0,6})\\.([0-9a-f]{32})$/.exec(String(token ?? ''))\n if (!match) return null\n const version = Number(match[1])\n if (!Number.isSafeInteger(version) || version > MAX_VERSION) return null\n return { version, signature: match[2] }\n}\n\n/** The token for one version of a quote's link, or null when the signing secret is unset. */\nexport function mintQuoteLinkToken(hostId: string, quoteId: string, version: number): string | null {\n if (!hostId || !quoteId || !Number.isSafeInteger(version) || version < 1 || version > MAX_VERSION) {\n return null\n }\n try {\n return `${version}.${signQuoteLink(hostId, quoteId, version)}`\n } catch {\n return null\n }\n}\n\n/**\n * Checks a presented token in constant time and answers the link version it\n * was signed for, or null for a token that is wrong, malformed or unverifiable\n * (no secret). The caller compares the version with the quote's own.\n */\nexport function verifyQuoteLinkToken(hostId: string, quoteId: string, token: string): number | null {\n if (!hostId || !quoteId) return null\n const parsed = parseQuoteLinkToken(token)\n if (!parsed) return null\n let expected: string\n try {\n expected = signQuoteLink(hostId, quoteId, parsed.version)\n } catch {\n return null\n }\n const a = Buffer.from(parsed.signature)\n const b = Buffer.from(expected)\n if (a.length !== b.length) return null\n return timingSafeEqual(new Uint8Array(a), new Uint8Array(b)) ? parsed.version : null\n}\n\n/**\n * The absolute pay link for a quote at a link version, or null when either\n * half is missing: a site with no public address, or no signing secret. What\n * the console copies, and what a quote or invoice email puts in front of the\n * customer.\n */\nexport function quotePayLinkUrl(\n host: { cname?: unknown; subdomain?: unknown } | null | undefined,\n hostId: string,\n quoteId: string,\n version: number,\n): string | null {\n const origin = hostPublicOrigin({\n cname: typeof host?.cname === 'string' ? host.cname : null,\n subdomain: typeof host?.subdomain === 'string' ? host.subdomain : null,\n })\n const token = mintQuoteLinkToken(hostId, quoteId, version)\n if (!origin || !token) return null\n return `${origin}${quotePageHref(quoteId, token)}`\n}\n"],"names":["hostPublicOrigin","createHmac","timingSafeEqual","quotePageHref","tokenSigningSecret","SIGNATURE_LENGTH","MAX_VERSION","signQuoteLink","hostId","quoteId","version","update","digest","slice","parseQuoteLinkToken","token","match","exec","String","Number","isSafeInteger","signature","mintQuoteLinkToken","verifyQuoteLinkToken","parsed","expected","a","Buffer","from","b","length","Uint8Array","quotePayLinkUrl","host","origin","cname","subdomain"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gBAAgB,QAAQ,sBAAqB;AACtD,SAASC,UAAU,EAAEC,eAAe,QAAQ,SAAQ;AACpD,SAASC,aAAa,QAAQ,6BAAyB;AACvD,SAASC,kBAAkB,QAAQ,gBAAY;AAE/C;;;;;;;;;;;;;;;;;;CAkBC,GACD,MAAMC,mBAAmB;AACzB,MAAMC,cAAc;AAEpB,SAASC,cAAcC,MAAc,EAAEC,OAAe,EAAEC,OAAe;IACrE,OAAOT,WAAW,UAAUG,sBACzBO,MAAM,CAAC,CAAC,WAAW,EAAEH,OAAO,CAAC,EAAEC,QAAQ,CAAC,EAAEC,SAAS,EACnDE,MAAM,CAAC,OACPC,KAAK,CAAC,GAAGR;AACd;AAEA,qEAAqE,GACrE,OAAO,SAASS,oBAAoBC,KAAa;IAC/C,MAAMC,QAAQ,mCAAmCC,IAAI,CAACC,OAAOH,gBAAAA,QAAS;IACtE,IAAI,CAACC,OAAO,OAAO;IACnB,MAAMN,UAAUS,OAAOH,KAAK,CAAC,EAAE;IAC/B,IAAI,CAACG,OAAOC,aAAa,CAACV,YAAYA,UAAUJ,aAAa,OAAO;IACpE,OAAO;QAAEI;QAASW,WAAWL,KAAK,CAAC,EAAE;IAAC;AACxC;AAEA,2FAA2F,GAC3F,OAAO,SAASM,mBAAmBd,MAAc,EAAEC,OAAe,EAAEC,OAAe;IACjF,IAAI,CAACF,UAAU,CAACC,WAAW,CAACU,OAAOC,aAAa,CAACV,YAAYA,UAAU,KAAKA,UAAUJ,aAAa;QACjG,OAAO;IACT;IACA,IAAI;QACF,OAAO,GAAGI,QAAQ,CAAC,EAAEH,cAAcC,QAAQC,SAASC,UAAU;IAChE,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASa,qBAAqBf,MAAc,EAAEC,OAAe,EAAEM,KAAa;IACjF,IAAI,CAACP,UAAU,CAACC,SAAS,OAAO;IAChC,MAAMe,SAASV,oBAAoBC;IACnC,IAAI,CAACS,QAAQ,OAAO;IACpB,IAAIC;IACJ,IAAI;QACFA,WAAWlB,cAAcC,QAAQC,SAASe,OAAOd,OAAO;IAC1D,EAAE,eAAM;QACN,OAAO;IACT;IACA,MAAMgB,IAAIC,OAAOC,IAAI,CAACJ,OAAOH,SAAS;IACtC,MAAMQ,IAAIF,OAAOC,IAAI,CAACH;IACtB,IAAIC,EAAEI,MAAM,KAAKD,EAAEC,MAAM,EAAE,OAAO;IAClC,OAAO5B,gBAAgB,IAAI6B,WAAWL,IAAI,IAAIK,WAAWF,MAAML,OAAOd,OAAO,GAAG;AAClF;AAEA;;;;;CAKC,GACD,OAAO,SAASsB,gBACdC,IAAiE,EACjEzB,MAAc,EACdC,OAAe,EACfC,OAAe;IAEf,MAAMwB,SAASlC,iBAAiB;QAC9BmC,OAAO,QAAOF,wBAAAA,KAAME,KAAK,MAAK,WAAWF,KAAKE,KAAK,GAAG;QACtDC,WAAW,QAAOH,wBAAAA,KAAMG,SAAS,MAAK,WAAWH,KAAKG,SAAS,GAAG;IACpE;IACA,MAAMrB,QAAQO,mBAAmBd,QAAQC,SAASC;IAClD,IAAI,CAACwB,UAAU,CAACnB,OAAO,OAAO;IAC9B,OAAO,GAAGmB,SAAS/B,cAAcM,SAASM,QAAQ;AACpD"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/quote-link-token.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { hostPublicOrigin } from '@aglyn/aglyn/server'\nimport { createHmac, timingSafeEqual } from 'crypto'\nimport { quotePageHref } from '../constants/quote-page'\nimport { tokenSigningSecret } from './download'\n\n/**\n * A quote's pay link (AGL-4094): `/pay-quote?q=<quoteId>&t=<token>` on the\n * business's own site, where the customer sees the quote and pays it.\n *\n * The token is `<version>.<signature>`: the quote's link version\n * (`quote.linkVersion`, absent reading as 1) and the first 32 hex characters of\n * `HMAC(quote-link:hostId:quoteId:version)` under the commerce\n * `TOKEN_SIGNING_SECRET`. A pure function of what the quote already holds, so\n * nothing is stored per link and every message that carried the link carries\n * the same working one. The `quote-link:` prefix keeps it from being replayed\n * as an order-status, download or supplier token, which sign other payloads\n * under the same secret.\n *\n * NO EXPIRY. A quote is paid when the customer is ready, and a link that died\n * first would be a quote they could not pay. The version is what retires a\n * link: replacing it bumps the version on the quote, and a link signed for an\n * older version still verifies, so its page can say the quote was replaced,\n * without showing any of it.\n */\nconst SIGNATURE_LENGTH = 32\nconst MAX_VERSION = 1_000_000\n\nfunction signQuoteLink(hostId: string, quoteId: string, version: number): string {\n return createHmac('sha256', tokenSigningSecret())\n .update(`quote-link:${hostId}:${quoteId}:${version}`)\n .digest('hex')\n .slice(0, SIGNATURE_LENGTH)\n}\n\n/** The token's two halves, or null for a token not shaped like one. */\nexport function parseQuoteLinkToken(token: string): { version: number; signature: string } | null {\n const match = /^([1-9]\\d{0,6})\\.([0-9a-f]{32})$/.exec(String(token ?? ''))\n if (!match) return null\n const version = Number(match[1])\n if (!Number.isSafeInteger(version) || version > MAX_VERSION) return null\n return { version, signature: match[2] }\n}\n\n/** The token for one version of a quote's link, or null when the signing secret is unset. */\nexport function mintQuoteLinkToken(hostId: string, quoteId: string, version: number): string | null {\n if (!hostId || !quoteId || !Number.isSafeInteger(version) || version < 1 || version > MAX_VERSION) {\n return null\n }\n try {\n return `${version}.${signQuoteLink(hostId, quoteId, version)}`\n } catch {\n return null\n }\n}\n\n/**\n * Checks a presented token in constant time and answers the link version it\n * was signed for, or null for a token that is wrong, malformed or unverifiable\n * (no secret). The caller compares the version with the quote's own.\n */\nexport function verifyQuoteLinkToken(hostId: string, quoteId: string, token: string): number | null {\n if (!hostId || !quoteId) return null\n const parsed = parseQuoteLinkToken(token)\n if (!parsed) return null\n let expected: string\n try {\n expected = signQuoteLink(hostId, quoteId, parsed.version)\n } catch {\n return null\n }\n const a = Buffer.from(parsed.signature)\n const b = Buffer.from(expected)\n if (a.length !== b.length) return null\n return timingSafeEqual(new Uint8Array(a), new Uint8Array(b)) ? parsed.version : null\n}\n\n/**\n * The absolute pay link for a quote at a link version, or null when either\n * half is missing: a site with no public address, or no signing secret. What\n * the console copies, and what a quote or invoice email puts in front of the\n * customer.\n */\nexport function quotePayLinkUrl(\n host: { cname?: unknown; subdomain?: unknown } | null | undefined,\n hostId: string,\n quoteId: string,\n version: number,\n): string | null {\n const origin = hostPublicOrigin({\n cname: typeof host?.cname === 'string' ? host.cname : null,\n subdomain: typeof host?.subdomain === 'string' ? host.subdomain : null,\n })\n const token = mintQuoteLinkToken(hostId, quoteId, version)\n if (!origin || !token) return null\n return `${origin}${quotePageHref(quoteId, token)}`\n}\n\n/**\n * The absolute address of one of the current revision's proofs, behind the\n * same signed link as the page (`GET /api/commerce/quote-proof`, AGL-4096),\n * or null when the site has no public address or no signing secret. What the\n * quote email draws a proof image from (AGL-4097); a link the business\n * replaces stops serving it, with the page.\n */\nexport function quoteProofLinkUrl(\n host: { cname?: unknown; subdomain?: unknown } | null | undefined,\n hostId: string,\n quoteId: string,\n version: number,\n proofId: string,\n): string | null {\n const origin = hostPublicOrigin({\n cname: typeof host?.cname === 'string' ? host.cname : null,\n subdomain: typeof host?.subdomain === 'string' ? host.subdomain : null,\n })\n const token = mintQuoteLinkToken(hostId, quoteId, version)\n if (!origin || !token || !proofId) return null\n const query = new URLSearchParams({ hostId, q: quoteId, t: token, file: proofId })\n return `${origin}/api/commerce/quote-proof?${query.toString()}`\n}\n"],"names":["hostPublicOrigin","createHmac","timingSafeEqual","quotePageHref","tokenSigningSecret","SIGNATURE_LENGTH","MAX_VERSION","signQuoteLink","hostId","quoteId","version","update","digest","slice","parseQuoteLinkToken","token","match","exec","String","Number","isSafeInteger","signature","mintQuoteLinkToken","verifyQuoteLinkToken","parsed","expected","a","Buffer","from","b","length","Uint8Array","quotePayLinkUrl","host","origin","cname","subdomain","quoteProofLinkUrl","proofId","query","URLSearchParams","q","t","file","toString"],"mappings":"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"}
|
|
@@ -69,12 +69,17 @@ export interface LoadedQuote {
|
|
|
69
69
|
order: CommerceModel.HostOrder;
|
|
70
70
|
storeName: string;
|
|
71
71
|
store: FirebaseFirestore.DocumentSnapshot;
|
|
72
|
+
/** A quote, or an order's invoice (only for a route that opens invoices). */
|
|
73
|
+
kind: CommerceModel.OrderPayLinkKind;
|
|
72
74
|
}
|
|
73
75
|
/**
|
|
74
76
|
* The quote a link names, after its token: the quote, the replaced answer for
|
|
75
77
|
* a link to an older version, or the one not-found answer for everything else.
|
|
78
|
+
* With `invoices`, a draft order's invoice too (AGL-4099).
|
|
76
79
|
*/
|
|
77
|
-
export declare function loadLinkedQuote(deps: QuotePayDeps, hostId: string, quoteId: string, token: string
|
|
80
|
+
export declare function loadLinkedQuote(deps: QuotePayDeps, hostId: string, quoteId: string, token: string, options?: {
|
|
81
|
+
invoices?: boolean;
|
|
82
|
+
}): Promise<{
|
|
78
83
|
loaded: LoadedQuote;
|
|
79
84
|
} | {
|
|
80
85
|
outcome: QuoteLinkOutcome;
|
|
@@ -88,12 +93,12 @@ export declare function quoteLinkParams(source: Record<string, unknown>): {
|
|
|
88
93
|
};
|
|
89
94
|
/** `GET /api/commerce/quote-view`: the quote page's data. */
|
|
90
95
|
export declare function handleQuoteView(deps: QuotePayDeps, req: PluginApiRequest): Promise<QuoteLinkOutcome>;
|
|
91
|
-
/** The Checkout Session a quote's Pay opens, as form params. Pure over its inputs. */
|
|
96
|
+
/** The Checkout Session a quote's (or an invoice's) Pay opens, as form params. Pure over its inputs. */
|
|
92
97
|
export declare function buildQuoteSessionParams(input: {
|
|
93
98
|
hostId: string;
|
|
94
99
|
quoteId: string;
|
|
95
100
|
order: CommerceModel.HostOrder;
|
|
96
|
-
charge: CommerceModel.QuoteCheckoutCharge
|
|
101
|
+
charge: CommerceModel.QuoteCheckoutCharge & Partial<Pick<CommerceModel.InvoiceCheckoutCharge, 'shippingOptions' | 'shippingCountries'>>;
|
|
97
102
|
accountId: string;
|
|
98
103
|
feeCents: number;
|
|
99
104
|
taxRateId: string;
|