@aglyn/plugins-commerce 1.0.0-beta.287 → 1.0.0-beta.288

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/commerce-site-keys.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 { hostStampedSitePermission } from '@aglyn/aglyn/app-utils/host-permissions'\nimport type { HostAccessRole } from '@aglyn/aglyn/foundation/definitions/organization.types'\nimport { COMMERCE_SITE_PERMISSIONS, type CommerceSitePermission } from '../model/plugin-permissions'\n\n/** Each commerce site key's site-role preset: the declarations' own table. */\nconst COMMERCE_SITE_PRESETS: Readonly<Record<string, Readonly<Partial<Record<HostAccessRole, boolean>>>>> =\n Object.fromEntries(COMMERCE_SITE_PERMISSIONS.map((entry) => [entry.key, entry.hostRoleDefaults ?? {}]))\n\n/**\n * Does this member hold one of commerce's site keys on this site, as the\n * rules would read it (AGL-4047): the site's `memberPermissions` stamp, else\n * the preset of their `memberRoles` entry. `host` is the host snapshot or its\n * data, which every door here reads already.\n */\nexport function holdsCommerceSiteKey(\n host: { get(field: string): unknown } | { memberPermissions?: unknown; memberRoles?: unknown } | null | undefined,\n uid: string,\n key: CommerceSitePermission,\n): boolean {\n if (!host || !uid) return false\n const read = (field: string): unknown =>\n typeof (host as { get?: unknown }).get === 'function'\n ? (host as { get(field: string): unknown }).get(field)\n : (host as Record<string, unknown>)[field]\n return hostStampedSitePermission(\n { memberPermissions: read('memberPermissions'), memberRoles: read('memberRoles') },\n uid,\n key,\n COMMERCE_SITE_PRESETS[key] ?? {},\n )\n}\n\n"],"names":["hostStampedSitePermission","COMMERCE_SITE_PERMISSIONS","COMMERCE_SITE_PRESETS","Object","fromEntries","map","entry","key","hostRoleDefaults","holdsCommerceSiteKey","host","uid","read","field","get","memberPermissions","memberRoles"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,yBAAyB,QAAQ,0CAAyC;AAEnF,SAASC,yBAAyB,QAAqC,iCAA6B;AAEpG,4EAA4E,GAC5E,MAAMC,wBACJC,OAAOC,WAAW,CAACH,0BAA0BI,GAAG,CAAC,CAACC;QAAsBA;WAAZ;QAACA,MAAMC,GAAG;SAAED,0BAAAA,MAAME,gBAAgB,YAAtBF,0BAA0B,CAAC;KAAE;;AAEvG;;;;;CAKC,GACD,OAAO,SAASG,qBACdC,IAAiH,EACjHC,GAAW,EACXJ,GAA2B;QAWzBL;IATF,IAAI,CAACQ,QAAQ,CAACC,KAAK,OAAO;IAC1B,MAAMC,OAAO,CAACC,QACZ,OAAO,AAACH,KAA2BI,GAAG,KAAK,aACvC,AAACJ,KAAyCI,GAAG,CAACD,SAC9C,AAACH,IAAgC,CAACG,MAAM;IAC9C,OAAOb,0BACL;QAAEe,mBAAmBH,KAAK;QAAsBI,aAAaJ,KAAK;IAAe,GACjFD,KACAJ,MACAL,6BAAAA,qBAAqB,CAACK,IAAI,YAA1BL,6BAA8B,CAAC;AAEnC"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/commerce-site-keys.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 { hostStampedSitePermission, hostStampedWorkspaceSitePermission } from '@aglyn/aglyn/app-utils/host-permissions'\nimport type { HostAccessRole } from '@aglyn/aglyn/foundation/definitions/organization.types'\nimport { COMMERCE_SITE_PERMISSIONS, type CommerceSitePermission } from '../model/plugin-permissions'\nimport {\n COMMERCE_WORKSPACE_SITE_PERMISSIONS,\n type CommerceWorkspaceSitePermission,\n} from '../model/workspace-site-permissions'\n\n/** Each commerce site key's site-role preset: the declarations' own table. */\nconst COMMERCE_SITE_PRESETS: Readonly<Record<string, Readonly<Partial<Record<HostAccessRole, boolean>>>>> =\n Object.fromEntries(\n [...COMMERCE_SITE_PERMISSIONS, ...COMMERCE_WORKSPACE_SITE_PERMISSIONS].map((entry) => [\n entry.key,\n entry.hostRoleDefaults ?? {},\n ]),\n )\n\n/**\n * Does this member hold one of commerce's site keys on this site, as the\n * rules would read it (AGL-4047): the site's `memberPermissions` stamp, else\n * the preset of their `memberRoles` entry. `host` is the host snapshot or its\n * data, which every door here reads already.\n */\nexport function holdsCommerceSiteKey(\n host: { get(field: string): unknown } | { memberPermissions?: unknown; memberRoles?: unknown } | null | undefined,\n uid: string,\n key: CommerceSitePermission,\n): boolean {\n if (!host || !uid) return false\n const read = (field: string): unknown =>\n typeof (host as { get?: unknown }).get === 'function'\n ? (host as { get(field: string): unknown }).get(field)\n : (host as Record<string, unknown>)[field]\n return hostStampedSitePermission(\n { memberPermissions: read('memberPermissions'), memberRoles: read('memberRoles') },\n uid,\n key,\n COMMERCE_SITE_PRESETS[key] ?? {},\n )\n}\n\n/**\n * Does this member hold one of commerce's keys whose preset holds for\n * members of the whole workspace only (AGL-4047): refunds, the register's\n * over-limit approvals, order webhooks. The site's stamp when the\n * projection wrote one, else exactly the gate the key replaced: the site's\n * Admin in `memberRoles`, a member of the whole workspace (`orgWide`), and\n * Admin on the site as resolved now (`hostRole`). `membership` is the\n * resolution every one of those doors reads already.\n */\nexport function holdsCommerceWorkspaceSiteKey(\n hostDoc: { get(field: string): unknown } | { memberPermissions?: unknown; memberRoles?: unknown } | null | undefined,\n uid: string,\n key: CommerceWorkspaceSitePermission,\n membership: { orgWide: boolean; hostRole: string | null | undefined },\n): boolean {\n if (!hostDoc || !uid) return false\n const read = (field: string): unknown =>\n typeof (hostDoc as { get?: unknown }).get === 'function'\n ? (hostDoc as { get(field: string): unknown }).get(field)\n : (hostDoc as Record<string, unknown>)[field]\n return hostStampedWorkspaceSitePermission(\n { memberPermissions: read('memberPermissions'), memberRoles: read('memberRoles') },\n uid,\n key,\n COMMERCE_SITE_PRESETS[key] ?? {},\n membership,\n )\n}\n"],"names":["hostStampedSitePermission","hostStampedWorkspaceSitePermission","COMMERCE_SITE_PERMISSIONS","COMMERCE_WORKSPACE_SITE_PERMISSIONS","COMMERCE_SITE_PRESETS","Object","fromEntries","map","entry","key","hostRoleDefaults","holdsCommerceSiteKey","host","uid","read","field","get","memberPermissions","memberRoles","holdsCommerceWorkspaceSiteKey","hostDoc","membership"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,yBAAyB,EAAEC,kCAAkC,QAAQ,0CAAyC;AAEvH,SAASC,yBAAyB,QAAqC,iCAA6B;AACpG,SACEC,mCAAmC,QAE9B,yCAAqC;AAE5C,4EAA4E,GAC5E,MAAMC,wBACJC,OAAOC,WAAW,CAChB;OAAIJ;OAA8BC;CAAoC,CAACI,GAAG,CAAC,CAACC;QAE1EA;WAFoF;QACpFA,MAAMC,GAAG;SACTD,0BAAAA,MAAME,gBAAgB,YAAtBF,0BAA0B,CAAC;KAC5B;;AAGL;;;;;CAKC,GACD,OAAO,SAASG,qBACdC,IAAiH,EACjHC,GAAW,EACXJ,GAA2B;QAWzBL;IATF,IAAI,CAACQ,QAAQ,CAACC,KAAK,OAAO;IAC1B,MAAMC,OAAO,CAACC,QACZ,OAAO,AAACH,KAA2BI,GAAG,KAAK,aACvC,AAACJ,KAAyCI,GAAG,CAACD,SAC9C,AAACH,IAAgC,CAACG,MAAM;IAC9C,OAAOf,0BACL;QAAEiB,mBAAmBH,KAAK;QAAsBI,aAAaJ,KAAK;IAAe,GACjFD,KACAJ,MACAL,6BAAAA,qBAAqB,CAACK,IAAI,YAA1BL,6BAA8B,CAAC;AAEnC;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASe,8BACdC,OAAoH,EACpHP,GAAW,EACXJ,GAAoC,EACpCY,UAAqE;QAWnEjB;IATF,IAAI,CAACgB,WAAW,CAACP,KAAK,OAAO;IAC7B,MAAMC,OAAO,CAACC,QACZ,OAAO,AAACK,QAA8BJ,GAAG,KAAK,aAC1C,AAACI,QAA4CJ,GAAG,CAACD,SACjD,AAACK,OAAmC,CAACL,MAAM;IACjD,OAAOd,mCACL;QAAEgB,mBAAmBH,KAAK;QAAsBI,aAAaJ,KAAK;IAAe,GACjFD,KACAJ,MACAL,6BAAAA,qBAAqB,CAACK,IAAI,YAA1BL,6BAA8B,CAAC,GAC/BiB;AAEJ"}
@@ -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 _hostSnapshot_get, _body_endpointId;
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
- if (((_hostSnapshot_get = hostSnapshot.get('memberRoles')) != null ? _hostSnapshot_get : {})[decoded.uid] !== 'admin') {
401
- return res.status(403).json({
402
- error: 'Webhooks are managed by a site admin'
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 (!membership.orgWide || membership.hostRole !== 'admin') {
408
+ if (!holdsCommerceWorkspaceSiteKey(hostSnapshot, decoded.uid, 'commerce.webhooks', membership)) {
409
409
  return res.status(403).json({
410
- error: 'Webhooks are managed by an admin of the whole workspace'
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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"}
@@ -83,7 +83,10 @@ export interface PosOpsStaff {
83
83
  orgId: string;
84
84
  org: Record<string, any> | null;
85
85
  membership: PosOpsMembership;
86
- /** An admin of the whole workspace: the one role with no refund limit. */
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
- * The one role whose say-so a refund above the cashier limit needs: an admin
110
- * of the whole workspace who is also this site's admin — the role
111
- * `refund.ts` has always required to refund at all.
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(memberRole: unknown, membership: PosOpsMembership): boolean;
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
- * The one role whose say-so a refund above the cashier limit needs: an admin
135
- * of the whole workspace who is also this site's admin — the role
136
- * `refund.ts` has always required to refund at all.
137
- */ export function isPosManager(memberRole, membership) {
138
- return memberRole === 'admin' && membership.orgWide && membership.hostRole === 'admin';
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, _hostSnapshot_get, _ref, _ref1, _ref2;
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(memberRole, membership),
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
- const host = await hostRef.get();
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
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/pos-ops-gate.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 * as Aglyn from '@aglyn/aglyn/server'\nimport type { PluginApiRequest } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost, getPluginConfig } from '@aglyn/tenant-data-admin'\nimport { findUserByUidAcrossPools } from '@aglyn/tenant-data-admin/server/auth-pools'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { createHmac, timingSafeEqual } from 'crypto'\nimport {\n POS_CASHIER_ASSERTION_TTL_MS,\n POS_MANAGER_ASSERTION_TTL_MS,\n} from '../model/commerce-pos-ops'\nimport type { PrintReport } from '../model/commerce-printers'\nimport {\n holdsPosPermission,\n posPermissionRefusal,\n type PosPermission,\n} from '../model/plugin-permissions'\nimport { posOpsSettings, type PosOpsSettings } from '../pos-ops-config'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\n\n/*==========================================\n * WHO MAY RUN THE REGISTER (AGL-3609), for the shift, PIN, customer and\n * return routes.\n *\n * The fact the sale route checks: `managePos`, as resolved for THIS site —\n * the member's org role, custom role and overrides for an org-wide member,\n * and the role they hold here and their overrides for a site collaborator.\n * Plus the plan's `pos` entitlement, re-read every time because a downgrade\n * happens between taps.\n *\n * THE KEYS DECIDE, NOT THE SITE ROLE (AGL-4047). This also asked for `admin`\n * or `editor` in the site's `memberRoles`, exactly the set `managePos` is on\n * for by default, so nobody who had the register loses it; a site viewer the\n * workspace grants the register now works it.\n *\n * Every Firebase and Stripe touch goes through {@link PosOpsDeps}, so the\n * routes' specs run each money path against an in-memory store and a counted\n * fetch without mocking a module.\n *=========================================*/\n\nexport interface PosOpsMembership {\n orgWide: boolean\n hostRole: string | null\n /** `managePos`, and the register's finer keys (AGL-4047) as resolved. */\n permissions: { managePos?: boolean } & Partial<Record<PosPermission, boolean>>\n}\n\n/** The register's finer keys a membership carries (AGL-4047). */\nconst POS_PERMISSION_KEYS: readonly PosPermission[] = [\n 'posSell',\n 'posRefund',\n 'posDiscount',\n 'posCashDrawer',\n 'posDevices',\n 'posBuyHardware',\n]\n\nexport interface PosOpsDeps {\n firestore(): FirebaseFirestore.Firestore\n verifyIdToken(token: string): Promise<{ uid: string }>\n membership(uid: string, hostId: string): Promise<PosOpsMembership>\n orgForHost(hostId: string): Promise<{ orgId: string; org: Record<string, any> | null } | null>\n pluginConfig(orgId: string, hostId: string): Promise<Record<string, unknown>>\n /** The plan includes the register. */\n posEntitled(org: Record<string, any> | null): boolean\n /** The HMAC secret staff assertions are signed with; throws when unset. */\n signingSecret(): string\n /** A member's name as a receipt or a report prints it. */\n memberName(uid: string): Promise<string>\n now(): number\n /** Stripe's REST API; `global.fetch` in production. */\n fetch: typeof fetch\n /** The register's cloud printers (AGL-3619): drawer kicks and shift reports. */\n printer: PosOpsPrinter\n}\n\n/** What the register's routes ask of the printer queue. Never throws. */\nexport interface PosOpsPrinter {\n kickDrawer(input: {\n hostId: string\n registerId: string\n reason: 'paid_in' | 'paid_out' | 'drop' | 'cash_refund'\n causeId: string\n createdBy?: string\n }): Promise<{ jobIds: string[] }>\n printReport(input: {\n hostId: string\n registerId: string\n shiftId: string\n report: PrintReport\n createdBy?: string\n attemptKey: string\n }): Promise<{ jobIds: string[] }>\n}\n\nexport function defaultPosOpsDeps(): PosOpsDeps {\n return {\n firestore: () => firebaseAdmin.app().firestore(),\n verifyIdToken: async (token) => {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(token)\n return { uid: decoded.uid }\n },\n membership: async (uid, hostId) => {\n const resolved = await resolveOrgPermissions(uid, { hostId })\n return {\n orgWide: resolved.orgWide === true,\n hostRole: resolved.hostRole ?? null,\n permissions: {\n managePos: resolved.permissions?.managePos === true,\n // Each finer key as resolved; absent when the map carries none,\n // which `holdsPosPermission` reads as following `managePos`.\n ...Object.fromEntries(\n POS_PERMISSION_KEYS.filter(\n (key) => typeof resolved.permissions?.[key] === 'boolean',\n ).map((key) => [key, resolved.permissions[key] === true]),\n ),\n },\n }\n },\n orgForHost: async (hostId) => {\n const found = await getOrgForHost(hostId)\n if (!found) return null\n return {\n orgId: String(found.org?.id ?? found.orgId ?? ''),\n org: (found.org as Record<string, any>) ?? null,\n }\n },\n pluginConfig: async (orgId, hostId) =>\n ((await getPluginConfig(orgId, 'commerce', { hostId })) ?? {}) as Record<string, unknown>,\n posEntitled: (org) => Aglyn.checkEntitlement(org as any, 'pos'),\n signingSecret: () => {\n const secret = process.env.TOKEN_SIGNING_SECRET\n if (!secret) throw new Error('TOKEN_SIGNING_SECRET is not configured')\n return secret\n },\n memberName: async (uid) => {\n // Every pool: a member who signs in through SSO lives in a GCIP tenant\n // pool, not the project one (AGL-1122).\n try {\n const user = (await findUserByUidAcrossPools(uid))?.record\n return user?.displayName || user?.email || 'Staff member'\n } catch {\n return 'Staff member'\n }\n },\n now: () => Date.now(),\n fetch: (input, init) => fetch(input, init),\n // Loaded on use: the printing module reaches the whole sale pipeline,\n // which a shift count or a PIN check never needs.\n printer: {\n kickDrawer: async (input) => (await import('./pos-print')).kickPosDrawer(input),\n printReport: async (input) => (await import('./pos-print')).printPosShiftReport(input),\n },\n }\n}\n\n/** A JSON body, whether the dispatcher parsed it or not. */\nexport function posOpsBody(req: PluginApiRequest): Record<string, any> {\n if (typeof req.body === 'string') {\n try {\n const parsed = JSON.parse(req.body)\n return parsed && typeof parsed === 'object' ? parsed : {}\n } catch {\n return {}\n }\n }\n return req.body && typeof req.body === 'object' ? (req.body as Record<string, any>) : {}\n}\n\n/** The request's idempotency key, either header spelling, bounded. */\nexport function posOpsIdempotencyKey(req: PluginApiRequest): string {\n return String(req.headers['idempotency-key'] ?? req.headers['Idempotency-Key'] ?? '')\n .trim()\n .slice(0, 200)\n}\n\n/** Who is at the register, once the gate admitted them. */\nexport interface PosOpsStaff {\n uid: string\n hostId: string\n orgId: string\n org: Record<string, any> | null\n membership: PosOpsMembership\n /** An admin of the whole workspace: the one role with no refund limit. */\n isManager: boolean\n settings: PosOpsSettings\n hostRef: FirebaseFirestore.DocumentReference\n}\n\nexport type PosOpsGate =\n | { ok: true; staff: PosOpsStaff }\n | { ok: false; status: number; error: string }\n\n/** A site id the dispatcher's own sentinels and path tricks cannot pass for. */\nfunction cleanId(value: unknown): string {\n const id = String(value ?? '').trim()\n return /^[A-Za-z0-9_-]{1,128}$/.test(id) && !/^__.*__$/.test(id) ? id : ''\n}\n\nexport { cleanId as posOpsCleanId }\n\n/**\n * Whether a member may work this site's register: `managePos`, as resolved\n * for the site (AGL-4047). A finer action also needs its own key:\n * `holdsPosPermission`.\n */\nexport function mayWorkRegister(membership: PosOpsMembership): boolean {\n return membership.permissions.managePos === true\n}\n\n/**\n * The one role whose say-so a refund above the cashier limit needs: an admin\n * of the whole workspace who is also this site's admin — the role\n * `refund.ts` has always required to refund at all.\n */\nexport function isPosManager(memberRole: unknown, membership: PosOpsMembership): boolean {\n return memberRole === 'admin' && membership.orgWide && membership.hostRole === 'admin'\n}\n\n/**\n * The register's gate for one route. `permission` names the finer register\n * key the action needs (AGL-4047) on top of working the register at all; a\n * member without it is answered 403 naming it.\n */\nexport async function authorizePosOps(\n deps: PosOpsDeps,\n req: PluginApiRequest,\n rawHostId: unknown,\n permission?: PosPermission,\n): Promise<PosOpsGate> {\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : ''\n if (!idToken) return { ok: false, status: 401, error: 'Unauthenticated' }\n const hostId = cleanId(rawHostId)\n if (!hostId) return { ok: false, status: 400, error: 'Missing hostId' }\n let uid: string\n try {\n uid = (await deps.verifyIdToken(idToken)).uid\n } catch (error) {\n if (!isRefusedIdToken(error)) throw error\n return { ok: false, status: 401, error: 'Unauthenticated' }\n }\n const hostRef = deps.firestore().collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) return { ok: false, status: 404, error: 'Unknown site' }\n // Still read: whether the member is a workspace admin and this site's\n // admin decides who may approve above the cashier's limits.\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[uid]\n const membership = await deps.membership(uid, hostId)\n if (!mayWorkRegister(membership)) {\n return { ok: false, status: 403, error: 'Not permitted' }\n }\n if (permission && !holdsPosPermission(membership.permissions, permission)) {\n return { ok: false, status: 403, error: posPermissionRefusal(permission) }\n }\n const owner = await deps.orgForHost(hostId)\n if (!deps.posEntitled(owner?.org ?? null)) {\n return { ok: false, status: 403, error: 'POS requires the Pro plan or above' }\n }\n const settings = posOpsSettings(\n owner?.orgId ? await deps.pluginConfig(owner.orgId, hostId).catch(() => ({})) : {},\n )\n return {\n ok: true,\n staff: {\n uid,\n hostId,\n orgId: owner?.orgId ?? '',\n org: owner?.org ?? null,\n membership,\n isManager: isPosManager(memberRole, membership),\n settings,\n hostRef,\n },\n }\n}\n\n/** A register on this site, or a refusal: a register id from another site is unknown here. */\nexport async function readPosRegister(\n staff: PosOpsStaff,\n rawRegisterId: unknown,\n): Promise<\n | { ok: true; ref: FirebaseFirestore.DocumentReference; data: Record<string, any> }\n | { ok: false; status: number; error: string }\n> {\n const registerId = cleanId(rawRegisterId)\n if (!registerId) return { ok: false, status: 400, error: 'Missing registerId' }\n const ref = staff.hostRef.collection('registers').doc(registerId)\n const snapshot = await ref.get()\n if (!snapshot.exists) return { ok: false, status: 404, error: 'Unknown register' }\n return { ok: true, ref, data: (snapshot.data() ?? {}) as Record<string, any> }\n}\n\n/*==========================================\n * STAFF ASSERTIONS: what a PIN buys.\n *\n * A PIN never signs anybody in. It mints a short-lived, signed statement\n * that one member of THIS site is at THIS register — `{hostId, registerId,\n * memberUid, purpose, expiry}` under an HMAC — and the routes that take one\n * re-check that member's permissions on every use, so a PIN never\n * carries more than its member holds right now, and a revoked member's\n * assertion stops working on the next tap rather than at expiry.\n *=========================================*/\n\nexport type PosAssertionPurpose = 'cashier' | 'manager'\n\ninterface AssertionClaims {\n v: 1\n h: string\n r: string\n u: string\n p: PosAssertionPurpose\n e: number\n}\n\nfunction signClaims(deps: PosOpsDeps, payload: string): string {\n return createHmac('sha256', deps.signingSecret()).update(`pos-assertion:${payload}`).digest('hex')\n}\n\nexport function mintPosAssertion(\n deps: PosOpsDeps,\n input: { hostId: string; registerId: string; memberUid: string; purpose: PosAssertionPurpose },\n): { token: string; expiresAtMs: number } {\n const expiresAtMs =\n deps.now() +\n (input.purpose === 'manager' ? POS_MANAGER_ASSERTION_TTL_MS : POS_CASHIER_ASSERTION_TTL_MS)\n const claims: AssertionClaims = {\n v: 1,\n h: input.hostId,\n r: input.registerId,\n u: input.memberUid,\n p: input.purpose,\n e: expiresAtMs,\n }\n const payload = Buffer.from(JSON.stringify(claims)).toString('base64url')\n return { token: `${payload}.${signClaims(deps, payload)}`, expiresAtMs }\n}\n\n/**\n * The member an assertion names, or `null` — for a forged, expired, wrong-site,\n * wrong-register or wrong-purpose one alike. It does NOT re-check the member:\n * {@link resolvePosStaffAssertion} does that.\n */\nexport function readPosAssertion(\n deps: PosOpsDeps,\n token: unknown,\n expected: { hostId: string; registerId: string; purpose: PosAssertionPurpose },\n): { memberUid: string; expiresAtMs: number } | null {\n const value = typeof token === 'string' ? token.trim() : ''\n const dot = value.lastIndexOf('.')\n if (dot <= 0 || value.length > 2000) return null\n const payload = value.slice(0, dot)\n const signature = value.slice(dot + 1)\n let expected64: string\n try {\n expected64 = signClaims(deps, payload)\n } catch {\n return null\n }\n const a = Buffer.from(signature)\n const b = Buffer.from(expected64)\n if (a.length !== b.length || !timingSafeEqual(new Uint8Array(a), new Uint8Array(b))) return null\n let claims: AssertionClaims\n try {\n claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'))\n } catch {\n return null\n }\n if (\n claims?.v !== 1 ||\n claims.h !== expected.hostId ||\n claims.r !== expected.registerId ||\n claims.p !== expected.purpose ||\n typeof claims.u !== 'string' ||\n !claims.u ||\n !(Number(claims.e) > deps.now())\n ) {\n return null\n }\n return { memberUid: claims.u, expiresAtMs: Number(claims.e) }\n}\n\n/**\n * The member an assertion puts at the register, re-checked NOW against the\n * site's roles and the organization's permissions — or `null` when the\n * assertion is bad or the member may no longer do what it was minted for.\n */\nexport async function resolvePosStaffAssertion(\n deps: PosOpsDeps,\n hostRef: FirebaseFirestore.DocumentReference,\n token: unknown,\n expected: { hostId: string; registerId: string; purpose: PosAssertionPurpose },\n): Promise<string | null> {\n const read = readPosAssertion(deps, token, expected)\n if (!read) return null\n const membership = await deps.membership(read.memberUid, expected.hostId)\n if (expected.purpose !== 'manager') return mayWorkRegister(membership) ? read.memberUid : null\n const host = await hostRef.get()\n const memberRole = (host.get('memberRoles') ?? {})[read.memberUid]\n return isPosManager(memberRole, membership) ? read.memberUid : null\n}\n\n/**\n * Who rang this: the member a valid cashier assertion names, or the member\n * signed in on the device when there is none or it no longer holds. The\n * signed-in member already passed the gate, so the fallback never admits\n * anybody new; it only decides whose name the sale carries.\n */\nexport async function resolvePosCashier(\n deps: PosOpsDeps,\n staff: Pick<PosOpsStaff, 'uid' | 'hostId' | 'hostRef'>,\n registerId: string,\n token: unknown,\n): Promise<{ cashierId: string; via: 'pin' | 'session' }> {\n if (token) {\n const memberUid = await resolvePosStaffAssertion(deps, staff.hostRef, token, {\n hostId: staff.hostId,\n registerId,\n purpose: 'cashier',\n })\n if (memberUid) return { cashierId: memberUid, via: 'pin' }\n }\n return { cashierId: staff.uid, via: 'session' }\n}\n"],"names":["Aglyn","firebaseAdmin","getOrgForHost","getPluginConfig","findUserByUidAcrossPools","resolveOrgPermissions","createHmac","timingSafeEqual","POS_CASHIER_ASSERTION_TTL_MS","POS_MANAGER_ASSERTION_TTL_MS","holdsPosPermission","posPermissionRefusal","posOpsSettings","isRefusedIdToken","POS_PERMISSION_KEYS","defaultPosOpsDeps","firestore","app","verifyIdToken","token","decoded","auth","uid","membership","hostId","resolved","orgWide","hostRole","permissions","managePos","Object","fromEntries","filter","key","map","orgForHost","found","orgId","String","org","id","pluginConfig","posEntitled","checkEntitlement","signingSecret","secret","process","env","TOKEN_SIGNING_SECRET","Error","memberName","user","record","displayName","email","now","Date","fetch","input","init","printer","kickDrawer","kickPosDrawer","printReport","printPosShiftReport","posOpsBody","req","body","parsed","JSON","parse","posOpsIdempotencyKey","headers","trim","slice","cleanId","value","test","posOpsCleanId","mayWorkRegister","isPosManager","memberRole","authorizePosOps","deps","rawHostId","permission","hostSnapshot","authorization","idToken","startsWith","length","ok","status","error","hostRef","collection","doc","get","exists","owner","settings","catch","staff","isManager","readPosRegister","rawRegisterId","snapshot","registerId","ref","data","signClaims","payload","update","digest","mintPosAssertion","expiresAtMs","purpose","claims","v","h","r","u","memberUid","p","e","Buffer","from","stringify","toString","readPosAssertion","expected","dot","lastIndexOf","signature","expected64","a","b","Uint8Array","Number","resolvePosStaffAssertion","host","read","resolvePosCashier","cashierId","via"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/pos-ops-gate.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 * as Aglyn from '@aglyn/aglyn/server'\nimport type { PluginApiRequest } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost, getPluginConfig } from '@aglyn/tenant-data-admin'\nimport { findUserByUidAcrossPools } from '@aglyn/tenant-data-admin/server/auth-pools'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { createHmac, timingSafeEqual } from 'crypto'\nimport {\n POS_CASHIER_ASSERTION_TTL_MS,\n POS_MANAGER_ASSERTION_TTL_MS,\n} from '../model/commerce-pos-ops'\nimport type { PrintReport } from '../model/commerce-printers'\nimport {\n holdsPosPermission,\n posPermissionRefusal,\n type PosPermission,\n} from '../model/plugin-permissions'\nimport { posOpsSettings, type PosOpsSettings } from '../pos-ops-config'\nimport { holdsCommerceWorkspaceSiteKey } from './commerce-site-keys'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\n\n/*==========================================\n * WHO MAY RUN THE REGISTER (AGL-3609), for the shift, PIN, customer and\n * return routes.\n *\n * The fact the sale route checks: `managePos`, as resolved for THIS site —\n * the member's org role, custom role and overrides for an org-wide member,\n * and the role they hold here and their overrides for a site collaborator.\n * Plus the plan's `pos` entitlement, re-read every time because a downgrade\n * happens between taps.\n *\n * THE KEYS DECIDE, NOT THE SITE ROLE (AGL-4047). This also asked for `admin`\n * or `editor` in the site's `memberRoles`, exactly the set `managePos` is on\n * for by default, so nobody who had the register loses it; a site viewer the\n * workspace grants the register now works it.\n *\n * Every Firebase and Stripe touch goes through {@link PosOpsDeps}, so the\n * routes' specs run each money path against an in-memory store and a counted\n * fetch without mocking a module.\n *=========================================*/\n\nexport interface PosOpsMembership {\n orgWide: boolean\n hostRole: string | null\n /** `managePos`, and the register's finer keys (AGL-4047) as resolved. */\n permissions: { managePos?: boolean } & Partial<Record<PosPermission, boolean>>\n}\n\n/** The register's finer keys a membership carries (AGL-4047). */\nconst POS_PERMISSION_KEYS: readonly PosPermission[] = [\n 'posSell',\n 'posRefund',\n 'posDiscount',\n 'posCashDrawer',\n 'posDevices',\n 'posBuyHardware',\n]\n\nexport interface PosOpsDeps {\n firestore(): FirebaseFirestore.Firestore\n verifyIdToken(token: string): Promise<{ uid: string }>\n membership(uid: string, hostId: string): Promise<PosOpsMembership>\n orgForHost(hostId: string): Promise<{ orgId: string; org: Record<string, any> | null } | null>\n pluginConfig(orgId: string, hostId: string): Promise<Record<string, unknown>>\n /** The plan includes the register. */\n posEntitled(org: Record<string, any> | null): boolean\n /** The HMAC secret staff assertions are signed with; throws when unset. */\n signingSecret(): string\n /** A member's name as a receipt or a report prints it. */\n memberName(uid: string): Promise<string>\n now(): number\n /** Stripe's REST API; `global.fetch` in production. */\n fetch: typeof fetch\n /** The register's cloud printers (AGL-3619): drawer kicks and shift reports. */\n printer: PosOpsPrinter\n}\n\n/** What the register's routes ask of the printer queue. Never throws. */\nexport interface PosOpsPrinter {\n kickDrawer(input: {\n hostId: string\n registerId: string\n reason: 'paid_in' | 'paid_out' | 'drop' | 'cash_refund'\n causeId: string\n createdBy?: string\n }): Promise<{ jobIds: string[] }>\n printReport(input: {\n hostId: string\n registerId: string\n shiftId: string\n report: PrintReport\n createdBy?: string\n attemptKey: string\n }): Promise<{ jobIds: string[] }>\n}\n\nexport function defaultPosOpsDeps(): PosOpsDeps {\n return {\n firestore: () => firebaseAdmin.app().firestore(),\n verifyIdToken: async (token) => {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(token)\n return { uid: decoded.uid }\n },\n membership: async (uid, hostId) => {\n const resolved = await resolveOrgPermissions(uid, { hostId })\n return {\n orgWide: resolved.orgWide === true,\n hostRole: resolved.hostRole ?? null,\n permissions: {\n managePos: resolved.permissions?.managePos === true,\n // Each finer key as resolved; absent when the map carries none,\n // which `holdsPosPermission` reads as following `managePos`.\n ...Object.fromEntries(\n POS_PERMISSION_KEYS.filter(\n (key) => typeof resolved.permissions?.[key] === 'boolean',\n ).map((key) => [key, resolved.permissions[key] === true]),\n ),\n },\n }\n },\n orgForHost: async (hostId) => {\n const found = await getOrgForHost(hostId)\n if (!found) return null\n return {\n orgId: String(found.org?.id ?? found.orgId ?? ''),\n org: (found.org as Record<string, any>) ?? null,\n }\n },\n pluginConfig: async (orgId, hostId) =>\n ((await getPluginConfig(orgId, 'commerce', { hostId })) ?? {}) as Record<string, unknown>,\n posEntitled: (org) => Aglyn.checkEntitlement(org as any, 'pos'),\n signingSecret: () => {\n const secret = process.env.TOKEN_SIGNING_SECRET\n if (!secret) throw new Error('TOKEN_SIGNING_SECRET is not configured')\n return secret\n },\n memberName: async (uid) => {\n // Every pool: a member who signs in through SSO lives in a GCIP tenant\n // pool, not the project one (AGL-1122).\n try {\n const user = (await findUserByUidAcrossPools(uid))?.record\n return user?.displayName || user?.email || 'Staff member'\n } catch {\n return 'Staff member'\n }\n },\n now: () => Date.now(),\n fetch: (input, init) => fetch(input, init),\n // Loaded on use: the printing module reaches the whole sale pipeline,\n // which a shift count or a PIN check never needs.\n printer: {\n kickDrawer: async (input) => (await import('./pos-print')).kickPosDrawer(input),\n printReport: async (input) => (await import('./pos-print')).printPosShiftReport(input),\n },\n }\n}\n\n/** A JSON body, whether the dispatcher parsed it or not. */\nexport function posOpsBody(req: PluginApiRequest): Record<string, any> {\n if (typeof req.body === 'string') {\n try {\n const parsed = JSON.parse(req.body)\n return parsed && typeof parsed === 'object' ? parsed : {}\n } catch {\n return {}\n }\n }\n return req.body && typeof req.body === 'object' ? (req.body as Record<string, any>) : {}\n}\n\n/** The request's idempotency key, either header spelling, bounded. */\nexport function posOpsIdempotencyKey(req: PluginApiRequest): string {\n return String(req.headers['idempotency-key'] ?? req.headers['Idempotency-Key'] ?? '')\n .trim()\n .slice(0, 200)\n}\n\n/** Who is at the register, once the gate admitted them. */\nexport interface PosOpsStaff {\n uid: string\n hostId: string\n orgId: string\n org: Record<string, any> | null\n membership: PosOpsMembership\n /**\n * \"Approve refunds over the register's limit\" (`commerce.posApprovals`,\n * AGL-4047): no refund limit, a manager's approval, other members' PINs.\n */\n isManager: boolean\n settings: PosOpsSettings\n hostRef: FirebaseFirestore.DocumentReference\n}\n\nexport type PosOpsGate =\n | { ok: true; staff: PosOpsStaff }\n | { ok: false; status: number; error: string }\n\n/** A site id the dispatcher's own sentinels and path tricks cannot pass for. */\nfunction cleanId(value: unknown): string {\n const id = String(value ?? '').trim()\n return /^[A-Za-z0-9_-]{1,128}$/.test(id) && !/^__.*__$/.test(id) ? id : ''\n}\n\nexport { cleanId as posOpsCleanId }\n\n/**\n * Whether a member may work this site's register: `managePos`, as resolved\n * for the site (AGL-4047). A finer action also needs its own key:\n * `holdsPosPermission`.\n */\nexport function mayWorkRegister(membership: PosOpsMembership): boolean {\n return membership.permissions.managePos === true\n}\n\n/**\n * Whose say-so a refund above the cashier limit needs, and who sets other\n * members' PINs: \"Approve refunds over the register's limit\"\n * (`commerce.posApprovals`, AGL-4047). Its preset is the gate this asked\n * before: an admin of the whole workspace who is also this site's admin,\n * which someone added to just this site as its admin never was. `hostDoc` is\n * the site's document (its stamp and `memberRoles`).\n */\nexport function isPosManager(\n hostDoc: { get(field: string): unknown } | null | undefined,\n uid: string,\n membership: PosOpsMembership,\n): boolean {\n return holdsCommerceWorkspaceSiteKey(hostDoc, uid, 'commerce.posApprovals', membership)\n}\n\n/**\n * The register's gate for one route. `permission` names the finer register\n * key the action needs (AGL-4047) on top of working the register at all; a\n * member without it is answered 403 naming it.\n */\nexport async function authorizePosOps(\n deps: PosOpsDeps,\n req: PluginApiRequest,\n rawHostId: unknown,\n permission?: PosPermission,\n): Promise<PosOpsGate> {\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : ''\n if (!idToken) return { ok: false, status: 401, error: 'Unauthenticated' }\n const hostId = cleanId(rawHostId)\n if (!hostId) return { ok: false, status: 400, error: 'Missing hostId' }\n let uid: string\n try {\n uid = (await deps.verifyIdToken(idToken)).uid\n } catch (error) {\n if (!isRefusedIdToken(error)) throw error\n return { ok: false, status: 401, error: 'Unauthenticated' }\n }\n const hostRef = deps.firestore().collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) return { ok: false, status: 404, error: 'Unknown site' }\n const membership = await deps.membership(uid, hostId)\n if (!mayWorkRegister(membership)) {\n return { ok: false, status: 403, error: 'Not permitted' }\n }\n if (permission && !holdsPosPermission(membership.permissions, permission)) {\n return { ok: false, status: 403, error: posPermissionRefusal(permission) }\n }\n const owner = await deps.orgForHost(hostId)\n if (!deps.posEntitled(owner?.org ?? null)) {\n return { ok: false, status: 403, error: 'POS requires the Pro plan or above' }\n }\n const settings = posOpsSettings(\n owner?.orgId ? await deps.pluginConfig(owner.orgId, hostId).catch(() => ({})) : {},\n )\n return {\n ok: true,\n staff: {\n uid,\n hostId,\n orgId: owner?.orgId ?? '',\n org: owner?.org ?? null,\n membership,\n isManager: isPosManager(hostSnapshot, uid, membership),\n settings,\n hostRef,\n },\n }\n}\n\n/** A register on this site, or a refusal: a register id from another site is unknown here. */\nexport async function readPosRegister(\n staff: PosOpsStaff,\n rawRegisterId: unknown,\n): Promise<\n | { ok: true; ref: FirebaseFirestore.DocumentReference; data: Record<string, any> }\n | { ok: false; status: number; error: string }\n> {\n const registerId = cleanId(rawRegisterId)\n if (!registerId) return { ok: false, status: 400, error: 'Missing registerId' }\n const ref = staff.hostRef.collection('registers').doc(registerId)\n const snapshot = await ref.get()\n if (!snapshot.exists) return { ok: false, status: 404, error: 'Unknown register' }\n return { ok: true, ref, data: (snapshot.data() ?? {}) as Record<string, any> }\n}\n\n/*==========================================\n * STAFF ASSERTIONS: what a PIN buys.\n *\n * A PIN never signs anybody in. It mints a short-lived, signed statement\n * that one member of THIS site is at THIS register — `{hostId, registerId,\n * memberUid, purpose, expiry}` under an HMAC — and the routes that take one\n * re-check that member's permissions on every use, so a PIN never\n * carries more than its member holds right now, and a revoked member's\n * assertion stops working on the next tap rather than at expiry.\n *=========================================*/\n\nexport type PosAssertionPurpose = 'cashier' | 'manager'\n\ninterface AssertionClaims {\n v: 1\n h: string\n r: string\n u: string\n p: PosAssertionPurpose\n e: number\n}\n\nfunction signClaims(deps: PosOpsDeps, payload: string): string {\n return createHmac('sha256', deps.signingSecret()).update(`pos-assertion:${payload}`).digest('hex')\n}\n\nexport function mintPosAssertion(\n deps: PosOpsDeps,\n input: { hostId: string; registerId: string; memberUid: string; purpose: PosAssertionPurpose },\n): { token: string; expiresAtMs: number } {\n const expiresAtMs =\n deps.now() +\n (input.purpose === 'manager' ? POS_MANAGER_ASSERTION_TTL_MS : POS_CASHIER_ASSERTION_TTL_MS)\n const claims: AssertionClaims = {\n v: 1,\n h: input.hostId,\n r: input.registerId,\n u: input.memberUid,\n p: input.purpose,\n e: expiresAtMs,\n }\n const payload = Buffer.from(JSON.stringify(claims)).toString('base64url')\n return { token: `${payload}.${signClaims(deps, payload)}`, expiresAtMs }\n}\n\n/**\n * The member an assertion names, or `null` — for a forged, expired, wrong-site,\n * wrong-register or wrong-purpose one alike. It does NOT re-check the member:\n * {@link resolvePosStaffAssertion} does that.\n */\nexport function readPosAssertion(\n deps: PosOpsDeps,\n token: unknown,\n expected: { hostId: string; registerId: string; purpose: PosAssertionPurpose },\n): { memberUid: string; expiresAtMs: number } | null {\n const value = typeof token === 'string' ? token.trim() : ''\n const dot = value.lastIndexOf('.')\n if (dot <= 0 || value.length > 2000) return null\n const payload = value.slice(0, dot)\n const signature = value.slice(dot + 1)\n let expected64: string\n try {\n expected64 = signClaims(deps, payload)\n } catch {\n return null\n }\n const a = Buffer.from(signature)\n const b = Buffer.from(expected64)\n if (a.length !== b.length || !timingSafeEqual(new Uint8Array(a), new Uint8Array(b))) return null\n let claims: AssertionClaims\n try {\n claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'))\n } catch {\n return null\n }\n if (\n claims?.v !== 1 ||\n claims.h !== expected.hostId ||\n claims.r !== expected.registerId ||\n claims.p !== expected.purpose ||\n typeof claims.u !== 'string' ||\n !claims.u ||\n !(Number(claims.e) > deps.now())\n ) {\n return null\n }\n return { memberUid: claims.u, expiresAtMs: Number(claims.e) }\n}\n\n/**\n * The member an assertion puts at the register, re-checked NOW against the\n * site's roles and the organization's permissions — or `null` when the\n * assertion is bad or the member may no longer do what it was minted for.\n */\nexport async function resolvePosStaffAssertion(\n deps: PosOpsDeps,\n hostRef: FirebaseFirestore.DocumentReference,\n token: unknown,\n expected: { hostId: string; registerId: string; purpose: PosAssertionPurpose },\n): Promise<string | null> {\n const read = readPosAssertion(deps, token, expected)\n if (!read) return null\n const membership = await deps.membership(read.memberUid, expected.hostId)\n if (expected.purpose !== 'manager') return mayWorkRegister(membership) ? read.memberUid : null\n return isPosManager(await hostRef.get(), read.memberUid, membership) ? read.memberUid : null\n}\n\n/**\n * Who rang this: the member a valid cashier assertion names, or the member\n * signed in on the device when there is none or it no longer holds. The\n * signed-in member already passed the gate, so the fallback never admits\n * anybody new; it only decides whose name the sale carries.\n */\nexport async function resolvePosCashier(\n deps: PosOpsDeps,\n staff: Pick<PosOpsStaff, 'uid' | 'hostId' | 'hostRef'>,\n registerId: string,\n token: unknown,\n): Promise<{ cashierId: string; via: 'pin' | 'session' }> {\n if (token) {\n const memberUid = await resolvePosStaffAssertion(deps, staff.hostRef, token, {\n hostId: staff.hostId,\n registerId,\n purpose: 'cashier',\n })\n if (memberUid) return { cashierId: memberUid, via: 'pin' }\n }\n return { cashierId: staff.uid, via: 'session' }\n}\n"],"names":["Aglyn","firebaseAdmin","getOrgForHost","getPluginConfig","findUserByUidAcrossPools","resolveOrgPermissions","createHmac","timingSafeEqual","POS_CASHIER_ASSERTION_TTL_MS","POS_MANAGER_ASSERTION_TTL_MS","holdsPosPermission","posPermissionRefusal","posOpsSettings","holdsCommerceWorkspaceSiteKey","isRefusedIdToken","POS_PERMISSION_KEYS","defaultPosOpsDeps","firestore","app","verifyIdToken","token","decoded","auth","uid","membership","hostId","resolved","orgWide","hostRole","permissions","managePos","Object","fromEntries","filter","key","map","orgForHost","found","orgId","String","org","id","pluginConfig","posEntitled","checkEntitlement","signingSecret","secret","process","env","TOKEN_SIGNING_SECRET","Error","memberName","user","record","displayName","email","now","Date","fetch","input","init","printer","kickDrawer","kickPosDrawer","printReport","printPosShiftReport","posOpsBody","req","body","parsed","JSON","parse","posOpsIdempotencyKey","headers","trim","slice","cleanId","value","test","posOpsCleanId","mayWorkRegister","isPosManager","hostDoc","authorizePosOps","deps","rawHostId","permission","authorization","idToken","startsWith","length","ok","status","error","hostRef","collection","doc","hostSnapshot","get","exists","owner","settings","catch","staff","isManager","readPosRegister","rawRegisterId","snapshot","registerId","ref","data","signClaims","payload","update","digest","mintPosAssertion","expiresAtMs","purpose","claims","v","h","r","u","memberUid","p","e","Buffer","from","stringify","toString","readPosAssertion","expected","dot","lastIndexOf","signature","expected64","a","b","Uint8Array","Number","resolvePosStaffAssertion","read","resolvePosCashier","cashierId","via"],"mappings":"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"}
@@ -33,7 +33,7 @@ export declare function decidePosRefundAuthority(deps: PosOpsDeps, input: {
33
33
  registerId: string;
34
34
  refundCents: number;
35
35
  limitCents: number;
36
- /** The member asking is a workspace admin, or a manager's PIN stands behind them. */
36
+ /** The member asking holds "Approve refunds over the register's limit" (AGL-4047). */
37
37
  isManager: boolean;
38
38
  managerAssertion: unknown;
39
39
  }): Promise<PosRefundAuthority>;
@@ -57,4 +57,6 @@ export declare function posRegisterRefundAuthority(input: {
57
57
  lineCents: number | null;
58
58
  registerId: string;
59
59
  managerAssertion: unknown;
60
+ /** The member asking holds "Approve refunds over the register's limit" themselves (AGL-4047). */
61
+ isManager?: boolean;
60
62
  }, deps?: PosOpsDeps): Promise<PosRefundAuthority>;
@@ -14,6 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ import { posMoney } from "../model/commerce-pos-ops.js";
17
+ import { commerceWorkspacePermissionRefusal } from "../model/workspace-site-permissions.js";
17
18
  import { posOpsSettings } from "../pos-ops-config.js";
18
19
  import { defaultPosOpsDeps, posOpsCleanId, resolvePosStaffAssertion } from "./pos-ops-gate.js";
19
20
  /** The limit question, with the money already known. */ export async function decidePosRefundAuthority(deps, input) {
@@ -50,7 +51,7 @@ import { defaultPosOpsDeps, posOpsCleanId, resolvePosStaffAssertion } from "./po
50
51
  return {
51
52
  ok: false,
52
53
  body: {
53
- error: 'Refunds of online orders require an admin of the whole workspace'
54
+ error: commerceWorkspacePermissionRefusal('commerce.refunds')
54
55
  }
55
56
  };
56
57
  }
@@ -64,7 +65,7 @@ import { defaultPosOpsDeps, posOpsCleanId, resolvePosStaffAssertion } from "./po
64
65
  registerId: input.registerId,
65
66
  refundCents: Math.min(asked, remaining),
66
67
  limitCents: settings.refundLimitCents,
67
- isManager: false,
68
+ isManager: input.isManager === true,
68
69
  managerAssertion: input.managerAssertion
69
70
  });
70
71
  }