@solumflow-app/crm-client 0.1.0

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/refusal.ts","../src/tags.ts","../src/client.ts","../src/generated/api-types.ts"],"sourcesContent":["/**\n * Read a CRM catalogue and send orders back to it, from your own website.\n *\n * Five lines is the whole of it:\n *\n * ```ts\n * import { createClient } from '@solumflow-app/crm-client';\n *\n * export const crm = createClient({\n * apiKey: process.env.CRM_API_KEY!,\n * baseUrl: process.env.CRM_BASE_URL!,\n * });\n * ```\n *\n * Two things about the surface are worth knowing before reading further.\n *\n * The reading functions come in two kinds and the names are the only warning\n * you get: `getProducts` and `getProduct` are cached and may be a minute old;\n * `getAvailability` is never cached anywhere. Use the second one for anything\n * a visitor would call wrong — stock is the example, and a cached \"in stock\"\n * is the first thing to lie.\n *\n * The webhook half lives behind `@solumflow-app/crm-client/webhooks`, and the\n * catalogue mirror behind `@solumflow-app/crm-client/mirror`, so a shop that only\n * lists products never has to resolve `node:crypto` or `next/cache`.\n */\nexport { createClient } from './client';\nexport type {\n CrmClient,\n CrmClientOptions,\n EventListOptions,\n FetchLike,\n ListOptions,\n ProductListOptions,\n WriteOptions,\n} from './client';\n\nexport { CrmApiError } from './errors';\n\nexport {\n eventTag,\n eventsTag,\n productTag,\n productsTag,\n tagsForDelivery,\n} from './tags';\n\nexport {\n ALL_API_ERROR_CODES,\n ALL_API_SCOPES,\n ALL_API_WEBHOOK_EVENTS,\n} from './types';\n\nexport type {\n ApiErrorBody,\n ApiErrorCode,\n ApiScope,\n ApiWebhookEvent,\n ApiWebhookPayload,\n ContactInput,\n ContactResult,\n EventAvailability,\n EventDetail,\n EventList,\n EventListItem,\n FormSubmissionInput,\n FormSubmissionResult,\n OrderAddress,\n OrderBuyer,\n OrderInput,\n OrderLine,\n OrderResult,\n ProductDetail,\n ProductImage,\n ProductList,\n ProductListItem,\n PublicProductCategory,\n PublicProductPrice,\n PublicProductSummary,\n RequestInput,\n RequestResult,\n StockStatus,\n} from './types';\n","import type { ApiErrorCode } from './generated/api-types';\n\n/**\n * A refusal from the API, with the machine-readable reason kept.\n *\n * Match on `code`, never on `message`. The code is a closed set that this\n * package generates from the server's own list; the message is written for\n * somebody reading a log and gets reworded.\n *\n * Three of the codes are deliberately vaguer than they could be, and knowing\n * that saves an afternoon:\n *\n * - `unauthorized` means the key was not accepted, and says nothing about\n * which of \"missing\", \"mistyped\", \"revoked\" or \"expired\" applies. That is on\n * purpose — the alternative is an endpoint that tells a stranger whether a\n * key they guessed exists.\n * - `forbidden` covers both \"this key does not carry that permission\" and \"this\n * is a browser-safe key and you asked it to write\", for the same reason.\n * - `not_found` is also the answer for something that exists but belongs to\n * somebody else. A shop cannot tell the two apart, and should not be able to.\n */\nexport class CrmApiError extends Error {\n readonly code: ApiErrorCode;\n readonly status: number;\n readonly fields: { path: string; message: string }[];\n /** Present when the API refused before the request reached a handler. */\n readonly requestUrl: string;\n\n constructor(input: {\n code: ApiErrorCode;\n message: string;\n status: number;\n fields?: { path: string; message: string }[];\n requestUrl: string;\n }) {\n super(input.message);\n\n this.name = 'CrmApiError';\n this.code = input.code;\n this.status = input.status;\n this.fields = input.fields ?? [];\n this.requestUrl = input.requestUrl;\n }\n\n /**\n * Whether trying the same request again could plausibly work.\n *\n * A rate limit clears and a server error may be a blip; a rejected key and a\n * malformed body will be refused just as firmly the second time. Write\n * requests should be retried with the *same* idempotency key, which this\n * client fills in for you, so a retry after a timeout cannot become a second\n * order.\n */\n get retryable() {\n return this.code === 'rate_limited' || this.code === 'internal_error';\n }\n}\n","import { CrmApiError } from './errors';\nimport type { ApiErrorCode } from './generated/api-types';\n\nexport async function readJson(response: Response) {\n try {\n return (await response.json()) as unknown;\n } catch {\n return null;\n }\n}\n\n/**\n * A refusal, whatever shape it arrived in.\n *\n * Almost everything answers `{ error: { code, message } }`, and then the code\n * is taken at its word — it is a closed set this package generates from the\n * server's own list, so a code that does not appear here means the two have\n * drifted and a build somewhere should already have gone red.\n *\n * The events listing is the exception: it answers `{ error: \"...\" }`, because\n * it predates the shared error helper and is read by script tags on sites\n * nobody here can redeploy. For that one the status code is all there is, and\n * the one place it cannot be precise is 403, where \"your key lacks this scope\"\n * and \"that module is switched off for this account\" share a number. It is\n * reported as `forbidden`, the one of the two a developer can act on.\n */\nexport function refusalOf(status: number, body: unknown, requestUrl: string) {\n const structured = (\n body as { error?: { code?: string; message?: string; fields?: unknown } }\n )?.error;\n\n if (structured && typeof structured === 'object' && structured.code) {\n return new CrmApiError({\n code: structured.code as ApiErrorCode,\n message: structured.message ?? structured.code,\n status,\n fields: (structured.fields ?? []) as { path: string; message: string }[],\n requestUrl,\n });\n }\n\n const plain = (body as { error?: unknown } | null)?.error;\n\n return new CrmApiError({\n code: codeForStatus(status),\n message:\n typeof plain === 'string' ? plain : `The request failed with ${status}.`,\n status,\n requestUrl,\n });\n}\n\nfunction codeForStatus(status: number): ApiErrorCode {\n switch (status) {\n case 400:\n return 'invalid_request';\n case 401:\n return 'unauthorized';\n case 403:\n return 'forbidden';\n case 404:\n return 'not_found';\n case 409:\n return 'request_in_progress';\n case 429:\n return 'rate_limited';\n default:\n return 'internal_error';\n }\n}\n","/**\n * The names a cached answer is filed under, so a webhook can throw it away.\n *\n * This is the part of the package that actually does something. A `getProducts`\n * that only fetches is three lines anybody writes themselves; a `getProducts`\n * whose answer sits under a name the shipped revalidation route already knows\n * how to clear is the piece that makes the connection work at all.\n *\n * **Every read carries the collection tag, detail reads included**, and that is\n * not sloppiness. A delivery names ids and nothing else — on purpose, so a\n * late message still leads to the right state and no message can hand out data\n * the receiving key could not have read. But a product page is usually fetched\n * by *slug*, and nothing on this side can turn an id into the slug somebody\n * asked for. So the per-item tag is a narrower handle for a site that fetches\n * by id and for its own manual invalidations, and the collection tag is the one\n * that guarantees a slug-fetched page ever updates.\n */\nconst PREFIX = 'crm';\n\n/** Everything that lists products. Cleared by every catalogue delivery. */\nexport function productsTag() {\n return `${PREFIX}:products`;\n}\n\n/** One product, by whichever key it was fetched with. */\nexport function productTag(slugOrId: string) {\n return `${PREFIX}:product:${slugOrId}`;\n}\n\nexport function eventsTag() {\n return `${PREFIX}:events`;\n}\n\nexport function eventTag(idOrSlug: string) {\n return `${PREFIX}:event:${idOrSlug}`;\n}\n\n/**\n * What a delivery of this type should clear.\n *\n * `truncated` means the batch stopped collecting and `ids` is incomplete, so\n * the only honest answer is the whole collection — which the collection tag\n * already is. The per-item tags are added on top when the list can be trusted.\n */\nexport function tagsForDelivery(input: {\n type: string;\n ids: readonly string[];\n truncated: boolean;\n}) {\n const collection = input.type.startsWith('product.')\n ? productsTag()\n : input.type.startsWith('event.')\n ? eventsTag()\n : null;\n\n if (collection === null) {\n return [];\n }\n\n if (input.truncated) {\n return [collection];\n }\n\n const item = input.type.startsWith('product.') ? productTag : eventTag;\n\n return [collection, ...input.ids.map((id) => item(id))];\n}\n","import { CrmApiError } from './errors';\nimport { readJson, refusalOf } from './refusal';\nimport { eventTag, eventsTag, productTag, productsTag } from './tags';\nimport type {\n ContactInput,\n ContactResult,\n EventAvailability,\n EventDetail,\n EventList,\n FormSubmissionInput,\n FormSubmissionResult,\n OrderInput,\n OrderResult,\n ProductDetail,\n ProductList,\n RequestInput,\n RequestResult,\n StockStatus,\n} from './types';\n\nconst KEY_FORM = /^crm[ps]_[A-Za-z0-9_-]{43}$/;\nconst SECRET_PREFIX = 'crms_';\nconst BASE_PATH = '/api/public/v1';\nconst DEFAULT_REVALIDATE = 60;\n\n/**\n * The fetch options this package sets that are not in the web standard.\n *\n * `next` is read by Next.js and ignored by every other runtime, which is\n * exactly the behaviour wanted: the tags do nothing outside a framework that\n * caches, and nothing breaks in one that does not.\n */\ntype CachedInit = RequestInit & {\n next?: { tags?: string[]; revalidate?: number | false };\n};\n\nexport type FetchLike = (input: string, init?: CachedInit) => Promise<Response>;\n\nexport interface CrmClientOptions {\n /**\n * The key, whole.\n *\n * A `crmp_` key may be read by anyone who views source and may only read; a\n * `crms_` key may write and must never leave a server. This client refuses a\n * `crms_` key outright when it finds itself in a browser — see the note on\n * `createClient`.\n */\n apiKey: string;\n /**\n * Where the CRM lives: an origin, with no path on it.\n *\n * `https://app.example.com`, not `https://app.example.com/api/public/v1`.\n * The version lives in this package so that a shop upgrading the package is\n * the thing that moves it, rather than a string in someone's environment\n * file that nobody remembers to change.\n */\n baseUrl: string;\n /**\n * How long a cached answer may be served before it is refetched, in seconds.\n *\n * Defaults to a minute. `false` means never on a timer — only when a webhook\n * clears the tag. That is the right setting for a shop whose revalidation\n * route is wired up and reachable, and the wrong one for a shop where it is\n * not, because then nothing ever expires at all.\n */\n revalidate?: number | false;\n /** For tests and for runtimes with their own instrumented fetch. */\n fetch?: FetchLike;\n}\n\nexport interface ListOptions {\n limit?: number;\n cursor?: string | null;\n}\n\nexport interface ProductListOptions extends ListOptions {\n /** A category id. Passing something that is not a uuid is refused, loudly. */\n category?: string;\n}\n\nexport interface EventListOptions extends ListOptions {\n /** ISO timestamps. Both ends are optional and both are inclusive. */\n from?: string;\n to?: string;\n}\n\nexport interface WriteOptions {\n /**\n * The key that makes a retry safe.\n *\n * Generated per call when you leave it out, which is right for a first\n * attempt and wrong for a retry: a website that timed out does not know\n * whether the order arrived, and sending it again under a *new* key is how\n * one customer gets charged twice. Keep the key you used and resend with it.\n */\n idempotencyKey?: string;\n}\n\nexport interface CrmClient {\n getProducts(options?: ProductListOptions): Promise<ProductList>;\n getProduct(slugOrId: string): Promise<ProductDetail | null>;\n /** Live. Never cached, at any layer. */\n getAvailability(slugOrId: string): Promise<StockStatus | null>;\n getEvents(options?: EventListOptions): Promise<EventList>;\n getEvent(idOrSlug: string): Promise<EventDetail | null>;\n /** Live. Never cached, at any layer. */\n getEventAvailability(eventId: string): Promise<EventAvailability | null>;\n submitOrder(input: OrderInput, options?: WriteOptions): Promise<OrderResult>;\n upsertContact(\n input: ContactInput,\n options?: WriteOptions,\n ): Promise<ContactResult>;\n submitRequest(\n input: RequestInput,\n options?: WriteOptions,\n ): Promise<RequestResult>;\n submitForm(\n formId: string,\n input: FormSubmissionInput,\n options?: WriteOptions,\n ): Promise<FormSubmissionResult>;\n}\n\n/**\n * A client for one account's public API.\n *\n * Two layers, and the names are the whole warning. `getProducts` and\n * `getProduct` are cached at the edge and filed under tags the shipped\n * revalidation route knows how to clear. `getAvailability` is not cached\n * anywhere and never will be — a stock figure in a cached answer is the number\n * that lies first and lies worst, and a developer who reaches for `getProduct`\n * to show \"in stock\" gets no warning from the type system, so the split has to\n * be in the name.\n *\n * The browser check is not decoration either. A `crms_` key can create orders\n * and read contacts; pasted into a client component it ends up in a JavaScript\n * bundle that anybody can open. Throwing at construction turns that into a\n * build-time failure on the first render instead of a quiet leak.\n */\nexport function createClient(options: CrmClientOptions): CrmClient {\n const apiKey = options.apiKey?.trim() ?? '';\n\n if (!KEY_FORM.test(apiKey)) {\n throw new Error(\n 'crm-client: the API key is not in the expected form. It starts with ' +\n '`crmp_` or `crms_` and is 48 characters long — a truncated paste ' +\n 'looks exactly like a revoked key once it reaches the server.',\n );\n }\n\n if (apiKey.startsWith(SECRET_PREFIX) && inSomethingTheUserCanRead()) {\n throw new Error(\n 'crm-client: a `crms_` key may not be used in a browser. It can write ' +\n 'orders and read contacts, and anything a browser holds is public — ' +\n 'including a service worker or a web worker, which is why this is ' +\n 'refused there too. Read from a server component or a route handler, ' +\n 'or issue a `crmp_` key for the parts of the catalogue a page reads ' +\n 'directly.',\n );\n }\n\n const base = options.baseUrl?.replace(/\\/+$/, '') ?? '';\n\n if (!/^https?:\\/\\/[^/]+$/.test(base)) {\n throw new Error(\n `crm-client: baseUrl should be an origin and nothing more, such as ` +\n `\"https://app.example.com\". Received \"${options.baseUrl}\".`,\n );\n }\n\n const doFetch: FetchLike =\n options.fetch ?? ((input, init) => fetch(input, init));\n const revalidate = options.revalidate ?? DEFAULT_REVALIDATE;\n\n async function send<T>(\n path: string,\n init: CachedInit,\n unwrap: (body: unknown) => T,\n notFoundIsNull: boolean,\n ): Promise<T | null> {\n const url = `${base}${BASE_PATH}${path}`;\n\n const response = await doFetch(url, {\n ...init,\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${apiKey}`,\n ...init.headers,\n },\n });\n\n const body = await readJson(response);\n\n if (!response.ok) {\n const refusal = refusalOf(response.status, body, url);\n\n if (notFoundIsNull && refusal.code === 'not_found') {\n return null;\n }\n\n throw refusal;\n }\n\n const answer = unwrap(body);\n\n /*\n * A 2xx whose body is not the shape this method promised. It happens: a\n * proxy that answers its own page, a CDN error dressed as a 200, a version\n * of this package older than the route it is talking to.\n *\n * Without this the value is cast and handed back typed as an answer, and\n * the failure surfaces as `TypeError: cannot read properties of undefined`\n * three frames away in somebody's shop -- the one kind of error this\n * package's whole design is meant to avoid producing.\n */\n if (answer === undefined || answer === null) {\n throw new CrmApiError({\n code: 'internal_error',\n message:\n 'The API answered successfully with a body this client did not ' +\n 'recognise. Either something between here and it replaced the ' +\n 'answer, or this package is older than the route it called.',\n status: response.status,\n requestUrl: url,\n });\n }\n\n return answer;\n }\n\n function cached(tags: string[]): CachedInit {\n return { method: 'GET', next: { tags, revalidate } };\n }\n\n const live: CachedInit = { method: 'GET', cache: 'no-store' };\n\n function write(body: unknown, options?: WriteOptions): CachedInit {\n return {\n method: 'POST',\n cache: 'no-store',\n headers: {\n 'content-type': 'application/json',\n 'idempotency-key': options?.idempotencyKey ?? newIdempotencyKey(),\n },\n body: JSON.stringify(body),\n };\n }\n\n return {\n async getProducts(listOptions) {\n const query = new URLSearchParams();\n\n if (listOptions?.limit !== undefined) {\n query.set('limit', `${listOptions.limit}`);\n }\n\n if (listOptions?.cursor) {\n query.set('cursor', listOptions.cursor);\n }\n\n if (listOptions?.category) {\n query.set('category', listOptions.category);\n }\n\n const result = await send(\n `/products${suffix(query)}`,\n cached([productsTag()]),\n (body) => listOrNothing(body, 'data') as ProductList | undefined,\n false,\n );\n\n return result as ProductList;\n },\n\n getProduct(slugOrId) {\n return send(\n `/products/${encodeURIComponent(slugOrId)}`,\n cached([productsTag(), productTag(slugOrId)]),\n (body) => dataOf(body) as ProductDetail,\n true,\n );\n },\n\n getAvailability(slugOrId) {\n return send(\n `/products/${encodeURIComponent(slugOrId)}/availability`,\n live,\n (body) => dataOf(body) as StockStatus,\n true,\n );\n },\n\n async getEvents(listOptions) {\n const query = new URLSearchParams();\n\n if (listOptions?.limit !== undefined) {\n query.set('limit', `${listOptions.limit}`);\n }\n\n if (listOptions?.cursor) {\n query.set('cursor', listOptions.cursor);\n }\n\n if (listOptions?.from) {\n query.set('from', listOptions.from);\n }\n\n if (listOptions?.to) {\n query.set('to', listOptions.to);\n }\n\n const result = await send(\n `/events${suffix(query)}`,\n cached([eventsTag()]),\n (body) => listOrNothing(body, 'events') as EventList | undefined,\n false,\n );\n\n return result as EventList;\n },\n\n getEvent(idOrSlug) {\n return send(\n `/events/${encodeURIComponent(idOrSlug)}`,\n cached([eventsTag(), eventTag(idOrSlug)]),\n (body) => dataOf(body) as EventDetail,\n true,\n );\n },\n\n getEventAvailability(eventId) {\n return send(\n `/events/${encodeURIComponent(eventId)}/availability`,\n live,\n (body) => body as EventAvailability,\n true,\n );\n },\n\n async submitOrder(input, writeOptions) {\n const result = await send(\n '/orders',\n write(input, writeOptions),\n (body) => dataOf(body) as OrderResult,\n false,\n );\n\n return result as OrderResult;\n },\n\n async upsertContact(input, writeOptions) {\n const result = await send(\n '/contacts',\n write(input, writeOptions),\n (body) => dataOf(body) as ContactResult,\n false,\n );\n\n return result as ContactResult;\n },\n\n async submitRequest(input, writeOptions) {\n const result = await send(\n '/requests',\n write(input, writeOptions),\n (body) => dataOf(body) as RequestResult,\n false,\n );\n\n return result as RequestResult;\n },\n\n async submitForm(formId, input, writeOptions) {\n const result = await send(\n `/forms/${encodeURIComponent(formId)}/submissions`,\n write(input, writeOptions),\n (body) => dataOf(body) as FormSubmissionResult,\n false,\n );\n\n return result as FormSubmissionResult;\n },\n };\n}\n\nfunction suffix(query: URLSearchParams) {\n const rendered = query.toString();\n\n return rendered ? `?${rendered}` : '';\n}\n\nfunction dataOf(body: unknown) {\n return (body as { data: unknown } | null)?.data;\n}\n\n/**\n * A listing, or nothing at all when the body is not one.\n *\n * `undefined` is the signal `send` turns into a legible error. Checking for the\n * array rather than merely for the key matters: a proxy answering its own page,\n * or a CDN dressing an error as a 200, produces a body with neither — and\n * without this the caller receives it typed as a page of products and finds out\n * when `.map` is not a function, somewhere else entirely.\n *\n * The key differs per listing because the events one answers `events` rather\n * than `data`, which it has done since before this convention existed.\n */\nfunction listOrNothing(body: unknown, key: 'data' | 'events') {\n const listing = body as Record<string, unknown> | null;\n\n return Array.isArray(listing?.[key]) ? listing : undefined;\n}\n\n/**\n * A fresh key for a first attempt.\n *\n * Deliberately not derived from the body. Two identical orders placed a minute\n * apart by the same customer are two orders, and a fingerprint would quietly\n * collapse them into one.\n *\n * `crypto.randomUUID` is not everywhere it looks like it is: browsers expose it\n * only in a secure context, so a shop still served over plain http has\n * `crypto` and not that method. Reaching for it unguarded turns every write\n * into `TypeError: crypto.randomUUID is not a function`, thrown while building\n * the headers, before any request — which tells the shop's developer nothing\n * about what is actually missing.\n *\n * The weakest fallback is acceptable *here specifically*, and nowhere else in\n * this package: an idempotency key needs to be unique within one account for a\n * day. It is not a credential, it guesses nothing and guards nothing; a caller\n * who wants a stronger one passes their own.\n */\nfunction newIdempotencyKey() {\n const source = globalThis.crypto;\n\n if (typeof source?.randomUUID === 'function') {\n return source.randomUUID();\n }\n\n if (typeof source?.getRandomValues === 'function') {\n const bytes = source.getRandomValues(new Uint8Array(16));\n\n return [...bytes]\n .map((byte) => byte.toString(16).padStart(2, '0'))\n .join('');\n }\n\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}`;\n}\n\n/**\n * Whether this is somewhere an end user could read what the code holds.\n *\n * `typeof window !== 'undefined'` is the usual test and it is not enough: a\n * service worker and a web worker both have no `window` and are both JavaScript\n * delivered to a browser, openable in devtools like any other script. Treating\n * \"no window\" as \"a trusted server\" is exactly the assumption that lets a\n * writing key into an offline bundle.\n */\nfunction inSomethingTheUserCanRead() {\n if (typeof window !== 'undefined') {\n return true;\n }\n\n const scope = globalThis as { importScripts?: unknown; self?: unknown };\n\n return typeof scope.importScripts === 'function';\n}\n","// GENERATED FILE -- edit the projections, not this.\n//\n// Every declaration below was lifted, comments and all, out of the services\n// that build the public API's answers. Run `pnpm --filter @solumflow-app/crm-client\n// generate:types` after changing one of them; a test in this package fails\n// when this file and those sources disagree.\n//\n// Sources:\n// packages/features/crm-invoicing/src/shared/public-product-item.ts\n// packages/features/crm-events/src/shared/public-event-list-item.ts\n// packages/features/crm-events/src/shared/public-event-detail-item.ts\n// packages/api-keys/src/webhook-events.ts\n// packages/api-keys/src/scopes.ts\n// apps/web/app/api/public/_lib/errors.ts\n\n/** What a key is allowed to reach. A key carries one or more of these. */\nexport type ApiScope =\n | 'catalog:read'\n | 'events:read'\n | 'orders:write'\n | 'contacts:write'\n | 'forms:write';\n\nexport const ALL_API_SCOPES: readonly ApiScope[] = [\n 'catalog:read',\n 'events:read',\n 'orders:write',\n 'contacts:write',\n 'forms:write',\n];\n\n/** Everything an endpoint can be told about. A delivery names one. */\nexport type ApiWebhookEvent =\n | 'product.changed'\n | 'product.deleted'\n | 'event.changed'\n | 'order.status_changed';\n\nexport const ALL_API_WEBHOOK_EVENTS: readonly ApiWebhookEvent[] = [\n 'product.changed',\n 'product.deleted',\n 'event.changed',\n 'order.status_changed',\n];\n\n/** The `error.code` of a refusal. Match on this, never on the message -- the message is written for a person reading a log and may be reworded. */\nexport type ApiErrorCode =\n | 'unauthorized'\n | 'forbidden'\n | 'feature_unavailable'\n | 'not_found'\n | 'invalid_request'\n | 'idempotency_key_reused'\n | 'request_in_progress'\n | 'rate_limited'\n | 'internal_error';\n\nexport const ALL_API_ERROR_CODES: readonly ApiErrorCode[] = [\n 'unauthorized',\n 'forbidden',\n 'feature_unavailable',\n 'not_found',\n 'invalid_request',\n 'idempotency_key_reused',\n 'request_in_progress',\n 'rate_limited',\n 'internal_error',\n];\n\n/**\n * One product as a stranger's website sees it.\n *\n * These interfaces are the public API's contract, not internal convenience\n * types. They are served cross-origin to pages nobody here controls, and a\n * customer's shop reads these exact keys — so a field removed or renamed breaks\n * a site that will not be redeployed. Add fields; do not repurpose them.\n *\n * What is deliberately absent is as much the contract as what is here.\n *\n * - **`sku`** is the number this business uses to find the thing in its own\n * stockroom. It says how many suppliers there are, which one a line came\n * from, and often what was paid. `gtin` is the number printed on the box and\n * is offered instead, in the detail: that one is meant to be public, and a\n * shop needs it for a product feed.\n * - **The stock count** never leaves. `inStock` answers the only question a\n * visitor has, and the number itself is a business fact a competitor would\n * like and a cached answer would get wrong within the minute.\n * - **`unit_price_cents` raw** is not it either; see `priceFromCents`.\n * - **Non-public custom fields.** `fields` carries only attributes a member\n * marked `is_public`, which defaults to off. A field called \"purchase price\"\n * stays home unless somebody says otherwise, once, per field.\n *\n * And one field that an event has and a product does not: **`url`**. An event\n * is sold on a page this system hosts, so its listing can hand out a link. A\n * product is sold on the customer's own site — that is the entire reason this\n * API exists — and this system does not know what they called their product\n * page. A guessed link is worse than none.\n */\nexport interface PublicProductListItem {\n id: string;\n /** Always present: a product without an address cannot be published. */\n slug: string;\n name: string;\n productType: 'digital' | 'physical';\n /**\n * The storage path, not a URL. The route turns it into a public URL, because\n * only the route knows the origin its own storage is served from.\n */\n imagePath: string | null;\n /**\n * The cheapest way in, over the active price options — not the default one.\n * A card that says \"from €12\" next to a product whose entry price is €12 and\n * whose default is the €40 yearly plan is answering the question the visitor\n * actually asked.\n *\n * Never null, unlike the same field on an event. An event without a ticket\n * tier on sale genuinely has no price; a product always has `unit_price_cents`\n * on its own row, so when no separate price option is active that column is\n * the answer — including when it is `0`, which is a real price for something\n * given away.\n */\n priceFromCents: number;\n /** The struck-through price beside it, when the seller set one. */\n compareAtCents: number | null;\n currency: string;\n /**\n * Whether a visitor can buy it right now — never how many are left.\n *\n * True whenever the seller does not track stock at all, which is the common\n * case and the honest answer for anything made to order. When they do track\n * it, this counts what is on the shelf minus what other people already hold\n * in a checkout, and it stays true for a seller who accepts backorders.\n */\n inStock: boolean;\n /**\n * The custom fields this account invented, keyed by the slug they chose, and\n * only the ones marked public. An empty object when none are.\n */\n fields: Record<string, unknown>;\n}\n\n/**\n * The same product on its own page, where more may be shown.\n *\n * The detail answers for things the listing hides — an unlisted product, one\n * reachable by anyone holding the link — for the same reason the events detail\n * answers for a cancelled evening: somebody was sent here on purpose, and a 404\n * would be a lie. What it still refuses is anything unpublished.\n */\nexport interface PublicProductDetail extends PublicProductListItem {\n /** The one-line text that lands on a quote or an invoice row. */\n description: string | null;\n /** The long story, as sanitised HTML. Written by the seller, for this page. */\n longDescription: string | null;\n /** The barcode number: printed on the box, wanted by every product feed. */\n gtin: string | null;\n metaTitle: string | null;\n metaDescription: string | null;\n images: PublicProductImage[];\n prices: PublicProductPrice[];\n categories: PublicProductCategory[];\n /** What the seller linked this to — a companion, a refill, a bigger model. */\n related: PublicProductSummary[];\n}\n\nexport interface PublicProductImage {\n /** A path, turned into a URL by the route. Same reason as `imagePath`. */\n path: string;\n alt: string | null;\n}\n\n/**\n * One way to buy the thing.\n *\n * `stripe_price_id` and `stripe_account_id` are absent and must stay absent:\n * they name objects in the seller's payment account, and a checkout is started\n * through this system's own order route, never by a stranger quoting an id.\n */\nexport interface PublicProductPrice {\n id: string;\n kind: 'one_off' | 'recurring' | 'installment';\n label: string | null;\n amountCents: number;\n /** What the whole thing costs when paid in parts; null for the other kinds. */\n totalCents: number | null;\n installmentCount: number | null;\n recurringInterval: 'day' | 'week' | 'month' | 'year' | null;\n recurringIntervalCount: number | null;\n compareAtCents: number | null;\n trialPeriodDays: number | null;\n isDefault: boolean;\n}\n\nexport interface PublicProductCategory {\n id: string;\n name: string;\n}\n\nexport interface PublicProductSummary {\n id: string;\n slug: string;\n name: string;\n}\n\n/**\n * One event as a stranger's website sees it.\n *\n * This interface is the public API's contract, not an internal convenience\n * type. It is served cross-origin to pages nobody here controls, and a widget\n * on a customer's homepage reads these exact keys — so a field removed or\n * renamed breaks a site that will not be redeployed. Add fields; do not\n * repurpose them.\n *\n * What is deliberately absent is as much the contract as what is here: no\n * description (a paragraph no card renders), no joining link (that is\n * admission), no counts of who bought what, and nothing an organiser uses to\n * manage the event.\n */\nexport interface PublicEventListItem {\n id: string;\n slug: string;\n title: string;\n subtitle: string | null;\n startsAt: string;\n endsAt: string | null;\n timezone: string;\n locationName: string | null;\n /**\n * The storage path, not a URL. The route turns it into a public URL, because\n * only the route knows the origin its own storage is served from.\n */\n coverImagePath: string | null;\n /** Null when nothing is on sale — never 0, which is a real price. */\n priceFromCents: number | null;\n currency: string | null;\n soldOut: boolean;\n /** Null when the event has no ceiling to count down from. */\n seatsLeft: number | null;\n /** Path on the hosted site, so a widget can link straight to checkout. */\n url: string;\n /**\n * The custom fields this account invented, keyed by the slug they chose, and\n * only the ones marked public. An empty object when none are, which is every\n * account until somebody turns one on.\n *\n * Added rather than repurposed, which is the rule this interface states at\n * the top: a widget written before this field existed keeps working, because\n * it simply never reads the key.\n */\n fields: Record<string, unknown>;\n}\n\n/**\n * One event as the public API hands it out, on its own page.\n *\n * Not the same thing as `PublicEventView`, which feeds the detail page this\n * system hosts — and the difference is the reason this type exists rather than\n * the other one being reused. That view carries `online_url`: the link you join\n * the evening by. On a page we host, behind a ticket, that is correct. Through\n * an API that answers any holder of a read key, it is a free seat.\n *\n * It also carries `account_id`, which is ours and not the caller's business.\n *\n * So this is a separate, narrower projection, and everything in\n * `PublicEventListItem` about adding rather than repurposing fields applies\n * here too.\n */\nexport interface PublicEventDetailItem extends PublicEventListItem {\n /** The organiser's own text for the evening. */\n description: string | null;\n /**\n * `on_sale`, `closed` or `cancelled` — never `draft`, which does not answer\n * at all. A ticket holder has to be able to read that the evening is off, so\n * this route replies for the last two where the listing withholds them.\n */\n status: 'on_sale' | 'closed' | 'cancelled';\n doorsOpenAt: string | null;\n salesStartAt: string | null;\n salesEndAt: string | null;\n /** The postal address, as the organiser entered it. */\n address: unknown;\n metaTitle: string | null;\n metaDescription: string | null;\n ticketTypes: PublicEventTicketType[];\n /** Custom fields the account marked public, keyed by their own slug. */\n fields: Record<string, unknown>;\n}\n\n/**\n * One way in, as a stranger's page may show it.\n *\n * `reserved_count` and `sold_count` are absent: how many were sold is the\n * organiser's figure, and `seatsLeft` already answers the only question a\n * visitor has. The capacity behind it stays home for the same reason a stock\n * count does.\n */\nexport interface PublicEventTicketType {\n id: string;\n name: string;\n description: string | null;\n priceCents: number;\n currency: string;\n taxPercentage: number | null;\n minPerOrder: number | null;\n maxPerOrder: number | null;\n salesStartAt: string | null;\n salesEndAt: string | null;\n /** Null when this tier has no ceiling to count down from. */\n seatsLeft: number | null;\n soldOut: boolean;\n onSale: boolean;\n}\n\n/**\n * The body of one delivery.\n *\n * IDS AND NOTHING ELSE, and the three reasons are worth keeping together. A\n * message that arrives late still leads to the right state, because the\n * receiver fetches the current row rather than trusting a snapshot from twenty\n * minutes ago. A message can never hand out something the receiving key was\n * not allowed to read, because it hands out nothing. And five hundred changes\n * fit in a body of a few kilobytes instead of a few megabytes.\n *\n * `truncated` means the batch stopped collecting at its ceiling and `ids` is\n * therefore incomplete: the receiver should refetch the whole collection. It\n * is always present rather than only when true, so a receiver that reads it\n * cannot mistake \"absent\" for \"false\" in the one direction that matters.\n */\nexport interface ApiWebhookPayload {\n type: ApiWebhookEvent;\n occurredAt: string;\n ids: string[];\n truncated: boolean;\n}\n"],"mappings":"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","names":["options"]}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Read a CRM catalogue and send orders back to it, from your own website.
3
+ *
4
+ * Five lines is the whole of it:
5
+ *
6
+ * ```ts
7
+ * import { createClient } from '@solumflow-app/crm-client';
8
+ *
9
+ * export const crm = createClient({
10
+ * apiKey: process.env.CRM_API_KEY!,
11
+ * baseUrl: process.env.CRM_BASE_URL!,
12
+ * });
13
+ * ```
14
+ *
15
+ * Two things about the surface are worth knowing before reading further.
16
+ *
17
+ * The reading functions come in two kinds and the names are the only warning
18
+ * you get: `getProducts` and `getProduct` are cached and may be a minute old;
19
+ * `getAvailability` is never cached anywhere. Use the second one for anything
20
+ * a visitor would call wrong — stock is the example, and a cached "in stock"
21
+ * is the first thing to lie.
22
+ *
23
+ * The webhook half lives behind `@solumflow-app/crm-client/webhooks`, and the
24
+ * catalogue mirror behind `@solumflow-app/crm-client/mirror`, so a shop that only
25
+ * lists products never has to resolve `node:crypto` or `next/cache`.
26
+ */
27
+ export { createClient } from './client';
28
+ export type { CrmClient, CrmClientOptions, EventListOptions, FetchLike, ListOptions, ProductListOptions, WriteOptions, } from './client';
29
+ export { CrmApiError } from './errors';
30
+ export { eventTag, eventsTag, productTag, productsTag, tagsForDelivery, } from './tags';
31
+ export { ALL_API_ERROR_CODES, ALL_API_SCOPES, ALL_API_WEBHOOK_EVENTS, } from './types';
32
+ export type { ApiErrorBody, ApiErrorCode, ApiScope, ApiWebhookEvent, ApiWebhookPayload, ContactInput, ContactResult, EventAvailability, EventDetail, EventList, EventListItem, FormSubmissionInput, FormSubmissionResult, OrderAddress, OrderBuyer, OrderInput, OrderLine, OrderResult, ProductDetail, ProductImage, ProductList, ProductListItem, PublicProductCategory, PublicProductPrice, PublicProductSummary, RequestInput, RequestResult, StockStatus, } from './types';
package/dist/index.js ADDED
@@ -0,0 +1,345 @@
1
+ // src/errors.ts
2
+ var CrmApiError = class extends Error {
3
+ constructor(input) {
4
+ super(input.message);
5
+ this.name = "CrmApiError";
6
+ this.code = input.code;
7
+ this.status = input.status;
8
+ this.fields = input.fields ?? [];
9
+ this.requestUrl = input.requestUrl;
10
+ }
11
+ /**
12
+ * Whether trying the same request again could plausibly work.
13
+ *
14
+ * A rate limit clears and a server error may be a blip; a rejected key and a
15
+ * malformed body will be refused just as firmly the second time. Write
16
+ * requests should be retried with the *same* idempotency key, which this
17
+ * client fills in for you, so a retry after a timeout cannot become a second
18
+ * order.
19
+ */
20
+ get retryable() {
21
+ return this.code === "rate_limited" || this.code === "internal_error";
22
+ }
23
+ };
24
+
25
+ // src/refusal.ts
26
+ async function readJson(response) {
27
+ try {
28
+ return await response.json();
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ function refusalOf(status, body, requestUrl) {
34
+ const structured = body?.error;
35
+ if (structured && typeof structured === "object" && structured.code) {
36
+ return new CrmApiError({
37
+ code: structured.code,
38
+ message: structured.message ?? structured.code,
39
+ status,
40
+ fields: structured.fields ?? [],
41
+ requestUrl
42
+ });
43
+ }
44
+ const plain = body?.error;
45
+ return new CrmApiError({
46
+ code: codeForStatus(status),
47
+ message: typeof plain === "string" ? plain : `The request failed with ${status}.`,
48
+ status,
49
+ requestUrl
50
+ });
51
+ }
52
+ function codeForStatus(status) {
53
+ switch (status) {
54
+ case 400:
55
+ return "invalid_request";
56
+ case 401:
57
+ return "unauthorized";
58
+ case 403:
59
+ return "forbidden";
60
+ case 404:
61
+ return "not_found";
62
+ case 409:
63
+ return "request_in_progress";
64
+ case 429:
65
+ return "rate_limited";
66
+ default:
67
+ return "internal_error";
68
+ }
69
+ }
70
+
71
+ // src/tags.ts
72
+ var PREFIX = "crm";
73
+ function productsTag() {
74
+ return `${PREFIX}:products`;
75
+ }
76
+ function productTag(slugOrId) {
77
+ return `${PREFIX}:product:${slugOrId}`;
78
+ }
79
+ function eventsTag() {
80
+ return `${PREFIX}:events`;
81
+ }
82
+ function eventTag(idOrSlug) {
83
+ return `${PREFIX}:event:${idOrSlug}`;
84
+ }
85
+ function tagsForDelivery(input) {
86
+ const collection = input.type.startsWith("product.") ? productsTag() : input.type.startsWith("event.") ? eventsTag() : null;
87
+ if (collection === null) {
88
+ return [];
89
+ }
90
+ if (input.truncated) {
91
+ return [collection];
92
+ }
93
+ const item = input.type.startsWith("product.") ? productTag : eventTag;
94
+ return [collection, ...input.ids.map((id) => item(id))];
95
+ }
96
+
97
+ // src/client.ts
98
+ var KEY_FORM = /^crm[ps]_[A-Za-z0-9_-]{43}$/;
99
+ var SECRET_PREFIX = "crms_";
100
+ var BASE_PATH = "/api/public/v1";
101
+ var DEFAULT_REVALIDATE = 60;
102
+ function createClient(options) {
103
+ const apiKey = options.apiKey?.trim() ?? "";
104
+ if (!KEY_FORM.test(apiKey)) {
105
+ throw new Error(
106
+ "crm-client: the API key is not in the expected form. It starts with `crmp_` or `crms_` and is 48 characters long \u2014 a truncated paste looks exactly like a revoked key once it reaches the server."
107
+ );
108
+ }
109
+ if (apiKey.startsWith(SECRET_PREFIX) && inSomethingTheUserCanRead()) {
110
+ throw new Error(
111
+ "crm-client: a `crms_` key may not be used in a browser. It can write orders and read contacts, and anything a browser holds is public \u2014 including a service worker or a web worker, which is why this is refused there too. Read from a server component or a route handler, or issue a `crmp_` key for the parts of the catalogue a page reads directly."
112
+ );
113
+ }
114
+ const base = options.baseUrl?.replace(/\/+$/, "") ?? "";
115
+ if (!/^https?:\/\/[^/]+$/.test(base)) {
116
+ throw new Error(
117
+ `crm-client: baseUrl should be an origin and nothing more, such as "https://app.example.com". Received "${options.baseUrl}".`
118
+ );
119
+ }
120
+ const doFetch = options.fetch ?? ((input, init) => fetch(input, init));
121
+ const revalidate = options.revalidate ?? DEFAULT_REVALIDATE;
122
+ async function send(path, init, unwrap, notFoundIsNull) {
123
+ const url = `${base}${BASE_PATH}${path}`;
124
+ const response = await doFetch(url, {
125
+ ...init,
126
+ headers: {
127
+ accept: "application/json",
128
+ authorization: `Bearer ${apiKey}`,
129
+ ...init.headers
130
+ }
131
+ });
132
+ const body = await readJson(response);
133
+ if (!response.ok) {
134
+ const refusal = refusalOf(response.status, body, url);
135
+ if (notFoundIsNull && refusal.code === "not_found") {
136
+ return null;
137
+ }
138
+ throw refusal;
139
+ }
140
+ const answer = unwrap(body);
141
+ if (answer === void 0 || answer === null) {
142
+ throw new CrmApiError({
143
+ code: "internal_error",
144
+ message: "The API answered successfully with a body this client did not recognise. Either something between here and it replaced the answer, or this package is older than the route it called.",
145
+ status: response.status,
146
+ requestUrl: url
147
+ });
148
+ }
149
+ return answer;
150
+ }
151
+ function cached(tags) {
152
+ return { method: "GET", next: { tags, revalidate } };
153
+ }
154
+ const live = { method: "GET", cache: "no-store" };
155
+ function write(body, options2) {
156
+ return {
157
+ method: "POST",
158
+ cache: "no-store",
159
+ headers: {
160
+ "content-type": "application/json",
161
+ "idempotency-key": options2?.idempotencyKey ?? newIdempotencyKey()
162
+ },
163
+ body: JSON.stringify(body)
164
+ };
165
+ }
166
+ return {
167
+ async getProducts(listOptions) {
168
+ const query = new URLSearchParams();
169
+ if (listOptions?.limit !== void 0) {
170
+ query.set("limit", `${listOptions.limit}`);
171
+ }
172
+ if (listOptions?.cursor) {
173
+ query.set("cursor", listOptions.cursor);
174
+ }
175
+ if (listOptions?.category) {
176
+ query.set("category", listOptions.category);
177
+ }
178
+ const result = await send(
179
+ `/products${suffix(query)}`,
180
+ cached([productsTag()]),
181
+ (body) => listOrNothing(body, "data"),
182
+ false
183
+ );
184
+ return result;
185
+ },
186
+ getProduct(slugOrId) {
187
+ return send(
188
+ `/products/${encodeURIComponent(slugOrId)}`,
189
+ cached([productsTag(), productTag(slugOrId)]),
190
+ (body) => dataOf(body),
191
+ true
192
+ );
193
+ },
194
+ getAvailability(slugOrId) {
195
+ return send(
196
+ `/products/${encodeURIComponent(slugOrId)}/availability`,
197
+ live,
198
+ (body) => dataOf(body),
199
+ true
200
+ );
201
+ },
202
+ async getEvents(listOptions) {
203
+ const query = new URLSearchParams();
204
+ if (listOptions?.limit !== void 0) {
205
+ query.set("limit", `${listOptions.limit}`);
206
+ }
207
+ if (listOptions?.cursor) {
208
+ query.set("cursor", listOptions.cursor);
209
+ }
210
+ if (listOptions?.from) {
211
+ query.set("from", listOptions.from);
212
+ }
213
+ if (listOptions?.to) {
214
+ query.set("to", listOptions.to);
215
+ }
216
+ const result = await send(
217
+ `/events${suffix(query)}`,
218
+ cached([eventsTag()]),
219
+ (body) => listOrNothing(body, "events"),
220
+ false
221
+ );
222
+ return result;
223
+ },
224
+ getEvent(idOrSlug) {
225
+ return send(
226
+ `/events/${encodeURIComponent(idOrSlug)}`,
227
+ cached([eventsTag(), eventTag(idOrSlug)]),
228
+ (body) => dataOf(body),
229
+ true
230
+ );
231
+ },
232
+ getEventAvailability(eventId) {
233
+ return send(
234
+ `/events/${encodeURIComponent(eventId)}/availability`,
235
+ live,
236
+ (body) => body,
237
+ true
238
+ );
239
+ },
240
+ async submitOrder(input, writeOptions) {
241
+ const result = await send(
242
+ "/orders",
243
+ write(input, writeOptions),
244
+ (body) => dataOf(body),
245
+ false
246
+ );
247
+ return result;
248
+ },
249
+ async upsertContact(input, writeOptions) {
250
+ const result = await send(
251
+ "/contacts",
252
+ write(input, writeOptions),
253
+ (body) => dataOf(body),
254
+ false
255
+ );
256
+ return result;
257
+ },
258
+ async submitRequest(input, writeOptions) {
259
+ const result = await send(
260
+ "/requests",
261
+ write(input, writeOptions),
262
+ (body) => dataOf(body),
263
+ false
264
+ );
265
+ return result;
266
+ },
267
+ async submitForm(formId, input, writeOptions) {
268
+ const result = await send(
269
+ `/forms/${encodeURIComponent(formId)}/submissions`,
270
+ write(input, writeOptions),
271
+ (body) => dataOf(body),
272
+ false
273
+ );
274
+ return result;
275
+ }
276
+ };
277
+ }
278
+ function suffix(query) {
279
+ const rendered = query.toString();
280
+ return rendered ? `?${rendered}` : "";
281
+ }
282
+ function dataOf(body) {
283
+ return body?.data;
284
+ }
285
+ function listOrNothing(body, key) {
286
+ const listing = body;
287
+ return Array.isArray(listing?.[key]) ? listing : void 0;
288
+ }
289
+ function newIdempotencyKey() {
290
+ const source = globalThis.crypto;
291
+ if (typeof source?.randomUUID === "function") {
292
+ return source.randomUUID();
293
+ }
294
+ if (typeof source?.getRandomValues === "function") {
295
+ const bytes = source.getRandomValues(new Uint8Array(16));
296
+ return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
297
+ }
298
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}`;
299
+ }
300
+ function inSomethingTheUserCanRead() {
301
+ if (typeof window !== "undefined") {
302
+ return true;
303
+ }
304
+ const scope = globalThis;
305
+ return typeof scope.importScripts === "function";
306
+ }
307
+
308
+ // src/generated/api-types.ts
309
+ var ALL_API_SCOPES = [
310
+ "catalog:read",
311
+ "events:read",
312
+ "orders:write",
313
+ "contacts:write",
314
+ "forms:write"
315
+ ];
316
+ var ALL_API_WEBHOOK_EVENTS = [
317
+ "product.changed",
318
+ "product.deleted",
319
+ "event.changed",
320
+ "order.status_changed"
321
+ ];
322
+ var ALL_API_ERROR_CODES = [
323
+ "unauthorized",
324
+ "forbidden",
325
+ "feature_unavailable",
326
+ "not_found",
327
+ "invalid_request",
328
+ "idempotency_key_reused",
329
+ "request_in_progress",
330
+ "rate_limited",
331
+ "internal_error"
332
+ ];
333
+ export {
334
+ ALL_API_ERROR_CODES,
335
+ ALL_API_SCOPES,
336
+ ALL_API_WEBHOOK_EVENTS,
337
+ CrmApiError,
338
+ createClient,
339
+ eventTag,
340
+ eventsTag,
341
+ productTag,
342
+ productsTag,
343
+ tagsForDelivery
344
+ };
345
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/refusal.ts","../src/tags.ts","../src/client.ts","../src/generated/api-types.ts"],"sourcesContent":["import type { ApiErrorCode } from './generated/api-types';\n\n/**\n * A refusal from the API, with the machine-readable reason kept.\n *\n * Match on `code`, never on `message`. The code is a closed set that this\n * package generates from the server's own list; the message is written for\n * somebody reading a log and gets reworded.\n *\n * Three of the codes are deliberately vaguer than they could be, and knowing\n * that saves an afternoon:\n *\n * - `unauthorized` means the key was not accepted, and says nothing about\n * which of \"missing\", \"mistyped\", \"revoked\" or \"expired\" applies. That is on\n * purpose — the alternative is an endpoint that tells a stranger whether a\n * key they guessed exists.\n * - `forbidden` covers both \"this key does not carry that permission\" and \"this\n * is a browser-safe key and you asked it to write\", for the same reason.\n * - `not_found` is also the answer for something that exists but belongs to\n * somebody else. A shop cannot tell the two apart, and should not be able to.\n */\nexport class CrmApiError extends Error {\n readonly code: ApiErrorCode;\n readonly status: number;\n readonly fields: { path: string; message: string }[];\n /** Present when the API refused before the request reached a handler. */\n readonly requestUrl: string;\n\n constructor(input: {\n code: ApiErrorCode;\n message: string;\n status: number;\n fields?: { path: string; message: string }[];\n requestUrl: string;\n }) {\n super(input.message);\n\n this.name = 'CrmApiError';\n this.code = input.code;\n this.status = input.status;\n this.fields = input.fields ?? [];\n this.requestUrl = input.requestUrl;\n }\n\n /**\n * Whether trying the same request again could plausibly work.\n *\n * A rate limit clears and a server error may be a blip; a rejected key and a\n * malformed body will be refused just as firmly the second time. Write\n * requests should be retried with the *same* idempotency key, which this\n * client fills in for you, so a retry after a timeout cannot become a second\n * order.\n */\n get retryable() {\n return this.code === 'rate_limited' || this.code === 'internal_error';\n }\n}\n","import { CrmApiError } from './errors';\nimport type { ApiErrorCode } from './generated/api-types';\n\nexport async function readJson(response: Response) {\n try {\n return (await response.json()) as unknown;\n } catch {\n return null;\n }\n}\n\n/**\n * A refusal, whatever shape it arrived in.\n *\n * Almost everything answers `{ error: { code, message } }`, and then the code\n * is taken at its word — it is a closed set this package generates from the\n * server's own list, so a code that does not appear here means the two have\n * drifted and a build somewhere should already have gone red.\n *\n * The events listing is the exception: it answers `{ error: \"...\" }`, because\n * it predates the shared error helper and is read by script tags on sites\n * nobody here can redeploy. For that one the status code is all there is, and\n * the one place it cannot be precise is 403, where \"your key lacks this scope\"\n * and \"that module is switched off for this account\" share a number. It is\n * reported as `forbidden`, the one of the two a developer can act on.\n */\nexport function refusalOf(status: number, body: unknown, requestUrl: string) {\n const structured = (\n body as { error?: { code?: string; message?: string; fields?: unknown } }\n )?.error;\n\n if (structured && typeof structured === 'object' && structured.code) {\n return new CrmApiError({\n code: structured.code as ApiErrorCode,\n message: structured.message ?? structured.code,\n status,\n fields: (structured.fields ?? []) as { path: string; message: string }[],\n requestUrl,\n });\n }\n\n const plain = (body as { error?: unknown } | null)?.error;\n\n return new CrmApiError({\n code: codeForStatus(status),\n message:\n typeof plain === 'string' ? plain : `The request failed with ${status}.`,\n status,\n requestUrl,\n });\n}\n\nfunction codeForStatus(status: number): ApiErrorCode {\n switch (status) {\n case 400:\n return 'invalid_request';\n case 401:\n return 'unauthorized';\n case 403:\n return 'forbidden';\n case 404:\n return 'not_found';\n case 409:\n return 'request_in_progress';\n case 429:\n return 'rate_limited';\n default:\n return 'internal_error';\n }\n}\n","/**\n * The names a cached answer is filed under, so a webhook can throw it away.\n *\n * This is the part of the package that actually does something. A `getProducts`\n * that only fetches is three lines anybody writes themselves; a `getProducts`\n * whose answer sits under a name the shipped revalidation route already knows\n * how to clear is the piece that makes the connection work at all.\n *\n * **Every read carries the collection tag, detail reads included**, and that is\n * not sloppiness. A delivery names ids and nothing else — on purpose, so a\n * late message still leads to the right state and no message can hand out data\n * the receiving key could not have read. But a product page is usually fetched\n * by *slug*, and nothing on this side can turn an id into the slug somebody\n * asked for. So the per-item tag is a narrower handle for a site that fetches\n * by id and for its own manual invalidations, and the collection tag is the one\n * that guarantees a slug-fetched page ever updates.\n */\nconst PREFIX = 'crm';\n\n/** Everything that lists products. Cleared by every catalogue delivery. */\nexport function productsTag() {\n return `${PREFIX}:products`;\n}\n\n/** One product, by whichever key it was fetched with. */\nexport function productTag(slugOrId: string) {\n return `${PREFIX}:product:${slugOrId}`;\n}\n\nexport function eventsTag() {\n return `${PREFIX}:events`;\n}\n\nexport function eventTag(idOrSlug: string) {\n return `${PREFIX}:event:${idOrSlug}`;\n}\n\n/**\n * What a delivery of this type should clear.\n *\n * `truncated` means the batch stopped collecting and `ids` is incomplete, so\n * the only honest answer is the whole collection — which the collection tag\n * already is. The per-item tags are added on top when the list can be trusted.\n */\nexport function tagsForDelivery(input: {\n type: string;\n ids: readonly string[];\n truncated: boolean;\n}) {\n const collection = input.type.startsWith('product.')\n ? productsTag()\n : input.type.startsWith('event.')\n ? eventsTag()\n : null;\n\n if (collection === null) {\n return [];\n }\n\n if (input.truncated) {\n return [collection];\n }\n\n const item = input.type.startsWith('product.') ? productTag : eventTag;\n\n return [collection, ...input.ids.map((id) => item(id))];\n}\n","import { CrmApiError } from './errors';\nimport { readJson, refusalOf } from './refusal';\nimport { eventTag, eventsTag, productTag, productsTag } from './tags';\nimport type {\n ContactInput,\n ContactResult,\n EventAvailability,\n EventDetail,\n EventList,\n FormSubmissionInput,\n FormSubmissionResult,\n OrderInput,\n OrderResult,\n ProductDetail,\n ProductList,\n RequestInput,\n RequestResult,\n StockStatus,\n} from './types';\n\nconst KEY_FORM = /^crm[ps]_[A-Za-z0-9_-]{43}$/;\nconst SECRET_PREFIX = 'crms_';\nconst BASE_PATH = '/api/public/v1';\nconst DEFAULT_REVALIDATE = 60;\n\n/**\n * The fetch options this package sets that are not in the web standard.\n *\n * `next` is read by Next.js and ignored by every other runtime, which is\n * exactly the behaviour wanted: the tags do nothing outside a framework that\n * caches, and nothing breaks in one that does not.\n */\ntype CachedInit = RequestInit & {\n next?: { tags?: string[]; revalidate?: number | false };\n};\n\nexport type FetchLike = (input: string, init?: CachedInit) => Promise<Response>;\n\nexport interface CrmClientOptions {\n /**\n * The key, whole.\n *\n * A `crmp_` key may be read by anyone who views source and may only read; a\n * `crms_` key may write and must never leave a server. This client refuses a\n * `crms_` key outright when it finds itself in a browser — see the note on\n * `createClient`.\n */\n apiKey: string;\n /**\n * Where the CRM lives: an origin, with no path on it.\n *\n * `https://app.example.com`, not `https://app.example.com/api/public/v1`.\n * The version lives in this package so that a shop upgrading the package is\n * the thing that moves it, rather than a string in someone's environment\n * file that nobody remembers to change.\n */\n baseUrl: string;\n /**\n * How long a cached answer may be served before it is refetched, in seconds.\n *\n * Defaults to a minute. `false` means never on a timer — only when a webhook\n * clears the tag. That is the right setting for a shop whose revalidation\n * route is wired up and reachable, and the wrong one for a shop where it is\n * not, because then nothing ever expires at all.\n */\n revalidate?: number | false;\n /** For tests and for runtimes with their own instrumented fetch. */\n fetch?: FetchLike;\n}\n\nexport interface ListOptions {\n limit?: number;\n cursor?: string | null;\n}\n\nexport interface ProductListOptions extends ListOptions {\n /** A category id. Passing something that is not a uuid is refused, loudly. */\n category?: string;\n}\n\nexport interface EventListOptions extends ListOptions {\n /** ISO timestamps. Both ends are optional and both are inclusive. */\n from?: string;\n to?: string;\n}\n\nexport interface WriteOptions {\n /**\n * The key that makes a retry safe.\n *\n * Generated per call when you leave it out, which is right for a first\n * attempt and wrong for a retry: a website that timed out does not know\n * whether the order arrived, and sending it again under a *new* key is how\n * one customer gets charged twice. Keep the key you used and resend with it.\n */\n idempotencyKey?: string;\n}\n\nexport interface CrmClient {\n getProducts(options?: ProductListOptions): Promise<ProductList>;\n getProduct(slugOrId: string): Promise<ProductDetail | null>;\n /** Live. Never cached, at any layer. */\n getAvailability(slugOrId: string): Promise<StockStatus | null>;\n getEvents(options?: EventListOptions): Promise<EventList>;\n getEvent(idOrSlug: string): Promise<EventDetail | null>;\n /** Live. Never cached, at any layer. */\n getEventAvailability(eventId: string): Promise<EventAvailability | null>;\n submitOrder(input: OrderInput, options?: WriteOptions): Promise<OrderResult>;\n upsertContact(\n input: ContactInput,\n options?: WriteOptions,\n ): Promise<ContactResult>;\n submitRequest(\n input: RequestInput,\n options?: WriteOptions,\n ): Promise<RequestResult>;\n submitForm(\n formId: string,\n input: FormSubmissionInput,\n options?: WriteOptions,\n ): Promise<FormSubmissionResult>;\n}\n\n/**\n * A client for one account's public API.\n *\n * Two layers, and the names are the whole warning. `getProducts` and\n * `getProduct` are cached at the edge and filed under tags the shipped\n * revalidation route knows how to clear. `getAvailability` is not cached\n * anywhere and never will be — a stock figure in a cached answer is the number\n * that lies first and lies worst, and a developer who reaches for `getProduct`\n * to show \"in stock\" gets no warning from the type system, so the split has to\n * be in the name.\n *\n * The browser check is not decoration either. A `crms_` key can create orders\n * and read contacts; pasted into a client component it ends up in a JavaScript\n * bundle that anybody can open. Throwing at construction turns that into a\n * build-time failure on the first render instead of a quiet leak.\n */\nexport function createClient(options: CrmClientOptions): CrmClient {\n const apiKey = options.apiKey?.trim() ?? '';\n\n if (!KEY_FORM.test(apiKey)) {\n throw new Error(\n 'crm-client: the API key is not in the expected form. It starts with ' +\n '`crmp_` or `crms_` and is 48 characters long — a truncated paste ' +\n 'looks exactly like a revoked key once it reaches the server.',\n );\n }\n\n if (apiKey.startsWith(SECRET_PREFIX) && inSomethingTheUserCanRead()) {\n throw new Error(\n 'crm-client: a `crms_` key may not be used in a browser. It can write ' +\n 'orders and read contacts, and anything a browser holds is public — ' +\n 'including a service worker or a web worker, which is why this is ' +\n 'refused there too. Read from a server component or a route handler, ' +\n 'or issue a `crmp_` key for the parts of the catalogue a page reads ' +\n 'directly.',\n );\n }\n\n const base = options.baseUrl?.replace(/\\/+$/, '') ?? '';\n\n if (!/^https?:\\/\\/[^/]+$/.test(base)) {\n throw new Error(\n `crm-client: baseUrl should be an origin and nothing more, such as ` +\n `\"https://app.example.com\". Received \"${options.baseUrl}\".`,\n );\n }\n\n const doFetch: FetchLike =\n options.fetch ?? ((input, init) => fetch(input, init));\n const revalidate = options.revalidate ?? DEFAULT_REVALIDATE;\n\n async function send<T>(\n path: string,\n init: CachedInit,\n unwrap: (body: unknown) => T,\n notFoundIsNull: boolean,\n ): Promise<T | null> {\n const url = `${base}${BASE_PATH}${path}`;\n\n const response = await doFetch(url, {\n ...init,\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${apiKey}`,\n ...init.headers,\n },\n });\n\n const body = await readJson(response);\n\n if (!response.ok) {\n const refusal = refusalOf(response.status, body, url);\n\n if (notFoundIsNull && refusal.code === 'not_found') {\n return null;\n }\n\n throw refusal;\n }\n\n const answer = unwrap(body);\n\n /*\n * A 2xx whose body is not the shape this method promised. It happens: a\n * proxy that answers its own page, a CDN error dressed as a 200, a version\n * of this package older than the route it is talking to.\n *\n * Without this the value is cast and handed back typed as an answer, and\n * the failure surfaces as `TypeError: cannot read properties of undefined`\n * three frames away in somebody's shop -- the one kind of error this\n * package's whole design is meant to avoid producing.\n */\n if (answer === undefined || answer === null) {\n throw new CrmApiError({\n code: 'internal_error',\n message:\n 'The API answered successfully with a body this client did not ' +\n 'recognise. Either something between here and it replaced the ' +\n 'answer, or this package is older than the route it called.',\n status: response.status,\n requestUrl: url,\n });\n }\n\n return answer;\n }\n\n function cached(tags: string[]): CachedInit {\n return { method: 'GET', next: { tags, revalidate } };\n }\n\n const live: CachedInit = { method: 'GET', cache: 'no-store' };\n\n function write(body: unknown, options?: WriteOptions): CachedInit {\n return {\n method: 'POST',\n cache: 'no-store',\n headers: {\n 'content-type': 'application/json',\n 'idempotency-key': options?.idempotencyKey ?? newIdempotencyKey(),\n },\n body: JSON.stringify(body),\n };\n }\n\n return {\n async getProducts(listOptions) {\n const query = new URLSearchParams();\n\n if (listOptions?.limit !== undefined) {\n query.set('limit', `${listOptions.limit}`);\n }\n\n if (listOptions?.cursor) {\n query.set('cursor', listOptions.cursor);\n }\n\n if (listOptions?.category) {\n query.set('category', listOptions.category);\n }\n\n const result = await send(\n `/products${suffix(query)}`,\n cached([productsTag()]),\n (body) => listOrNothing(body, 'data') as ProductList | undefined,\n false,\n );\n\n return result as ProductList;\n },\n\n getProduct(slugOrId) {\n return send(\n `/products/${encodeURIComponent(slugOrId)}`,\n cached([productsTag(), productTag(slugOrId)]),\n (body) => dataOf(body) as ProductDetail,\n true,\n );\n },\n\n getAvailability(slugOrId) {\n return send(\n `/products/${encodeURIComponent(slugOrId)}/availability`,\n live,\n (body) => dataOf(body) as StockStatus,\n true,\n );\n },\n\n async getEvents(listOptions) {\n const query = new URLSearchParams();\n\n if (listOptions?.limit !== undefined) {\n query.set('limit', `${listOptions.limit}`);\n }\n\n if (listOptions?.cursor) {\n query.set('cursor', listOptions.cursor);\n }\n\n if (listOptions?.from) {\n query.set('from', listOptions.from);\n }\n\n if (listOptions?.to) {\n query.set('to', listOptions.to);\n }\n\n const result = await send(\n `/events${suffix(query)}`,\n cached([eventsTag()]),\n (body) => listOrNothing(body, 'events') as EventList | undefined,\n false,\n );\n\n return result as EventList;\n },\n\n getEvent(idOrSlug) {\n return send(\n `/events/${encodeURIComponent(idOrSlug)}`,\n cached([eventsTag(), eventTag(idOrSlug)]),\n (body) => dataOf(body) as EventDetail,\n true,\n );\n },\n\n getEventAvailability(eventId) {\n return send(\n `/events/${encodeURIComponent(eventId)}/availability`,\n live,\n (body) => body as EventAvailability,\n true,\n );\n },\n\n async submitOrder(input, writeOptions) {\n const result = await send(\n '/orders',\n write(input, writeOptions),\n (body) => dataOf(body) as OrderResult,\n false,\n );\n\n return result as OrderResult;\n },\n\n async upsertContact(input, writeOptions) {\n const result = await send(\n '/contacts',\n write(input, writeOptions),\n (body) => dataOf(body) as ContactResult,\n false,\n );\n\n return result as ContactResult;\n },\n\n async submitRequest(input, writeOptions) {\n const result = await send(\n '/requests',\n write(input, writeOptions),\n (body) => dataOf(body) as RequestResult,\n false,\n );\n\n return result as RequestResult;\n },\n\n async submitForm(formId, input, writeOptions) {\n const result = await send(\n `/forms/${encodeURIComponent(formId)}/submissions`,\n write(input, writeOptions),\n (body) => dataOf(body) as FormSubmissionResult,\n false,\n );\n\n return result as FormSubmissionResult;\n },\n };\n}\n\nfunction suffix(query: URLSearchParams) {\n const rendered = query.toString();\n\n return rendered ? `?${rendered}` : '';\n}\n\nfunction dataOf(body: unknown) {\n return (body as { data: unknown } | null)?.data;\n}\n\n/**\n * A listing, or nothing at all when the body is not one.\n *\n * `undefined` is the signal `send` turns into a legible error. Checking for the\n * array rather than merely for the key matters: a proxy answering its own page,\n * or a CDN dressing an error as a 200, produces a body with neither — and\n * without this the caller receives it typed as a page of products and finds out\n * when `.map` is not a function, somewhere else entirely.\n *\n * The key differs per listing because the events one answers `events` rather\n * than `data`, which it has done since before this convention existed.\n */\nfunction listOrNothing(body: unknown, key: 'data' | 'events') {\n const listing = body as Record<string, unknown> | null;\n\n return Array.isArray(listing?.[key]) ? listing : undefined;\n}\n\n/**\n * A fresh key for a first attempt.\n *\n * Deliberately not derived from the body. Two identical orders placed a minute\n * apart by the same customer are two orders, and a fingerprint would quietly\n * collapse them into one.\n *\n * `crypto.randomUUID` is not everywhere it looks like it is: browsers expose it\n * only in a secure context, so a shop still served over plain http has\n * `crypto` and not that method. Reaching for it unguarded turns every write\n * into `TypeError: crypto.randomUUID is not a function`, thrown while building\n * the headers, before any request — which tells the shop's developer nothing\n * about what is actually missing.\n *\n * The weakest fallback is acceptable *here specifically*, and nowhere else in\n * this package: an idempotency key needs to be unique within one account for a\n * day. It is not a credential, it guesses nothing and guards nothing; a caller\n * who wants a stronger one passes their own.\n */\nfunction newIdempotencyKey() {\n const source = globalThis.crypto;\n\n if (typeof source?.randomUUID === 'function') {\n return source.randomUUID();\n }\n\n if (typeof source?.getRandomValues === 'function') {\n const bytes = source.getRandomValues(new Uint8Array(16));\n\n return [...bytes]\n .map((byte) => byte.toString(16).padStart(2, '0'))\n .join('');\n }\n\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}`;\n}\n\n/**\n * Whether this is somewhere an end user could read what the code holds.\n *\n * `typeof window !== 'undefined'` is the usual test and it is not enough: a\n * service worker and a web worker both have no `window` and are both JavaScript\n * delivered to a browser, openable in devtools like any other script. Treating\n * \"no window\" as \"a trusted server\" is exactly the assumption that lets a\n * writing key into an offline bundle.\n */\nfunction inSomethingTheUserCanRead() {\n if (typeof window !== 'undefined') {\n return true;\n }\n\n const scope = globalThis as { importScripts?: unknown; self?: unknown };\n\n return typeof scope.importScripts === 'function';\n}\n","// GENERATED FILE -- edit the projections, not this.\n//\n// Every declaration below was lifted, comments and all, out of the services\n// that build the public API's answers. Run `pnpm --filter @solumflow-app/crm-client\n// generate:types` after changing one of them; a test in this package fails\n// when this file and those sources disagree.\n//\n// Sources:\n// packages/features/crm-invoicing/src/shared/public-product-item.ts\n// packages/features/crm-events/src/shared/public-event-list-item.ts\n// packages/features/crm-events/src/shared/public-event-detail-item.ts\n// packages/api-keys/src/webhook-events.ts\n// packages/api-keys/src/scopes.ts\n// apps/web/app/api/public/_lib/errors.ts\n\n/** What a key is allowed to reach. A key carries one or more of these. */\nexport type ApiScope =\n | 'catalog:read'\n | 'events:read'\n | 'orders:write'\n | 'contacts:write'\n | 'forms:write';\n\nexport const ALL_API_SCOPES: readonly ApiScope[] = [\n 'catalog:read',\n 'events:read',\n 'orders:write',\n 'contacts:write',\n 'forms:write',\n];\n\n/** Everything an endpoint can be told about. A delivery names one. */\nexport type ApiWebhookEvent =\n | 'product.changed'\n | 'product.deleted'\n | 'event.changed'\n | 'order.status_changed';\n\nexport const ALL_API_WEBHOOK_EVENTS: readonly ApiWebhookEvent[] = [\n 'product.changed',\n 'product.deleted',\n 'event.changed',\n 'order.status_changed',\n];\n\n/** The `error.code` of a refusal. Match on this, never on the message -- the message is written for a person reading a log and may be reworded. */\nexport type ApiErrorCode =\n | 'unauthorized'\n | 'forbidden'\n | 'feature_unavailable'\n | 'not_found'\n | 'invalid_request'\n | 'idempotency_key_reused'\n | 'request_in_progress'\n | 'rate_limited'\n | 'internal_error';\n\nexport const ALL_API_ERROR_CODES: readonly ApiErrorCode[] = [\n 'unauthorized',\n 'forbidden',\n 'feature_unavailable',\n 'not_found',\n 'invalid_request',\n 'idempotency_key_reused',\n 'request_in_progress',\n 'rate_limited',\n 'internal_error',\n];\n\n/**\n * One product as a stranger's website sees it.\n *\n * These interfaces are the public API's contract, not internal convenience\n * types. They are served cross-origin to pages nobody here controls, and a\n * customer's shop reads these exact keys — so a field removed or renamed breaks\n * a site that will not be redeployed. Add fields; do not repurpose them.\n *\n * What is deliberately absent is as much the contract as what is here.\n *\n * - **`sku`** is the number this business uses to find the thing in its own\n * stockroom. It says how many suppliers there are, which one a line came\n * from, and often what was paid. `gtin` is the number printed on the box and\n * is offered instead, in the detail: that one is meant to be public, and a\n * shop needs it for a product feed.\n * - **The stock count** never leaves. `inStock` answers the only question a\n * visitor has, and the number itself is a business fact a competitor would\n * like and a cached answer would get wrong within the minute.\n * - **`unit_price_cents` raw** is not it either; see `priceFromCents`.\n * - **Non-public custom fields.** `fields` carries only attributes a member\n * marked `is_public`, which defaults to off. A field called \"purchase price\"\n * stays home unless somebody says otherwise, once, per field.\n *\n * And one field that an event has and a product does not: **`url`**. An event\n * is sold on a page this system hosts, so its listing can hand out a link. A\n * product is sold on the customer's own site — that is the entire reason this\n * API exists — and this system does not know what they called their product\n * page. A guessed link is worse than none.\n */\nexport interface PublicProductListItem {\n id: string;\n /** Always present: a product without an address cannot be published. */\n slug: string;\n name: string;\n productType: 'digital' | 'physical';\n /**\n * The storage path, not a URL. The route turns it into a public URL, because\n * only the route knows the origin its own storage is served from.\n */\n imagePath: string | null;\n /**\n * The cheapest way in, over the active price options — not the default one.\n * A card that says \"from €12\" next to a product whose entry price is €12 and\n * whose default is the €40 yearly plan is answering the question the visitor\n * actually asked.\n *\n * Never null, unlike the same field on an event. An event without a ticket\n * tier on sale genuinely has no price; a product always has `unit_price_cents`\n * on its own row, so when no separate price option is active that column is\n * the answer — including when it is `0`, which is a real price for something\n * given away.\n */\n priceFromCents: number;\n /** The struck-through price beside it, when the seller set one. */\n compareAtCents: number | null;\n currency: string;\n /**\n * Whether a visitor can buy it right now — never how many are left.\n *\n * True whenever the seller does not track stock at all, which is the common\n * case and the honest answer for anything made to order. When they do track\n * it, this counts what is on the shelf minus what other people already hold\n * in a checkout, and it stays true for a seller who accepts backorders.\n */\n inStock: boolean;\n /**\n * The custom fields this account invented, keyed by the slug they chose, and\n * only the ones marked public. An empty object when none are.\n */\n fields: Record<string, unknown>;\n}\n\n/**\n * The same product on its own page, where more may be shown.\n *\n * The detail answers for things the listing hides — an unlisted product, one\n * reachable by anyone holding the link — for the same reason the events detail\n * answers for a cancelled evening: somebody was sent here on purpose, and a 404\n * would be a lie. What it still refuses is anything unpublished.\n */\nexport interface PublicProductDetail extends PublicProductListItem {\n /** The one-line text that lands on a quote or an invoice row. */\n description: string | null;\n /** The long story, as sanitised HTML. Written by the seller, for this page. */\n longDescription: string | null;\n /** The barcode number: printed on the box, wanted by every product feed. */\n gtin: string | null;\n metaTitle: string | null;\n metaDescription: string | null;\n images: PublicProductImage[];\n prices: PublicProductPrice[];\n categories: PublicProductCategory[];\n /** What the seller linked this to — a companion, a refill, a bigger model. */\n related: PublicProductSummary[];\n}\n\nexport interface PublicProductImage {\n /** A path, turned into a URL by the route. Same reason as `imagePath`. */\n path: string;\n alt: string | null;\n}\n\n/**\n * One way to buy the thing.\n *\n * `stripe_price_id` and `stripe_account_id` are absent and must stay absent:\n * they name objects in the seller's payment account, and a checkout is started\n * through this system's own order route, never by a stranger quoting an id.\n */\nexport interface PublicProductPrice {\n id: string;\n kind: 'one_off' | 'recurring' | 'installment';\n label: string | null;\n amountCents: number;\n /** What the whole thing costs when paid in parts; null for the other kinds. */\n totalCents: number | null;\n installmentCount: number | null;\n recurringInterval: 'day' | 'week' | 'month' | 'year' | null;\n recurringIntervalCount: number | null;\n compareAtCents: number | null;\n trialPeriodDays: number | null;\n isDefault: boolean;\n}\n\nexport interface PublicProductCategory {\n id: string;\n name: string;\n}\n\nexport interface PublicProductSummary {\n id: string;\n slug: string;\n name: string;\n}\n\n/**\n * One event as a stranger's website sees it.\n *\n * This interface is the public API's contract, not an internal convenience\n * type. It is served cross-origin to pages nobody here controls, and a widget\n * on a customer's homepage reads these exact keys — so a field removed or\n * renamed breaks a site that will not be redeployed. Add fields; do not\n * repurpose them.\n *\n * What is deliberately absent is as much the contract as what is here: no\n * description (a paragraph no card renders), no joining link (that is\n * admission), no counts of who bought what, and nothing an organiser uses to\n * manage the event.\n */\nexport interface PublicEventListItem {\n id: string;\n slug: string;\n title: string;\n subtitle: string | null;\n startsAt: string;\n endsAt: string | null;\n timezone: string;\n locationName: string | null;\n /**\n * The storage path, not a URL. The route turns it into a public URL, because\n * only the route knows the origin its own storage is served from.\n */\n coverImagePath: string | null;\n /** Null when nothing is on sale — never 0, which is a real price. */\n priceFromCents: number | null;\n currency: string | null;\n soldOut: boolean;\n /** Null when the event has no ceiling to count down from. */\n seatsLeft: number | null;\n /** Path on the hosted site, so a widget can link straight to checkout. */\n url: string;\n /**\n * The custom fields this account invented, keyed by the slug they chose, and\n * only the ones marked public. An empty object when none are, which is every\n * account until somebody turns one on.\n *\n * Added rather than repurposed, which is the rule this interface states at\n * the top: a widget written before this field existed keeps working, because\n * it simply never reads the key.\n */\n fields: Record<string, unknown>;\n}\n\n/**\n * One event as the public API hands it out, on its own page.\n *\n * Not the same thing as `PublicEventView`, which feeds the detail page this\n * system hosts — and the difference is the reason this type exists rather than\n * the other one being reused. That view carries `online_url`: the link you join\n * the evening by. On a page we host, behind a ticket, that is correct. Through\n * an API that answers any holder of a read key, it is a free seat.\n *\n * It also carries `account_id`, which is ours and not the caller's business.\n *\n * So this is a separate, narrower projection, and everything in\n * `PublicEventListItem` about adding rather than repurposing fields applies\n * here too.\n */\nexport interface PublicEventDetailItem extends PublicEventListItem {\n /** The organiser's own text for the evening. */\n description: string | null;\n /**\n * `on_sale`, `closed` or `cancelled` — never `draft`, which does not answer\n * at all. A ticket holder has to be able to read that the evening is off, so\n * this route replies for the last two where the listing withholds them.\n */\n status: 'on_sale' | 'closed' | 'cancelled';\n doorsOpenAt: string | null;\n salesStartAt: string | null;\n salesEndAt: string | null;\n /** The postal address, as the organiser entered it. */\n address: unknown;\n metaTitle: string | null;\n metaDescription: string | null;\n ticketTypes: PublicEventTicketType[];\n /** Custom fields the account marked public, keyed by their own slug. */\n fields: Record<string, unknown>;\n}\n\n/**\n * One way in, as a stranger's page may show it.\n *\n * `reserved_count` and `sold_count` are absent: how many were sold is the\n * organiser's figure, and `seatsLeft` already answers the only question a\n * visitor has. The capacity behind it stays home for the same reason a stock\n * count does.\n */\nexport interface PublicEventTicketType {\n id: string;\n name: string;\n description: string | null;\n priceCents: number;\n currency: string;\n taxPercentage: number | null;\n minPerOrder: number | null;\n maxPerOrder: number | null;\n salesStartAt: string | null;\n salesEndAt: string | null;\n /** Null when this tier has no ceiling to count down from. */\n seatsLeft: number | null;\n soldOut: boolean;\n onSale: boolean;\n}\n\n/**\n * The body of one delivery.\n *\n * IDS AND NOTHING ELSE, and the three reasons are worth keeping together. A\n * message that arrives late still leads to the right state, because the\n * receiver fetches the current row rather than trusting a snapshot from twenty\n * minutes ago. A message can never hand out something the receiving key was\n * not allowed to read, because it hands out nothing. And five hundred changes\n * fit in a body of a few kilobytes instead of a few megabytes.\n *\n * `truncated` means the batch stopped collecting at its ceiling and `ids` is\n * therefore incomplete: the receiver should refetch the whole collection. It\n * is always present rather than only when true, so a receiver that reads it\n * cannot mistake \"absent\" for \"false\" in the one direction that matters.\n */\nexport interface ApiWebhookPayload {\n type: ApiWebhookEvent;\n occurredAt: string;\n ids: string[];\n truncated: boolean;\n}\n"],"mappings":"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","names":["options"]}