@aglyn/plugins-commerce 1.0.0-beta.287 → 1.0.0-beta.289
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 +2 -2
- package/package.json +13 -13
- package/src/lib/components/commerce-console-page.js +29 -2
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/console/commerce-analytics-card.component.js +4 -2
- package/src/lib/components/console/commerce-analytics-card.component.js.map +1 -1
- package/src/lib/components/console/host-orders-card.component.js +5 -1
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/host-quotes-card.component.d.ts +34 -0
- package/src/lib/components/console/host-quotes-card.component.js +247 -0
- package/src/lib/components/console/host-quotes-card.component.js.map +1 -0
- package/src/lib/components/console/order-webhooks-card.component.js +1 -1
- package/src/lib/components/console/order-webhooks-card.component.js.map +1 -1
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js +33 -28
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js.map +1 -1
- package/src/lib/components/console/quote-editor-dialog.component.d.ts +45 -0
- package/src/lib/components/console/quote-editor-dialog.component.js +1266 -0
- package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -0
- package/src/lib/components/console/quote-pay-link.component.d.ts +38 -0
- package/src/lib/components/console/quote-pay-link.component.js +213 -0
- package/src/lib/components/console/quote-pay-link.component.js.map +1 -0
- package/src/lib/components/console/quote-settings-card.component.d.ts +29 -0
- package/src/lib/components/console/quote-settings-card.component.js +121 -0
- package/src/lib/components/console/quote-settings-card.component.js.map +1 -0
- package/src/lib/components/console/tax-settings-card.component.js +4 -1
- package/src/lib/components/console/tax-settings-card.component.js.map +1 -1
- package/src/lib/components/quote-page-body.d.ts +33 -0
- package/src/lib/components/quote-page-body.js +696 -0
- package/src/lib/components/quote-page-body.js.map +1 -0
- package/src/lib/components/quote-page.d.ts +29 -0
- package/src/lib/components/quote-page.js +96 -0
- package/src/lib/components/quote-page.js.map +1 -0
- package/src/lib/components/storefront-payment-element.d.ts +7 -1
- package/src/lib/components/storefront-payment-element.js +8 -3
- package/src/lib/components/storefront-payment-element.js.map +1 -1
- package/src/lib/constants/orders-list-query.js +6 -1
- package/src/lib/constants/orders-list-query.js.map +1 -1
- package/src/lib/constants/quote-page.d.ts +30 -0
- package/src/lib/constants/quote-page.js +35 -0
- package/src/lib/constants/quote-page.js.map +1 -0
- package/src/lib/declarations.js +5 -1
- package/src/lib/declarations.js.map +1 -1
- package/src/lib/model/commerce-orders.d.ts +25 -2
- package/src/lib/model/commerce-orders.js +10 -1
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/commerce-quote-pay.d.ts +229 -0
- package/src/lib/model/commerce-quote-pay.js +393 -0
- package/src/lib/model/commerce-quote-pay.js.map +1 -0
- package/src/lib/model/commerce-quotes.d.ts +279 -0
- package/src/lib/model/commerce-quotes.js +522 -0
- package/src/lib/model/commerce-quotes.js.map +1 -0
- package/src/lib/model/index.d.ts +2 -0
- package/src/lib/model/index.js +2 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/plugin-permissions.d.ts +8 -1
- package/src/lib/model/plugin-permissions.js +21 -1
- package/src/lib/model/plugin-permissions.js.map +1 -1
- package/src/lib/model/workspace-site-permissions.d.ts +26 -0
- package/src/lib/model/workspace-site-permissions.js +81 -0
- package/src/lib/model/workspace-site-permissions.js.map +1 -0
- package/src/lib/server/api-v1/orders-and-products.js +6 -2
- package/src/lib/server/api-v1/orders-and-products.js.map +1 -1
- package/src/lib/server/billing-webhook.js +127 -52
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/checkout-session-expiry.d.ts +36 -0
- package/src/lib/server/checkout-session-expiry.js +63 -0
- package/src/lib/server/checkout-session-expiry.js.map +1 -0
- package/src/lib/server/commerce-site-keys.d.ts +19 -0
- package/src/lib/server/commerce-site-keys.js +23 -2
- package/src/lib/server/commerce-site-keys.js.map +1 -1
- package/src/lib/server/draft-order.js +16 -8
- package/src/lib/server/draft-order.js.map +1 -1
- package/src/lib/server/order-webhooks.js +8 -8
- package/src/lib/server/order-webhooks.js.map +1 -1
- package/src/lib/server/person-eraser.d.ts +7 -0
- package/src/lib/server/person-eraser.js +24 -2
- package/src/lib/server/person-eraser.js.map +1 -1
- package/src/lib/server/pos-ops-gate.d.ts +13 -5
- package/src/lib/server/pos-ops-gate.js +12 -14
- package/src/lib/server/pos-ops-gate.js.map +1 -1
- package/src/lib/server/pos-refund-authority.d.ts +3 -1
- package/src/lib/server/pos-refund-authority.js +3 -2
- package/src/lib/server/pos-refund-authority.js.map +1 -1
- package/src/lib/server/pos-return.js +6 -7
- package/src/lib/server/pos-return.js.map +1 -1
- package/src/lib/server/pos-staff-pin.js +7 -7
- package/src/lib/server/pos-staff-pin.js.map +1 -1
- package/src/lib/server/quote-link-token.d.ts +39 -0
- package/src/lib/server/quote-link-token.js +97 -0
- package/src/lib/server/quote-link-token.js.map +1 -0
- package/src/lib/server/quote-pay.d.ts +83 -0
- package/src/lib/server/quote-pay.js +606 -0
- package/src/lib/server/quote-pay.js.map +1 -0
- package/src/lib/server/quote.d.ts +49 -0
- package/src/lib/server/quote.js +513 -0
- package/src/lib/server/quote.js.map +1 -0
- package/src/lib/server/refund.js +26 -36
- package/src/lib/server/refund.js.map +1 -1
- package/src/lib/server/related.js +2 -0
- package/src/lib/server/related.js.map +1 -1
- package/src/lib/server/site-page-resolver.d.ts +2 -0
- package/src/lib/server/site-page-resolver.js +75 -0
- package/src/lib/server/site-page-resolver.js.map +1 -1
- package/src/lib/server.js +14 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/site.js +6 -0
- package/src/lib/site.js.map +1 -1
- package/src/lib/transfer/tracking.server.js +6 -0
- package/src/lib/transfer/tracking.server.js.map +1 -1
|
@@ -23,6 +23,8 @@ import { createSecretBoxKey, openSecret, parseSecretBoxKeyring, sealSecret, SECR
|
|
|
23
23
|
import { createHmac, hkdfSync, randomBytes } from "node:crypto";
|
|
24
24
|
import { COMMERCE_EVENT_DECLARATIONS } from "../model/order-events.js";
|
|
25
25
|
import * as Webhooks from "../model/order-webhooks.js";
|
|
26
|
+
import { commerceWorkspacePermissionRefusal } from "../model/workspace-site-permissions.js";
|
|
27
|
+
import { holdsCommerceWorkspaceSiteKey } from "./commerce-site-keys.js";
|
|
26
28
|
/**
|
|
27
29
|
* Merchant order webhooks (AGL-3611): the console's door and the delivery
|
|
28
30
|
* behind it. The model (`model/order-webhooks.ts`) says what is stored where.
|
|
@@ -389,7 +391,7 @@ const cleanDescription = (raw)=>String(raw != null ? raw : '').trim().slice(0, 1
|
|
|
389
391
|
error: 'Unknown action'
|
|
390
392
|
});
|
|
391
393
|
try {
|
|
392
|
-
var
|
|
394
|
+
var _body_endpointId;
|
|
393
395
|
const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
|
|
394
396
|
const firestore = firebaseAdmin.app().firestore();
|
|
395
397
|
const hostRef = hostRefFor(firestore, hostId);
|
|
@@ -397,17 +399,15 @@ const cleanDescription = (raw)=>String(raw != null ? raw : '').trim().slice(0, 1
|
|
|
397
399
|
if (!hostSnapshot.exists) return res.status(404).json({
|
|
398
400
|
error: 'Unknown site'
|
|
399
401
|
});
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
});
|
|
404
|
-
}
|
|
402
|
+
// "Manage order webhooks" (`commerce.webhooks`, AGL-4047): its preset is
|
|
403
|
+
// the gate this asked before, the site's Admin who is a member of the
|
|
404
|
+
// whole workspace (a webhook's secret signs every order it is sent).
|
|
405
405
|
const membership = await resolveOrgPermissions(decoded.uid, {
|
|
406
406
|
hostId
|
|
407
407
|
});
|
|
408
|
-
if (!
|
|
408
|
+
if (!holdsCommerceWorkspaceSiteKey(hostSnapshot, decoded.uid, 'commerce.webhooks', membership)) {
|
|
409
409
|
return res.status(403).json({
|
|
410
|
-
error: '
|
|
410
|
+
error: commerceWorkspacePermissionRefusal('commerce.webhooks')
|
|
411
411
|
});
|
|
412
412
|
}
|
|
413
413
|
const keyring = readCommerceSecretKeyring();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/order-webhooks.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 { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { PLATFORM_BRAND_NAME } from '@aglyn/aglyn/app-utils/platform-brand'\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport type { PluginDomainEventEnvelope } from '@aglyn/aglyn/plugin-manager/plugin-domain-events'\nimport {\n describeConfiguredUrlRefusal,\n configuredUrlRefusal,\n fetchConfiguredPublicUrl,\n firebaseAdmin,\n notifyHostManagers,\n} from '@aglyn/tenant-data-admin'\nimport { PLUGIN_EVENT_MAX_ATTEMPTS } from '@aglyn/tenant-data-admin/server/plugin-event-outbox'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n createSecretBoxKey,\n openSecret,\n parseSecretBoxKeyring,\n sealSecret,\n SECRET_BOX_KEY_BYTES,\n type SecretBoxKey,\n type SecretBoxKeyring,\n} from '@aglyn/shared-util-tools/secret-box'\nimport { createHmac, hkdfSync, randomBytes } from 'node:crypto'\nimport { COMMERCE_EVENT_DECLARATIONS } from '../model/order-events'\nimport * as Webhooks from '../model/order-webhooks'\n\n/**\n * Merchant order webhooks (AGL-3611): the console's door and the delivery\n * behind it. The model (`model/order-webhooks.ts`) says what is stored where.\n *\n * DELIVERY RIDES THE PLUGIN EVENT OUTBOX. Commerce subscribes to its own\n * order and return events under the name `webhooks`; the subscriber posts the\n * event to every endpoint that takes it and records each attempt in the\n * delivery log. An endpoint that already took an event is not posted it\n * again, so when one of three endpoints fails, the subscriber throws, the\n * outbox retries it with its backoff (30s, 2m, 10m, 30m, 1h, 6h, 12h), and\n * only the failing endpoint is called again. On the last attempt the delivery\n * is marked failed and the site's managers are told.\n *\n * EVERY CALL GOES THROUGH `fetchConfiguredPublicUrl`: https on the default\n * port, a public address pinned at connect time, no redirect followed. A\n * merchant's endpoint is a stranger's server to us.\n *\n * THE SECRET IS SEALED with the shared secret box, under the keyring\n * `readCommerceSecretKeyring` answers, bound to its endpoint by the seal's\n * context. The plaintext leaves the server once, in the answer to the request\n * that made or rolled it.\n */\n\nexport const COMMERCE_SECRET_KEY_ENV = 'COMMERCE_SECRET_KEY'\n\nconst SECRETS = 'orderWebhookSecrets'\n\n/** Delivery rows live this long, then Firestore's TTL removes them. */\nexport const ORDER_WEBHOOK_LOG_RETENTION_MS = 30 * 86_400_000\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The id of the key derived from `TOKEN_SIGNING_SECRET`. */\nexport const DERIVED_COMMERCE_KEY_ID = 'tss1'\n\n/**\n * A secret-box key derived from `TOKEN_SIGNING_SECRET` with HKDF, under its\n * own label, so the commerce tokens and the sealed secrets never share key\n * material. `null` when that secret is unset.\n */\nfunction derivedCommerceKey(): SecretBoxKey | null {\n const base = String(process.env['TOKEN_SIGNING_SECRET'] ?? '')\n if (!base) return null\n const material = new Uint8Array(hkdfSync('sha256', base, 'aglyn-commerce', 'secret-box:v1', SECRET_BOX_KEY_BYTES))\n return createSecretBoxKey(material, DERIVED_COMMERCE_KEY_ID)\n}\n\n/**\n * The keyring commerce seals its stored secrets with. `COMMERCE_SECRET_KEY`\n * (a secret-box keyring) when it is set, its first key sealing; otherwise the\n * key derived from `TOKEN_SIGNING_SECRET`, which every deployment that sells\n * already has. The derived key stays in the ring after a dedicated key is\n * configured, so what it sealed still opens. `null` when neither is usable.\n * Never throws.\n */\nexport function readCommerceSecretKeyring(): SecretBoxKeyring | null {\n let derived: SecretBoxKey | null = null\n try {\n derived = derivedCommerceKey()\n } catch {\n derived = null\n }\n const raw = String(process.env[COMMERCE_SECRET_KEY_ENV] ?? '').trim()\n if (raw) {\n try {\n const ring = parseSecretBoxKeyring(raw)\n const keys = derived && !ring.keys.some((key) => key.id === derived!.id) ? [...ring.keys, derived] : ring.keys\n return { current: ring.current, keys }\n } catch {\n // An unusable dedicated key falls back to the derived one rather than\n // sealing nothing.\n }\n }\n return derived ? { current: derived, keys: [derived] } : null\n}\n\n/** Whether this deployment can sign webhooks at all. */\nexport function orderWebhooksConfigured(): boolean {\n return readCommerceSecretKeyring() !== null\n}\n\nconst sealContext = (hostId: string, endpointId: string) => `hosts/${hostId}/${SECRETS}/${endpointId}#secret`\n\n/** A fresh signing secret: `whsec_` and 32 random bytes. */\nexport function mintOrderWebhookSecret(): string {\n return `whsec_${randomBytes(32).toString('base64url')}`\n}\n\n/** The HMAC-SHA256 of `{t}.{body}` under the secret, as hex. */\nexport function signOrderWebhook(secret: string, timestampSeconds: number, body: string): string {\n return createHmac('sha256', secret).update(Webhooks.orderWebhookSignedPayload(timestampSeconds, body)).digest('hex')\n}\n\nconst hostRefFor = (firestore: Firestore, hostId: string) => firestore.collection('hosts').doc(hostId)\n\nasync function readSecret(\n firestore: Firestore,\n hostId: string,\n endpointId: string,\n keyring: SecretBoxKeyring,\n): Promise<string> {\n const hostRef = hostRefFor(firestore, hostId)\n const snapshot = await hostRef.collection('orderWebhookSecrets').doc(endpointId).get()\n const sealed = String(snapshot.get('sealedSecret') ?? '')\n if (!sealed) throw new Error('This endpoint has no signing secret')\n return openSecret(sealed, keyring, { context: sealContext(hostId, endpointId) }).plaintext\n}\n\nfunction sealedSecretRecord(\n hostId: string,\n endpointId: string,\n secret: string,\n keyring: SecretBoxKeyring,\n now: number,\n): Webhooks.OrderWebhookSecretRecord {\n return {\n sealedSecret: sealSecret(secret, keyring.current, { context: sealContext(hostId, endpointId) }),\n secretKeyId: keyring.current.id,\n updatedAtMs: now,\n }\n}\n\n/** What one POST came to. */\nexport interface OrderWebhookPostResult {\n attempt: Webhooks.OrderWebhookAttempt\n delivered: boolean\n}\n\n/**\n * Posts one body to one endpoint, signed. Never throws: a refusal, a timeout\n * or a reset is an attempt that failed, with the reason in words.\n */\nexport async function postOrderWebhook(input: {\n url: string\n secret: string\n event: string\n eventId: string\n body: string\n now?: () => number\n fetchImpl?: typeof fetchConfiguredPublicUrl\n}): Promise<OrderWebhookPostResult> {\n const clock = input.now ?? Date.now\n const startedAt = clock()\n const timestamp = Math.floor(startedAt / 1000)\n const signature = Webhooks.formatOrderWebhookSignature(timestamp, signOrderWebhook(input.secret, timestamp, input.body))\n let httpStatus: number | null = null\n let error: string | undefined\n try {\n const result = await (input.fetchImpl ?? fetchConfiguredPublicUrl)(input.url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'user-agent': 'Aglyn-Webhooks/1.0',\n [Webhooks.ORDER_WEBHOOK_SIGNATURE_HEADER]: signature,\n [Webhooks.ORDER_WEBHOOK_EVENT_HEADER]: input.event,\n [Webhooks.ORDER_WEBHOOK_EVENT_ID_HEADER]: input.eventId,\n },\n body: input.body,\n signal: AbortSignal.timeout(Webhooks.ORDER_WEBHOOK_TIMEOUT_MS),\n })\n if ('refusal' in result) {\n error = `Not sent: ${describeConfiguredUrlRefusal(result.refusal)}`\n } else {\n httpStatus = result.status\n if (!Webhooks.orderWebhookAccepted(httpStatus)) {\n error =\n httpStatus >= 300 && httpStatus < 400\n ? `The endpoint answered ${httpStatus}, a redirect; redirects are not followed`\n : `The endpoint answered ${httpStatus}`\n }\n }\n } catch (caught) {\n const name = (caught as Error)?.name\n error =\n name === 'TimeoutError' || name === 'AbortError'\n ? `No answer within ${Webhooks.ORDER_WEBHOOK_TIMEOUT_MS / 1000} seconds`\n : `The connection failed: ${String((caught as Error)?.message ?? caught).slice(0, 200)}`\n }\n const attempt: Webhooks.OrderWebhookAttempt = {\n atMs: startedAt,\n httpStatus,\n ...(error ? { error } : {}),\n durationMs: Math.max(0, clock() - startedAt),\n }\n return { attempt, delivered: !error }\n}\n\nfunction expiresAt(now: number) {\n return firebaseAdmin.firestore.Timestamp.fromMillis(now + ORDER_WEBHOOK_LOG_RETENTION_MS)\n}\n\n/** Records an attempt on the delivery row and on the endpoint's summary. */\nasync function recordAttempt(\n firestore: Firestore,\n hostId: string,\n endpointId: string,\n delivery: {\n id: string\n eventId: string\n event: string\n orderId: string | null\n body: string\n test?: boolean\n prior: Webhooks.OrderWebhookDelivery | null\n },\n result: OrderWebhookPostResult,\n status: Webhooks.OrderWebhookDeliveryStatus,\n now: number,\n): Promise<void> {\n const hostRef = hostRefFor(firestore, hostId)\n const row: Webhooks.OrderWebhookDelivery & { expiresAt: unknown } = {\n endpointId,\n eventId: delivery.eventId,\n event: delivery.event,\n orderId: delivery.orderId,\n status,\n attempts: Webhooks.appendOrderWebhookAttempt(delivery.prior?.attempts, result.attempt),\n body: delivery.body.slice(0, Webhooks.ORDER_WEBHOOK_BODY_MAX),\n ...(delivery.test ? { test: true } : {}),\n createdAtMs: delivery.prior?.createdAtMs ?? now,\n updatedAtMs: now,\n expiresAt: expiresAt(now),\n }\n await hostRef.collection('orderWebhookDeliveries').doc(delivery.id).set(row)\n await hostRef\n .collection('orderWebhooks')\n .doc(endpointId)\n .update({\n lastDeliveryAtMs: now,\n lastDeliveryStatus: status,\n consecutiveFailures: result.delivered ? 0 : firebaseAdmin.firestore.FieldValue.increment(1),\n })\n .catch(() => undefined)\n}\n\n/**\n * The subscriber: posts one event to every endpoint of the site that takes\n * it and has not taken it already. Throws when any endpoint still owes it, so\n * the outbox retries; on the outbox's last attempt it records the failure and\n * tells the site's managers instead.\n */\nexport async function deliverOrderEventToWebhooks(\n envelope: PluginDomainEventEnvelope,\n deps: { fetchImpl?: typeof fetchConfiguredPublicUrl; now?: () => number } = {},\n): Promise<{ delivered: number; failed: number }> {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = hostRefFor(firestore, envelope.hostId)\n const endpoints = await hostRef.collection('orderWebhooks').where('enabled', '==', true).get()\n const taking = endpoints.docs.filter((doc) =>\n Webhooks.orderWebhookTakes(doc.data() as Webhooks.OrderWebhookEndpoint, envelope.event, envelope.occurredAtMs),\n )\n if (taking.length === 0) return { delivered: 0, failed: 0 }\n const keyring = readCommerceSecretKeyring()\n const clock = deps.now ?? Date.now\n const final = envelope.attempt >= PLUGIN_EVENT_MAX_ATTEMPTS\n const body = Webhooks.orderWebhookBody({\n eventId: envelope.id,\n event: envelope.event,\n occurredAtMs: envelope.occurredAtMs,\n hostId: envelope.hostId,\n payload: envelope.payload,\n })\n const orderId = String(((envelope.payload ?? {}) as { order?: { id?: unknown } }).order?.id ?? '') || null\n const outcomes = await Promise.all(\n taking.map(async (doc) => {\n const endpoint = doc.data() as Webhooks.OrderWebhookEndpoint\n const deliveryId = Webhooks.orderWebhookDeliveryId(envelope.id, doc.id)\n const priorSnapshot = await hostRef.collection('orderWebhookDeliveries').doc(deliveryId).get()\n const prior = priorSnapshot.exists ? (priorSnapshot.data() as Webhooks.OrderWebhookDelivery) : null\n // Taken already, on an earlier pass or by a resend from the console.\n if (prior?.status === 'delivered') return 'delivered' as const\n const settled = await attemptEndpoint({\n firestore,\n hostId: envelope.hostId,\n endpointId: doc.id,\n url: endpoint.url,\n keyring,\n event: envelope.event,\n eventId: envelope.id,\n body,\n clock,\n fetchImpl: deps.fetchImpl,\n })\n const status: Webhooks.OrderWebhookDeliveryStatus = settled.delivered ? 'delivered' : final ? 'failed' : 'retrying'\n await recordAttempt(\n firestore,\n envelope.hostId,\n doc.id,\n { id: deliveryId, eventId: envelope.id, event: envelope.event, orderId, body, prior },\n settled,\n status,\n clock(),\n )\n if (status === 'failed') {\n await notifyHostManagers(envelope.hostId, {\n type: 'content.order',\n title: 'An order webhook could not be delivered',\n body:\n `${envelope.event} never reached ${safeHost(endpoint.url)} on {site} after ` +\n `${envelope.attempt} attempts (${settled.attempt.error ?? 'no answer'}). ` +\n 'Resend it from the delivery log once the endpoint is fixed.',\n link: `/${envelope.hostId}/products/notifications`,\n }).catch(() => undefined)\n }\n return status\n }),\n )\n const delivered = outcomes.filter((status) => status === 'delivered').length\n const failed = outcomes.length - delivered\n if (failed > 0 && !final) {\n throw new Error(`${failed} of ${outcomes.length} order webhook endpoints did not take ${envelope.event}`)\n }\n return { delivered, failed }\n}\n\n/** One signed attempt at one endpoint; a missing key or secret is a failed attempt. */\nasync function attemptEndpoint(input: {\n firestore: Firestore\n hostId: string\n endpointId: string\n url: string\n keyring: SecretBoxKeyring | null\n event: string\n eventId: string\n body: string\n clock: () => number\n fetchImpl?: typeof fetchConfiguredPublicUrl\n}): Promise<OrderWebhookPostResult> {\n const failed = (error: string): OrderWebhookPostResult => ({\n delivered: false,\n attempt: { atMs: input.clock(), httpStatus: null, error, durationMs: 0 },\n })\n if (!input.keyring) return failed('Webhook signing is not configured')\n let secret: string\n try {\n secret = await readSecret(input.firestore, input.hostId, input.endpointId, input.keyring)\n } catch (error) {\n return failed(`The signing secret could not be read: ${String((error as Error)?.message ?? error).slice(0, 120)}`)\n }\n return postOrderWebhook({\n url: input.url,\n secret,\n event: input.event,\n eventId: input.eventId,\n body: input.body,\n fetchImpl: input.fetchImpl,\n now: input.clock,\n })\n}\n\nfunction safeHost(url: string): string {\n try {\n return new URL(url).hostname\n } catch {\n return 'the endpoint'\n }\n}\n\n// ---------------------------------------------------------------------------\n// The console's door\n// ---------------------------------------------------------------------------\n\nconst ACTIONS = ['status', 'create', 'update', 'roll-secret', 'delete', 'test', 'resend'] as const\ntype Action = (typeof ACTIONS)[number]\n\nconst isId = (value: string) => Boolean(value) && value.length <= 1500 && !/^__.*__$/.test(value) && !value.includes('/')\n\nfunction cleanUrl(raw: unknown): { url: string } | { error: string } {\n const url = String(raw ?? '').trim()\n if (!url) return { error: 'Enter the endpoint’s address' }\n if (url.length > 2048) return { error: 'That address is too long' }\n const refusal = configuredUrlRefusal(url)\n if (refusal) return { error: `This endpoint cannot be used: ${describeConfiguredUrlRefusal(refusal)}` }\n return { url }\n}\n\nconst cleanDescription = (raw: unknown) => String(raw ?? '').trim().slice(0, 120)\n\n/**\n * `POST /api/commerce/order-webhooks` with `{ hostId, action, … }`. Endpoints\n * post the order book to an outside server, so only an admin of the whole\n * workspace manages them — the bar a refund sets.\n *\n * - `status` → `{ configured, events: [{ event, label, description }] }`\n * - `create` `{ url, events, description? }` → `{ id, secret }` (shown once)\n * - `update` `{ endpointId, url?, events?, enabled?, description? }`\n * - `roll-secret` `{ endpointId }` → `{ secret }`\n * - `delete` `{ endpointId }`; the delivery log stays until it expires\n * - `test` `{ endpointId }` → posts a `webhook.test` event now\n * - `resend` `{ deliveryId }` → posts a logged delivery again now\n */\nexport const orderWebhooksHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n let body: Record<string, any>\n try {\n body = (typeof req.body === 'string' ? JSON.parse(req.body) : req.body) ?? {}\n } catch {\n return res.status(400).json({ error: 'Invalid JSON' })\n }\n const hostId = String(body.hostId ?? '')\n const action = String(body.action ?? '') as Action\n if (!isId(hostId)) return res.status(400).json({ error: 'Missing hostId' })\n if (!ACTIONS.includes(action)) return res.status(400).json({ error: 'Unknown action' })\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = hostRefFor(firestore, hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) return res.status(404).json({ error: 'Unknown site' })\n if ((hostSnapshot.get('memberRoles') ?? {})[decoded.uid] !== 'admin') {\n return res.status(403).json({ error: 'Webhooks are managed by a site admin' })\n }\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.orgWide || membership.hostRole !== 'admin') {\n return res.status(403).json({ error: 'Webhooks are managed by an admin of the whole workspace' })\n }\n const keyring = readCommerceSecretKeyring()\n if (action === 'status') {\n return res.status(200).json({\n configured: Boolean(keyring),\n maxEndpoints: Webhooks.ORDER_WEBHOOK_MAX_ENDPOINTS,\n events: COMMERCE_EVENT_DECLARATIONS.map((entry) => ({\n event: entry.event.id,\n label: entry.label,\n description: entry.description,\n })),\n })\n }\n if (!keyring) {\n return res.status(503).json({ error: 'Order webhooks are not configured on this deployment' })\n }\n const now = Date.now()\n const endpoints = hostRef.collection('orderWebhooks')\n\n if (action === 'create') {\n const url = cleanUrl(body.url)\n if ('error' in url) return res.status(400).json({ error: url.error })\n const events = Webhooks.normalizeOrderWebhookEvents(body.events)\n if (events.length === 0) return res.status(400).json({ error: 'Choose at least one event' })\n const secret = mintOrderWebhookSecret()\n const ref = endpoints.doc(createResourceUid())\n const created = await firestore.runTransaction(async (transaction) => {\n const existing = await transaction.get(endpoints.limit(Webhooks.ORDER_WEBHOOK_MAX_ENDPOINTS))\n if (existing.size >= Webhooks.ORDER_WEBHOOK_MAX_ENDPOINTS) return false\n const endpoint: Webhooks.OrderWebhookEndpoint = {\n url: url.url,\n events,\n enabled: true,\n ...(cleanDescription(body.description) ? { description: cleanDescription(body.description) } : {}),\n secretHint: secret.slice(-4),\n consecutiveFailures: 0,\n createdAtMs: now,\n updatedAtMs: now,\n createdBy: decoded.uid,\n }\n transaction.create(ref, endpoint)\n transaction.create(hostRef.collection('orderWebhookSecrets').doc(ref.id), sealedSecretRecord(hostId, ref.id, secret, keyring, now))\n return true\n })\n if (!created) {\n return res.status(409).json({ error: `A store can keep ${Webhooks.ORDER_WEBHOOK_MAX_ENDPOINTS} webhook endpoints` })\n }\n return res.status(200).json({ ok: true, id: ref.id, secret })\n }\n\n if (action === 'resend') {\n const deliveryId = String(body.deliveryId ?? '')\n if (!isId(deliveryId)) return res.status(400).json({ error: 'Missing deliveryId' })\n const deliveryRef = hostRef.collection('orderWebhookDeliveries').doc(deliveryId)\n const deliverySnapshot = await deliveryRef.get()\n if (!deliverySnapshot.exists) return res.status(404).json({ error: 'Unknown delivery' })\n const delivery = deliverySnapshot.data() as Webhooks.OrderWebhookDelivery\n const endpointSnapshot = await endpoints.doc(delivery.endpointId).get()\n if (!endpointSnapshot.exists) return res.status(404).json({ error: 'That endpoint was deleted' })\n if (delivery.body.length >= Webhooks.ORDER_WEBHOOK_BODY_MAX) {\n return res.status(409).json({ error: 'This delivery was too large to keep, so it cannot be resent' })\n }\n const endpoint = endpointSnapshot.data() as Webhooks.OrderWebhookEndpoint\n const secret = await readSecret(firestore, hostId, delivery.endpointId, keyring)\n const result = await postOrderWebhook({ url: endpoint.url, secret, event: delivery.event, eventId: delivery.eventId, body: delivery.body })\n const status: Webhooks.OrderWebhookDeliveryStatus = result.delivered\n ? 'delivered'\n : delivery.status === 'delivered'\n ? 'delivered'\n : delivery.status\n await recordAttempt(\n firestore,\n hostId,\n delivery.endpointId,\n { id: deliveryId, eventId: delivery.eventId, event: delivery.event, orderId: delivery.orderId, body: delivery.body, test: delivery.test, prior: delivery },\n result,\n status,\n Date.now(),\n )\n return res.status(200).json({ ok: true, delivered: result.delivered, attempt: result.attempt })\n }\n\n const endpointId = String(body.endpointId ?? '')\n if (!isId(endpointId)) return res.status(400).json({ error: 'Missing endpointId' })\n const endpointRef = endpoints.doc(endpointId)\n const endpointSnapshot = await endpointRef.get()\n if (!endpointSnapshot.exists) return res.status(404).json({ error: 'Unknown endpoint' })\n const endpoint = endpointSnapshot.data() as Webhooks.OrderWebhookEndpoint\n\n if (action === 'update') {\n const patch: Partial<Webhooks.OrderWebhookEndpoint> = { updatedAtMs: now }\n if (body.url !== undefined) {\n const url = cleanUrl(body.url)\n if ('error' in url) return res.status(400).json({ error: url.error })\n patch.url = url.url\n }\n if (body.events !== undefined) {\n const events = Webhooks.normalizeOrderWebhookEvents(body.events)\n if (events.length === 0) return res.status(400).json({ error: 'Choose at least one event' })\n patch.events = events\n }\n if (body.enabled !== undefined) {\n patch.enabled = Boolean(body.enabled)\n if (patch.enabled) patch.consecutiveFailures = 0\n }\n if (body.description !== undefined) patch.description = cleanDescription(body.description)\n await endpointRef.update(patch)\n return res.status(200).json({ ok: true })\n }\n\n if (action === 'roll-secret') {\n const secret = mintOrderWebhookSecret()\n const batch = firestore.batch()\n batch.set(hostRef.collection('orderWebhookSecrets').doc(endpointId), sealedSecretRecord(hostId, endpointId, secret, keyring, now))\n batch.update(endpointRef, { secretHint: secret.slice(-4), updatedAtMs: now })\n await batch.commit()\n return res.status(200).json({ ok: true, secret })\n }\n\n if (action === 'delete') {\n const batch = firestore.batch()\n batch.delete(endpointRef)\n batch.delete(hostRef.collection('orderWebhookSecrets').doc(endpointId))\n await batch.commit()\n return res.status(200).json({ ok: true })\n }\n\n // test\n const eventId = `test_${randomBytes(9).toString('base64url')}`\n const testBody = Webhooks.orderWebhookBody({\n eventId,\n event: Webhooks.ORDER_WEBHOOK_TEST_EVENT,\n occurredAtMs: now,\n hostId,\n payload: { message: `A test event from ${PLATFORM_BRAND_NAME}. Answer with any 2xx status.` },\n })\n const secret = await readSecret(firestore, hostId, endpointId, keyring)\n const result = await postOrderWebhook({ url: endpoint.url, secret, event: Webhooks.ORDER_WEBHOOK_TEST_EVENT, eventId, body: testBody })\n await recordAttempt(\n firestore,\n hostId,\n endpointId,\n { id: Webhooks.orderWebhookDeliveryId(eventId, endpointId), eventId, event: Webhooks.ORDER_WEBHOOK_TEST_EVENT, orderId: null, body: testBody, test: true, prior: null },\n result,\n result.delivered ? 'delivered' : 'failed',\n Date.now(),\n )\n return res.status(200).json({ ok: true, delivered: result.delivered, attempt: result.attempt })\n } catch (error) {\n console.error('order webhooks action failed', action, error)\n return res.status(500).json({ error: 'The webhook could not be updated. Please try again.' })\n }\n}\n"],"names":["createResourceUid","PLATFORM_BRAND_NAME","describeConfiguredUrlRefusal","configuredUrlRefusal","fetchConfiguredPublicUrl","firebaseAdmin","notifyHostManagers","PLUGIN_EVENT_MAX_ATTEMPTS","resolveOrgPermissions","createSecretBoxKey","openSecret","parseSecretBoxKeyring","sealSecret","SECRET_BOX_KEY_BYTES","createHmac","hkdfSync","randomBytes","COMMERCE_EVENT_DECLARATIONS","Webhooks","COMMERCE_SECRET_KEY_ENV","SECRETS","ORDER_WEBHOOK_LOG_RETENTION_MS","DERIVED_COMMERCE_KEY_ID","derivedCommerceKey","process","base","String","env","material","Uint8Array","readCommerceSecretKeyring","derived","raw","trim","ring","keys","some","key","id","current","orderWebhooksConfigured","sealContext","hostId","endpointId","mintOrderWebhookSecret","toString","signOrderWebhook","secret","timestampSeconds","body","update","orderWebhookSignedPayload","digest","hostRefFor","firestore","collection","doc","readSecret","keyring","snapshot","hostRef","get","sealed","Error","context","plaintext","sealedSecretRecord","now","sealedSecret","secretKeyId","updatedAtMs","postOrderWebhook","input","clock","Date","startedAt","timestamp","Math","floor","signature","formatOrderWebhookSignature","httpStatus","error","result","fetchImpl","url","method","headers","ORDER_WEBHOOK_SIGNATURE_HEADER","ORDER_WEBHOOK_EVENT_HEADER","event","ORDER_WEBHOOK_EVENT_ID_HEADER","eventId","signal","AbortSignal","timeout","ORDER_WEBHOOK_TIMEOUT_MS","refusal","status","orderWebhookAccepted","caught","name","message","slice","attempt","atMs","durationMs","max","delivered","expiresAt","Timestamp","fromMillis","recordAttempt","delivery","row","orderId","attempts","appendOrderWebhookAttempt","prior","ORDER_WEBHOOK_BODY_MAX","test","createdAtMs","set","lastDeliveryAtMs","lastDeliveryStatus","consecutiveFailures","FieldValue","increment","catch","undefined","deliverOrderEventToWebhooks","envelope","deps","app","endpoints","where","taking","docs","filter","orderWebhookTakes","data","occurredAtMs","length","failed","final","orderWebhookBody","payload","order","outcomes","Promise","all","map","endpoint","deliveryId","orderWebhookDeliveryId","priorSnapshot","exists","settled","attemptEndpoint","type","title","safeHost","link","URL","hostname","ACTIONS","isId","value","Boolean","includes","cleanUrl","cleanDescription","orderWebhooksHandler","req","res","json","authorization","idToken","startsWith","JSON","parse","action","hostSnapshot","decoded","auth","verifyIdToken","uid","membership","orgWide","hostRole","configured","maxEndpoints","ORDER_WEBHOOK_MAX_ENDPOINTS","events","entry","label","description","normalizeOrderWebhookEvents","ref","created","runTransaction","transaction","existing","limit","size","enabled","secretHint","createdBy","create","ok","deliveryRef","deliverySnapshot","endpointSnapshot","endpointRef","patch","batch","commit","delete","testBody","ORDER_WEBHOOK_TEST_EVENT","console"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/order-webhooks.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 { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { PLATFORM_BRAND_NAME } from '@aglyn/aglyn/app-utils/platform-brand'\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport type { PluginDomainEventEnvelope } from '@aglyn/aglyn/plugin-manager/plugin-domain-events'\nimport {\n describeConfiguredUrlRefusal,\n configuredUrlRefusal,\n fetchConfiguredPublicUrl,\n firebaseAdmin,\n notifyHostManagers,\n} from '@aglyn/tenant-data-admin'\nimport { PLUGIN_EVENT_MAX_ATTEMPTS } from '@aglyn/tenant-data-admin/server/plugin-event-outbox'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n createSecretBoxKey,\n openSecret,\n parseSecretBoxKeyring,\n sealSecret,\n SECRET_BOX_KEY_BYTES,\n type SecretBoxKey,\n type SecretBoxKeyring,\n} from '@aglyn/shared-util-tools/secret-box'\nimport { createHmac, hkdfSync, randomBytes } from 'node:crypto'\nimport { COMMERCE_EVENT_DECLARATIONS } from '../model/order-events'\nimport * as Webhooks from '../model/order-webhooks'\nimport { commerceWorkspacePermissionRefusal } from '../model/workspace-site-permissions'\nimport { holdsCommerceWorkspaceSiteKey } from './commerce-site-keys'\n\n/**\n * Merchant order webhooks (AGL-3611): the console's door and the delivery\n * behind it. The model (`model/order-webhooks.ts`) says what is stored where.\n *\n * DELIVERY RIDES THE PLUGIN EVENT OUTBOX. Commerce subscribes to its own\n * order and return events under the name `webhooks`; the subscriber posts the\n * event to every endpoint that takes it and records each attempt in the\n * delivery log. An endpoint that already took an event is not posted it\n * again, so when one of three endpoints fails, the subscriber throws, the\n * outbox retries it with its backoff (30s, 2m, 10m, 30m, 1h, 6h, 12h), and\n * only the failing endpoint is called again. On the last attempt the delivery\n * is marked failed and the site's managers are told.\n *\n * EVERY CALL GOES THROUGH `fetchConfiguredPublicUrl`: https on the default\n * port, a public address pinned at connect time, no redirect followed. A\n * merchant's endpoint is a stranger's server to us.\n *\n * THE SECRET IS SEALED with the shared secret box, under the keyring\n * `readCommerceSecretKeyring` answers, bound to its endpoint by the seal's\n * context. The plaintext leaves the server once, in the answer to the request\n * that made or rolled it.\n */\n\nexport const COMMERCE_SECRET_KEY_ENV = 'COMMERCE_SECRET_KEY'\n\nconst SECRETS = 'orderWebhookSecrets'\n\n/** Delivery rows live this long, then Firestore's TTL removes them. */\nexport const ORDER_WEBHOOK_LOG_RETENTION_MS = 30 * 86_400_000\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The id of the key derived from `TOKEN_SIGNING_SECRET`. */\nexport const DERIVED_COMMERCE_KEY_ID = 'tss1'\n\n/**\n * A secret-box key derived from `TOKEN_SIGNING_SECRET` with HKDF, under its\n * own label, so the commerce tokens and the sealed secrets never share key\n * material. `null` when that secret is unset.\n */\nfunction derivedCommerceKey(): SecretBoxKey | null {\n const base = String(process.env['TOKEN_SIGNING_SECRET'] ?? '')\n if (!base) return null\n const material = new Uint8Array(hkdfSync('sha256', base, 'aglyn-commerce', 'secret-box:v1', SECRET_BOX_KEY_BYTES))\n return createSecretBoxKey(material, DERIVED_COMMERCE_KEY_ID)\n}\n\n/**\n * The keyring commerce seals its stored secrets with. `COMMERCE_SECRET_KEY`\n * (a secret-box keyring) when it is set, its first key sealing; otherwise the\n * key derived from `TOKEN_SIGNING_SECRET`, which every deployment that sells\n * already has. The derived key stays in the ring after a dedicated key is\n * configured, so what it sealed still opens. `null` when neither is usable.\n * Never throws.\n */\nexport function readCommerceSecretKeyring(): SecretBoxKeyring | null {\n let derived: SecretBoxKey | null = null\n try {\n derived = derivedCommerceKey()\n } catch {\n derived = null\n }\n const raw = String(process.env[COMMERCE_SECRET_KEY_ENV] ?? '').trim()\n if (raw) {\n try {\n const ring = parseSecretBoxKeyring(raw)\n const keys = derived && !ring.keys.some((key) => key.id === derived!.id) ? [...ring.keys, derived] : ring.keys\n return { current: ring.current, keys }\n } catch {\n // An unusable dedicated key falls back to the derived one rather than\n // sealing nothing.\n }\n }\n return derived ? { current: derived, keys: [derived] } : null\n}\n\n/** Whether this deployment can sign webhooks at all. */\nexport function orderWebhooksConfigured(): boolean {\n return readCommerceSecretKeyring() !== null\n}\n\nconst sealContext = (hostId: string, endpointId: string) => `hosts/${hostId}/${SECRETS}/${endpointId}#secret`\n\n/** A fresh signing secret: `whsec_` and 32 random bytes. */\nexport function mintOrderWebhookSecret(): string {\n return `whsec_${randomBytes(32).toString('base64url')}`\n}\n\n/** The HMAC-SHA256 of `{t}.{body}` under the secret, as hex. */\nexport function signOrderWebhook(secret: string, timestampSeconds: number, body: string): string {\n return createHmac('sha256', secret).update(Webhooks.orderWebhookSignedPayload(timestampSeconds, body)).digest('hex')\n}\n\nconst hostRefFor = (firestore: Firestore, hostId: string) => firestore.collection('hosts').doc(hostId)\n\nasync function readSecret(\n firestore: Firestore,\n hostId: string,\n endpointId: string,\n keyring: SecretBoxKeyring,\n): Promise<string> {\n const hostRef = hostRefFor(firestore, hostId)\n const snapshot = await hostRef.collection('orderWebhookSecrets').doc(endpointId).get()\n const sealed = String(snapshot.get('sealedSecret') ?? '')\n if (!sealed) throw new Error('This endpoint has no signing secret')\n return openSecret(sealed, keyring, { context: sealContext(hostId, endpointId) }).plaintext\n}\n\nfunction sealedSecretRecord(\n hostId: string,\n endpointId: string,\n secret: string,\n keyring: SecretBoxKeyring,\n now: number,\n): Webhooks.OrderWebhookSecretRecord {\n return {\n sealedSecret: sealSecret(secret, keyring.current, { context: sealContext(hostId, endpointId) }),\n secretKeyId: keyring.current.id,\n updatedAtMs: now,\n }\n}\n\n/** What one POST came to. */\nexport interface OrderWebhookPostResult {\n attempt: Webhooks.OrderWebhookAttempt\n delivered: boolean\n}\n\n/**\n * Posts one body to one endpoint, signed. Never throws: a refusal, a timeout\n * or a reset is an attempt that failed, with the reason in words.\n */\nexport async function postOrderWebhook(input: {\n url: string\n secret: string\n event: string\n eventId: string\n body: string\n now?: () => number\n fetchImpl?: typeof fetchConfiguredPublicUrl\n}): Promise<OrderWebhookPostResult> {\n const clock = input.now ?? Date.now\n const startedAt = clock()\n const timestamp = Math.floor(startedAt / 1000)\n const signature = Webhooks.formatOrderWebhookSignature(timestamp, signOrderWebhook(input.secret, timestamp, input.body))\n let httpStatus: number | null = null\n let error: string | undefined\n try {\n const result = await (input.fetchImpl ?? fetchConfiguredPublicUrl)(input.url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'user-agent': 'Aglyn-Webhooks/1.0',\n [Webhooks.ORDER_WEBHOOK_SIGNATURE_HEADER]: signature,\n [Webhooks.ORDER_WEBHOOK_EVENT_HEADER]: input.event,\n [Webhooks.ORDER_WEBHOOK_EVENT_ID_HEADER]: input.eventId,\n },\n body: input.body,\n signal: AbortSignal.timeout(Webhooks.ORDER_WEBHOOK_TIMEOUT_MS),\n })\n if ('refusal' in result) {\n error = `Not sent: ${describeConfiguredUrlRefusal(result.refusal)}`\n } else {\n httpStatus = result.status\n if (!Webhooks.orderWebhookAccepted(httpStatus)) {\n error =\n httpStatus >= 300 && httpStatus < 400\n ? `The endpoint answered ${httpStatus}, a redirect; redirects are not followed`\n : `The endpoint answered ${httpStatus}`\n }\n }\n } catch (caught) {\n const name = (caught as Error)?.name\n error =\n name === 'TimeoutError' || name === 'AbortError'\n ? `No answer within ${Webhooks.ORDER_WEBHOOK_TIMEOUT_MS / 1000} seconds`\n : `The connection failed: ${String((caught as Error)?.message ?? caught).slice(0, 200)}`\n }\n const attempt: Webhooks.OrderWebhookAttempt = {\n atMs: startedAt,\n httpStatus,\n ...(error ? { error } : {}),\n durationMs: Math.max(0, clock() - startedAt),\n }\n return { attempt, delivered: !error }\n}\n\nfunction expiresAt(now: number) {\n return firebaseAdmin.firestore.Timestamp.fromMillis(now + ORDER_WEBHOOK_LOG_RETENTION_MS)\n}\n\n/** Records an attempt on the delivery row and on the endpoint's summary. */\nasync function recordAttempt(\n firestore: Firestore,\n hostId: string,\n endpointId: string,\n delivery: {\n id: string\n eventId: string\n event: string\n orderId: string | null\n body: string\n test?: boolean\n prior: Webhooks.OrderWebhookDelivery | null\n },\n result: OrderWebhookPostResult,\n status: Webhooks.OrderWebhookDeliveryStatus,\n now: number,\n): Promise<void> {\n const hostRef = hostRefFor(firestore, hostId)\n const row: Webhooks.OrderWebhookDelivery & { expiresAt: unknown } = {\n endpointId,\n eventId: delivery.eventId,\n event: delivery.event,\n orderId: delivery.orderId,\n status,\n attempts: Webhooks.appendOrderWebhookAttempt(delivery.prior?.attempts, result.attempt),\n body: delivery.body.slice(0, Webhooks.ORDER_WEBHOOK_BODY_MAX),\n ...(delivery.test ? { test: true } : {}),\n createdAtMs: delivery.prior?.createdAtMs ?? now,\n updatedAtMs: now,\n expiresAt: expiresAt(now),\n }\n await hostRef.collection('orderWebhookDeliveries').doc(delivery.id).set(row)\n await hostRef\n .collection('orderWebhooks')\n .doc(endpointId)\n .update({\n lastDeliveryAtMs: now,\n lastDeliveryStatus: status,\n consecutiveFailures: result.delivered ? 0 : firebaseAdmin.firestore.FieldValue.increment(1),\n })\n .catch(() => undefined)\n}\n\n/**\n * The subscriber: posts one event to every endpoint of the site that takes\n * it and has not taken it already. Throws when any endpoint still owes it, so\n * the outbox retries; on the outbox's last attempt it records the failure and\n * tells the site's managers instead.\n */\nexport async function deliverOrderEventToWebhooks(\n envelope: PluginDomainEventEnvelope,\n deps: { fetchImpl?: typeof fetchConfiguredPublicUrl; now?: () => number } = {},\n): Promise<{ delivered: number; failed: number }> {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = hostRefFor(firestore, envelope.hostId)\n const endpoints = await hostRef.collection('orderWebhooks').where('enabled', '==', true).get()\n const taking = endpoints.docs.filter((doc) =>\n Webhooks.orderWebhookTakes(doc.data() as Webhooks.OrderWebhookEndpoint, envelope.event, envelope.occurredAtMs),\n )\n if (taking.length === 0) return { delivered: 0, failed: 0 }\n const keyring = readCommerceSecretKeyring()\n const clock = deps.now ?? Date.now\n const final = envelope.attempt >= PLUGIN_EVENT_MAX_ATTEMPTS\n const body = Webhooks.orderWebhookBody({\n eventId: envelope.id,\n event: envelope.event,\n occurredAtMs: envelope.occurredAtMs,\n hostId: envelope.hostId,\n payload: envelope.payload,\n })\n const orderId = String(((envelope.payload ?? {}) as { order?: { id?: unknown } }).order?.id ?? '') || null\n const outcomes = await Promise.all(\n taking.map(async (doc) => {\n const endpoint = doc.data() as Webhooks.OrderWebhookEndpoint\n const deliveryId = Webhooks.orderWebhookDeliveryId(envelope.id, doc.id)\n const priorSnapshot = await hostRef.collection('orderWebhookDeliveries').doc(deliveryId).get()\n const prior = priorSnapshot.exists ? (priorSnapshot.data() as Webhooks.OrderWebhookDelivery) : null\n // Taken already, on an earlier pass or by a resend from the console.\n if (prior?.status === 'delivered') return 'delivered' as const\n const settled = await attemptEndpoint({\n firestore,\n hostId: envelope.hostId,\n endpointId: doc.id,\n url: endpoint.url,\n keyring,\n event: envelope.event,\n eventId: envelope.id,\n body,\n clock,\n fetchImpl: deps.fetchImpl,\n })\n const status: Webhooks.OrderWebhookDeliveryStatus = settled.delivered ? 'delivered' : final ? 'failed' : 'retrying'\n await recordAttempt(\n firestore,\n envelope.hostId,\n doc.id,\n { id: deliveryId, eventId: envelope.id, event: envelope.event, orderId, body, prior },\n settled,\n status,\n clock(),\n )\n if (status === 'failed') {\n await notifyHostManagers(envelope.hostId, {\n type: 'content.order',\n title: 'An order webhook could not be delivered',\n body:\n `${envelope.event} never reached ${safeHost(endpoint.url)} on {site} after ` +\n `${envelope.attempt} attempts (${settled.attempt.error ?? 'no answer'}). ` +\n 'Resend it from the delivery log once the endpoint is fixed.',\n link: `/${envelope.hostId}/products/notifications`,\n }).catch(() => undefined)\n }\n return status\n }),\n )\n const delivered = outcomes.filter((status) => status === 'delivered').length\n const failed = outcomes.length - delivered\n if (failed > 0 && !final) {\n throw new Error(`${failed} of ${outcomes.length} order webhook endpoints did not take ${envelope.event}`)\n }\n return { delivered, failed }\n}\n\n/** One signed attempt at one endpoint; a missing key or secret is a failed attempt. */\nasync function attemptEndpoint(input: {\n firestore: Firestore\n hostId: string\n endpointId: string\n url: string\n keyring: SecretBoxKeyring | null\n event: string\n eventId: string\n body: string\n clock: () => number\n fetchImpl?: typeof fetchConfiguredPublicUrl\n}): Promise<OrderWebhookPostResult> {\n const failed = (error: string): OrderWebhookPostResult => ({\n delivered: false,\n attempt: { atMs: input.clock(), httpStatus: null, error, durationMs: 0 },\n })\n if (!input.keyring) return failed('Webhook signing is not configured')\n let secret: string\n try {\n secret = await readSecret(input.firestore, input.hostId, input.endpointId, input.keyring)\n } catch (error) {\n return failed(`The signing secret could not be read: ${String((error as Error)?.message ?? error).slice(0, 120)}`)\n }\n return postOrderWebhook({\n url: input.url,\n secret,\n event: input.event,\n eventId: input.eventId,\n body: input.body,\n fetchImpl: input.fetchImpl,\n now: input.clock,\n })\n}\n\nfunction safeHost(url: string): string {\n try {\n return new URL(url).hostname\n } catch {\n return 'the endpoint'\n }\n}\n\n// ---------------------------------------------------------------------------\n// The console's door\n// ---------------------------------------------------------------------------\n\nconst ACTIONS = ['status', 'create', 'update', 'roll-secret', 'delete', 'test', 'resend'] as const\ntype Action = (typeof ACTIONS)[number]\n\nconst isId = (value: string) => Boolean(value) && value.length <= 1500 && !/^__.*__$/.test(value) && !value.includes('/')\n\nfunction cleanUrl(raw: unknown): { url: string } | { error: string } {\n const url = String(raw ?? '').trim()\n if (!url) return { error: 'Enter the endpoint’s address' }\n if (url.length > 2048) return { error: 'That address is too long' }\n const refusal = configuredUrlRefusal(url)\n if (refusal) return { error: `This endpoint cannot be used: ${describeConfiguredUrlRefusal(refusal)}` }\n return { url }\n}\n\nconst cleanDescription = (raw: unknown) => String(raw ?? '').trim().slice(0, 120)\n\n/**\n * `POST /api/commerce/order-webhooks` with `{ hostId, action, … }`. Endpoints\n * post the order book to an outside server, so only an admin of the whole\n * workspace manages them — the bar a refund sets.\n *\n * - `status` → `{ configured, events: [{ event, label, description }] }`\n * - `create` `{ url, events, description? }` → `{ id, secret }` (shown once)\n * - `update` `{ endpointId, url?, events?, enabled?, description? }`\n * - `roll-secret` `{ endpointId }` → `{ secret }`\n * - `delete` `{ endpointId }`; the delivery log stays until it expires\n * - `test` `{ endpointId }` → posts a `webhook.test` event now\n * - `resend` `{ deliveryId }` → posts a logged delivery again now\n */\nexport const orderWebhooksHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n let body: Record<string, any>\n try {\n body = (typeof req.body === 'string' ? JSON.parse(req.body) : req.body) ?? {}\n } catch {\n return res.status(400).json({ error: 'Invalid JSON' })\n }\n const hostId = String(body.hostId ?? '')\n const action = String(body.action ?? '') as Action\n if (!isId(hostId)) return res.status(400).json({ error: 'Missing hostId' })\n if (!ACTIONS.includes(action)) return res.status(400).json({ error: 'Unknown action' })\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = hostRefFor(firestore, hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) return res.status(404).json({ error: 'Unknown site' })\n // \"Manage order webhooks\" (`commerce.webhooks`, AGL-4047): its preset is\n // the gate this asked before, the site's Admin who is a member of the\n // whole workspace (a webhook's secret signs every order it is sent).\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!holdsCommerceWorkspaceSiteKey(hostSnapshot, decoded.uid, 'commerce.webhooks', membership)) {\n return res.status(403).json({ error: commerceWorkspacePermissionRefusal('commerce.webhooks') })\n }\n const keyring = readCommerceSecretKeyring()\n if (action === 'status') {\n return res.status(200).json({\n configured: Boolean(keyring),\n maxEndpoints: Webhooks.ORDER_WEBHOOK_MAX_ENDPOINTS,\n events: COMMERCE_EVENT_DECLARATIONS.map((entry) => ({\n event: entry.event.id,\n label: entry.label,\n description: entry.description,\n })),\n })\n }\n if (!keyring) {\n return res.status(503).json({ error: 'Order webhooks are not configured on this deployment' })\n }\n const now = Date.now()\n const endpoints = hostRef.collection('orderWebhooks')\n\n if (action === 'create') {\n const url = cleanUrl(body.url)\n if ('error' in url) return res.status(400).json({ error: url.error })\n const events = Webhooks.normalizeOrderWebhookEvents(body.events)\n if (events.length === 0) return res.status(400).json({ error: 'Choose at least one event' })\n const secret = mintOrderWebhookSecret()\n const ref = endpoints.doc(createResourceUid())\n const created = await firestore.runTransaction(async (transaction) => {\n const existing = await transaction.get(endpoints.limit(Webhooks.ORDER_WEBHOOK_MAX_ENDPOINTS))\n if (existing.size >= Webhooks.ORDER_WEBHOOK_MAX_ENDPOINTS) return false\n const endpoint: Webhooks.OrderWebhookEndpoint = {\n url: url.url,\n events,\n enabled: true,\n ...(cleanDescription(body.description) ? { description: cleanDescription(body.description) } : {}),\n secretHint: secret.slice(-4),\n consecutiveFailures: 0,\n createdAtMs: now,\n updatedAtMs: now,\n createdBy: decoded.uid,\n }\n transaction.create(ref, endpoint)\n transaction.create(hostRef.collection('orderWebhookSecrets').doc(ref.id), sealedSecretRecord(hostId, ref.id, secret, keyring, now))\n return true\n })\n if (!created) {\n return res.status(409).json({ error: `A store can keep ${Webhooks.ORDER_WEBHOOK_MAX_ENDPOINTS} webhook endpoints` })\n }\n return res.status(200).json({ ok: true, id: ref.id, secret })\n }\n\n if (action === 'resend') {\n const deliveryId = String(body.deliveryId ?? '')\n if (!isId(deliveryId)) return res.status(400).json({ error: 'Missing deliveryId' })\n const deliveryRef = hostRef.collection('orderWebhookDeliveries').doc(deliveryId)\n const deliverySnapshot = await deliveryRef.get()\n if (!deliverySnapshot.exists) return res.status(404).json({ error: 'Unknown delivery' })\n const delivery = deliverySnapshot.data() as Webhooks.OrderWebhookDelivery\n const endpointSnapshot = await endpoints.doc(delivery.endpointId).get()\n if (!endpointSnapshot.exists) return res.status(404).json({ error: 'That endpoint was deleted' })\n if (delivery.body.length >= Webhooks.ORDER_WEBHOOK_BODY_MAX) {\n return res.status(409).json({ error: 'This delivery was too large to keep, so it cannot be resent' })\n }\n const endpoint = endpointSnapshot.data() as Webhooks.OrderWebhookEndpoint\n const secret = await readSecret(firestore, hostId, delivery.endpointId, keyring)\n const result = await postOrderWebhook({ url: endpoint.url, secret, event: delivery.event, eventId: delivery.eventId, body: delivery.body })\n const status: Webhooks.OrderWebhookDeliveryStatus = result.delivered\n ? 'delivered'\n : delivery.status === 'delivered'\n ? 'delivered'\n : delivery.status\n await recordAttempt(\n firestore,\n hostId,\n delivery.endpointId,\n { id: deliveryId, eventId: delivery.eventId, event: delivery.event, orderId: delivery.orderId, body: delivery.body, test: delivery.test, prior: delivery },\n result,\n status,\n Date.now(),\n )\n return res.status(200).json({ ok: true, delivered: result.delivered, attempt: result.attempt })\n }\n\n const endpointId = String(body.endpointId ?? '')\n if (!isId(endpointId)) return res.status(400).json({ error: 'Missing endpointId' })\n const endpointRef = endpoints.doc(endpointId)\n const endpointSnapshot = await endpointRef.get()\n if (!endpointSnapshot.exists) return res.status(404).json({ error: 'Unknown endpoint' })\n const endpoint = endpointSnapshot.data() as Webhooks.OrderWebhookEndpoint\n\n if (action === 'update') {\n const patch: Partial<Webhooks.OrderWebhookEndpoint> = { updatedAtMs: now }\n if (body.url !== undefined) {\n const url = cleanUrl(body.url)\n if ('error' in url) return res.status(400).json({ error: url.error })\n patch.url = url.url\n }\n if (body.events !== undefined) {\n const events = Webhooks.normalizeOrderWebhookEvents(body.events)\n if (events.length === 0) return res.status(400).json({ error: 'Choose at least one event' })\n patch.events = events\n }\n if (body.enabled !== undefined) {\n patch.enabled = Boolean(body.enabled)\n if (patch.enabled) patch.consecutiveFailures = 0\n }\n if (body.description !== undefined) patch.description = cleanDescription(body.description)\n await endpointRef.update(patch)\n return res.status(200).json({ ok: true })\n }\n\n if (action === 'roll-secret') {\n const secret = mintOrderWebhookSecret()\n const batch = firestore.batch()\n batch.set(hostRef.collection('orderWebhookSecrets').doc(endpointId), sealedSecretRecord(hostId, endpointId, secret, keyring, now))\n batch.update(endpointRef, { secretHint: secret.slice(-4), updatedAtMs: now })\n await batch.commit()\n return res.status(200).json({ ok: true, secret })\n }\n\n if (action === 'delete') {\n const batch = firestore.batch()\n batch.delete(endpointRef)\n batch.delete(hostRef.collection('orderWebhookSecrets').doc(endpointId))\n await batch.commit()\n return res.status(200).json({ ok: true })\n }\n\n // test\n const eventId = `test_${randomBytes(9).toString('base64url')}`\n const testBody = Webhooks.orderWebhookBody({\n eventId,\n event: Webhooks.ORDER_WEBHOOK_TEST_EVENT,\n occurredAtMs: now,\n hostId,\n payload: { message: `A test event from ${PLATFORM_BRAND_NAME}. Answer with any 2xx status.` },\n })\n const secret = await readSecret(firestore, hostId, endpointId, keyring)\n const result = await postOrderWebhook({ url: endpoint.url, secret, event: Webhooks.ORDER_WEBHOOK_TEST_EVENT, eventId, body: testBody })\n await recordAttempt(\n firestore,\n hostId,\n endpointId,\n { id: Webhooks.orderWebhookDeliveryId(eventId, endpointId), eventId, event: Webhooks.ORDER_WEBHOOK_TEST_EVENT, orderId: null, body: testBody, test: true, prior: null },\n result,\n result.delivered ? 'delivered' : 'failed',\n Date.now(),\n )\n return res.status(200).json({ ok: true, delivered: result.delivered, attempt: result.attempt })\n } catch (error) {\n console.error('order webhooks action failed', action, error)\n return res.status(500).json({ error: 'The webhook could not be updated. Please try again.' })\n }\n}\n"],"names":["createResourceUid","PLATFORM_BRAND_NAME","describeConfiguredUrlRefusal","configuredUrlRefusal","fetchConfiguredPublicUrl","firebaseAdmin","notifyHostManagers","PLUGIN_EVENT_MAX_ATTEMPTS","resolveOrgPermissions","createSecretBoxKey","openSecret","parseSecretBoxKeyring","sealSecret","SECRET_BOX_KEY_BYTES","createHmac","hkdfSync","randomBytes","COMMERCE_EVENT_DECLARATIONS","Webhooks","commerceWorkspacePermissionRefusal","holdsCommerceWorkspaceSiteKey","COMMERCE_SECRET_KEY_ENV","SECRETS","ORDER_WEBHOOK_LOG_RETENTION_MS","DERIVED_COMMERCE_KEY_ID","derivedCommerceKey","process","base","String","env","material","Uint8Array","readCommerceSecretKeyring","derived","raw","trim","ring","keys","some","key","id","current","orderWebhooksConfigured","sealContext","hostId","endpointId","mintOrderWebhookSecret","toString","signOrderWebhook","secret","timestampSeconds","body","update","orderWebhookSignedPayload","digest","hostRefFor","firestore","collection","doc","readSecret","keyring","snapshot","hostRef","get","sealed","Error","context","plaintext","sealedSecretRecord","now","sealedSecret","secretKeyId","updatedAtMs","postOrderWebhook","input","clock","Date","startedAt","timestamp","Math","floor","signature","formatOrderWebhookSignature","httpStatus","error","result","fetchImpl","url","method","headers","ORDER_WEBHOOK_SIGNATURE_HEADER","ORDER_WEBHOOK_EVENT_HEADER","event","ORDER_WEBHOOK_EVENT_ID_HEADER","eventId","signal","AbortSignal","timeout","ORDER_WEBHOOK_TIMEOUT_MS","refusal","status","orderWebhookAccepted","caught","name","message","slice","attempt","atMs","durationMs","max","delivered","expiresAt","Timestamp","fromMillis","recordAttempt","delivery","row","orderId","attempts","appendOrderWebhookAttempt","prior","ORDER_WEBHOOK_BODY_MAX","test","createdAtMs","set","lastDeliveryAtMs","lastDeliveryStatus","consecutiveFailures","FieldValue","increment","catch","undefined","deliverOrderEventToWebhooks","envelope","deps","app","endpoints","where","taking","docs","filter","orderWebhookTakes","data","occurredAtMs","length","failed","final","orderWebhookBody","payload","order","outcomes","Promise","all","map","endpoint","deliveryId","orderWebhookDeliveryId","priorSnapshot","exists","settled","attemptEndpoint","type","title","safeHost","link","URL","hostname","ACTIONS","isId","value","Boolean","includes","cleanUrl","cleanDescription","orderWebhooksHandler","req","res","json","authorization","idToken","startsWith","JSON","parse","action","decoded","auth","verifyIdToken","hostSnapshot","membership","uid","configured","maxEndpoints","ORDER_WEBHOOK_MAX_ENDPOINTS","events","entry","label","description","normalizeOrderWebhookEvents","ref","created","runTransaction","transaction","existing","limit","size","enabled","secretHint","createdBy","create","ok","deliveryRef","deliverySnapshot","endpointSnapshot","endpointRef","patch","batch","commit","delete","testBody","ORDER_WEBHOOK_TEST_EVENT","console"],"mappings":"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"}
|
|
@@ -45,6 +45,13 @@ export interface CommercePersonEraserDeps {
|
|
|
45
45
|
/** The patch that takes the buyer off one order, as it stands. */
|
|
46
46
|
export declare function erasedOrderPatch(data: FirebaseFirestore.DocumentData | undefined, now: number): Record<string, unknown>;
|
|
47
47
|
/** The patch that takes the customer off one check-in at the window (AGL-4043). */
|
|
48
|
+
/**
|
|
49
|
+
* A quote's customer, erased (AGL-4093). An unpaid quote keeps its customer
|
|
50
|
+
* in its `quote` section rather than on the order's own fields, so it is
|
|
51
|
+
* found by `quote.customer.emailLower` and loses the person and both
|
|
52
|
+
* addresses; its lines and totals stay, as an order's do.
|
|
53
|
+
*/
|
|
54
|
+
export declare function erasedQuotePatch(data: FirebaseFirestore.DocumentData | undefined, now: number): Record<string, unknown>;
|
|
48
55
|
export declare function erasedCheckInPatch(data: FirebaseFirestore.DocumentData | undefined, now: number): Record<string, unknown>;
|
|
49
56
|
export declare function createCommercePersonEraser(deps: CommercePersonEraserDeps): PluginPersonEraser;
|
|
50
57
|
/** The eraser over the platform's own Firestore, as the declarations register it. */
|
|
@@ -18,6 +18,7 @@ import { updateWhereEquals } from "@aglyn/tenant-data-admin/server/paged-sweeps"
|
|
|
18
18
|
import { FieldValue } from "firebase-admin/firestore";
|
|
19
19
|
import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
20
20
|
const LABEL = 'commerce person eraser';
|
|
21
|
+
/** Where a quote keeps its customer's address (`commerce-quotes.ts`). */ const QUOTE_EMAIL_FIELD = 'quote.customer.emailLower';
|
|
21
22
|
/** The patch that takes the buyer off one order, as it stands. */ export function erasedOrderPatch(data, now) {
|
|
22
23
|
const addressTokens = new Set(Array.isArray(data == null ? void 0 : data['customerEmailTokens']) ? data['customerEmailTokens'] : []);
|
|
23
24
|
return _extends({
|
|
@@ -32,7 +33,24 @@ const LABEL = 'commerce person eraser';
|
|
|
32
33
|
searchTokens: data['searchTokens'].filter((token)=>!addressTokens.has(token))
|
|
33
34
|
} : {});
|
|
34
35
|
}
|
|
35
|
-
/** The patch that takes the customer off one check-in at the window (AGL-4043). */
|
|
36
|
+
/** The patch that takes the customer off one check-in at the window (AGL-4043). */ /**
|
|
37
|
+
* A quote's customer, erased (AGL-4093). An unpaid quote keeps its customer
|
|
38
|
+
* in its `quote` section rather than on the order's own fields, so it is
|
|
39
|
+
* found by `quote.customer.emailLower` and loses the person and both
|
|
40
|
+
* addresses; its lines and totals stay, as an order's do.
|
|
41
|
+
*/ export function erasedQuotePatch(data, now) {
|
|
42
|
+
var _ref;
|
|
43
|
+
const quote = (_ref = data == null ? void 0 : data['quote']) != null ? _ref : {};
|
|
44
|
+
return {
|
|
45
|
+
quote: _extends({}, quote, {
|
|
46
|
+
customer: {},
|
|
47
|
+
billTo: null,
|
|
48
|
+
shipTo: null
|
|
49
|
+
}),
|
|
50
|
+
customerErasedAtMs: now
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function erasedCheckInPatch(data, now) {
|
|
36
54
|
var _ref, _ref1;
|
|
37
55
|
// One key per order or stay, so the people one order brought still read
|
|
38
56
|
// as one party, and as nobody's name.
|
|
@@ -54,6 +72,7 @@ export function createCommercePersonEraser(deps) {
|
|
|
54
72
|
const hosts = await db.collection('hosts').where('orgId', '==', orgId).get();
|
|
55
73
|
let orders = 0;
|
|
56
74
|
let checkIns = 0;
|
|
75
|
+
let quotes = 0;
|
|
57
76
|
const emailLower = email.trim().toLowerCase();
|
|
58
77
|
for (const host of hosts.docs){
|
|
59
78
|
const placed = host.ref.collection('orders');
|
|
@@ -61,14 +80,17 @@ export function createCommercePersonEraser(deps) {
|
|
|
61
80
|
if (dryRun) {
|
|
62
81
|
orders += (await placed.where('customerEmail', '==', email).get()).size;
|
|
63
82
|
checkIns += (await checked.where('customerEmailLower', '==', emailLower).get()).size;
|
|
83
|
+
quotes += (await placed.where(QUOTE_EMAIL_FIELD, '==', emailLower).get()).size;
|
|
64
84
|
continue;
|
|
65
85
|
}
|
|
66
86
|
checkIns += await updateWhereEquals(db, checked, 'customerEmailLower', emailLower, (data)=>erasedCheckInPatch(data, atMs), LABEL);
|
|
67
87
|
orders += await updateWhereEquals(db, placed, 'customerEmail', email, (data)=>erasedOrderPatch(data, atMs), LABEL);
|
|
88
|
+
quotes += await updateWhereEquals(db, placed, QUOTE_EMAIL_FIELD, emailLower, (data)=>erasedQuotePatch(data, atMs), LABEL);
|
|
68
89
|
}
|
|
69
90
|
return {
|
|
70
91
|
orders,
|
|
71
|
-
checkIns
|
|
92
|
+
checkIns,
|
|
93
|
+
quotes
|
|
72
94
|
};
|
|
73
95
|
};
|
|
74
96
|
}
|
|
@@ -1 +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 check-in at the window (AGL-4043) copies the buyer's name, address and\n * phone, so the same person is taken off each of theirs, by the stored\n * lower-cased address; the check-in itself — who was due, who checked them\n * in, the wristband — stays, as the order does.\n *\n * A DRY RUN counts the orders and check-ins 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\n/** The patch that takes the customer off one check-in at the window (AGL-4043). */\nexport function erasedCheckInPatch(\n data: FirebaseFirestore.DocumentData | undefined,\n now: number,\n): Record<string, unknown> {\n // One key per order or stay, so the people one order brought still read\n // as one party, and as nobody's name.\n const source = String(data?.['orderId'] ?? data?.['reservationId'] ?? '')\n return {\n customerKey: `erased:${source}`,\n customerName: null,\n customerEmail: null,\n customerEmailLower: null,\n customerPhone: null,\n contactId: null,\n searchTokens: [],\n customerErasedAtMs: now,\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 let checkIns = 0\n const emailLower = email.trim().toLowerCase()\n for (const host of hosts.docs) {\n const placed = host.ref.collection('orders')\n const checked = host.ref.collection('checkIns')\n if (dryRun) {\n orders += (await placed.where('customerEmail', '==', email).get()).size\n checkIns += (await checked.where('customerEmailLower', '==', emailLower).get()).size\n continue\n }\n checkIns += await updateWhereEquals(\n db,\n checked,\n 'customerEmailLower',\n emailLower,\n (data) => erasedCheckInPatch(data, atMs),\n LABEL,\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, checkIns }\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","erasedCheckInPatch","source","String","customerKey","customerPhone","contactId","createCommercePersonEraser","deps","orgId","email","dryRun","atMs","db","firestore","hosts","collection","where","get","orders","checkIns","emailLower","trim","toLowerCase","host","docs","placed","ref","checked","size","commercePersonEraser","request","app"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAMD,SAASA,iBAAiB,QAAQ,+CAA8C;AAChF,SAASC,UAAU,QAAQ,2BAA0B;AACrD,SAASC,aAAa,QAAQ,iDAAgD;AAgC9E,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,
|
|
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 check-in at the window (AGL-4043) copies the buyer's name, address and\n * phone, so the same person is taken off each of theirs, by the stored\n * lower-cased address; the check-in itself — who was due, who checked them\n * in, the wristband — stays, as the order does.\n *\n * A DRY RUN counts the orders and check-ins 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/** Where a quote keeps its customer's address (`commerce-quotes.ts`). */\nconst QUOTE_EMAIL_FIELD = 'quote.customer.emailLower'\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\n/** The patch that takes the customer off one check-in at the window (AGL-4043). */\n/**\n * A quote's customer, erased (AGL-4093). An unpaid quote keeps its customer\n * in its `quote` section rather than on the order's own fields, so it is\n * found by `quote.customer.emailLower` and loses the person and both\n * addresses; its lines and totals stay, as an order's do.\n */\nexport function erasedQuotePatch(\n data: FirebaseFirestore.DocumentData | undefined,\n now: number,\n): Record<string, unknown> {\n const quote = (data?.['quote'] ?? {}) as Record<string, unknown>\n return {\n quote: { ...quote, customer: {}, billTo: null, shipTo: null },\n customerErasedAtMs: now,\n }\n}\n\nexport function erasedCheckInPatch(\n data: FirebaseFirestore.DocumentData | undefined,\n now: number,\n): Record<string, unknown> {\n // One key per order or stay, so the people one order brought still read\n // as one party, and as nobody's name.\n const source = String(data?.['orderId'] ?? data?.['reservationId'] ?? '')\n return {\n customerKey: `erased:${source}`,\n customerName: null,\n customerEmail: null,\n customerEmailLower: null,\n customerPhone: null,\n contactId: null,\n searchTokens: [],\n customerErasedAtMs: now,\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 let checkIns = 0\n let quotes = 0\n const emailLower = email.trim().toLowerCase()\n for (const host of hosts.docs) {\n const placed = host.ref.collection('orders')\n const checked = host.ref.collection('checkIns')\n if (dryRun) {\n orders += (await placed.where('customerEmail', '==', email).get()).size\n checkIns += (await checked.where('customerEmailLower', '==', emailLower).get()).size\n quotes += (await placed.where(QUOTE_EMAIL_FIELD, '==', emailLower).get()).size\n continue\n }\n checkIns += await updateWhereEquals(\n db,\n checked,\n 'customerEmailLower',\n emailLower,\n (data) => erasedCheckInPatch(data, atMs),\n LABEL,\n )\n orders += await updateWhereEquals(\n db,\n placed,\n 'customerEmail',\n email,\n (data) => erasedOrderPatch(data, atMs),\n LABEL,\n )\n quotes += await updateWhereEquals(\n db,\n placed,\n QUOTE_EMAIL_FIELD,\n emailLower,\n (data) => erasedQuotePatch(data, atMs),\n LABEL,\n )\n }\n return { orders, checkIns, quotes }\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","QUOTE_EMAIL_FIELD","erasedOrderPatch","data","now","addressTokens","Set","Array","isArray","customerEmail","customerName","shippingAddress","delete","billingAddress","customerErasedAtMs","customerEmailLower","customerEmailTokens","searchTokens","filter","token","has","erasedQuotePatch","quote","customer","billTo","shipTo","erasedCheckInPatch","source","String","customerKey","customerPhone","contactId","createCommercePersonEraser","deps","orgId","email","dryRun","atMs","db","firestore","hosts","collection","where","get","orders","checkIns","quotes","emailLower","trim","toLowerCase","host","docs","placed","ref","checked","size","commercePersonEraser","request","app"],"mappings":"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"}
|
|
@@ -83,7 +83,10 @@ export interface PosOpsStaff {
|
|
|
83
83
|
orgId: string;
|
|
84
84
|
org: Record<string, any> | null;
|
|
85
85
|
membership: PosOpsMembership;
|
|
86
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* "Approve refunds over the register's limit" (`commerce.posApprovals`,
|
|
88
|
+
* AGL-4047): no refund limit, a manager's approval, other members' PINs.
|
|
89
|
+
*/
|
|
87
90
|
isManager: boolean;
|
|
88
91
|
settings: PosOpsSettings;
|
|
89
92
|
hostRef: FirebaseFirestore.DocumentReference;
|
|
@@ -106,11 +109,16 @@ export { cleanId as posOpsCleanId };
|
|
|
106
109
|
*/
|
|
107
110
|
export declare function mayWorkRegister(membership: PosOpsMembership): boolean;
|
|
108
111
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* `
|
|
112
|
+
* Whose say-so a refund above the cashier limit needs, and who sets other
|
|
113
|
+
* members' PINs: "Approve refunds over the register's limit"
|
|
114
|
+
* (`commerce.posApprovals`, AGL-4047). Its preset is the gate this asked
|
|
115
|
+
* before: an admin of the whole workspace who is also this site's admin,
|
|
116
|
+
* which someone added to just this site as its admin never was. `hostDoc` is
|
|
117
|
+
* the site's document (its stamp and `memberRoles`).
|
|
112
118
|
*/
|
|
113
|
-
export declare function isPosManager(
|
|
119
|
+
export declare function isPosManager(hostDoc: {
|
|
120
|
+
get(field: string): unknown;
|
|
121
|
+
} | null | undefined, uid: string, membership: PosOpsMembership): boolean;
|
|
114
122
|
/**
|
|
115
123
|
* The register's gate for one route. `permission` names the finer register
|
|
116
124
|
* key the action needs (AGL-4047) on top of working the register at all; a
|
|
@@ -22,6 +22,7 @@ import { createHmac, timingSafeEqual } from "crypto";
|
|
|
22
22
|
import { POS_CASHIER_ASSERTION_TTL_MS, POS_MANAGER_ASSERTION_TTL_MS } from "../model/commerce-pos-ops.js";
|
|
23
23
|
import { holdsPosPermission, posPermissionRefusal } from "../model/plugin-permissions.js";
|
|
24
24
|
import { posOpsSettings } from "../pos-ops-config.js";
|
|
25
|
+
import { holdsCommerceWorkspaceSiteKey } from "./commerce-site-keys.js";
|
|
25
26
|
import { isRefusedIdToken } from "@aglyn/tenant-data-admin/server/id-token-refusal";
|
|
26
27
|
/** The register's finer keys a membership carries (AGL-4047). */ const POS_PERMISSION_KEYS = [
|
|
27
28
|
'posSell',
|
|
@@ -131,18 +132,21 @@ export { cleanId as posOpsCleanId };
|
|
|
131
132
|
return membership.permissions.managePos === true;
|
|
132
133
|
}
|
|
133
134
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* `
|
|
137
|
-
|
|
138
|
-
|
|
135
|
+
* Whose say-so a refund above the cashier limit needs, and who sets other
|
|
136
|
+
* members' PINs: "Approve refunds over the register's limit"
|
|
137
|
+
* (`commerce.posApprovals`, AGL-4047). Its preset is the gate this asked
|
|
138
|
+
* before: an admin of the whole workspace who is also this site's admin,
|
|
139
|
+
* which someone added to just this site as its admin never was. `hostDoc` is
|
|
140
|
+
* the site's document (its stamp and `memberRoles`).
|
|
141
|
+
*/ export function isPosManager(hostDoc, uid, membership) {
|
|
142
|
+
return holdsCommerceWorkspaceSiteKey(hostDoc, uid, 'commerce.posApprovals', membership);
|
|
139
143
|
}
|
|
140
144
|
/**
|
|
141
145
|
* The register's gate for one route. `permission` names the finer register
|
|
142
146
|
* key the action needs (AGL-4047) on top of working the register at all; a
|
|
143
147
|
* member without it is answered 403 naming it.
|
|
144
148
|
*/ export async function authorizePosOps(deps, req, rawHostId, permission) {
|
|
145
|
-
var _req_headers_authorization,
|
|
149
|
+
var _req_headers_authorization, _ref, _ref1, _ref2;
|
|
146
150
|
const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
|
|
147
151
|
const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : '';
|
|
148
152
|
if (!idToken) return {
|
|
@@ -174,9 +178,6 @@ export { cleanId as posOpsCleanId };
|
|
|
174
178
|
status: 404,
|
|
175
179
|
error: 'Unknown site'
|
|
176
180
|
};
|
|
177
|
-
// Still read: whether the member is a workspace admin and this site's
|
|
178
|
-
// admin decides who may approve above the cashier's limits.
|
|
179
|
-
const memberRole = ((_hostSnapshot_get = hostSnapshot.get('memberRoles')) != null ? _hostSnapshot_get : {})[uid];
|
|
180
181
|
const membership = await deps.membership(uid, hostId);
|
|
181
182
|
if (!mayWorkRegister(membership)) {
|
|
182
183
|
return {
|
|
@@ -209,7 +210,7 @@ export { cleanId as posOpsCleanId };
|
|
|
209
210
|
orgId: (_ref1 = owner == null ? void 0 : owner.orgId) != null ? _ref1 : '',
|
|
210
211
|
org: (_ref2 = owner == null ? void 0 : owner.org) != null ? _ref2 : null,
|
|
211
212
|
membership,
|
|
212
|
-
isManager: isPosManager(
|
|
213
|
+
isManager: isPosManager(hostSnapshot, uid, membership),
|
|
213
214
|
settings,
|
|
214
215
|
hostRef
|
|
215
216
|
}
|
|
@@ -293,14 +294,11 @@ export function mintPosAssertion(deps, input) {
|
|
|
293
294
|
* site's roles and the organization's permissions — or `null` when the
|
|
294
295
|
* assertion is bad or the member may no longer do what it was minted for.
|
|
295
296
|
*/ export async function resolvePosStaffAssertion(deps, hostRef, token, expected) {
|
|
296
|
-
var _host_get;
|
|
297
297
|
const read = readPosAssertion(deps, token, expected);
|
|
298
298
|
if (!read) return null;
|
|
299
299
|
const membership = await deps.membership(read.memberUid, expected.hostId);
|
|
300
300
|
if (expected.purpose !== 'manager') return mayWorkRegister(membership) ? read.memberUid : null;
|
|
301
|
-
|
|
302
|
-
const memberRole = ((_host_get = host.get('memberRoles')) != null ? _host_get : {})[read.memberUid];
|
|
303
|
-
return isPosManager(memberRole, membership) ? read.memberUid : null;
|
|
301
|
+
return isPosManager(await hostRef.get(), read.memberUid, membership) ? read.memberUid : null;
|
|
304
302
|
}
|
|
305
303
|
/**
|
|
306
304
|
* Who rang this: the member a valid cashier assertion names, or the member
|