@aglyn/plugins-commerce 1.0.0-beta.220 → 1.0.0-beta.222
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 +14 -13
- package/src/lib/components/console/catalog-organization-card.component.js +9 -0
- package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
- package/src/lib/components/console/discounts-card.component.js +8 -0
- package/src/lib/components/console/discounts-card.component.js.map +1 -1
- package/src/lib/components/console/gift-cards-card.component.js +8 -0
- package/src/lib/components/console/gift-cards-card.component.js.map +1 -1
- package/src/lib/components/console/host-coupons-card.component.js +8 -0
- package/src/lib/components/console/host-coupons-card.component.js.map +1 -1
- package/src/lib/components/console/host-orders-card.component.js +34 -70
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/product-zones.d.ts +7 -6
- package/src/lib/components/console/product-zones.js +5 -4
- package/src/lib/components/console/product-zones.js.map +1 -1
- package/src/lib/components/console/products-hub-card.component.js +86 -284
- package/src/lib/components/console/products-hub-card.component.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +38 -0
- package/src/lib/declarations.console-server.js +67 -0
- package/src/lib/declarations.console-server.js.map +1 -0
- package/src/lib/model/commerce-gift-cards.d.ts +26 -0
- package/src/lib/model/commerce-gift-cards.js +42 -0
- package/src/lib/model/commerce-gift-cards.js.map +1 -1
- package/src/lib/model/commerce-orders.d.ts +0 -57
- package/src/lib/model/commerce-orders.js +0 -80
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/index.d.ts +0 -1
- package/src/lib/model/index.js +0 -1
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/plugin.js +5 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/api-v1/openapi.d.ts +27 -0
- package/src/lib/server/api-v1/openapi.js +219 -0
- package/src/lib/server/api-v1/openapi.js.map +1 -0
- package/src/lib/server/api-v1/orders-and-products.d.ts +19 -0
- package/src/lib/server/api-v1/orders-and-products.js +419 -0
- package/src/lib/server/api-v1/orders-and-products.js.map +1 -0
- package/src/lib/server/billing-webhook.js +83 -56
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/download.js +4 -1
- package/src/lib/server/download.js.map +1 -1
- package/src/lib/server/fulfill-order.d.ts +52 -11
- package/src/lib/server/fulfill-order.js +6 -9
- package/src/lib/server/fulfill-order.js.map +1 -1
- package/src/lib/server/gift-card-issue.d.ts +57 -0
- package/src/lib/server/gift-card-issue.js +155 -0
- package/src/lib/server/gift-card-issue.js.map +1 -0
- package/src/lib/server/gift-cards.js +25 -84
- package/src/lib/server/gift-cards.js.map +1 -1
- package/src/lib/server/membership-register.js +9 -8
- package/src/lib/server/membership-register.js.map +1 -1
- package/src/lib/server/newsletter.js +7 -5
- package/src/lib/server/newsletter.js.map +1 -1
- package/src/lib/server/paid-media-delivery.d.ts +21 -0
- package/src/lib/server/paid-media-delivery.js +23 -5
- package/src/lib/server/paid-media-delivery.js.map +1 -1
- package/src/lib/server/paid-media-serve-count.d.ts +28 -0
- package/src/lib/server/paid-media-serve-count.js +46 -0
- package/src/lib/server/paid-media-serve-count.js.map +1 -0
- package/src/lib/server/person-eraser.d.ts +45 -0
- package/src/lib/server/person-eraser.js +57 -0
- package/src/lib/server/person-eraser.js.map +1 -0
- package/src/lib/server/pos-order.js +17 -9
- package/src/lib/server/pos-order.js.map +1 -1
- package/src/lib/server/refund.js +15 -15
- package/src/lib/server/refund.js.map +1 -1
- package/src/lib/server/restock-flag.d.ts +2 -2
- package/src/lib/server/restock-flag.js +1 -1
- package/src/lib/server/restock-flag.js.map +1 -1
- package/src/lib/server/stream.js +4 -1
- package/src/lib/server/stream.js.map +1 -1
- package/src/lib/server.js +5 -21
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-person-records.d.ts +43 -0
- package/src/lib/testing/stand-in-person-records.js +63 -0
- package/src/lib/testing/stand-in-person-records.js.map +1 -0
- package/src/lib/testing/stand-in-record-system.d.ts +11 -2
- package/src/lib/testing/stand-in-record-system.js +5 -3
- package/src/lib/testing/stand-in-record-system.js.map +1 -1
- package/src/lib/transfer/gift-card-confirm-step.component.d.ts +22 -0
- package/src/lib/transfer/gift-card-confirm-step.component.js +259 -0
- package/src/lib/transfer/gift-card-confirm-step.component.js.map +1 -0
- package/src/lib/transfer/gift-cards-plan.d.ts +29 -0
- package/src/lib/transfer/gift-cards-plan.js +40 -0
- package/src/lib/transfer/gift-cards-plan.js.map +1 -0
- package/src/lib/transfer/gift-cards.server.d.ts +34 -0
- package/src/lib/transfer/gift-cards.server.js +296 -0
- package/src/lib/transfer/gift-cards.server.js.map +1 -0
- package/src/lib/transfer/list-filter.d.ts +29 -0
- package/src/lib/transfer/list-filter.js +112 -0
- package/src/lib/transfer/list-filter.js.map +1 -0
- package/src/lib/transfer/product-import-results.d.ts +29 -0
- package/src/lib/transfer/product-import-results.js +49 -0
- package/src/lib/transfer/product-import-results.js.map +1 -0
- package/src/lib/transfer/product-import-step.component.d.ts +30 -0
- package/src/lib/transfer/product-import-step.component.js +99 -0
- package/src/lib/transfer/product-import-step.component.js.map +1 -0
- package/src/lib/transfer/product-transfer.d.ts +211 -0
- package/src/lib/transfer/product-transfer.js +1594 -0
- package/src/lib/transfer/product-transfer.js.map +1 -0
- package/src/lib/transfer/products.server.d.ts +44 -0
- package/src/lib/transfer/products.server.js +550 -0
- package/src/lib/transfer/products.server.js.map +1 -0
- package/src/lib/transfer/records-transfer.d.ts +182 -0
- package/src/lib/transfer/records-transfer.js +1153 -0
- package/src/lib/transfer/records-transfer.js.map +1 -0
- package/src/lib/transfer/records.server.d.ts +68 -0
- package/src/lib/transfer/records.server.js +455 -0
- package/src/lib/transfer/records.server.js.map +1 -0
- package/src/lib/transfer/register-transfer-resources.d.ts +17 -0
- package/src/lib/transfer/register-transfer-resources.js +171 -0
- package/src/lib/transfer/register-transfer-resources.js.map +1 -0
- package/src/lib/transfer/register-transfer-ui.d.ts +23 -0
- package/src/lib/transfer/register-transfer-ui.js +78 -0
- package/src/lib/transfer/register-transfer-ui.js.map +1 -0
- package/src/lib/transfer/server-common.d.ts +76 -0
- package/src/lib/transfer/server-common.js +165 -0
- package/src/lib/transfer/server-common.js.map +1 -0
- package/src/lib/transfer/transfer-header-actions.component.d.ts +39 -0
- package/src/lib/transfer/transfer-header-actions.component.js +79 -0
- package/src/lib/transfer/transfer-header-actions.component.js.map +1 -0
- package/src/lib/transfer/transfer-keys.d.ts +31 -0
- package/src/lib/transfer/transfer-keys.js +56 -0
- package/src/lib/transfer/transfer-keys.js.map +1 -0
- package/src/lib/model/commerce-io.d.ts +0 -36
- package/src/lib/model/commerce-io.js +0 -245
- package/src/lib/model/commerce-io.js.map +0 -1
- package/src/lib/server/contact-refund.d.ts +0 -117
- package/src/lib/server/contact-refund.js +0 -196
- package/src/lib/server/contact-refund.js.map +0 -1
|
@@ -89,7 +89,10 @@ import { mediaCdnScopeRefusal, parseMediaCdnScope, selectAutoRendition } from "@
|
|
|
89
89
|
location: redirect.location,
|
|
90
90
|
expiresAtMs: redirect.expiresAtMs,
|
|
91
91
|
scope: asset.deliveryScope,
|
|
92
|
-
mediaId: asset.mediaId
|
|
92
|
+
mediaId: asset.mediaId,
|
|
93
|
+
collection: asset.collection,
|
|
94
|
+
scopeId: asset.scopeId,
|
|
95
|
+
sizeBytes: redirect.sizeBytes
|
|
93
96
|
};
|
|
94
97
|
};
|
|
95
98
|
const deliverAsset = async (scope, mediaId)=>{
|
|
@@ -174,6 +177,7 @@ import { mediaCdnScopeRefusal, parseMediaCdnScope, selectAutoRendition } from "@
|
|
|
174
177
|
return deliverAsset(source.scope, source.mediaId);
|
|
175
178
|
case 'storage-object':
|
|
176
179
|
{
|
|
180
|
+
var _ref;
|
|
177
181
|
if (!io.bucketName || source.bucket !== io.bucketName) {
|
|
178
182
|
return refuse('foreign-storage');
|
|
179
183
|
}
|
|
@@ -197,14 +201,21 @@ import { mediaCdnScopeRefusal, parseMediaCdnScope, selectAutoRendition } from "@
|
|
|
197
201
|
if (asset) return deliverAsset(asset.scope, asset.mediaId);
|
|
198
202
|
assertMediaSignatureTtl(ttlMs);
|
|
199
203
|
const expiresAtMs = nowMs + ttlMs;
|
|
204
|
+
const [location, sizeBytes] = await Promise.all([
|
|
205
|
+
io.signStorageRead(objectPath, expiresAtMs, {
|
|
206
|
+
attachment: storageReadIsAttachment(objectPath, options.cdnParams)
|
|
207
|
+
}),
|
|
208
|
+
(_ref = io.storageObjectSize == null ? void 0 : io.storageObjectSize.call(io, objectPath).catch(()=>0)) != null ? _ref : Promise.resolve(0)
|
|
209
|
+
]);
|
|
200
210
|
return {
|
|
201
211
|
ok: true,
|
|
202
212
|
via: 'signed-storage',
|
|
203
|
-
location
|
|
204
|
-
attachment: storageReadIsAttachment(objectPath, options.cdnParams)
|
|
205
|
-
}),
|
|
213
|
+
location,
|
|
206
214
|
expiresAtMs,
|
|
207
|
-
objectPath
|
|
215
|
+
objectPath,
|
|
216
|
+
collection: segments[0] === 'orgs' ? 'orgs' : 'hosts',
|
|
217
|
+
scopeId: segments[1],
|
|
218
|
+
sizeBytes
|
|
208
219
|
};
|
|
209
220
|
}
|
|
210
221
|
}
|
|
@@ -232,6 +243,13 @@ import { mediaCdnScopeRefusal, parseMediaCdnScope, selectAutoRendition } from "@
|
|
|
232
243
|
responseDisposition: 'attachment'
|
|
233
244
|
} : {}));
|
|
234
245
|
return String(url);
|
|
246
|
+
},
|
|
247
|
+
storageObjectSize: async (objectPath)=>{
|
|
248
|
+
const file = bucket.file(objectPath);
|
|
249
|
+
if (!file.getMetadata) return 0;
|
|
250
|
+
const [metadata] = await file.getMetadata();
|
|
251
|
+
const size = Number(metadata == null ? void 0 : metadata.size);
|
|
252
|
+
return Number.isSafeInteger(size) && size > 0 ? size : 0;
|
|
235
253
|
}
|
|
236
254
|
};
|
|
237
255
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/paid-media-delivery.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 {\n hostQualifiedScope,\n MEDIA_CDN_RENDITION_AUTO,\n MEDIA_CDN_RENDITION_PARAM,\n MEDIA_CDN_ROUTE,\n paidMediaAssetOfObject,\n parseMediaRenditions,\n parsePaidMediaSource,\n} from '@aglyn/aglyn/server'\nimport {\n type MediaDeliveryProvider,\n mediaDeliveryProvider,\n} from '@aglyn/aglyn/plugin-manager/media-delivery-provider'\nimport { lookalikeBrandForHost } from '@aglyn/shared-util-email/outbound-phishing-screen'\nimport { mediaDeliveryRedirect } from '@aglyn/tenant-data-admin/server/media-delivery'\nimport {\n assertMediaSignatureTtl,\n mediaSignatureQuery,\n mintMediaSignature,\n} from '@aglyn/tenant-data-admin/server/media-signing'\nimport { isMediaStoragePathInScope } from '@aglyn/tenant-data-admin/server/media-storage-path'\nimport {\n mediaCdnScopeRefusal,\n parseMediaCdnScope,\n selectAutoRendition,\n} from '@aglyn/tenant-data-admin/server/serve-media-cdn'\n\n/**\n * Turning a URL stored on a product into a link a buyer may follow (AGL-2814).\n *\n * A product's paid media — its members videos — is stored as whatever the\n * author's picker wrote: a media reference, a CDN path, a raw Storage download\n * URL, or a hotlink somebody typed. `parsePaidMediaSource` says which of those\n * a value is. The commerce routes verify who is entitled and then redirect;\n * this module decides WHERE they may redirect, and its whole job is that the\n * answer is never a URL that outlives the purchase.\n *\n * ## The rules, in the order they are applied\n *\n * 1. **A library asset is delivered only when it is PRIVATE**, as a CDN URL\n * signed with {@link mintMediaSignature}. The CDN refuses a private asset on\n * every path without an unexpired signature, which is what makes the link\n * die. A public asset is refused here instead of redirected to: its URL\n * works for anyone forever, so handing it to a buyer is the leak itself.\n * 2. **A Storage download URL is never redirected to.** Its token lives as\n * long as the object. When the object belongs to a library asset, rule 1\n * applies to that asset. When no asset owns it (a legacy object, a derived\n * rendition file), it gets a V4 signed read URL with the same bounded\n * lifetime. When it is not in this platform's bucket, or not inside the\n * site's own library or its org's, it is refused.\n * 3. **Only the requesting site's own libraries are signed.** A product is\n * author data. Signing whatever scope it names would let one site's author\n * mint working links to another org's private files, so a host scope must\n * be this site and an org scope must be this site's org, and the signed\n * scope is always qualified with this site so the CDN's `visibleTo` check\n * runs against the site actually selling it.\n * 4. **A hotlink to somebody else's server passes through unchanged.** No code\n * of ours runs there and nothing here can make its URL expire. That is the\n * author's hosting decision, and the docs say so. EXCEPT a hotlink whose\n * host wears a brand it is not (`lookalikeBrandForHost`, the phishing\n * screen's rule): the redirect leaves from the merchant's own domain, so a\n * lure behind it is one no screen of the page or the receipt ever saw. It\n * is refused for every workspace, like a lookalike link in an email\n * (AGL-3363), and the caller files it.\n * 5. **A signed Storage read is a download when it is one.** A download, or\n * an object whose type a browser would run (HTML, SVG, XML), is signed\n * with `response-content-disposition: attachment`, so the file is saved\n * rather than rendered (AGL-3363). The CDN already does this for its own\n * deliveries; a V4 URL carried nothing.\n *\n * ## A video with a copy at the delivery provider (AGL-2824)\n *\n * A caller that asks for it with `delivery` gets, for a video that passes\n * rule 1, the delivery provider's own short-lived URL instead of the signed CDN\n * URL, whenever `mediaDeliveryRedirect` finds a current copy and the release\n * flag is on for the owning org. It is the URL the CDN would redirect the\n * signed request to anyway, minted one hop earlier and bounded by the same\n * lifetime. Anything else takes the signed CDN URL exactly as before.\n */\n\n/** Why a stored URL was not turned into a link. */\nexport type PaidMediaRefusal =\n /** A library asset the public can still fetch without a signature. */\n | 'not-private'\n /** No such asset, a deleted one, or one not shared with this site. */\n | 'not-found'\n /** Another site's library, another org's, or outside the media prefixes. */\n | 'out-of-scope'\n /** A Storage URL for a bucket this platform does not serve from. */\n | 'foreign-storage'\n /** Not a value any delivery can be built from. */\n | 'malformed'\n /** A hotlink to a host that wears another brand's name (AGL-3363). */\n | 'lookalike'\n\nexport type PaidMediaDelivery =\n | {\n ok: true\n via: 'signed-cdn'\n location: string\n expiresAtMs: number\n /** The scope segment the signature covers. */\n scope: string\n mediaId: string\n }\n | {\n ok: true\n via: 'signed-storage'\n location: string\n expiresAtMs: number\n objectPath: string\n }\n | { ok: true; via: 'external'; location: string }\n | {\n ok: true\n via: 'delivery'\n location: string\n expiresAtMs: number\n /** The scope segment the delivery was minted for. */\n scope: string\n mediaId: string\n }\n | {\n ok: false\n refusal: PaidMediaRefusal\n /** For a `lookalike`: the host it wears a brand in, for the caller to file. */\n host?: string\n }\n\n/** A document read, reduced to what this module asks of one. */\nexport interface PaidMediaDocument {\n exists: boolean\n get(field: string): unknown\n}\n\n/** Every read and signature this module needs, injectable for tests. */\nexport interface PaidMediaDeliveryIo {\n /** The platform's media bucket; empty when unconfigured. */\n bucketName: string\n readMedia(\n collection: 'hosts' | 'orgs',\n scopeId: string,\n mediaId: string,\n ): Promise<PaidMediaDocument>\n /** The org a site belongs to, or null for an unindexed site. */\n orgIdForHost(hostId: string): Promise<string | null>\n /**\n * A V4 signed read URL for one object in {@link bucketName}. `attachment`\n * signs `response-content-disposition: attachment` into it (AGL-3363).\n */\n signStorageRead(\n objectPath: string,\n expiresAtMs: number,\n options?: { attachment?: boolean },\n ): Promise<string>\n}\n\n/** Object types a browser runs rather than shows: always saved, never rendered. */\nconst ACTIVE_OBJECT_EXTENSION = /\\.(html?|xhtml|xht|svgz?|xml|xsl|xslt|mht|mhtml|js|mjs)$/i\n\n/** Is this signed read a download, by the caller's ask or by what the object is? */\nexport function storageReadIsAttachment(\n objectPath: string,\n cdnParams: ReadonlyArray<readonly [string, string]> | undefined,\n): boolean {\n if ((cdnParams ?? []).some(([key, value]) => key === 'download' && value === '1')) {\n return true\n }\n return ACTIVE_OBJECT_EXTENSION.test(objectPath)\n}\n\n/**\n * Resolves a stored product media URL to a redirect target that expires, or\n * says why there is none. See the module comment for the rules.\n *\n * `cdnParams` ride on a signed CDN URL ahead of the signature, which covers\n * `(scope, mediaId, exp)` and never the parameters, so they select a\n * representation of the one asset the signature names and nothing else.\n */\nexport async function resolvePaidMediaDelivery(options: {\n stored: unknown\n /** The site selling the media; the only libraries it may sign from. */\n hostId: string\n /** The lifetime of the link; bounded by `assertMediaSignatureTtl`. */\n ttlMs: number\n cdnParams?: ReadonlyArray<readonly [string, string]>\n /**\n * Hand a video with a current copy at the delivery provider its URL there\n * (AGL-2824). `accept` is the request's `Accept`, which chooses the\n * rendition when `cdnParams` asks for `?r=auto`. The provider and the flag\n * verdict default to the registered provider and the release flag.\n */\n delivery?: {\n accept?: unknown\n provider?: MediaDeliveryProvider | null\n enabled?: (orgId: string | null) => Promise<boolean>\n }\n nowMs?: number\n io: PaidMediaDeliveryIo\n}): Promise<PaidMediaDelivery> {\n const { hostId, ttlMs, io } = options\n const nowMs = options.nowMs ?? Date.now()\n const refuse = (refusal: PaidMediaRefusal): PaidMediaDelivery => ({\n ok: false,\n refusal,\n })\n if (!hostId) return refuse('out-of-scope')\n\n // One lookup per call, and only for the forms that need to know the org.\n let orgId: Promise<string | null> | undefined\n const siteOrgId = () => (orgId ??= io.orgIdForHost(hostId))\n\n /**\n * The delivery provider's URL for a private library video, or null for\n * the signed CDN URL. The representation is the one the CDN would pick for\n * the same `?r=` and `Accept`, so the buyer gets the copy the signed URL\n * would have been redirected to.\n */\n const deliverFromProvider = async (\n delivery: NonNullable<typeof options.delivery>,\n asset: {\n collection: 'hosts' | 'orgs'\n scopeId: string\n orgScope: boolean\n deliveryScope: string\n mediaId: string\n document: PaidMediaDocument\n },\n ): Promise<PaidMediaDelivery | null> => {\n const provider =\n delivery.provider === undefined ? mediaDeliveryProvider('deliver') : delivery.provider\n if (!provider) return null\n const requested = (options.cdnParams ?? []).find(\n ([key]) => key === MEDIA_CDN_RENDITION_PARAM,\n )?.[1]\n const renditions = parseMediaRenditions(asset.document.get('videoRenditions'))\n const rendition = !requested\n ? undefined\n : requested === MEDIA_CDN_RENDITION_AUTO\n ? selectAutoRendition(renditions, delivery.accept)\n : renditions.find((entry) => entry.key === requested)\n const redirect = await mediaDeliveryRedirect({\n provider,\n asset: {\n collection: asset.collection,\n scopeId: asset.scopeId,\n mediaId: asset.mediaId,\n },\n document: asset.document,\n rendition,\n servedType: rendition ? rendition.contentType : asset.document.get('contentType'),\n claims: { scope: asset.deliveryScope, mediaId: asset.mediaId, hostId },\n orgId: async () => (asset.orgScope ? asset.scopeId : await siteOrgId()),\n ...(delivery.enabled ? { deliveryEnabled: delivery.enabled } : {}),\n ttlMs,\n nowMs,\n }).catch(() => null)\n if (!redirect) return null\n return {\n ok: true,\n via: 'delivery',\n location: redirect.location,\n expiresAtMs: redirect.expiresAtMs,\n scope: asset.deliveryScope,\n mediaId: asset.mediaId,\n }\n }\n\n const deliverAsset = async (\n scope: string,\n mediaId: string,\n ): Promise<PaidMediaDelivery> => {\n const parsed = parseMediaCdnScope(scope)\n if (!parsed) return refuse('malformed')\n let collection: 'hosts' | 'orgs'\n let deliveryScope: string\n if (parsed.isOrg) {\n const owner = await siteOrgId()\n if (!owner || parsed.scopeId !== owner) return refuse('out-of-scope')\n collection = 'orgs'\n deliveryScope = hostQualifiedScope(scope, hostId)\n } else {\n if (parsed.scopeId !== hostId) return refuse('out-of-scope')\n collection = 'hosts'\n deliveryScope = hostId\n }\n const deliveryParsed = parseMediaCdnScope(deliveryScope)\n if (!deliveryParsed) return refuse('malformed')\n const document = await io.readMedia(collection, parsed.scopeId, mediaId)\n if (!document?.exists || document.get('deletedAt')) {\n return refuse('not-found')\n }\n // The CDN would 404 an org asset this site is not allowed to use, so a\n // link to one is a link that cannot play.\n if (mediaCdnScopeRefusal(deliveryParsed, document.get('visibleTo'))) {\n return refuse('not-found')\n }\n if (document.get('private') !== true) return refuse('not-private')\n assertMediaSignatureTtl(ttlMs)\n const delivered = options.delivery\n ? await deliverFromProvider(options.delivery, {\n collection,\n scopeId: parsed.scopeId,\n orgScope: parsed.isOrg,\n deliveryScope,\n mediaId,\n document,\n })\n : null\n if (delivered) return delivered\n const signature = mintMediaSignature(deliveryScope, mediaId, nowMs, ttlMs)\n const params = new URLSearchParams()\n for (const [key, value] of options.cdnParams ?? []) {\n if (key !== 'exp' && key !== 'sig') params.set(key, value)\n }\n const prefix = params.toString()\n return {\n ok: true,\n via: 'signed-cdn',\n location:\n `${MEDIA_CDN_ROUTE}/${deliveryScope}/${mediaId}?` +\n `${prefix ? `${prefix}&` : ''}${mediaSignatureQuery(signature)}`,\n expiresAtMs: signature.exp,\n scope: deliveryScope,\n mediaId,\n }\n }\n\n const source = parsePaidMediaSource(options.stored)\n switch (source.kind) {\n case 'malformed':\n return refuse('malformed')\n case 'external': {\n let externalHost: string\n try {\n externalHost = new URL(source.url).hostname\n } catch {\n return refuse('malformed')\n }\n if (lookalikeBrandForHost(externalHost)) {\n return { ok: false, refusal: 'lookalike', host: externalHost }\n }\n return { ok: true, via: 'external', location: source.url }\n }\n case 'asset':\n return deliverAsset(source.scope, source.mediaId)\n case 'storage-object': {\n if (!io.bucketName || source.bucket !== io.bucketName) {\n return refuse('foreign-storage')\n }\n const { objectPath } = source\n const segments = objectPath.split('/')\n let base: string | null = null\n if (segments[0] === 'hosts' && segments[1] === hostId) {\n base = `hosts/${hostId}`\n } else if (segments[0] === 'orgs' && segments[1]) {\n const owner = await siteOrgId()\n if (owner && segments[1] === owner) base = `orgs/${owner}`\n }\n // The same prefix test the CDN applies to a document's `storagePath`,\n // which also refuses traversal and empty segments.\n if (!base || !isMediaStoragePathInScope(objectPath, base)) {\n return refuse('out-of-scope')\n }\n // An object keyed like an asset IS that asset, and the asset's own rules\n // decide — including refusing it when the asset is still public.\n const asset = paidMediaAssetOfObject(objectPath)\n if (asset) return deliverAsset(asset.scope, asset.mediaId)\n assertMediaSignatureTtl(ttlMs)\n const expiresAtMs = nowMs + ttlMs\n return {\n ok: true,\n via: 'signed-storage',\n location: await io.signStorageRead(objectPath, expiresAtMs, {\n attachment: storageReadIsAttachment(objectPath, options.cdnParams),\n }),\n expiresAtMs,\n objectPath,\n }\n }\n }\n}\n\n/**\n * The production reads behind {@link resolvePaidMediaDelivery}, built on the\n * caller's Firestore and bucket handles so a route's own mocks reach them.\n */\nexport function createPaidMediaDeliveryIo(options: {\n firestore: {\n collection(name: string): {\n doc(id: string): {\n get(): Promise<PaidMediaDocument>\n collection(name: string): {\n doc(id: string): { get(): Promise<PaidMediaDocument> }\n }\n }\n }\n }\n bucket: {\n name?: string\n file(path: string): {\n getSignedUrl(config: {\n version: 'v4'\n action: 'read'\n expires: number\n responseDisposition?: string\n }): Promise<[string] | string[]>\n }\n }\n}): PaidMediaDeliveryIo {\n const { firestore, bucket } = options\n return {\n bucketName: String(bucket?.name ?? ''),\n readMedia: (collection, scopeId, mediaId) =>\n firestore\n .collection(collection)\n .doc(scopeId)\n .collection('media')\n .doc(mediaId)\n .get(),\n orgIdForHost: async (hostId) => {\n const index = await firestore.collection('hostIndex').doc(hostId).get()\n const orgId = index.exists ? index.get('orgId') : undefined\n return typeof orgId === 'string' && orgId ? orgId : null\n },\n signStorageRead: async (objectPath, expiresAtMs, signOptions) => {\n const [url] = await bucket.file(objectPath).getSignedUrl({\n version: 'v4',\n action: 'read',\n expires: expiresAtMs,\n ...(signOptions?.attachment ? { responseDisposition: 'attachment' } : {}),\n })\n return String(url)\n },\n }\n}\n"],"names":["hostQualifiedScope","MEDIA_CDN_RENDITION_AUTO","MEDIA_CDN_RENDITION_PARAM","MEDIA_CDN_ROUTE","paidMediaAssetOfObject","parseMediaRenditions","parsePaidMediaSource","mediaDeliveryProvider","lookalikeBrandForHost","mediaDeliveryRedirect","assertMediaSignatureTtl","mediaSignatureQuery","mintMediaSignature","isMediaStoragePathInScope","mediaCdnScopeRefusal","parseMediaCdnScope","selectAutoRendition","ACTIVE_OBJECT_EXTENSION","storageReadIsAttachment","objectPath","cdnParams","some","key","value","test","resolvePaidMediaDelivery","options","hostId","ttlMs","io","nowMs","Date","now","refuse","refusal","ok","orgId","siteOrgId","orgIdForHost","deliverFromProvider","delivery","asset","provider","undefined","requested","find","renditions","document","get","rendition","accept","entry","redirect","collection","scopeId","mediaId","servedType","contentType","claims","scope","deliveryScope","orgScope","enabled","deliveryEnabled","catch","via","location","expiresAtMs","deliverAsset","parsed","isOrg","owner","deliveryParsed","readMedia","exists","delivered","signature","params","URLSearchParams","set","prefix","toString","exp","source","stored","kind","externalHost","URL","url","hostname","host","bucketName","bucket","segments","split","base","signStorageRead","attachment","createPaidMediaDeliveryIo","firestore","String","name","doc","index","signOptions","file","getSignedUrl","version","action","expires","responseDisposition"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/paid-media-delivery.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 {\n hostQualifiedScope,\n MEDIA_CDN_RENDITION_AUTO,\n MEDIA_CDN_RENDITION_PARAM,\n MEDIA_CDN_ROUTE,\n paidMediaAssetOfObject,\n parseMediaRenditions,\n parsePaidMediaSource,\n} from '@aglyn/aglyn/server'\nimport {\n type MediaDeliveryProvider,\n mediaDeliveryProvider,\n} from '@aglyn/aglyn/plugin-manager/media-delivery-provider'\nimport { lookalikeBrandForHost } from '@aglyn/shared-util-email/outbound-phishing-screen'\nimport { mediaDeliveryRedirect } from '@aglyn/tenant-data-admin/server/media-delivery'\nimport {\n assertMediaSignatureTtl,\n mediaSignatureQuery,\n mintMediaSignature,\n} from '@aglyn/tenant-data-admin/server/media-signing'\nimport { isMediaStoragePathInScope } from '@aglyn/tenant-data-admin/server/media-storage-path'\nimport {\n mediaCdnScopeRefusal,\n parseMediaCdnScope,\n selectAutoRendition,\n} from '@aglyn/tenant-data-admin/server/serve-media-cdn'\n\n/**\n * Turning a URL stored on a product into a link a buyer may follow (AGL-2814).\n *\n * A product's paid media — its members videos — is stored as whatever the\n * author's picker wrote: a media reference, a CDN path, a raw Storage download\n * URL, or a hotlink somebody typed. `parsePaidMediaSource` says which of those\n * a value is. The commerce routes verify who is entitled and then redirect;\n * this module decides WHERE they may redirect, and its whole job is that the\n * answer is never a URL that outlives the purchase.\n *\n * ## The rules, in the order they are applied\n *\n * 1. **A library asset is delivered only when it is PRIVATE**, as a CDN URL\n * signed with {@link mintMediaSignature}. The CDN refuses a private asset on\n * every path without an unexpired signature, which is what makes the link\n * die. A public asset is refused here instead of redirected to: its URL\n * works for anyone forever, so handing it to a buyer is the leak itself.\n * 2. **A Storage download URL is never redirected to.** Its token lives as\n * long as the object. When the object belongs to a library asset, rule 1\n * applies to that asset. When no asset owns it (a legacy object, a derived\n * rendition file), it gets a V4 signed read URL with the same bounded\n * lifetime. When it is not in this platform's bucket, or not inside the\n * site's own library or its org's, it is refused.\n * 3. **Only the requesting site's own libraries are signed.** A product is\n * author data. Signing whatever scope it names would let one site's author\n * mint working links to another org's private files, so a host scope must\n * be this site and an org scope must be this site's org, and the signed\n * scope is always qualified with this site so the CDN's `visibleTo` check\n * runs against the site actually selling it.\n * 4. **A hotlink to somebody else's server passes through unchanged.** No code\n * of ours runs there and nothing here can make its URL expire. That is the\n * author's hosting decision, and the docs say so. EXCEPT a hotlink whose\n * host wears a brand it is not (`lookalikeBrandForHost`, the phishing\n * screen's rule): the redirect leaves from the merchant's own domain, so a\n * lure behind it is one no screen of the page or the receipt ever saw. It\n * is refused for every workspace, like a lookalike link in an email\n * (AGL-3363), and the caller files it.\n * 5. **A signed Storage read is a download when it is one.** A download, or\n * an object whose type a browser would run (HTML, SVG, XML), is signed\n * with `response-content-disposition: attachment`, so the file is saved\n * rather than rendered (AGL-3363). The CDN already does this for its own\n * deliveries; a V4 URL carried nothing.\n *\n * ## A video with a copy at the delivery provider (AGL-2824)\n *\n * A caller that asks for it with `delivery` gets, for a video that passes\n * rule 1, the delivery provider's own short-lived URL instead of the signed CDN\n * URL, whenever `mediaDeliveryRedirect` finds a current copy and the release\n * flag is on for the owning org. It is the URL the CDN would redirect the\n * signed request to anyway, minted one hop earlier and bounded by the same\n * lifetime. Anything else takes the signed CDN URL exactly as before.\n */\n\n/** Why a stored URL was not turned into a link. */\nexport type PaidMediaRefusal =\n /** A library asset the public can still fetch without a signature. */\n | 'not-private'\n /** No such asset, a deleted one, or one not shared with this site. */\n | 'not-found'\n /** Another site's library, another org's, or outside the media prefixes. */\n | 'out-of-scope'\n /** A Storage URL for a bucket this platform does not serve from. */\n | 'foreign-storage'\n /** Not a value any delivery can be built from. */\n | 'malformed'\n /** A hotlink to a host that wears another brand's name (AGL-3363). */\n | 'lookalike'\n\nexport type PaidMediaDelivery =\n | {\n ok: true\n via: 'signed-cdn'\n location: string\n expiresAtMs: number\n /** The scope segment the signature covers. */\n scope: string\n mediaId: string\n }\n | {\n ok: true\n via: 'signed-storage'\n location: string\n expiresAtMs: number\n objectPath: string\n /** Whose library the object is in, for the bandwidth count. */\n collection: 'hosts' | 'orgs'\n scopeId: string\n /** The object's size, 0 when Storage did not say. */\n sizeBytes: number\n }\n | { ok: true; via: 'external'; location: string }\n | {\n ok: true\n via: 'delivery'\n location: string\n expiresAtMs: number\n /** The scope segment the delivery was minted for. */\n scope: string\n mediaId: string\n /** Whose library the asset is in, for the bandwidth count. */\n collection: 'hosts' | 'orgs'\n scopeId: string\n /** The size of the copy the provider serves. */\n sizeBytes: number\n }\n | {\n ok: false\n refusal: PaidMediaRefusal\n /** For a `lookalike`: the host it wears a brand in, for the caller to file. */\n host?: string\n }\n\n/** A document read, reduced to what this module asks of one. */\nexport interface PaidMediaDocument {\n exists: boolean\n get(field: string): unknown\n}\n\n/** Every read and signature this module needs, injectable for tests. */\nexport interface PaidMediaDeliveryIo {\n /** The platform's media bucket; empty when unconfigured. */\n bucketName: string\n readMedia(\n collection: 'hosts' | 'orgs',\n scopeId: string,\n mediaId: string,\n ): Promise<PaidMediaDocument>\n /** The org a site belongs to, or null for an unindexed site. */\n orgIdForHost(hostId: string): Promise<string | null>\n /**\n * A V4 signed read URL for one object in {@link bucketName}. `attachment`\n * signs `response-content-disposition: attachment` into it (AGL-3363).\n */\n signStorageRead(\n objectPath: string,\n expiresAtMs: number,\n options?: { attachment?: boolean },\n ): Promise<string>\n /**\n * The object's size in bytes, for the bandwidth a signed read can send;\n * 0 when it cannot be read. Optional, so a double that never signs a\n * Storage read need not answer it.\n */\n storageObjectSize?(objectPath: string): Promise<number>\n}\n\n/** Object types a browser runs rather than shows: always saved, never rendered. */\nconst ACTIVE_OBJECT_EXTENSION = /\\.(html?|xhtml|xht|svgz?|xml|xsl|xslt|mht|mhtml|js|mjs)$/i\n\n/** Is this signed read a download, by the caller's ask or by what the object is? */\nexport function storageReadIsAttachment(\n objectPath: string,\n cdnParams: ReadonlyArray<readonly [string, string]> | undefined,\n): boolean {\n if ((cdnParams ?? []).some(([key, value]) => key === 'download' && value === '1')) {\n return true\n }\n return ACTIVE_OBJECT_EXTENSION.test(objectPath)\n}\n\n/**\n * Resolves a stored product media URL to a redirect target that expires, or\n * says why there is none. See the module comment for the rules.\n *\n * `cdnParams` ride on a signed CDN URL ahead of the signature, which covers\n * `(scope, mediaId, exp)` and never the parameters, so they select a\n * representation of the one asset the signature names and nothing else.\n */\nexport async function resolvePaidMediaDelivery(options: {\n stored: unknown\n /** The site selling the media; the only libraries it may sign from. */\n hostId: string\n /** The lifetime of the link; bounded by `assertMediaSignatureTtl`. */\n ttlMs: number\n cdnParams?: ReadonlyArray<readonly [string, string]>\n /**\n * Hand a video with a current copy at the delivery provider its URL there\n * (AGL-2824). `accept` is the request's `Accept`, which chooses the\n * rendition when `cdnParams` asks for `?r=auto`. The provider and the flag\n * verdict default to the registered provider and the release flag.\n */\n delivery?: {\n accept?: unknown\n provider?: MediaDeliveryProvider | null\n enabled?: (orgId: string | null) => Promise<boolean>\n }\n nowMs?: number\n io: PaidMediaDeliveryIo\n}): Promise<PaidMediaDelivery> {\n const { hostId, ttlMs, io } = options\n const nowMs = options.nowMs ?? Date.now()\n const refuse = (refusal: PaidMediaRefusal): PaidMediaDelivery => ({\n ok: false,\n refusal,\n })\n if (!hostId) return refuse('out-of-scope')\n\n // One lookup per call, and only for the forms that need to know the org.\n let orgId: Promise<string | null> | undefined\n const siteOrgId = () => (orgId ??= io.orgIdForHost(hostId))\n\n /**\n * The delivery provider's URL for a private library video, or null for\n * the signed CDN URL. The representation is the one the CDN would pick for\n * the same `?r=` and `Accept`, so the buyer gets the copy the signed URL\n * would have been redirected to.\n */\n const deliverFromProvider = async (\n delivery: NonNullable<typeof options.delivery>,\n asset: {\n collection: 'hosts' | 'orgs'\n scopeId: string\n orgScope: boolean\n deliveryScope: string\n mediaId: string\n document: PaidMediaDocument\n },\n ): Promise<PaidMediaDelivery | null> => {\n const provider =\n delivery.provider === undefined ? mediaDeliveryProvider('deliver') : delivery.provider\n if (!provider) return null\n const requested = (options.cdnParams ?? []).find(\n ([key]) => key === MEDIA_CDN_RENDITION_PARAM,\n )?.[1]\n const renditions = parseMediaRenditions(asset.document.get('videoRenditions'))\n const rendition = !requested\n ? undefined\n : requested === MEDIA_CDN_RENDITION_AUTO\n ? selectAutoRendition(renditions, delivery.accept)\n : renditions.find((entry) => entry.key === requested)\n const redirect = await mediaDeliveryRedirect({\n provider,\n asset: {\n collection: asset.collection,\n scopeId: asset.scopeId,\n mediaId: asset.mediaId,\n },\n document: asset.document,\n rendition,\n servedType: rendition ? rendition.contentType : asset.document.get('contentType'),\n claims: { scope: asset.deliveryScope, mediaId: asset.mediaId, hostId },\n orgId: async () => (asset.orgScope ? asset.scopeId : await siteOrgId()),\n ...(delivery.enabled ? { deliveryEnabled: delivery.enabled } : {}),\n ttlMs,\n nowMs,\n }).catch(() => null)\n if (!redirect) return null\n return {\n ok: true,\n via: 'delivery',\n location: redirect.location,\n expiresAtMs: redirect.expiresAtMs,\n scope: asset.deliveryScope,\n mediaId: asset.mediaId,\n collection: asset.collection,\n scopeId: asset.scopeId,\n sizeBytes: redirect.sizeBytes,\n }\n }\n\n const deliverAsset = async (\n scope: string,\n mediaId: string,\n ): Promise<PaidMediaDelivery> => {\n const parsed = parseMediaCdnScope(scope)\n if (!parsed) return refuse('malformed')\n let collection: 'hosts' | 'orgs'\n let deliveryScope: string\n if (parsed.isOrg) {\n const owner = await siteOrgId()\n if (!owner || parsed.scopeId !== owner) return refuse('out-of-scope')\n collection = 'orgs'\n deliveryScope = hostQualifiedScope(scope, hostId)\n } else {\n if (parsed.scopeId !== hostId) return refuse('out-of-scope')\n collection = 'hosts'\n deliveryScope = hostId\n }\n const deliveryParsed = parseMediaCdnScope(deliveryScope)\n if (!deliveryParsed) return refuse('malformed')\n const document = await io.readMedia(collection, parsed.scopeId, mediaId)\n if (!document?.exists || document.get('deletedAt')) {\n return refuse('not-found')\n }\n // The CDN would 404 an org asset this site is not allowed to use, so a\n // link to one is a link that cannot play.\n if (mediaCdnScopeRefusal(deliveryParsed, document.get('visibleTo'))) {\n return refuse('not-found')\n }\n if (document.get('private') !== true) return refuse('not-private')\n assertMediaSignatureTtl(ttlMs)\n const delivered = options.delivery\n ? await deliverFromProvider(options.delivery, {\n collection,\n scopeId: parsed.scopeId,\n orgScope: parsed.isOrg,\n deliveryScope,\n mediaId,\n document,\n })\n : null\n if (delivered) return delivered\n const signature = mintMediaSignature(deliveryScope, mediaId, nowMs, ttlMs)\n const params = new URLSearchParams()\n for (const [key, value] of options.cdnParams ?? []) {\n if (key !== 'exp' && key !== 'sig') params.set(key, value)\n }\n const prefix = params.toString()\n return {\n ok: true,\n via: 'signed-cdn',\n location:\n `${MEDIA_CDN_ROUTE}/${deliveryScope}/${mediaId}?` +\n `${prefix ? `${prefix}&` : ''}${mediaSignatureQuery(signature)}`,\n expiresAtMs: signature.exp,\n scope: deliveryScope,\n mediaId,\n }\n }\n\n const source = parsePaidMediaSource(options.stored)\n switch (source.kind) {\n case 'malformed':\n return refuse('malformed')\n case 'external': {\n let externalHost: string\n try {\n externalHost = new URL(source.url).hostname\n } catch {\n return refuse('malformed')\n }\n if (lookalikeBrandForHost(externalHost)) {\n return { ok: false, refusal: 'lookalike', host: externalHost }\n }\n return { ok: true, via: 'external', location: source.url }\n }\n case 'asset':\n return deliverAsset(source.scope, source.mediaId)\n case 'storage-object': {\n if (!io.bucketName || source.bucket !== io.bucketName) {\n return refuse('foreign-storage')\n }\n const { objectPath } = source\n const segments = objectPath.split('/')\n let base: string | null = null\n if (segments[0] === 'hosts' && segments[1] === hostId) {\n base = `hosts/${hostId}`\n } else if (segments[0] === 'orgs' && segments[1]) {\n const owner = await siteOrgId()\n if (owner && segments[1] === owner) base = `orgs/${owner}`\n }\n // The same prefix test the CDN applies to a document's `storagePath`,\n // which also refuses traversal and empty segments.\n if (!base || !isMediaStoragePathInScope(objectPath, base)) {\n return refuse('out-of-scope')\n }\n // An object keyed like an asset IS that asset, and the asset's own rules\n // decide — including refusing it when the asset is still public.\n const asset = paidMediaAssetOfObject(objectPath)\n if (asset) return deliverAsset(asset.scope, asset.mediaId)\n assertMediaSignatureTtl(ttlMs)\n const expiresAtMs = nowMs + ttlMs\n const [location, sizeBytes] = await Promise.all([\n io.signStorageRead(objectPath, expiresAtMs, {\n attachment: storageReadIsAttachment(objectPath, options.cdnParams),\n }),\n io.storageObjectSize?.(objectPath).catch(() => 0) ?? Promise.resolve(0),\n ])\n return {\n ok: true,\n via: 'signed-storage',\n location,\n expiresAtMs,\n objectPath,\n collection: segments[0] === 'orgs' ? 'orgs' : 'hosts',\n scopeId: segments[1],\n sizeBytes,\n }\n }\n }\n}\n\n/**\n * The production reads behind {@link resolvePaidMediaDelivery}, built on the\n * caller's Firestore and bucket handles so a route's own mocks reach them.\n */\nexport function createPaidMediaDeliveryIo(options: {\n firestore: {\n collection(name: string): {\n doc(id: string): {\n get(): Promise<PaidMediaDocument>\n collection(name: string): {\n doc(id: string): { get(): Promise<PaidMediaDocument> }\n }\n }\n }\n }\n bucket: {\n name?: string\n file(path: string): {\n getSignedUrl(config: {\n version: 'v4'\n action: 'read'\n expires: number\n responseDisposition?: string\n }): Promise<[string] | string[]>\n getMetadata?(): Promise<[{ size?: unknown }, ...unknown[]] | Array<{ size?: unknown }>>\n }\n }\n}): PaidMediaDeliveryIo {\n const { firestore, bucket } = options\n return {\n bucketName: String(bucket?.name ?? ''),\n readMedia: (collection, scopeId, mediaId) =>\n firestore\n .collection(collection)\n .doc(scopeId)\n .collection('media')\n .doc(mediaId)\n .get(),\n orgIdForHost: async (hostId) => {\n const index = await firestore.collection('hostIndex').doc(hostId).get()\n const orgId = index.exists ? index.get('orgId') : undefined\n return typeof orgId === 'string' && orgId ? orgId : null\n },\n signStorageRead: async (objectPath, expiresAtMs, signOptions) => {\n const [url] = await bucket.file(objectPath).getSignedUrl({\n version: 'v4',\n action: 'read',\n expires: expiresAtMs,\n ...(signOptions?.attachment ? { responseDisposition: 'attachment' } : {}),\n })\n return String(url)\n },\n storageObjectSize: async (objectPath) => {\n const file = bucket.file(objectPath)\n if (!file.getMetadata) return 0\n const [metadata] = await file.getMetadata()\n const size = Number(metadata?.size)\n return Number.isSafeInteger(size) && size > 0 ? size : 0\n },\n }\n}\n"],"names":["hostQualifiedScope","MEDIA_CDN_RENDITION_AUTO","MEDIA_CDN_RENDITION_PARAM","MEDIA_CDN_ROUTE","paidMediaAssetOfObject","parseMediaRenditions","parsePaidMediaSource","mediaDeliveryProvider","lookalikeBrandForHost","mediaDeliveryRedirect","assertMediaSignatureTtl","mediaSignatureQuery","mintMediaSignature","isMediaStoragePathInScope","mediaCdnScopeRefusal","parseMediaCdnScope","selectAutoRendition","ACTIVE_OBJECT_EXTENSION","storageReadIsAttachment","objectPath","cdnParams","some","key","value","test","resolvePaidMediaDelivery","options","hostId","ttlMs","io","nowMs","Date","now","refuse","refusal","ok","orgId","siteOrgId","orgIdForHost","deliverFromProvider","delivery","asset","provider","undefined","requested","find","renditions","document","get","rendition","accept","entry","redirect","collection","scopeId","mediaId","servedType","contentType","claims","scope","deliveryScope","orgScope","enabled","deliveryEnabled","catch","via","location","expiresAtMs","sizeBytes","deliverAsset","parsed","isOrg","owner","deliveryParsed","readMedia","exists","delivered","signature","params","URLSearchParams","set","prefix","toString","exp","source","stored","kind","externalHost","URL","url","hostname","host","bucketName","bucket","segments","split","base","Promise","all","signStorageRead","attachment","storageObjectSize","resolve","createPaidMediaDeliveryIo","firestore","String","name","doc","index","signOptions","file","getSignedUrl","version","action","expires","responseDisposition","getMetadata","metadata","size","Number","isSafeInteger"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
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 { recordMediaServe } from '@aglyn/tenant-data-admin/server/media-serve-count';
|
|
18
|
+
import type { PaidMediaDelivery } from './paid-media-delivery';
|
|
19
|
+
/**
|
|
20
|
+
* Counts the bandwidth a paid link sends from somewhere other than our CDN
|
|
21
|
+
* route (AGL-3474). A delivery provider's URL and a signed Storage URL are
|
|
22
|
+
* fetched straight from the provider or the bucket, so the request that hands
|
|
23
|
+
* one out is the only one we see, and it counts what the link opens at full
|
|
24
|
+
* size. A signed CDN URL is not counted here: its bytes reach `serveMediaCdn`,
|
|
25
|
+
* which counts them. Awaited after the redirect, so the function lives until
|
|
26
|
+
* the count is written.
|
|
27
|
+
*/
|
|
28
|
+
export declare function countOffRouteServe(firestore: Parameters<typeof recordMediaServe>[0]['firestore'], delivery: PaidMediaDelivery): Promise<void>;
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
*/ import { recordMediaServe } from "@aglyn/tenant-data-admin/server/media-serve-count";
|
|
17
|
+
/**
|
|
18
|
+
* Counts the bandwidth a paid link sends from somewhere other than our CDN
|
|
19
|
+
* route (AGL-3474). A delivery provider's URL and a signed Storage URL are
|
|
20
|
+
* fetched straight from the provider or the bucket, so the request that hands
|
|
21
|
+
* one out is the only one we see, and it counts what the link opens at full
|
|
22
|
+
* size. A signed CDN URL is not counted here: its bytes reach `serveMediaCdn`,
|
|
23
|
+
* which counts them. Awaited after the redirect, so the function lives until
|
|
24
|
+
* the count is written.
|
|
25
|
+
*/ export async function countOffRouteServe(firestore, delivery) {
|
|
26
|
+
if (delivery.ok !== true) return;
|
|
27
|
+
if (delivery.via === 'delivery') {
|
|
28
|
+
await recordMediaServe({
|
|
29
|
+
firestore,
|
|
30
|
+
collection: delivery.collection,
|
|
31
|
+
scopeId: delivery.scopeId,
|
|
32
|
+
mediaId: delivery.mediaId,
|
|
33
|
+
bandwidthBytes: delivery.sizeBytes,
|
|
34
|
+
redirect: true
|
|
35
|
+
});
|
|
36
|
+
} else if (delivery.via === 'signed-storage' && delivery.sizeBytes > 0) {
|
|
37
|
+
await recordMediaServe({
|
|
38
|
+
firestore,
|
|
39
|
+
collection: delivery.collection,
|
|
40
|
+
scopeId: delivery.scopeId,
|
|
41
|
+
bandwidthBytes: delivery.sizeBytes
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//# sourceMappingURL=paid-media-serve-count.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/paid-media-serve-count.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 { recordMediaServe } from '@aglyn/tenant-data-admin/server/media-serve-count'\nimport type { PaidMediaDelivery } from './paid-media-delivery'\n\n/**\n * Counts the bandwidth a paid link sends from somewhere other than our CDN\n * route (AGL-3474). A delivery provider's URL and a signed Storage URL are\n * fetched straight from the provider or the bucket, so the request that hands\n * one out is the only one we see, and it counts what the link opens at full\n * size. A signed CDN URL is not counted here: its bytes reach `serveMediaCdn`,\n * which counts them. Awaited after the redirect, so the function lives until\n * the count is written.\n */\nexport async function countOffRouteServe(\n firestore: Parameters<typeof recordMediaServe>[0]['firestore'],\n delivery: PaidMediaDelivery,\n): Promise<void> {\n if (delivery.ok !== true) return\n if (delivery.via === 'delivery') {\n await recordMediaServe({\n firestore,\n collection: delivery.collection,\n scopeId: delivery.scopeId,\n mediaId: delivery.mediaId,\n bandwidthBytes: delivery.sizeBytes,\n redirect: true,\n })\n } else if (delivery.via === 'signed-storage' && delivery.sizeBytes > 0) {\n await recordMediaServe({\n firestore,\n collection: delivery.collection,\n scopeId: delivery.scopeId,\n bandwidthBytes: delivery.sizeBytes,\n })\n }\n}\n"],"names":["recordMediaServe","countOffRouteServe","firestore","delivery","ok","via","collection","scopeId","mediaId","bandwidthBytes","sizeBytes","redirect"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gBAAgB,QAAQ,oDAAmD;AAGpF;;;;;;;;CAQC,GACD,OAAO,eAAeC,mBACpBC,SAA8D,EAC9DC,QAA2B;IAE3B,IAAIA,SAASC,EAAE,KAAK,MAAM;IAC1B,IAAID,SAASE,GAAG,KAAK,YAAY;QAC/B,MAAML,iBAAiB;YACrBE;YACAI,YAAYH,SAASG,UAAU;YAC/BC,SAASJ,SAASI,OAAO;YACzBC,SAASL,SAASK,OAAO;YACzBC,gBAAgBN,SAASO,SAAS;YAClCC,UAAU;QACZ;IACF,OAAO,IAAIR,SAASE,GAAG,KAAK,oBAAoBF,SAASO,SAAS,GAAG,GAAG;QACtE,MAAMV,iBAAiB;YACrBE;YACAI,YAAYH,SAASG,UAAU;YAC/BC,SAASJ,SAASI,OAAO;YACzBE,gBAAgBN,SAASO,SAAS;QACpC;IACF;AACF"}
|
|
@@ -0,0 +1,45 @@
|
|
|
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 { PluginPersonEraser } from '@aglyn/aglyn/plugin-manager/plugin-person-erasure';
|
|
18
|
+
/**
|
|
19
|
+
* COMMERCE'S SHARE OF A PERSON ERASURE (AGL-2623, AGL-3080).
|
|
20
|
+
*
|
|
21
|
+
* An order is the merchant's record of a sale: the amounts, the line items
|
|
22
|
+
* and the tax are theirs to keep and the law expects them kept. So the person
|
|
23
|
+
* is taken OFF every order they placed on a site of the workspace — name,
|
|
24
|
+
* email, both addresses — and a stamp says when; the order itself stays.
|
|
25
|
+
*
|
|
26
|
+
* The address also lives in what the orders list queries by (AGL-3321): its
|
|
27
|
+
* lower-cased key, its search prefixes, and the quick search's token array,
|
|
28
|
+
* which holds those prefixes beside the order number's and the item names'.
|
|
29
|
+
* The first two go; the prefixes are taken back out of the third. A prefix
|
|
30
|
+
* the number or an item name shares with the address goes with it — a search
|
|
31
|
+
* for that prefix misses this order until the orders backfill restamps it —
|
|
32
|
+
* and nothing left on the order answers to the address.
|
|
33
|
+
*
|
|
34
|
+
* A DRY RUN counts the orders and writes nothing.
|
|
35
|
+
*/
|
|
36
|
+
type Firestore = FirebaseFirestore.Firestore;
|
|
37
|
+
export interface CommercePersonEraserDeps {
|
|
38
|
+
firestore(): Firestore;
|
|
39
|
+
}
|
|
40
|
+
/** The patch that takes the buyer off one order, as it stands. */
|
|
41
|
+
export declare function erasedOrderPatch(data: FirebaseFirestore.DocumentData | undefined, now: number): Record<string, unknown>;
|
|
42
|
+
export declare function createCommercePersonEraser(deps: CommercePersonEraserDeps): PluginPersonEraser;
|
|
43
|
+
/** The eraser over the platform's own Firestore, as the declarations register it. */
|
|
44
|
+
export declare const commercePersonEraser: PluginPersonEraser;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { updateWhereEquals } from "@aglyn/tenant-data-admin/server/paged-sweeps";
|
|
18
|
+
import { FieldValue } from "firebase-admin/firestore";
|
|
19
|
+
import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
20
|
+
const LABEL = 'commerce person eraser';
|
|
21
|
+
/** The patch that takes the buyer off one order, as it stands. */ export function erasedOrderPatch(data, now) {
|
|
22
|
+
const addressTokens = new Set(Array.isArray(data == null ? void 0 : data['customerEmailTokens']) ? data['customerEmailTokens'] : []);
|
|
23
|
+
return _extends({
|
|
24
|
+
customerEmail: null,
|
|
25
|
+
customerName: null,
|
|
26
|
+
shippingAddress: FieldValue.delete(),
|
|
27
|
+
billingAddress: FieldValue.delete(),
|
|
28
|
+
customerErasedAtMs: now,
|
|
29
|
+
customerEmailLower: null,
|
|
30
|
+
customerEmailTokens: []
|
|
31
|
+
}, Array.isArray(data == null ? void 0 : data['searchTokens']) ? {
|
|
32
|
+
searchTokens: data['searchTokens'].filter((token)=>!addressTokens.has(token))
|
|
33
|
+
} : {});
|
|
34
|
+
}
|
|
35
|
+
export function createCommercePersonEraser(deps) {
|
|
36
|
+
return async ({ orgId, email, dryRun, atMs })=>{
|
|
37
|
+
const db = deps.firestore();
|
|
38
|
+
const hosts = await db.collection('hosts').where('orgId', '==', orgId).get();
|
|
39
|
+
let orders = 0;
|
|
40
|
+
for (const host of hosts.docs){
|
|
41
|
+
const placed = host.ref.collection('orders');
|
|
42
|
+
if (dryRun) {
|
|
43
|
+
orders += (await placed.where('customerEmail', '==', email).get()).size;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
orders += await updateWhereEquals(db, placed, 'customerEmail', email, (data)=>erasedOrderPatch(data, atMs), LABEL);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
orders
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** The eraser over the platform's own Firestore, as the declarations register it. */ export const commercePersonEraser = (request)=>createCommercePersonEraser({
|
|
54
|
+
firestore: ()=>firebaseAdmin.app().firestore()
|
|
55
|
+
})(request);
|
|
56
|
+
|
|
57
|
+
//# sourceMappingURL=person-eraser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/person-eraser.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 type {\n PluginPersonEraser,\n PluginPersonErasureReport,\n} from '@aglyn/aglyn/plugin-manager/plugin-person-erasure'\nimport { updateWhereEquals } from '@aglyn/tenant-data-admin/server/paged-sweeps'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\n\n/**\n * COMMERCE'S SHARE OF A PERSON ERASURE (AGL-2623, AGL-3080).\n *\n * An order is the merchant's record of a sale: the amounts, the line items\n * and the tax are theirs to keep and the law expects them kept. So the person\n * is taken OFF every order they placed on a site of the workspace — name,\n * email, both addresses — and a stamp says when; the order itself stays.\n *\n * The address also lives in what the orders list queries by (AGL-3321): its\n * lower-cased key, its search prefixes, and the quick search's token array,\n * which holds those prefixes beside the order number's and the item names'.\n * The first two go; the prefixes are taken back out of the third. A prefix\n * the number or an item name shares with the address goes with it — a search\n * for that prefix misses this order until the orders backfill restamps it —\n * and nothing left on the order answers to the address.\n *\n * A DRY RUN counts the orders and writes nothing.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface CommercePersonEraserDeps {\n firestore(): Firestore\n}\n\nconst LABEL = 'commerce person eraser'\n\n/** The patch that takes the buyer off one order, as it stands. */\nexport function erasedOrderPatch(\n data: FirebaseFirestore.DocumentData | undefined,\n now: number,\n): Record<string, unknown> {\n const addressTokens = new Set(\n Array.isArray(data?.['customerEmailTokens']) ? data['customerEmailTokens'] : [],\n )\n return {\n customerEmail: null,\n customerName: null,\n shippingAddress: FieldValue.delete(),\n billingAddress: FieldValue.delete(),\n customerErasedAtMs: now,\n customerEmailLower: null,\n customerEmailTokens: [],\n ...(Array.isArray(data?.['searchTokens'])\n ? {\n searchTokens: (data['searchTokens'] as unknown[]).filter(\n (token) => !addressTokens.has(token),\n ),\n }\n : {}),\n }\n}\n\nexport function createCommercePersonEraser(deps: CommercePersonEraserDeps): PluginPersonEraser {\n return async ({ orgId, email, dryRun, atMs }): Promise<PluginPersonErasureReport> => {\n const db = deps.firestore()\n const hosts = await db.collection('hosts').where('orgId', '==', orgId).get()\n let orders = 0\n for (const host of hosts.docs) {\n const placed = host.ref.collection('orders')\n if (dryRun) {\n orders += (await placed.where('customerEmail', '==', email).get()).size\n continue\n }\n orders += await updateWhereEquals(\n db,\n placed,\n 'customerEmail',\n email,\n (data) => erasedOrderPatch(data, atMs),\n LABEL,\n )\n }\n return { orders }\n }\n}\n\n/** The eraser over the platform's own Firestore, as the declarations register it. */\nexport const commercePersonEraser: PluginPersonEraser = (request) =>\n createCommercePersonEraser({ firestore: () => firebaseAdmin.app().firestore() })(request)\n"],"names":["updateWhereEquals","FieldValue","firebaseAdmin","LABEL","erasedOrderPatch","data","now","addressTokens","Set","Array","isArray","customerEmail","customerName","shippingAddress","delete","billingAddress","customerErasedAtMs","customerEmailLower","customerEmailTokens","searchTokens","filter","token","has","createCommercePersonEraser","deps","orgId","email","dryRun","atMs","db","firestore","hosts","collection","where","get","orders","host","docs","placed","ref","size","commercePersonEraser","request","app"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAMD,SAASA,iBAAiB,QAAQ,+CAA8C;AAChF,SAASC,UAAU,QAAQ,2BAA0B;AACrD,SAASC,aAAa,QAAQ,iDAAgD;AA2B9E,MAAMC,QAAQ;AAEd,gEAAgE,GAChE,OAAO,SAASC,iBACdC,IAAgD,EAChDC,GAAW;IAEX,MAAMC,gBAAgB,IAAIC,IACxBC,MAAMC,OAAO,CAACL,wBAAAA,IAAM,CAAC,sBAAsB,IAAIA,IAAI,CAAC,sBAAsB,GAAG,EAAE;IAEjF,OAAO;QACLM,eAAe;QACfC,cAAc;QACdC,iBAAiBZ,WAAWa,MAAM;QAClCC,gBAAgBd,WAAWa,MAAM;QACjCE,oBAAoBV;QACpBW,oBAAoB;QACpBC,qBAAqB,EAAE;OACnBT,MAAMC,OAAO,CAACL,wBAAAA,IAAM,CAAC,eAAe,IACpC;QACEc,cAAc,AAACd,IAAI,CAAC,eAAe,CAAee,MAAM,CACtD,CAACC,QAAU,CAACd,cAAce,GAAG,CAACD;IAElC,IACA,CAAC;AAET;AAEA,OAAO,SAASE,2BAA2BC,IAA8B;IACvE,OAAO,OAAO,EAAEC,KAAK,EAAEC,KAAK,EAAEC,MAAM,EAAEC,IAAI,EAAE;QAC1C,MAAMC,KAAKL,KAAKM,SAAS;QACzB,MAAMC,QAAQ,MAAMF,GAAGG,UAAU,CAAC,SAASC,KAAK,CAAC,SAAS,MAAMR,OAAOS,GAAG;QAC1E,IAAIC,SAAS;QACb,KAAK,MAAMC,QAAQL,MAAMM,IAAI,CAAE;YAC7B,MAAMC,SAASF,KAAKG,GAAG,CAACP,UAAU,CAAC;YACnC,IAAIL,QAAQ;gBACVQ,UAAU,AAAC,CAAA,MAAMG,OAAOL,KAAK,CAAC,iBAAiB,MAAMP,OAAOQ,GAAG,EAAC,EAAGM,IAAI;gBACvE;YACF;YACAL,UAAU,MAAMnC,kBACd6B,IACAS,QACA,iBACAZ,OACA,CAACrB,OAASD,iBAAiBC,MAAMuB,OACjCzB;QAEJ;QACA,OAAO;YAAEgC;QAAO;IAClB;AACF;AAEA,mFAAmF,GACnF,OAAO,MAAMM,uBAA2C,CAACC,UACvDnB,2BAA2B;QAAEO,WAAW,IAAM5B,cAAcyC,GAAG,GAAGb,SAAS;IAAG,GAAGY,SAAQ"}
|
|
@@ -17,7 +17,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
17
17
|
*/ import * as Aglyn from "@aglyn/aglyn/server";
|
|
18
18
|
import * as CommerceModel from "../model/index.js";
|
|
19
19
|
import { firebaseAdmin, getOrgForHost, getPluginConfig } from "@aglyn/tenant-data-admin";
|
|
20
|
-
import
|
|
20
|
+
import recordCapturedContact from "@aglyn/aglyn/plugin-manager/record-captured-contact";
|
|
21
21
|
import { merchantAccountIsReady } from "@aglyn/tenant-data-admin/server/payment-provider";
|
|
22
22
|
import { checkoutSessionCardAuthenticationParams } from "@aglyn/tenant-data-admin/server/stripe-card-authentication";
|
|
23
23
|
import { buildRoute, claimAttempt, Route } from "@aglyn/aglyn/server";
|
|
@@ -1055,16 +1055,23 @@ import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
|
|
|
1055
1055
|
// again on the same key. A duplicate paid order is a far larger defect
|
|
1056
1056
|
// than a missing contact row. So the failure is logged and dropped
|
|
1057
1057
|
// here, and the sale still returns 200.
|
|
1058
|
-
|
|
1058
|
+
//
|
|
1059
|
+
// Through the contact-capture contract (AGL-3080): the plugin that keeps
|
|
1060
|
+
// people records the sale, and commerce imports none of it. The seam
|
|
1061
|
+
// never throws, and the `catch` stays as the last line of defence.
|
|
1062
|
+
await recordCapturedContact({
|
|
1063
|
+
orgId: '',
|
|
1059
1064
|
hostId,
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
+
identity: _extends({
|
|
1066
|
+
email: contactEmail
|
|
1067
|
+
}, contactName ? {
|
|
1068
|
+
name: contactName
|
|
1069
|
+
} : {}),
|
|
1070
|
+
// A sale makes the person a relationship (AGL-3232).
|
|
1071
|
+
surface: 'relationship',
|
|
1065
1072
|
// A sale makes a customer of anybody who was not yet one and never
|
|
1066
1073
|
// moves anybody back (AGL-2612).
|
|
1067
|
-
|
|
1074
|
+
lifecycleFloor: 'customer',
|
|
1068
1075
|
// AGL-1748: the amount was formatted into the summary STRING below and
|
|
1069
1076
|
// never passed to the field that exists to hold it, so `ltvCents`
|
|
1070
1077
|
// counted online sales only — a shop-counter merchant's best customers
|
|
@@ -1081,6 +1088,7 @@ import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
|
|
|
1081
1088
|
// write does not already have.
|
|
1082
1089
|
purchaseCents: totals.totalCents,
|
|
1083
1090
|
interaction: {
|
|
1091
|
+
source: 'order',
|
|
1084
1092
|
refId: orderRef.id,
|
|
1085
1093
|
// `source` stays `'order'` for both (AGL-1757): a folio sale is a
|
|
1086
1094
|
// shop sale that happens to be settled against a stay, and the
|
|
@@ -1089,7 +1097,7 @@ import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
|
|
|
1089
1097
|
// the timeline can tell a counter sale from a room charge.
|
|
1090
1098
|
summary: payment === 'folio' ? `Room charge ($${(totals.totalCents / 100).toFixed(2)})` : `In-store purchase ($${(totals.totalCents / 100).toFixed(2)})`
|
|
1091
1099
|
}
|
|
1092
|
-
})
|
|
1100
|
+
}).catch((error)=>{
|
|
1093
1101
|
console.error('[pos-order] contact upsert failed', error);
|
|
1094
1102
|
});
|
|
1095
1103
|
}
|