@open-mercato/checkout 0.6.5-develop.4882.1.901c3aa813 → 0.6.5-develop.5033.1.c970204a3f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/checkout/__integration__/TC-CHKT-008.spec.js +2 -0
- package/dist/modules/checkout/__integration__/TC-CHKT-008.spec.js.map +2 -2
- package/dist/modules/checkout/lib/utils.js +13 -3
- package/dist/modules/checkout/lib/utils.js.map +2 -2
- package/package.json +5 -5
- package/src/modules/checkout/__integration__/TC-CHKT-008.spec.ts +4 -0
- package/src/modules/checkout/lib/__tests__/utils.test.ts +112 -0
- package/src/modules/checkout/lib/utils.ts +25 -3
|
@@ -13,7 +13,9 @@ import {
|
|
|
13
13
|
waitForCheckoutStatus
|
|
14
14
|
} from "./helpers/fixtures.js";
|
|
15
15
|
test.describe("TC-CHKT-008: Attempt update on locked link, verify 422", () => {
|
|
16
|
+
test.setTimeout(12e4);
|
|
16
17
|
test("rejects link edits after the first transaction locks the record", async ({ request }) => {
|
|
18
|
+
test.slow();
|
|
17
19
|
let token = null;
|
|
18
20
|
let linkId = null;
|
|
19
21
|
let transactionId = null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/checkout/__integration__/TC-CHKT-008.spec.ts"],
|
|
4
|
-
"sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { getAuthToken } from '@open-mercato/core/modules/core/__integration__/helpers/api'\nimport {\n createCustomerData,\n createFixedTemplateInput,\n createLinkFixture,\n deleteCheckoutEntityIfExists,\n findGatewayTransactionIdForCheckout,\n readGatewayTransaction,\n sendMockGatewayWebhook,\n submitPayLink,\n updateLink,\n waitForCheckoutStatus,\n} from './helpers/fixtures'\n\ntest.describe('TC-CHKT-008: Attempt update on locked link, verify 422', () => {\n test('rejects link edits after the first transaction locks the record', async ({ request }) => {\n let token: string | null = null\n let linkId: string | null = null\n let transactionId: string | null = null\n\n try {\n token = await getAuthToken(request)\n const link = await createLinkFixture(request, token, {\n ...createFixedTemplateInput({\n status: 'active',\n gatewayProviderKey: 'mock_processing',\n }),\n })\n linkId = link.id\n\n const submitResponse = await submitPayLink(request, link.slug, {\n customerData: createCustomerData(),\n acceptedLegalConsents: {},\n amount: 49.99,\n })\n expect(submitResponse.status()).toBe(201)\n const submitBody = await submitResponse.json()\n transactionId = typeof submitBody.transactionId === 'string' ? submitBody.transactionId : null\n\n const updateResponse = await updateLink(request, token, link.id, {\n title: 'Should fail after lock',\n })\n expect(updateResponse.status()).toBe(422)\n\n const body = await updateResponse.json()\n expect(body.error).toContain('cannot be edited')\n } finally {\n if (token && transactionId) {\n const gatewayTransactionId = await findGatewayTransactionIdForCheckout(request, token, transactionId)\n const gatewayTransaction = await readGatewayTransaction(request, token, gatewayTransactionId)\n if (gatewayTransaction.providerSessionId) {\n await sendMockGatewayWebhook(request, token, gatewayTransaction.providerSessionId, 'captured', 49.99, {\n providerKey: 'mock_processing',\n })\n await waitForCheckoutStatus(request, token, transactionId, 'completed')\n }\n }\n await deleteCheckoutEntityIfExists(request, token, 'links', linkId)\n }\n })\n})\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,KAAK,SAAS,0DAA0D,MAAM;AAC5E,OAAK,mEAAmE,OAAO,EAAE,QAAQ,MAAM;AAC7F,QAAI,QAAuB;AAC3B,QAAI,SAAwB;AAC5B,QAAI,gBAA+B;AAEnC,QAAI;AACF,cAAQ,MAAM,aAAa,OAAO;AAClC,YAAM,OAAO,MAAM,kBAAkB,SAAS,OAAO;AAAA,QACnD,GAAG,yBAAyB;AAAA,UAC1B,QAAQ;AAAA,UACR,oBAAoB;AAAA,QACtB,CAAC;AAAA,MACH,CAAC;AACD,eAAS,KAAK;AAEd,YAAM,iBAAiB,MAAM,cAAc,SAAS,KAAK,MAAM;AAAA,QAC7D,cAAc,mBAAmB;AAAA,QACjC,uBAAuB,CAAC;AAAA,QACxB,QAAQ;AAAA,MACV,CAAC;AACD,aAAO,eAAe,OAAO,CAAC,EAAE,KAAK,GAAG;AACxC,YAAM,aAAa,MAAM,eAAe,KAAK;AAC7C,sBAAgB,OAAO,WAAW,kBAAkB,WAAW,WAAW,gBAAgB;AAE1F,YAAM,iBAAiB,MAAM,WAAW,SAAS,OAAO,KAAK,IAAI;AAAA,QAC/D,OAAO;AAAA,MACT,CAAC;AACD,aAAO,eAAe,OAAO,CAAC,EAAE,KAAK,GAAG;AAExC,YAAM,OAAO,MAAM,eAAe,KAAK;AACvC,aAAO,KAAK,KAAK,EAAE,UAAU,kBAAkB;AAAA,IACjD,UAAE;AACA,UAAI,SAAS,eAAe;AAC1B,cAAM,uBAAuB,MAAM,oCAAoC,SAAS,OAAO,aAAa;AACpG,cAAM,qBAAqB,MAAM,uBAAuB,SAAS,OAAO,oBAAoB;AAC5F,YAAI,mBAAmB,mBAAmB;AACxC,gBAAM,uBAAuB,SAAS,OAAO,mBAAmB,mBAAmB,YAAY,OAAO;AAAA,YACpG,aAAa;AAAA,UACf,CAAC;AACD,gBAAM,sBAAsB,SAAS,OAAO,eAAe,WAAW;AAAA,QACxE;AAAA,MACF;AACA,YAAM,6BAA6B,SAAS,OAAO,SAAS,MAAM;AAAA,IACpE;AAAA,EACF,CAAC;AACH,CAAC;",
|
|
4
|
+
"sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { getAuthToken } from '@open-mercato/core/modules/core/__integration__/helpers/api'\nimport {\n createCustomerData,\n createFixedTemplateInput,\n createLinkFixture,\n deleteCheckoutEntityIfExists,\n findGatewayTransactionIdForCheckout,\n readGatewayTransaction,\n sendMockGatewayWebhook,\n submitPayLink,\n updateLink,\n waitForCheckoutStatus,\n} from './helpers/fixtures'\n\ntest.describe('TC-CHKT-008: Attempt update on locked link, verify 422', () => {\n test.setTimeout(120_000)\n\n test('rejects link edits after the first transaction locks the record', async ({ request }) => {\n test.slow()\n\n let token: string | null = null\n let linkId: string | null = null\n let transactionId: string | null = null\n\n try {\n token = await getAuthToken(request)\n const link = await createLinkFixture(request, token, {\n ...createFixedTemplateInput({\n status: 'active',\n gatewayProviderKey: 'mock_processing',\n }),\n })\n linkId = link.id\n\n const submitResponse = await submitPayLink(request, link.slug, {\n customerData: createCustomerData(),\n acceptedLegalConsents: {},\n amount: 49.99,\n })\n expect(submitResponse.status()).toBe(201)\n const submitBody = await submitResponse.json()\n transactionId = typeof submitBody.transactionId === 'string' ? submitBody.transactionId : null\n\n const updateResponse = await updateLink(request, token, link.id, {\n title: 'Should fail after lock',\n })\n expect(updateResponse.status()).toBe(422)\n\n const body = await updateResponse.json()\n expect(body.error).toContain('cannot be edited')\n } finally {\n if (token && transactionId) {\n const gatewayTransactionId = await findGatewayTransactionIdForCheckout(request, token, transactionId)\n const gatewayTransaction = await readGatewayTransaction(request, token, gatewayTransactionId)\n if (gatewayTransaction.providerSessionId) {\n await sendMockGatewayWebhook(request, token, gatewayTransaction.providerSessionId, 'captured', 49.99, {\n providerKey: 'mock_processing',\n })\n await waitForCheckoutStatus(request, token, transactionId, 'completed')\n }\n }\n await deleteCheckoutEntityIfExists(request, token, 'links', linkId)\n }\n })\n})\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,KAAK,SAAS,0DAA0D,MAAM;AAC5E,OAAK,WAAW,IAAO;AAEvB,OAAK,mEAAmE,OAAO,EAAE,QAAQ,MAAM;AAC7F,SAAK,KAAK;AAEV,QAAI,QAAuB;AAC3B,QAAI,SAAwB;AAC5B,QAAI,gBAA+B;AAEnC,QAAI;AACF,cAAQ,MAAM,aAAa,OAAO;AAClC,YAAM,OAAO,MAAM,kBAAkB,SAAS,OAAO;AAAA,QACnD,GAAG,yBAAyB;AAAA,UAC1B,QAAQ;AAAA,UACR,oBAAoB;AAAA,QACtB,CAAC;AAAA,MACH,CAAC;AACD,eAAS,KAAK;AAEd,YAAM,iBAAiB,MAAM,cAAc,SAAS,KAAK,MAAM;AAAA,QAC7D,cAAc,mBAAmB;AAAA,QACjC,uBAAuB,CAAC;AAAA,QACxB,QAAQ;AAAA,MACV,CAAC;AACD,aAAO,eAAe,OAAO,CAAC,EAAE,KAAK,GAAG;AACxC,YAAM,aAAa,MAAM,eAAe,KAAK;AAC7C,sBAAgB,OAAO,WAAW,kBAAkB,WAAW,WAAW,gBAAgB;AAE1F,YAAM,iBAAiB,MAAM,WAAW,SAAS,OAAO,KAAK,IAAI;AAAA,QAC/D,OAAO;AAAA,MACT,CAAC;AACD,aAAO,eAAe,OAAO,CAAC,EAAE,KAAK,GAAG;AAExC,YAAM,OAAO,MAAM,eAAe,KAAK;AACvC,aAAO,KAAK,KAAK,EAAE,UAAU,kBAAkB;AAAA,IACjD,UAAE;AACA,UAAI,SAAS,eAAe;AAC1B,cAAM,uBAAuB,MAAM,oCAAoC,SAAS,OAAO,aAAa;AACpG,cAAM,qBAAqB,MAAM,uBAAuB,SAAS,OAAO,oBAAoB;AAC5F,YAAI,mBAAmB,mBAAmB;AACxC,gBAAM,uBAAuB,SAAS,OAAO,mBAAmB,mBAAmB,YAAY,OAAO;AAAA,YACpG,aAAa;AAAA,UACf,CAAC;AACD,gBAAM,sBAAsB,SAAS,OAAO,eAAe,WAAW;AAAA,QACxE;AAAA,MACF;AACA,YAAM,6BAA6B,SAAS,OAAO,SAAS,MAAM;AAAA,IACpE;AAAA,EACF,CAAC;AACH,CAAC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -191,11 +191,16 @@ function normalizeCheckoutAccessSessionVersion(value) {
|
|
|
191
191
|
if (!value) return null;
|
|
192
192
|
return value instanceof Date ? value.toISOString() : value;
|
|
193
193
|
}
|
|
194
|
+
function deriveCheckoutAccessSessionVersion(value) {
|
|
195
|
+
const normalized = normalizeCheckoutAccessSessionVersion(value);
|
|
196
|
+
if (normalized == null) return null;
|
|
197
|
+
return createHmac("sha256", getCheckoutAccessTokenSecret()).update(normalized).digest("base64url");
|
|
198
|
+
}
|
|
194
199
|
function signCheckoutAccessToken(slug, options) {
|
|
195
200
|
const payload = Buffer.from(JSON.stringify({
|
|
196
201
|
slug,
|
|
197
202
|
linkId: options?.linkId ?? null,
|
|
198
|
-
sessionVersion:
|
|
203
|
+
sessionVersion: deriveCheckoutAccessSessionVersion(options?.sessionVersion),
|
|
199
204
|
exp: Date.now() + 60 * 60 * 1e3
|
|
200
205
|
}), "utf-8").toString("base64url");
|
|
201
206
|
const signature = createHmac("sha256", getCheckoutAccessTokenSecret()).update(payload).digest("base64url");
|
|
@@ -213,7 +218,7 @@ function verifyCheckoutAccessToken(token, slug, options) {
|
|
|
213
218
|
if (parsed.slug !== slug || typeof parsed.exp !== "number" || parsed.exp <= Date.now()) return false;
|
|
214
219
|
if (options?.linkId && parsed.linkId !== options.linkId) return false;
|
|
215
220
|
if (options?.sessionVersion) {
|
|
216
|
-
return parsed.sessionVersion ===
|
|
221
|
+
return parsed.sessionVersion === deriveCheckoutAccessSessionVersion(options.sessionVersion);
|
|
217
222
|
}
|
|
218
223
|
return true;
|
|
219
224
|
} catch {
|
|
@@ -243,6 +248,10 @@ function applyTerminalTransactionState(link, status) {
|
|
|
243
248
|
usageLimitReached: status === "completed" && link.maxCompletions != null && link.completionCount >= link.maxCompletions
|
|
244
249
|
};
|
|
245
250
|
}
|
|
251
|
+
function computeConsentMarkdownHash(documentKey, markdown) {
|
|
252
|
+
return createHmac("sha256", getCheckoutAccessTokenSecret()).update(`${documentKey}
|
|
253
|
+
${markdown}`).digest("hex");
|
|
254
|
+
}
|
|
246
255
|
function buildConsentProof(link, acceptedLegalConsents) {
|
|
247
256
|
const proof = {};
|
|
248
257
|
const legalDocuments = link.legalDocuments && typeof link.legalDocuments === "object" ? link.legalDocuments : {};
|
|
@@ -255,7 +264,7 @@ function buildConsentProof(link, acceptedLegalConsents) {
|
|
|
255
264
|
title: document.title ?? key,
|
|
256
265
|
required: document.required === true,
|
|
257
266
|
acceptedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
258
|
-
markdownHash:
|
|
267
|
+
markdownHash: computeConsentMarkdownHash(key, document.markdown)
|
|
259
268
|
};
|
|
260
269
|
}
|
|
261
270
|
return proof;
|
|
@@ -393,6 +402,7 @@ export {
|
|
|
393
402
|
applyTerminalTransactionState,
|
|
394
403
|
buildCheckoutAttachmentPreviewUrl2 as buildCheckoutAttachmentPreviewUrl,
|
|
395
404
|
buildConsentProof,
|
|
405
|
+
computeConsentMarkdownHash,
|
|
396
406
|
deriveConfiguredCurrencies,
|
|
397
407
|
ensureUniqueSlug,
|
|
398
408
|
getCheckoutCustomerFieldSemanticType,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/checkout/lib/utils.ts"],
|
|
4
|
-
"sourcesContent": ["import { createHmac, timingSafeEqual } from 'crypto'\nimport bcrypt from 'bcryptjs'\nimport { slugify } from '@open-mercato/shared/lib/slugify'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { normalizeCustomFieldResponse } from '@open-mercato/shared/lib/custom-fields/normalize'\nimport { parseDecryptedFieldValue } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getPaymentGatewayDescriptor } from '@open-mercato/shared/modules/payment_gateways/types'\nimport { CheckoutLink, CheckoutLinkTemplate, CheckoutTransaction } from '../data/entities'\nimport { buildCheckoutAttachmentPreviewUrl, normalizeOptionalString } from './client-utils'\nimport type {\n CreateLinkInput,\n CreateTemplateInput,\n PublicSubmitInput,\n UpdateLinkInput,\n UpdateTemplateInput,\n} from '../data/validators'\nimport { CHECKOUT_TERMINAL_STATUSES } from './constants'\nexport {\n getCheckoutCustomerFieldSemanticType,\n isValidCheckoutEmail,\n isValidCheckoutPhone,\n validateCheckoutCustomerData,\n} from './customerDataValidation'\n\nexport type CheckoutScope = {\n organizationId: string\n tenantId: string\n}\n\nexport type CheckoutLinkStatus = 'draft' | 'active' | 'inactive'\n\nexport type CheckoutPayloadWithCustomFields<TInput> = {\n parsed: TInput\n customFields: Record<string, unknown>\n}\n\ntype TemplateOrLinkInput =\n | CreateTemplateInput\n | UpdateTemplateInput\n | CreateLinkInput\n | UpdateLinkInput\n\ntype TemplateOrLinkMutationInput = Omit<CreateLinkInput, 'password'> & {\n password?: string | null\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction isCheckoutLinkRecord(record: CheckoutLinkTemplate | CheckoutLink): record is CheckoutLink {\n return typeof (record as { slug?: unknown }).slug === 'string'\n}\n\nfunction normalizeMaybeStringifiedJsonObject(value: unknown): Record<string, unknown> {\n if (isRecord(value)) return value\n if (typeof value !== 'string') return {}\n\n const parsed = parseDecryptedFieldValue(value)\n return isRecord(parsed) ? parsed : {}\n}\n\nexport function pickExplicitParsedOverrides<TInput extends Record<string, unknown>>(\n rawInput: unknown,\n parsed: TInput,\n): Partial<TInput> {\n if (!isRecord(rawInput)) return {}\n\n const overrides: Partial<TInput> = {}\n for (const key of Object.keys(parsed) as Array<keyof TInput>) {\n if (!Object.prototype.hasOwnProperty.call(rawInput, key)) continue\n overrides[key] = parsed[key]\n }\n\n return overrides\n}\n\nexport function requireCheckoutScope(input: { auth?: { orgId?: string | null; tenantId?: string | null } | null }): CheckoutScope {\n const organizationId = input.auth?.orgId ?? null\n const tenantId = input.auth?.tenantId ?? null\n if (!organizationId || !tenantId) {\n throw new CrudHttpError(401, { error: 'Unauthorized' })\n }\n return { organizationId, tenantId }\n}\n\nexport function parseCheckoutInput<TInput>(raw: unknown, parser: (value: unknown) => TInput): CheckoutPayloadWithCustomFields<TInput> {\n const source = isRecord(raw) ? { ...raw } : {}\n const customFields = isRecord(source.customFields) ? source.customFields : {}\n delete source.customFields\n return {\n parsed: parser(source),\n customFields,\n }\n}\n\nexport function resolveLoadedCheckoutCustomFields(\n values: Record<string, unknown> | null | undefined,\n): Record<string, unknown> {\n return normalizeCustomFieldResponse(values) ?? {}\n}\n\nexport function toIsoString(value: unknown): string | null {\n if (!value) return null\n if (value instanceof Date) return value.toISOString()\n const parsed = new Date(value as string | number)\n return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString()\n}\n\nexport function toMoneyNumber(value: string | number | null | undefined): number | null {\n if (value == null) return null\n const numeric = Number(value)\n return Number.isFinite(numeric) ? numeric : null\n}\n\nexport function toMoneyString(value: string | number | null | undefined): string | null {\n const numeric = toMoneyNumber(value)\n return numeric == null ? null : numeric.toFixed(2)\n}\n\nexport { normalizeOptionalString, buildCheckoutAttachmentPreviewUrl } from './client-utils'\n\nfunction normalizeJsonRecord(value: unknown): Record<string, unknown> {\n if (!value) return {}\n if (typeof value === 'object' && !Array.isArray(value)) return value as Record<string, unknown>\n if (typeof value !== 'string') return {}\n try {\n const parsed = JSON.parse(value)\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? parsed as Record<string, unknown>\n : {}\n } catch {\n return {}\n }\n}\n\nexport function deriveConfiguredCurrencies(input: TemplateOrLinkInput): string[] {\n const currencies = new Set<string>()\n if (input.pricingMode === 'fixed' && input.fixedPriceCurrencyCode) currencies.add(input.fixedPriceCurrencyCode)\n if (input.pricingMode === 'custom_amount' && input.customAmountCurrencyCode) currencies.add(input.customAmountCurrencyCode)\n if (input.pricingMode === 'price_list') {\n for (const item of input.priceListItems ?? []) currencies.add(item.currencyCode)\n }\n return Array.from(currencies)\n}\n\nexport function toTemplateOrLinkMutationInput(\n record: CheckoutLinkTemplate | CheckoutLink,\n overrides: Partial<TemplateOrLinkMutationInput> = {},\n): TemplateOrLinkMutationInput {\n return {\n name: record.name,\n title: record.title ?? null,\n subtitle: record.subtitle ?? null,\n description: record.description ?? null,\n logoAttachmentId: record.logoAttachmentId ?? null,\n logoUrl: record.logoUrl ?? null,\n primaryColor: record.primaryColor ?? null,\n secondaryColor: record.secondaryColor ?? null,\n backgroundColor: record.backgroundColor ?? null,\n themeMode: record.themeMode,\n pricingMode: record.pricingMode,\n fixedPriceAmount: toMoneyNumber(record.fixedPriceAmount),\n fixedPriceCurrencyCode: record.fixedPriceCurrencyCode ?? null,\n fixedPriceIncludesTax: record.fixedPriceIncludesTax,\n fixedPriceOriginalAmount: toMoneyNumber(record.fixedPriceOriginalAmount),\n customAmountMin: toMoneyNumber(record.customAmountMin),\n customAmountMax: toMoneyNumber(record.customAmountMax),\n customAmountCurrencyCode: record.customAmountCurrencyCode ?? null,\n priceListItems: record.priceListItems ?? null,\n gatewayProviderKey: record.gatewayProviderKey ?? '',\n gatewaySettings: normalizeJsonRecord(record.gatewaySettings),\n customFieldsetCode: record.customFieldsetCode ?? null,\n collectCustomerDetails: record.collectCustomerDetails,\n customerFieldsSchema: (record.customerFieldsSchema ?? []) as CreateTemplateInput['customerFieldsSchema'],\n legalDocuments: (record.legalDocuments ?? undefined) as CreateTemplateInput['legalDocuments'],\n displayCustomFieldsOnPage: record.displayCustomFieldsOnPage,\n successTitle: record.successTitle ?? null,\n successMessage: record.successMessage ?? null,\n cancelTitle: record.cancelTitle ?? null,\n cancelMessage: record.cancelMessage ?? null,\n errorTitle: record.errorTitle ?? null,\n errorMessage: record.errorMessage ?? null,\n successEmailSubject: record.successEmailSubject ?? null,\n successEmailBody: record.successEmailBody ?? null,\n sendSuccessEmail: record.sendSuccessEmail,\n errorEmailSubject: record.errorEmailSubject ?? null,\n errorEmailBody: record.errorEmailBody ?? null,\n sendErrorEmail: record.sendErrorEmail,\n startEmailSubject: record.startEmailSubject ?? null,\n startEmailBody: record.startEmailBody ?? null,\n sendStartEmail: record.sendStartEmail,\n password: undefined,\n maxCompletions: record.maxCompletions ?? null,\n status: record.status,\n checkoutType: record.checkoutType,\n ...(isCheckoutLinkRecord(record) ? { slug: record.slug, templateId: record.templateId ?? null } : {}),\n ...overrides,\n }\n}\n\nexport function validateDescriptorCurrencies(providerKey: string | null | undefined, currencies: string[]): void {\n if (!providerKey || currencies.length === 0) return\n const descriptor = getPaymentGatewayDescriptor(providerKey)\n const supported = descriptor?.sessionConfig?.supportedCurrencies\n if (!descriptor || !supported || supported === '*') return\n const unsupported = currencies.filter((currency) => !supported.includes(currency))\n if (unsupported.length > 0) {\n throw new CrudHttpError(422, {\n error: `Unsupported currency for provider ${providerKey}: ${unsupported.join(', ')}`,\n })\n }\n}\n\nexport async function ensureUniqueSlug(\n em: EntityManager,\n _scope: CheckoutScope,\n requestedSlug: string | null | undefined,\n fallbackText: string,\n excludeId?: string | null,\n): Promise<string> {\n const base = slugify(requestedSlug || fallbackText || 'pay-link') || 'pay-link'\n let candidate = base\n let counter = 1\n while (true) {\n const existing = await em.findOne(CheckoutLink, {\n slug: candidate,\n deletedAt: null,\n ...(excludeId ? { id: { $ne: excludeId } } : {}),\n })\n if (!existing) return candidate\n counter += 1\n candidate = `${base}-${counter}`\n }\n}\n\nexport async function hashCheckoutPassword(password: string | null | undefined): Promise<string | null> {\n const normalized = normalizeOptionalString(password)\n if (!normalized) return null\n return bcrypt.hash(normalized, 10)\n}\n\nexport async function verifyCheckoutPassword(password: string, passwordHash: string | null | undefined): Promise<boolean> {\n if (!passwordHash) return false\n return bcrypt.compare(password, passwordHash)\n}\n\nfunction getCheckoutAccessTokenSecret(): string {\n const secret = process.env.AUTH_SECRET\n || process.env.NEXTAUTH_SECRET\n || process.env.JWT_SECRET\n || process.env.TENANT_DATA_ENCRYPTION_FALLBACK_KEY\n if (!secret) {\n throw new Error(\n 'Checkout password sessions require AUTH_SECRET, NEXTAUTH_SECRET, JWT_SECRET, or TENANT_DATA_ENCRYPTION_FALLBACK_KEY',\n )\n }\n return secret\n}\n\nfunction normalizeCheckoutAccessSessionVersion(value: Date | string | null | undefined): string | null {\n if (!value) return null\n return value instanceof Date ? value.toISOString() : value\n}\n\nexport function signCheckoutAccessToken(\n slug: string,\n options?: { linkId?: string | null; sessionVersion?: Date | string | null },\n): string {\n const payload = Buffer.from(JSON.stringify({\n slug,\n linkId: options?.linkId ?? null,\n sessionVersion: normalizeCheckoutAccessSessionVersion(options?.sessionVersion),\n exp: Date.now() + (60 * 60 * 1000),\n }), 'utf-8').toString('base64url')\n const signature = createHmac('sha256', getCheckoutAccessTokenSecret()).update(payload).digest('base64url')\n return `${payload}.${signature}`\n}\n\nexport function verifyCheckoutAccessToken(\n token: string | null | undefined,\n slug: string,\n options?: { linkId?: string | null; sessionVersion?: Date | string | null },\n): boolean {\n if (!token) return false\n const [payload, signature] = token.split('.')\n if (!payload || !signature) return false\n const expected = createHmac('sha256', getCheckoutAccessTokenSecret()).update(payload).digest()\n const actual = Buffer.from(signature, 'base64url')\n if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) return false\n try {\n const parsed = JSON.parse(Buffer.from(payload, 'base64url').toString('utf-8')) as {\n slug?: string\n linkId?: string | null\n sessionVersion?: string | null\n exp?: number\n }\n if (parsed.slug !== slug || typeof parsed.exp !== 'number' || parsed.exp <= Date.now()) return false\n if (options?.linkId && parsed.linkId !== options.linkId) return false\n if (options?.sessionVersion) {\n return parsed.sessionVersion === normalizeCheckoutAccessSessionVersion(options.sessionVersion)\n }\n return true\n } catch {\n return false\n }\n}\n\nexport function mapGatewayStatusToCheckoutStatus(status: string | null | undefined): CheckoutTransaction['status'] {\n if (status === 'captured' || status === 'authorized') return 'completed'\n if (status === 'cancelled') return 'cancelled'\n if (status === 'expired') return 'expired'\n if (status === 'failed') return 'failed'\n return 'processing'\n}\n\nexport function isTerminalCheckoutStatus(status: string | null | undefined): boolean {\n return typeof status === 'string' && CHECKOUT_TERMINAL_STATUSES.has(status)\n}\n\nexport function isCheckoutLinkPublic(status: CheckoutLinkStatus | string | null | undefined): boolean {\n return status === 'active'\n}\n\nexport function applyTerminalTransactionState(\n link: Pick<CheckoutLink, 'activeReservationCount' | 'completionCount' | 'isLocked' | 'maxCompletions'>,\n status: CheckoutTransaction['status'],\n): { usageLimitReached: boolean } {\n link.activeReservationCount = Math.max(0, link.activeReservationCount - 1)\n if (status === 'completed') {\n link.completionCount += 1\n }\n link.isLocked = link.activeReservationCount > 0\n return {\n usageLimitReached: status === 'completed'\n && link.maxCompletions != null\n && link.completionCount >= link.maxCompletions,\n }\n}\n\nexport function buildConsentProof(link: CheckoutLink, acceptedLegalConsents: PublicSubmitInput['acceptedLegalConsents']) {\n const proof: Record<string, unknown> = {}\n const legalDocuments = link.legalDocuments && typeof link.legalDocuments === 'object'\n ? link.legalDocuments as Record<string, { title?: string; markdown?: string; required?: boolean }>\n : {}\n for (const key of ['terms', 'privacyPolicy']) {\n const document = legalDocuments[key]\n if (!document?.markdown) continue\n const accepted = acceptedLegalConsents?.[key as keyof PublicSubmitInput['acceptedLegalConsents']] === true\n if (!accepted) continue\n proof[key] = {\n title: document.title ?? key,\n required: document.required === true,\n acceptedAt: new Date().toISOString(),\n markdownHash: createHmac('sha256', key).update(document.markdown).digest('hex'),\n }\n }\n return proof\n}\n\nexport function resolveSubmittedAmount(link: CheckoutLink, input: PublicSubmitInput): { amount: number; currencyCode: string; selectedPriceItemId: string | null } {\n if (link.pricingMode === 'fixed') {\n const expected = toMoneyNumber(link.fixedPriceAmount)\n if (expected == null || !link.fixedPriceCurrencyCode) {\n throw new CrudHttpError(422, { error: 'checkout.payPage.errors.submit' })\n }\n if (input.amount != null && Number(input.amount) !== expected) {\n throw new CrudHttpError(422, { error: 'checkout.payPage.errors.submit' })\n }\n return { amount: expected, currencyCode: link.fixedPriceCurrencyCode, selectedPriceItemId: null }\n }\n if (link.pricingMode === 'custom_amount') {\n if (input.amount == null || !link.customAmountCurrencyCode) {\n throw new CrudHttpError(422, {\n error: 'checkout.payPage.validation.fixErrors',\n fieldErrors: { amount: 'checkout.payPage.validation.amountRequired' },\n })\n }\n const min = toMoneyNumber(link.customAmountMin) ?? 0\n const max = toMoneyNumber(link.customAmountMax)\n const amount = Number(input.amount)\n if (amount < min || (max != null && amount > max)) {\n throw new CrudHttpError(422, {\n error: 'checkout.payPage.validation.fixErrors',\n fieldErrors: { amount: 'checkout.payPage.errors.submit' },\n })\n }\n return { amount, currencyCode: link.customAmountCurrencyCode, selectedPriceItemId: null }\n }\n const selectedPriceItem = (link.priceListItems ?? []).find((item) => item.id === input.selectedPriceItemId)\n if (!selectedPriceItem) {\n throw new CrudHttpError(422, {\n error: 'checkout.payPage.validation.fixErrors',\n fieldErrors: { selectedPriceItemId: 'checkout.payPage.validation.priceSelectionRequired' },\n })\n }\n if (input.amount != null && Number(input.amount) !== Number(selectedPriceItem.amount)) {\n throw new CrudHttpError(422, { error: 'checkout.payPage.errors.submit' })\n }\n return {\n amount: Number(selectedPriceItem.amount),\n currencyCode: selectedPriceItem.currencyCode,\n selectedPriceItemId: selectedPriceItem.id,\n }\n}\n\nexport function serializeTemplateOrLink(record: CheckoutLinkTemplate | CheckoutLink) {\n const logoPreviewUrl = buildCheckoutAttachmentPreviewUrl(record.logoAttachmentId) ?? record.logoUrl ?? null\n return {\n id: record.id,\n name: record.name,\n title: record.title ?? null,\n subtitle: record.subtitle ?? null,\n description: record.description ?? null,\n logoAttachmentId: record.logoAttachmentId ?? null,\n logoUrl: record.logoUrl ?? null,\n logoPreviewUrl,\n primaryColor: record.primaryColor ?? null,\n secondaryColor: record.secondaryColor ?? null,\n backgroundColor: record.backgroundColor ?? null,\n themeMode: record.themeMode,\n pricingMode: record.pricingMode,\n fixedPriceAmount: toMoneyNumber(record.fixedPriceAmount),\n fixedPriceCurrencyCode: record.fixedPriceCurrencyCode ?? null,\n fixedPriceIncludesTax: record.fixedPriceIncludesTax,\n fixedPriceOriginalAmount: toMoneyNumber(record.fixedPriceOriginalAmount),\n customAmountMin: toMoneyNumber(record.customAmountMin),\n customAmountMax: toMoneyNumber(record.customAmountMax),\n customAmountCurrencyCode: record.customAmountCurrencyCode ?? null,\n priceListItems: record.priceListItems ?? [],\n gatewayProviderKey: record.gatewayProviderKey ?? null,\n gatewaySettings: normalizeJsonRecord(record.gatewaySettings),\n customFieldsetCode: record.customFieldsetCode ?? null,\n collectCustomerDetails: record.collectCustomerDetails,\n customerFieldsSchema: record.customerFieldsSchema ?? [],\n legalDocuments: record.legalDocuments ?? {},\n displayCustomFieldsOnPage: record.displayCustomFieldsOnPage,\n successTitle: record.successTitle ?? null,\n successMessage: record.successMessage ?? null,\n cancelTitle: record.cancelTitle ?? null,\n cancelMessage: record.cancelMessage ?? null,\n errorTitle: record.errorTitle ?? null,\n errorMessage: record.errorMessage ?? null,\n successEmailSubject: record.successEmailSubject ?? null,\n successEmailBody: record.successEmailBody ?? null,\n sendSuccessEmail: record.sendSuccessEmail,\n errorEmailSubject: record.errorEmailSubject ?? null,\n errorEmailBody: record.errorEmailBody ?? null,\n sendErrorEmail: record.sendErrorEmail,\n startEmailSubject: record.startEmailSubject ?? null,\n startEmailBody: record.startEmailBody ?? null,\n sendStartEmail: record.sendStartEmail,\n maxCompletions: record.maxCompletions ?? null,\n status: record.status,\n checkoutType: record.checkoutType,\n createdAt: toIsoString(record.createdAt),\n updatedAt: toIsoString(record.updatedAt),\n ...(isCheckoutLinkRecord(record) ? {\n slug: record.slug,\n templateId: record.templateId ?? null,\n completionCount: record.completionCount,\n activeReservationCount: record.activeReservationCount,\n isLocked: record.isLocked,\n } : {}),\n }\n}\n\nexport function serializeTransaction(record: CheckoutTransaction, link?: CheckoutLink | null, includePii = false) {\n return {\n id: record.id,\n linkId: record.linkId,\n linkName: link?.name ?? null,\n linkSlug: link?.slug ?? null,\n amount: toMoneyNumber(record.amount),\n currencyCode: record.currencyCode,\n status: record.status,\n paymentStatus: record.paymentStatus ?? null,\n gatewayTransactionId: record.gatewayTransactionId ?? null,\n selectedPriceItemId: record.selectedPriceItemId ?? null,\n acceptedLegalConsents: includePii ? normalizeMaybeStringifiedJsonObject(record.acceptedLegalConsents) : null,\n customerData: includePii ? normalizeMaybeStringifiedJsonObject(record.customerData) : null,\n firstName: includePii ? (record.firstName ?? null) : null,\n lastName: includePii ? (record.lastName ?? null) : null,\n email: includePii ? (record.email ?? null) : null,\n phone: includePii ? (record.phone ?? null) : null,\n ipAddress: includePii ? (record.ipAddress ?? null) : null,\n userAgent: includePii ? (record.userAgent ?? null) : null,\n createdAt: toIsoString(record.createdAt),\n updatedAt: toIsoString(record.updatedAt),\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { createHmac, timingSafeEqual } from 'crypto'\nimport bcrypt from 'bcryptjs'\nimport { slugify } from '@open-mercato/shared/lib/slugify'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { normalizeCustomFieldResponse } from '@open-mercato/shared/lib/custom-fields/normalize'\nimport { parseDecryptedFieldValue } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getPaymentGatewayDescriptor } from '@open-mercato/shared/modules/payment_gateways/types'\nimport { CheckoutLink, CheckoutLinkTemplate, CheckoutTransaction } from '../data/entities'\nimport { buildCheckoutAttachmentPreviewUrl, normalizeOptionalString } from './client-utils'\nimport type {\n CreateLinkInput,\n CreateTemplateInput,\n PublicSubmitInput,\n UpdateLinkInput,\n UpdateTemplateInput,\n} from '../data/validators'\nimport { CHECKOUT_TERMINAL_STATUSES } from './constants'\nexport {\n getCheckoutCustomerFieldSemanticType,\n isValidCheckoutEmail,\n isValidCheckoutPhone,\n validateCheckoutCustomerData,\n} from './customerDataValidation'\n\nexport type CheckoutScope = {\n organizationId: string\n tenantId: string\n}\n\nexport type CheckoutLinkStatus = 'draft' | 'active' | 'inactive'\n\nexport type CheckoutPayloadWithCustomFields<TInput> = {\n parsed: TInput\n customFields: Record<string, unknown>\n}\n\ntype TemplateOrLinkInput =\n | CreateTemplateInput\n | UpdateTemplateInput\n | CreateLinkInput\n | UpdateLinkInput\n\ntype TemplateOrLinkMutationInput = Omit<CreateLinkInput, 'password'> & {\n password?: string | null\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction isCheckoutLinkRecord(record: CheckoutLinkTemplate | CheckoutLink): record is CheckoutLink {\n return typeof (record as { slug?: unknown }).slug === 'string'\n}\n\nfunction normalizeMaybeStringifiedJsonObject(value: unknown): Record<string, unknown> {\n if (isRecord(value)) return value\n if (typeof value !== 'string') return {}\n\n const parsed = parseDecryptedFieldValue(value)\n return isRecord(parsed) ? parsed : {}\n}\n\nexport function pickExplicitParsedOverrides<TInput extends Record<string, unknown>>(\n rawInput: unknown,\n parsed: TInput,\n): Partial<TInput> {\n if (!isRecord(rawInput)) return {}\n\n const overrides: Partial<TInput> = {}\n for (const key of Object.keys(parsed) as Array<keyof TInput>) {\n if (!Object.prototype.hasOwnProperty.call(rawInput, key)) continue\n overrides[key] = parsed[key]\n }\n\n return overrides\n}\n\nexport function requireCheckoutScope(input: { auth?: { orgId?: string | null; tenantId?: string | null } | null }): CheckoutScope {\n const organizationId = input.auth?.orgId ?? null\n const tenantId = input.auth?.tenantId ?? null\n if (!organizationId || !tenantId) {\n throw new CrudHttpError(401, { error: 'Unauthorized' })\n }\n return { organizationId, tenantId }\n}\n\nexport function parseCheckoutInput<TInput>(raw: unknown, parser: (value: unknown) => TInput): CheckoutPayloadWithCustomFields<TInput> {\n const source = isRecord(raw) ? { ...raw } : {}\n const customFields = isRecord(source.customFields) ? source.customFields : {}\n delete source.customFields\n return {\n parsed: parser(source),\n customFields,\n }\n}\n\nexport function resolveLoadedCheckoutCustomFields(\n values: Record<string, unknown> | null | undefined,\n): Record<string, unknown> {\n return normalizeCustomFieldResponse(values) ?? {}\n}\n\nexport function toIsoString(value: unknown): string | null {\n if (!value) return null\n if (value instanceof Date) return value.toISOString()\n const parsed = new Date(value as string | number)\n return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString()\n}\n\nexport function toMoneyNumber(value: string | number | null | undefined): number | null {\n if (value == null) return null\n const numeric = Number(value)\n return Number.isFinite(numeric) ? numeric : null\n}\n\nexport function toMoneyString(value: string | number | null | undefined): string | null {\n const numeric = toMoneyNumber(value)\n return numeric == null ? null : numeric.toFixed(2)\n}\n\nexport { normalizeOptionalString, buildCheckoutAttachmentPreviewUrl } from './client-utils'\n\nfunction normalizeJsonRecord(value: unknown): Record<string, unknown> {\n if (!value) return {}\n if (typeof value === 'object' && !Array.isArray(value)) return value as Record<string, unknown>\n if (typeof value !== 'string') return {}\n try {\n const parsed = JSON.parse(value)\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? parsed as Record<string, unknown>\n : {}\n } catch {\n return {}\n }\n}\n\nexport function deriveConfiguredCurrencies(input: TemplateOrLinkInput): string[] {\n const currencies = new Set<string>()\n if (input.pricingMode === 'fixed' && input.fixedPriceCurrencyCode) currencies.add(input.fixedPriceCurrencyCode)\n if (input.pricingMode === 'custom_amount' && input.customAmountCurrencyCode) currencies.add(input.customAmountCurrencyCode)\n if (input.pricingMode === 'price_list') {\n for (const item of input.priceListItems ?? []) currencies.add(item.currencyCode)\n }\n return Array.from(currencies)\n}\n\nexport function toTemplateOrLinkMutationInput(\n record: CheckoutLinkTemplate | CheckoutLink,\n overrides: Partial<TemplateOrLinkMutationInput> = {},\n): TemplateOrLinkMutationInput {\n return {\n name: record.name,\n title: record.title ?? null,\n subtitle: record.subtitle ?? null,\n description: record.description ?? null,\n logoAttachmentId: record.logoAttachmentId ?? null,\n logoUrl: record.logoUrl ?? null,\n primaryColor: record.primaryColor ?? null,\n secondaryColor: record.secondaryColor ?? null,\n backgroundColor: record.backgroundColor ?? null,\n themeMode: record.themeMode,\n pricingMode: record.pricingMode,\n fixedPriceAmount: toMoneyNumber(record.fixedPriceAmount),\n fixedPriceCurrencyCode: record.fixedPriceCurrencyCode ?? null,\n fixedPriceIncludesTax: record.fixedPriceIncludesTax,\n fixedPriceOriginalAmount: toMoneyNumber(record.fixedPriceOriginalAmount),\n customAmountMin: toMoneyNumber(record.customAmountMin),\n customAmountMax: toMoneyNumber(record.customAmountMax),\n customAmountCurrencyCode: record.customAmountCurrencyCode ?? null,\n priceListItems: record.priceListItems ?? null,\n gatewayProviderKey: record.gatewayProviderKey ?? '',\n gatewaySettings: normalizeJsonRecord(record.gatewaySettings),\n customFieldsetCode: record.customFieldsetCode ?? null,\n collectCustomerDetails: record.collectCustomerDetails,\n customerFieldsSchema: (record.customerFieldsSchema ?? []) as CreateTemplateInput['customerFieldsSchema'],\n legalDocuments: (record.legalDocuments ?? undefined) as CreateTemplateInput['legalDocuments'],\n displayCustomFieldsOnPage: record.displayCustomFieldsOnPage,\n successTitle: record.successTitle ?? null,\n successMessage: record.successMessage ?? null,\n cancelTitle: record.cancelTitle ?? null,\n cancelMessage: record.cancelMessage ?? null,\n errorTitle: record.errorTitle ?? null,\n errorMessage: record.errorMessage ?? null,\n successEmailSubject: record.successEmailSubject ?? null,\n successEmailBody: record.successEmailBody ?? null,\n sendSuccessEmail: record.sendSuccessEmail,\n errorEmailSubject: record.errorEmailSubject ?? null,\n errorEmailBody: record.errorEmailBody ?? null,\n sendErrorEmail: record.sendErrorEmail,\n startEmailSubject: record.startEmailSubject ?? null,\n startEmailBody: record.startEmailBody ?? null,\n sendStartEmail: record.sendStartEmail,\n password: undefined,\n maxCompletions: record.maxCompletions ?? null,\n status: record.status,\n checkoutType: record.checkoutType,\n ...(isCheckoutLinkRecord(record) ? { slug: record.slug, templateId: record.templateId ?? null } : {}),\n ...overrides,\n }\n}\n\nexport function validateDescriptorCurrencies(providerKey: string | null | undefined, currencies: string[]): void {\n if (!providerKey || currencies.length === 0) return\n const descriptor = getPaymentGatewayDescriptor(providerKey)\n const supported = descriptor?.sessionConfig?.supportedCurrencies\n if (!descriptor || !supported || supported === '*') return\n const unsupported = currencies.filter((currency) => !supported.includes(currency))\n if (unsupported.length > 0) {\n throw new CrudHttpError(422, {\n error: `Unsupported currency for provider ${providerKey}: ${unsupported.join(', ')}`,\n })\n }\n}\n\nexport async function ensureUniqueSlug(\n em: EntityManager,\n _scope: CheckoutScope,\n requestedSlug: string | null | undefined,\n fallbackText: string,\n excludeId?: string | null,\n): Promise<string> {\n const base = slugify(requestedSlug || fallbackText || 'pay-link') || 'pay-link'\n let candidate = base\n let counter = 1\n while (true) {\n const existing = await em.findOne(CheckoutLink, {\n slug: candidate,\n deletedAt: null,\n ...(excludeId ? { id: { $ne: excludeId } } : {}),\n })\n if (!existing) return candidate\n counter += 1\n candidate = `${base}-${counter}`\n }\n}\n\nexport async function hashCheckoutPassword(password: string | null | undefined): Promise<string | null> {\n const normalized = normalizeOptionalString(password)\n if (!normalized) return null\n return bcrypt.hash(normalized, 10)\n}\n\nexport async function verifyCheckoutPassword(password: string, passwordHash: string | null | undefined): Promise<boolean> {\n if (!passwordHash) return false\n return bcrypt.compare(password, passwordHash)\n}\n\nfunction getCheckoutAccessTokenSecret(): string {\n const secret = process.env.AUTH_SECRET\n || process.env.NEXTAUTH_SECRET\n || process.env.JWT_SECRET\n || process.env.TENANT_DATA_ENCRYPTION_FALLBACK_KEY\n if (!secret) {\n throw new Error(\n 'Checkout password sessions require AUTH_SECRET, NEXTAUTH_SECRET, JWT_SECRET, or TENANT_DATA_ENCRYPTION_FALLBACK_KEY',\n )\n }\n return secret\n}\n\nfunction normalizeCheckoutAccessSessionVersion(value: Date | string | null | undefined): string | null {\n if (!value) return null\n return value instanceof Date ? value.toISOString() : value\n}\n\n// Derive a non-reversible cookie-safe representation of `sessionVersion`\n// so the embedded payload never exposes the raw input (typically the\n// checkout link's bcrypt passwordHash \u2014 see #2675). The HMAC keeps\n// rotation semantics intact: any change to the input produces a fresh\n// digest, invalidating outstanding cookies.\nfunction deriveCheckoutAccessSessionVersion(value: Date | string | null | undefined): string | null {\n const normalized = normalizeCheckoutAccessSessionVersion(value)\n if (normalized == null) return null\n return createHmac('sha256', getCheckoutAccessTokenSecret()).update(normalized).digest('base64url')\n}\n\nexport function signCheckoutAccessToken(\n slug: string,\n options?: { linkId?: string | null; sessionVersion?: Date | string | null },\n): string {\n const payload = Buffer.from(JSON.stringify({\n slug,\n linkId: options?.linkId ?? null,\n sessionVersion: deriveCheckoutAccessSessionVersion(options?.sessionVersion),\n exp: Date.now() + (60 * 60 * 1000),\n }), 'utf-8').toString('base64url')\n const signature = createHmac('sha256', getCheckoutAccessTokenSecret()).update(payload).digest('base64url')\n return `${payload}.${signature}`\n}\n\nexport function verifyCheckoutAccessToken(\n token: string | null | undefined,\n slug: string,\n options?: { linkId?: string | null; sessionVersion?: Date | string | null },\n): boolean {\n if (!token) return false\n const [payload, signature] = token.split('.')\n if (!payload || !signature) return false\n const expected = createHmac('sha256', getCheckoutAccessTokenSecret()).update(payload).digest()\n const actual = Buffer.from(signature, 'base64url')\n if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) return false\n try {\n const parsed = JSON.parse(Buffer.from(payload, 'base64url').toString('utf-8')) as {\n slug?: string\n linkId?: string | null\n sessionVersion?: string | null\n exp?: number\n }\n if (parsed.slug !== slug || typeof parsed.exp !== 'number' || parsed.exp <= Date.now()) return false\n if (options?.linkId && parsed.linkId !== options.linkId) return false\n if (options?.sessionVersion) {\n return parsed.sessionVersion === deriveCheckoutAccessSessionVersion(options.sessionVersion)\n }\n return true\n } catch {\n return false\n }\n}\n\nexport function mapGatewayStatusToCheckoutStatus(status: string | null | undefined): CheckoutTransaction['status'] {\n if (status === 'captured' || status === 'authorized') return 'completed'\n if (status === 'cancelled') return 'cancelled'\n if (status === 'expired') return 'expired'\n if (status === 'failed') return 'failed'\n return 'processing'\n}\n\nexport function isTerminalCheckoutStatus(status: string | null | undefined): boolean {\n return typeof status === 'string' && CHECKOUT_TERMINAL_STATUSES.has(status)\n}\n\nexport function isCheckoutLinkPublic(status: CheckoutLinkStatus | string | null | undefined): boolean {\n return status === 'active'\n}\n\nexport function applyTerminalTransactionState(\n link: Pick<CheckoutLink, 'activeReservationCount' | 'completionCount' | 'isLocked' | 'maxCompletions'>,\n status: CheckoutTransaction['status'],\n): { usageLimitReached: boolean } {\n link.activeReservationCount = Math.max(0, link.activeReservationCount - 1)\n if (status === 'completed') {\n link.completionCount += 1\n }\n link.isLocked = link.activeReservationCount > 0\n return {\n usageLimitReached: status === 'completed'\n && link.maxCompletions != null\n && link.completionCount >= link.maxCompletions,\n }\n}\n\n// Security model: the consent proof is a GDPR/consent audit trail, so `markdownHash`\n// must be tamper-evident. It is an HMAC keyed with the server-side checkout secret\n// (never a public constant), which an attacker who can edit a stored consent row\n// cannot recompute without the secret. The document key is folded into the HMAC\n// message to namespace each document while keeping a single server-held key.\nexport function computeConsentMarkdownHash(documentKey: string, markdown: string): string {\n return createHmac('sha256', getCheckoutAccessTokenSecret())\n .update(`${documentKey}\\n${markdown}`)\n .digest('hex')\n}\n\nexport function buildConsentProof(link: CheckoutLink, acceptedLegalConsents: PublicSubmitInput['acceptedLegalConsents']) {\n const proof: Record<string, unknown> = {}\n const legalDocuments = link.legalDocuments && typeof link.legalDocuments === 'object'\n ? link.legalDocuments as Record<string, { title?: string; markdown?: string; required?: boolean }>\n : {}\n for (const key of ['terms', 'privacyPolicy']) {\n const document = legalDocuments[key]\n if (!document?.markdown) continue\n const accepted = acceptedLegalConsents?.[key as keyof PublicSubmitInput['acceptedLegalConsents']] === true\n if (!accepted) continue\n proof[key] = {\n title: document.title ?? key,\n required: document.required === true,\n acceptedAt: new Date().toISOString(),\n markdownHash: computeConsentMarkdownHash(key, document.markdown),\n }\n }\n return proof\n}\n\nexport function resolveSubmittedAmount(link: CheckoutLink, input: PublicSubmitInput): { amount: number; currencyCode: string; selectedPriceItemId: string | null } {\n if (link.pricingMode === 'fixed') {\n const expected = toMoneyNumber(link.fixedPriceAmount)\n if (expected == null || !link.fixedPriceCurrencyCode) {\n throw new CrudHttpError(422, { error: 'checkout.payPage.errors.submit' })\n }\n if (input.amount != null && Number(input.amount) !== expected) {\n throw new CrudHttpError(422, { error: 'checkout.payPage.errors.submit' })\n }\n return { amount: expected, currencyCode: link.fixedPriceCurrencyCode, selectedPriceItemId: null }\n }\n if (link.pricingMode === 'custom_amount') {\n if (input.amount == null || !link.customAmountCurrencyCode) {\n throw new CrudHttpError(422, {\n error: 'checkout.payPage.validation.fixErrors',\n fieldErrors: { amount: 'checkout.payPage.validation.amountRequired' },\n })\n }\n const min = toMoneyNumber(link.customAmountMin) ?? 0\n const max = toMoneyNumber(link.customAmountMax)\n const amount = Number(input.amount)\n if (amount < min || (max != null && amount > max)) {\n throw new CrudHttpError(422, {\n error: 'checkout.payPage.validation.fixErrors',\n fieldErrors: { amount: 'checkout.payPage.errors.submit' },\n })\n }\n return { amount, currencyCode: link.customAmountCurrencyCode, selectedPriceItemId: null }\n }\n const selectedPriceItem = (link.priceListItems ?? []).find((item) => item.id === input.selectedPriceItemId)\n if (!selectedPriceItem) {\n throw new CrudHttpError(422, {\n error: 'checkout.payPage.validation.fixErrors',\n fieldErrors: { selectedPriceItemId: 'checkout.payPage.validation.priceSelectionRequired' },\n })\n }\n if (input.amount != null && Number(input.amount) !== Number(selectedPriceItem.amount)) {\n throw new CrudHttpError(422, { error: 'checkout.payPage.errors.submit' })\n }\n return {\n amount: Number(selectedPriceItem.amount),\n currencyCode: selectedPriceItem.currencyCode,\n selectedPriceItemId: selectedPriceItem.id,\n }\n}\n\nexport function serializeTemplateOrLink(record: CheckoutLinkTemplate | CheckoutLink) {\n const logoPreviewUrl = buildCheckoutAttachmentPreviewUrl(record.logoAttachmentId) ?? record.logoUrl ?? null\n return {\n id: record.id,\n name: record.name,\n title: record.title ?? null,\n subtitle: record.subtitle ?? null,\n description: record.description ?? null,\n logoAttachmentId: record.logoAttachmentId ?? null,\n logoUrl: record.logoUrl ?? null,\n logoPreviewUrl,\n primaryColor: record.primaryColor ?? null,\n secondaryColor: record.secondaryColor ?? null,\n backgroundColor: record.backgroundColor ?? null,\n themeMode: record.themeMode,\n pricingMode: record.pricingMode,\n fixedPriceAmount: toMoneyNumber(record.fixedPriceAmount),\n fixedPriceCurrencyCode: record.fixedPriceCurrencyCode ?? null,\n fixedPriceIncludesTax: record.fixedPriceIncludesTax,\n fixedPriceOriginalAmount: toMoneyNumber(record.fixedPriceOriginalAmount),\n customAmountMin: toMoneyNumber(record.customAmountMin),\n customAmountMax: toMoneyNumber(record.customAmountMax),\n customAmountCurrencyCode: record.customAmountCurrencyCode ?? null,\n priceListItems: record.priceListItems ?? [],\n gatewayProviderKey: record.gatewayProviderKey ?? null,\n gatewaySettings: normalizeJsonRecord(record.gatewaySettings),\n customFieldsetCode: record.customFieldsetCode ?? null,\n collectCustomerDetails: record.collectCustomerDetails,\n customerFieldsSchema: record.customerFieldsSchema ?? [],\n legalDocuments: record.legalDocuments ?? {},\n displayCustomFieldsOnPage: record.displayCustomFieldsOnPage,\n successTitle: record.successTitle ?? null,\n successMessage: record.successMessage ?? null,\n cancelTitle: record.cancelTitle ?? null,\n cancelMessage: record.cancelMessage ?? null,\n errorTitle: record.errorTitle ?? null,\n errorMessage: record.errorMessage ?? null,\n successEmailSubject: record.successEmailSubject ?? null,\n successEmailBody: record.successEmailBody ?? null,\n sendSuccessEmail: record.sendSuccessEmail,\n errorEmailSubject: record.errorEmailSubject ?? null,\n errorEmailBody: record.errorEmailBody ?? null,\n sendErrorEmail: record.sendErrorEmail,\n startEmailSubject: record.startEmailSubject ?? null,\n startEmailBody: record.startEmailBody ?? null,\n sendStartEmail: record.sendStartEmail,\n maxCompletions: record.maxCompletions ?? null,\n status: record.status,\n checkoutType: record.checkoutType,\n createdAt: toIsoString(record.createdAt),\n updatedAt: toIsoString(record.updatedAt),\n ...(isCheckoutLinkRecord(record) ? {\n slug: record.slug,\n templateId: record.templateId ?? null,\n completionCount: record.completionCount,\n activeReservationCount: record.activeReservationCount,\n isLocked: record.isLocked,\n } : {}),\n }\n}\n\nexport function serializeTransaction(record: CheckoutTransaction, link?: CheckoutLink | null, includePii = false) {\n return {\n id: record.id,\n linkId: record.linkId,\n linkName: link?.name ?? null,\n linkSlug: link?.slug ?? null,\n amount: toMoneyNumber(record.amount),\n currencyCode: record.currencyCode,\n status: record.status,\n paymentStatus: record.paymentStatus ?? null,\n gatewayTransactionId: record.gatewayTransactionId ?? null,\n selectedPriceItemId: record.selectedPriceItemId ?? null,\n acceptedLegalConsents: includePii ? normalizeMaybeStringifiedJsonObject(record.acceptedLegalConsents) : null,\n customerData: includePii ? normalizeMaybeStringifiedJsonObject(record.customerData) : null,\n firstName: includePii ? (record.firstName ?? null) : null,\n lastName: includePii ? (record.lastName ?? null) : null,\n email: includePii ? (record.email ?? null) : null,\n phone: includePii ? (record.phone ?? null) : null,\n ipAddress: includePii ? (record.ipAddress ?? null) : null,\n userAgent: includePii ? (record.userAgent ?? null) : null,\n createdAt: toIsoString(record.createdAt),\n updatedAt: toIsoString(record.updatedAt),\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["normalizeOptionalString", "buildCheckoutAttachmentPreviewUrl"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/checkout",
|
|
3
|
-
"version": "0.6.5-develop.
|
|
3
|
+
"version": "0.6.5-develop.5033.1.c970204a3f",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -61,18 +61,18 @@
|
|
|
61
61
|
}
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@open-mercato/core": "0.6.5-develop.
|
|
65
|
-
"@open-mercato/ui": "0.6.5-develop.
|
|
64
|
+
"@open-mercato/core": "0.6.5-develop.5033.1.c970204a3f",
|
|
65
|
+
"@open-mercato/ui": "0.6.5-develop.5033.1.c970204a3f",
|
|
66
66
|
"bcryptjs": "^3.0.3"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
69
|
"@mikro-orm/postgresql": "^7.0.14",
|
|
70
|
-
"@open-mercato/shared": "0.6.5-develop.
|
|
70
|
+
"@open-mercato/shared": "0.6.5-develop.5033.1.c970204a3f",
|
|
71
71
|
"react": "^19.0.0",
|
|
72
72
|
"react-dom": "^19.0.0"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
|
-
"@open-mercato/shared": "0.6.5-develop.
|
|
75
|
+
"@open-mercato/shared": "0.6.5-develop.5033.1.c970204a3f",
|
|
76
76
|
"@types/jest": "^30.0.0",
|
|
77
77
|
"@types/react": "^19.2.16",
|
|
78
78
|
"@types/react-dom": "^19.2.3",
|
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
} from './helpers/fixtures'
|
|
15
15
|
|
|
16
16
|
test.describe('TC-CHKT-008: Attempt update on locked link, verify 422', () => {
|
|
17
|
+
test.setTimeout(120_000)
|
|
18
|
+
|
|
17
19
|
test('rejects link edits after the first transaction locks the record', async ({ request }) => {
|
|
20
|
+
test.slow()
|
|
21
|
+
|
|
18
22
|
let token: string | null = null
|
|
19
23
|
let linkId: string | null = null
|
|
20
24
|
let transactionId: string | null = null
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHmac } from 'crypto'
|
|
1
2
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
3
|
import { clearPaymentGatewayDescriptors, registerPaymentGatewayDescriptor } from '@open-mercato/shared/modules/payment_gateways/types'
|
|
3
4
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
@@ -157,6 +158,32 @@ describe('checkout utils', () => {
|
|
|
157
158
|
expect(buildConsentProof(link, { terms: true, privacyPolicy: false })).not.toHaveProperty('privacyPolicy')
|
|
158
159
|
})
|
|
159
160
|
|
|
161
|
+
it('derives consent markdownHash from the server secret so it is tamper-evident', () => {
|
|
162
|
+
const link = createLink({
|
|
163
|
+
legalDocuments: {
|
|
164
|
+
terms: { title: 'Terms', markdown: 'terms body', required: true },
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
process.env.AUTH_SECRET = 'consent-secret-a'
|
|
169
|
+
const proofWithSecretA = buildConsentProof(link, { terms: true, privacyPolicy: false }) as {
|
|
170
|
+
terms: { markdownHash: string }
|
|
171
|
+
}
|
|
172
|
+
const hashWithSecretA = proofWithSecretA.terms.markdownHash
|
|
173
|
+
|
|
174
|
+
const forgedHashFromPublicConstant = createHmac('sha256', 'terms').update('terms body').digest('hex')
|
|
175
|
+
expect(hashWithSecretA).not.toBe(forgedHashFromPublicConstant)
|
|
176
|
+
|
|
177
|
+
const forgedHashFromPlainSha = createHmac('sha256', 'terms').update('terms\nterms body').digest('hex')
|
|
178
|
+
expect(hashWithSecretA).not.toBe(forgedHashFromPlainSha)
|
|
179
|
+
|
|
180
|
+
process.env.AUTH_SECRET = 'consent-secret-b'
|
|
181
|
+
const proofWithSecretB = buildConsentProof(link, { terms: true, privacyPolicy: false }) as {
|
|
182
|
+
terms: { markdownHash: string }
|
|
183
|
+
}
|
|
184
|
+
expect(proofWithSecretB.terms.markdownHash).not.toBe(hashWithSecretA)
|
|
185
|
+
})
|
|
186
|
+
|
|
160
187
|
it('verifies password access tokens only for the matching slug', () => {
|
|
161
188
|
const token = signCheckoutAccessToken('launch-offer', {
|
|
162
189
|
linkId: 'link_1',
|
|
@@ -208,6 +235,91 @@ describe('checkout utils', () => {
|
|
|
208
235
|
})).toBe(true)
|
|
209
236
|
})
|
|
210
237
|
|
|
238
|
+
// #2675 — the checkout access cookie used to embed the link's bcrypt
|
|
239
|
+
// passwordHash verbatim as `sessionVersion`. The payload segment is only
|
|
240
|
+
// signed, not encrypted, so anyone with the cookie could base64url-decode
|
|
241
|
+
// it and run an offline crack against the hash. The fix derives a
|
|
242
|
+
// non-reversible HMAC of the input before embedding it.
|
|
243
|
+
describe('access cookie payload does not expose the raw sessionVersion (#2675)', () => {
|
|
244
|
+
const bcryptHash = '$2b$10$abcdefghijklmnopqrstuv1234567890ABCDEFGHIJKLMNOPQRSTUVwxyz12'
|
|
245
|
+
const BCRYPT_PREFIX_RE = /^\$2[abxy]\$/
|
|
246
|
+
|
|
247
|
+
function decodePayloadSegment(token: string): { sessionVersion?: string | null } {
|
|
248
|
+
const [encoded] = token.split('.')
|
|
249
|
+
const json = Buffer.from(encoded, 'base64url').toString('utf-8')
|
|
250
|
+
return JSON.parse(json) as { sessionVersion?: string | null }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
it('never embeds a bcrypt-shaped sessionVersion in the payload', () => {
|
|
254
|
+
const token = signCheckoutAccessToken('launch-offer', {
|
|
255
|
+
linkId: 'link_1',
|
|
256
|
+
sessionVersion: bcryptHash,
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
const payload = decodePayloadSegment(token)
|
|
260
|
+
expect(payload.sessionVersion).toBeTruthy()
|
|
261
|
+
expect(payload.sessionVersion).not.toBe(bcryptHash)
|
|
262
|
+
expect(payload.sessionVersion ?? '').not.toMatch(BCRYPT_PREFIX_RE)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('verifies a cookie signed with the same passwordHash', () => {
|
|
266
|
+
const token = signCheckoutAccessToken('launch-offer', {
|
|
267
|
+
linkId: 'link_1',
|
|
268
|
+
sessionVersion: bcryptHash,
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
expect(verifyCheckoutAccessToken(token, 'launch-offer', {
|
|
272
|
+
linkId: 'link_1',
|
|
273
|
+
sessionVersion: bcryptHash,
|
|
274
|
+
})).toBe(true)
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('rotates the cookie when the passwordHash changes (old token rejected against new hash)', () => {
|
|
278
|
+
const token = signCheckoutAccessToken('launch-offer', {
|
|
279
|
+
linkId: 'link_1',
|
|
280
|
+
sessionVersion: bcryptHash,
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
const rotatedHash = '$2b$10$ZZZZZZZZZZZZZZZZZZZZZuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuQ'
|
|
284
|
+
expect(verifyCheckoutAccessToken(token, 'launch-offer', {
|
|
285
|
+
linkId: 'link_1',
|
|
286
|
+
sessionVersion: rotatedHash,
|
|
287
|
+
})).toBe(false)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('handles a null sessionVersion defensively without revealing input', () => {
|
|
291
|
+
const token = signCheckoutAccessToken('launch-offer', {
|
|
292
|
+
linkId: 'link_1',
|
|
293
|
+
sessionVersion: null,
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
const payload = decodePayloadSegment(token)
|
|
297
|
+
expect(payload.sessionVersion).toBeNull()
|
|
298
|
+
// A caller that does not enforce a sessionVersion still accepts the cookie
|
|
299
|
+
// (the signature + slug + linkId + exp are the floor of authenticity).
|
|
300
|
+
expect(verifyCheckoutAccessToken(token, 'launch-offer', {
|
|
301
|
+
linkId: 'link_1',
|
|
302
|
+
})).toBe(true)
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('produces a different cookie payload for two different bcrypt hashes (rotation semantics intact)', () => {
|
|
306
|
+
const tokenA = signCheckoutAccessToken('launch-offer', {
|
|
307
|
+
linkId: 'link_1',
|
|
308
|
+
sessionVersion: bcryptHash,
|
|
309
|
+
})
|
|
310
|
+
const tokenB = signCheckoutAccessToken('launch-offer', {
|
|
311
|
+
linkId: 'link_1',
|
|
312
|
+
sessionVersion: '$2b$10$differentdifferentdifferentdifferentdifferentdifferent',
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
const decodedA = decodePayloadSegment(tokenA).sessionVersion
|
|
316
|
+
const decodedB = decodePayloadSegment(tokenB).sessionVersion
|
|
317
|
+
expect(decodedA).toBeTruthy()
|
|
318
|
+
expect(decodedB).toBeTruthy()
|
|
319
|
+
expect(decodedA).not.toBe(decodedB)
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
211
323
|
it('releases the link lock when a reserved transaction reaches a terminal status', () => {
|
|
212
324
|
const link = createLink({
|
|
213
325
|
activeReservationCount: 1,
|
|
@@ -264,6 +264,17 @@ function normalizeCheckoutAccessSessionVersion(value: Date | string | null | und
|
|
|
264
264
|
return value instanceof Date ? value.toISOString() : value
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
// Derive a non-reversible cookie-safe representation of `sessionVersion`
|
|
268
|
+
// so the embedded payload never exposes the raw input (typically the
|
|
269
|
+
// checkout link's bcrypt passwordHash — see #2675). The HMAC keeps
|
|
270
|
+
// rotation semantics intact: any change to the input produces a fresh
|
|
271
|
+
// digest, invalidating outstanding cookies.
|
|
272
|
+
function deriveCheckoutAccessSessionVersion(value: Date | string | null | undefined): string | null {
|
|
273
|
+
const normalized = normalizeCheckoutAccessSessionVersion(value)
|
|
274
|
+
if (normalized == null) return null
|
|
275
|
+
return createHmac('sha256', getCheckoutAccessTokenSecret()).update(normalized).digest('base64url')
|
|
276
|
+
}
|
|
277
|
+
|
|
267
278
|
export function signCheckoutAccessToken(
|
|
268
279
|
slug: string,
|
|
269
280
|
options?: { linkId?: string | null; sessionVersion?: Date | string | null },
|
|
@@ -271,7 +282,7 @@ export function signCheckoutAccessToken(
|
|
|
271
282
|
const payload = Buffer.from(JSON.stringify({
|
|
272
283
|
slug,
|
|
273
284
|
linkId: options?.linkId ?? null,
|
|
274
|
-
sessionVersion:
|
|
285
|
+
sessionVersion: deriveCheckoutAccessSessionVersion(options?.sessionVersion),
|
|
275
286
|
exp: Date.now() + (60 * 60 * 1000),
|
|
276
287
|
}), 'utf-8').toString('base64url')
|
|
277
288
|
const signature = createHmac('sha256', getCheckoutAccessTokenSecret()).update(payload).digest('base64url')
|
|
@@ -299,7 +310,7 @@ export function verifyCheckoutAccessToken(
|
|
|
299
310
|
if (parsed.slug !== slug || typeof parsed.exp !== 'number' || parsed.exp <= Date.now()) return false
|
|
300
311
|
if (options?.linkId && parsed.linkId !== options.linkId) return false
|
|
301
312
|
if (options?.sessionVersion) {
|
|
302
|
-
return parsed.sessionVersion ===
|
|
313
|
+
return parsed.sessionVersion === deriveCheckoutAccessSessionVersion(options.sessionVersion)
|
|
303
314
|
}
|
|
304
315
|
return true
|
|
305
316
|
} catch {
|
|
@@ -339,6 +350,17 @@ export function applyTerminalTransactionState(
|
|
|
339
350
|
}
|
|
340
351
|
}
|
|
341
352
|
|
|
353
|
+
// Security model: the consent proof is a GDPR/consent audit trail, so `markdownHash`
|
|
354
|
+
// must be tamper-evident. It is an HMAC keyed with the server-side checkout secret
|
|
355
|
+
// (never a public constant), which an attacker who can edit a stored consent row
|
|
356
|
+
// cannot recompute without the secret. The document key is folded into the HMAC
|
|
357
|
+
// message to namespace each document while keeping a single server-held key.
|
|
358
|
+
export function computeConsentMarkdownHash(documentKey: string, markdown: string): string {
|
|
359
|
+
return createHmac('sha256', getCheckoutAccessTokenSecret())
|
|
360
|
+
.update(`${documentKey}\n${markdown}`)
|
|
361
|
+
.digest('hex')
|
|
362
|
+
}
|
|
363
|
+
|
|
342
364
|
export function buildConsentProof(link: CheckoutLink, acceptedLegalConsents: PublicSubmitInput['acceptedLegalConsents']) {
|
|
343
365
|
const proof: Record<string, unknown> = {}
|
|
344
366
|
const legalDocuments = link.legalDocuments && typeof link.legalDocuments === 'object'
|
|
@@ -353,7 +375,7 @@ export function buildConsentProof(link: CheckoutLink, acceptedLegalConsents: Pub
|
|
|
353
375
|
title: document.title ?? key,
|
|
354
376
|
required: document.required === true,
|
|
355
377
|
acceptedAt: new Date().toISOString(),
|
|
356
|
-
markdownHash:
|
|
378
|
+
markdownHash: computeConsentMarkdownHash(key, document.markdown),
|
|
357
379
|
}
|
|
358
380
|
}
|
|
359
381
|
return proof
|