@tesseraloyalty/sdk 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.
- package/LICENSE +21 -0
- package/README.md +238 -0
- package/dist/index.cjs +1210 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1136 -0
- package/dist/index.d.ts +1136 -0
- package/dist/index.js +1187 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/http.ts","../src/internal/camel.ts","../src/resources/rewards.ts","../src/internal/scope.ts","../src/internal/serialize.ts","../src/resources/members.ts","../src/resources/customers.ts","../src/resources/redemptions.ts","../src/signing.ts","../src/resources/events.ts","../src/client.ts","../src/index.ts"],"sourcesContent":["/**\n * The SDK error taxonomy. One structured base class (`TesseraError`) carrying the\n * stable, code-first envelope shape (`apps/api/src/errors.ts`), plus a typed subclass\n * per stable wire `code` so a consumer can `catch` by TYPE — not by string-matching a\n * `code` — and read `err.requestId` for support without ever parsing an HTTP body.\n *\n * The class identity comes from the snake_case `code`; the HTTP `status` flows from the\n * response (never hard-coded here). `CODE_TO_ERROR` maps each `code` to its subclass and\n * `errorFromEnvelope` constructs the right one. An UNKNOWN code yields the base\n * `TesseraError` (forward-compat: a new server code never crashes an old SDK), so the\n * factory NEVER throws on an unrecognized code.\n */\n\nexport interface TesseraErrorInit {\n /** The stable, snake_case error `code` (from the API envelope, or an SDK-local code). */\n code: string;\n /** Human-readable detail. Never branch on this — branch on `code` / the error type. */\n message: string;\n /** The `X-Request-Id` correlating this failure, for support. Absent pre-network. */\n requestId?: string;\n /** The HTTP status, when the error came from a response. Absent for pre-network errors. */\n status?: number;\n /** Structured, code-specific detail from the envelope (e.g. a redemption shortfall). */\n details?: unknown;\n}\n\nexport class TesseraError extends Error {\n /** The stable, snake_case error `code`; the contract consumers branch on. */\n readonly code: string;\n /** The `X-Request-Id` of the failing request, when available. */\n readonly requestId?: string;\n /** The HTTP status, when this error originated from a response. */\n readonly status?: number;\n /** Structured, code-specific detail carried from the envelope `details`. */\n readonly details?: unknown;\n\n constructor(init: TesseraErrorInit) {\n super(init.message);\n // `new.target` is the concrete subclass being constructed, so every subclass gets its\n // own `name` (e.g. `InsufficientBalanceError`) with no per-class boilerplate; for a\n // direct `new TesseraError(...)` it is `'TesseraError'`.\n this.name = new.target.name;\n this.code = init.code;\n this.requestId = init.requestId;\n this.status = init.status;\n this.details = init.details;\n // Preserve the prototype chain so `instanceof` holds for the concrete subclass across\n // bundling/transpilation (down-level class targets otherwise break `instanceof`).\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Read a field off the envelope `details` record, tolerantly (never throws; `undefined` if absent). */\nfunction detailValue(details: unknown, key: string): unknown {\n if (details && typeof details === 'object' && key in details) {\n return (details as Record<string, unknown>)[key];\n }\n return undefined;\n}\n\n// ── 400 ──────────────────────────────────────────────────────────────────────────────────────\n\n/** `invalid_request` (400) — a malformed request body / query the API rejected at its boundary. */\nexport class ValidationError extends TesseraError {}\n\n// ── 401 ──────────────────────────────────────────────────────────────────────────────────────\n\n/** `unauthorized` (401) — missing, unknown, or revoked API key. */\nexport class UnauthorizedError extends TesseraError {}\n\n/** `signature_invalid` (401) — an event body's HMAC signature is absent or did not verify. */\nexport class SignatureInvalidError extends TesseraError {}\n\n/** `replay_detected` (401) — an event timestamp is outside the skew window, or its nonce was reused. */\nexport class ReplayDetectedError extends TesseraError {}\n\n// ── 403 ──────────────────────────────────────────────────────────────────────────────────────\n\n/** `forbidden` (403) — the authenticated key lacks authority for this operation. */\nexport class ForbiddenError extends TesseraError {}\n\n/** `tier_gated` (403) — the redemption option requires a higher tier than the customer holds. */\nexport class TierGatedError extends TesseraError {}\n\n/** `publishable_key_forbidden` (403) — a publishable key was used on a secret-key-only endpoint. */\nexport class PublishableKeyForbiddenError extends TesseraError {}\n\n// ── 404 ──────────────────────────────────────────────────────────────────────────────────────\n\n/** `not_found` (404) — the requested resource does not exist within the caller's tenant. */\nexport class NotFoundError extends TesseraError {}\n\n/** `customer_not_found` (404) — no such customer in this tenant (no cross-tenant existence leak). */\nexport class CustomerNotFoundError extends TesseraError {}\n\n/** `redemption_not_found` (404) — no such redemption in this tenant. */\nexport class RedemptionNotFoundError extends TesseraError {}\n\n// ── 409 ──────────────────────────────────────────────────────────────────────────────────────\n\n/** `conflict` (409) — a generic state conflict. */\nexport class ConflictError extends TesseraError {}\n\n/**\n * `idempotency_key_conflict` / `idempotency_key_reuse` (409) — an idempotency key was reused with a\n * DIFFERENT payload. Both wire codes (the events-endpoint-specific `..._conflict` and the\n * platform-wide `..._reuse`) surface as this one type.\n */\nexport class IdempotencyConflictError extends TesseraError {}\n\n/** `version_conflict` (409) — an optimistic-concurrency stale write; re-read and retry. */\nexport class VersionConflictError extends TesseraError {}\n\n/** `cap_reached` (409) — the redemption limit for this option has been reached. */\nexport class CapReachedError extends TesseraError {}\n\n/** `not_stackable` (409) — a non-stackable option conflicts with an existing active hold. */\nexport class NotStackableError extends TesseraError {}\n\n/** `hold_already_released` (409) — finalizing a redemption hold that is already released (terminal). */\nexport class HoldAlreadyReleasedError extends TesseraError {}\n\n// ── 410 ──────────────────────────────────────────────────────────────────────────────────────\n\n/** `customer_erased` (410) — the customer's PII has been erased (GDPR); the profile is gone. */\nexport class CustomerErasedError extends TesseraError {}\n\n// ── 413 ──────────────────────────────────────────────────────────────────────────────────────\n\n/** `payload_too_large` (413) — the request body exceeded the API's size limit. */\nexport class PayloadTooLargeError extends TesseraError {}\n\n// ── 422 ──────────────────────────────────────────────────────────────────────────────────────\n\n/**\n * `insufficient_balance` (422) — the customer's `available` was below the redemption cost, so no hold\n * was placed. `shortfall` is how far `available` fell short (read from the envelope `details`).\n */\nexport class InsufficientBalanceError extends TesseraError {\n /** How far `available` fell short of the cost, from `details.shortfall`; `undefined` if absent. */\n get shortfall(): number | undefined {\n const value = detailValue(this.details, 'shortfall');\n return typeof value === 'number' ? value : undefined;\n }\n}\n\n/** `option_not_eligible` (422) — the option exists but a non-tier/non-cap eligibility rule failed. */\nexport class OptionNotEligibleError extends TesseraError {}\n\n/** `invalid_payload` (422) — a well-formed event body that failed the purchase-event contract. */\nexport class InvalidPayloadError extends TesseraError {}\n\n/** `unsupported_event_type` (422) — an event type not supported in this phase. */\nexport class UnsupportedEventTypeError extends TesseraError {}\n\n/**\n * `profile_validation_error` (422) — a semantic profile-field validation failure (email/birthday/\n * locale). `field` names the offending field (read from the envelope `details`).\n */\nexport class ProfileValidationError extends TesseraError {\n /** The offending profile field, from `details.field`; `undefined` if absent. */\n get field(): string | undefined {\n const value = detailValue(this.details, 'field');\n return typeof value === 'string' ? value : undefined;\n }\n}\n\n// ── 429 ──────────────────────────────────────────────────────────────────────────────────────\n\n/** `rate_limited` (429) — the tenant exceeded its rate limit; honor `Retry-After` and back off. */\nexport class RateLimitedError extends TesseraError {}\n\n// ── 500 / 501 ─────────────────────────────────────────────────────────────────────────────────\n\n/** `internal_error` (500) — an opaque server error. */\nexport class InternalError extends TesseraError {}\n\n/** `not_implemented` (501) — a valid-vocabulary option that is not implemented this phase. */\nexport class NotImplementedError extends TesseraError {}\n\n/** The shape of an error-envelope subclass constructor. */\ntype TesseraErrorClass = new (init: TesseraErrorInit) => TesseraError;\n\n/**\n * The authoritative map from the operations API's stable snake_case wire `code` to the SDK error\n * subclass a consumer catches. Keyed by `code` (NOT status) — status flows from the response. The\n * contract-test suite (§8) asserts this covers every `code` the API can emit, so the server cannot\n * add a code the SDK silently downgrades to the base error.\n */\nexport const CODE_TO_ERROR: Record<string, TesseraErrorClass> = {\n // 400\n invalid_request: ValidationError,\n // 401\n unauthorized: UnauthorizedError,\n signature_invalid: SignatureInvalidError,\n replay_detected: ReplayDetectedError,\n // 403\n forbidden: ForbiddenError,\n tier_gated: TierGatedError,\n publishable_key_forbidden: PublishableKeyForbiddenError,\n // 404\n not_found: NotFoundError,\n customer_not_found: CustomerNotFoundError,\n redemption_not_found: RedemptionNotFoundError,\n // 409\n conflict: ConflictError,\n idempotency_key_conflict: IdempotencyConflictError,\n idempotency_key_reuse: IdempotencyConflictError,\n version_conflict: VersionConflictError,\n cap_reached: CapReachedError,\n not_stackable: NotStackableError,\n hold_already_released: HoldAlreadyReleasedError,\n // 410\n customer_erased: CustomerErasedError,\n // 413\n payload_too_large: PayloadTooLargeError,\n // 422\n insufficient_balance: InsufficientBalanceError,\n option_not_eligible: OptionNotEligibleError,\n invalid_payload: InvalidPayloadError,\n unsupported_event_type: UnsupportedEventTypeError,\n profile_validation_error: ProfileValidationError,\n // 429\n rate_limited: RateLimitedError,\n // 500 / 501\n internal_error: InternalError,\n not_implemented: NotImplementedError,\n};\n\ninterface EnvelopeFields {\n code?: string;\n message?: string;\n requestId?: string;\n details?: unknown;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** Extract `{ error: { code, message, request_id, details } }` from a parsed body, tolerantly. */\nfunction extractEnvelope(body: unknown): EnvelopeFields {\n if (!isRecord(body)) return {};\n const error = isRecord(body.error) ? body.error : undefined;\n if (!error) return {};\n return {\n code: typeof error.code === 'string' ? error.code : undefined,\n message: typeof error.message === 'string' ? error.message : undefined,\n requestId: typeof error.request_id === 'string' ? error.request_id : undefined,\n details: error.details,\n };\n}\n\n/**\n * Build the right `TesseraError` subclass from a non-2xx response. `body` is the tolerantly-parsed\n * JSON body (any shape); `requestId` is the captured `X-Request-Id` header (preferred over the\n * envelope's `request_id`). An unknown or missing `code` yields the base `TesseraError` — this\n * NEVER throws, so a new server code cannot crash an old SDK.\n */\nexport function errorFromEnvelope(status: number, body: unknown, requestId?: string): TesseraError {\n const env = extractEnvelope(body);\n const code = env.code ?? 'unknown_error';\n const init: TesseraErrorInit = {\n code,\n message: env.message ?? `Request failed with HTTP ${status}.`,\n status,\n requestId: requestId ?? env.requestId,\n details: env.details,\n };\n const ErrorClass = CODE_TO_ERROR[code];\n return ErrorClass ? new ErrorClass(init) : new TesseraError(init);\n}\n","import { TesseraError, errorFromEnvelope } from './errors';\n\n/**\n * The HTTP request core shared by every resource. It is runtime-self-contained —\n * it uses only global Web standards (`fetch`, `Headers`, `AbortController`,\n * `URLSearchParams`) so it runs unchanged on Node 20+, edge/Workers, Deno, and Bun.\n * No `@loyalty/*` (or any runtime dependency) is imported.\n *\n * Responsibilities: bearer auth, JSON writes, idempotency-key passthrough, a\n * per-attempt timeout, bounded retries with backoff (honoring `Retry-After`),\n * `X-Request-Id` capture, tolerant success decoding, and envelope→`TesseraError`.\n */\n\n/** A `fetch`-compatible function. `globalThis.fetch` satisfies this; a caller may inject their own. */\nexport type FetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>;\n\nexport type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';\n\nexport type QueryValue = string | number | boolean | null | undefined;\n\nexport interface RequestOptions {\n /** Query params; `undefined`/`null` values are skipped. */\n query?: Record<string, QueryValue>;\n /** A JSON-serializable request body. Its presence sets `Content-Type: application/json`. */\n body?: unknown;\n /**\n * A pre-serialized request body string, transmitted BYTE-FOR-BYTE (never re-serialized). This is\n * the signed-bytes path for event ingestion (`POST /v1/events`): the caller signs the exact string\n * it passes here, so re-serializing an object (`JSON.stringify` reordering keys / changing\n * whitespace) can never invalidate that signature. Presence sets `Content-Type: application/json`\n * (a rawBody IS a body, including an empty string). Takes precedence over `body` when both are set.\n */\n rawBody?: string;\n /** Extra headers layered under the SDK's own (auth/content-type/idempotency win). */\n headers?: Record<string, string>;\n /** Sets the `Idempotency-Key` header; held constant across this call's retries. */\n idempotencyKey?: string;\n /**\n * A hint for the publishable-vs-secret key GUARD added in a later ticket (TER-86).\n * Accepted here for a stable signature; `request` itself is key-scope agnostic.\n */\n keyScope?: 'secret' | 'publishable' | 'any';\n}\n\nexport interface HttpCoreConfig {\n /** The `/v1` base URL; trailing slashes are trimmed. */\n baseUrl: string;\n /** The resolved bearer token (secret or publishable key). */\n token: string;\n /** Per-attempt timeout in milliseconds. */\n timeoutMs: number;\n /** Maximum retry attempts on retriable failures (network / 5xx / 429). */\n maxRetries: number;\n /** The `fetch` implementation to use. */\n fetchImpl: FetchLike;\n}\n\n/** Internal marker: the per-attempt timeout fired and aborted the request. */\nclass TimeoutSignal extends Error {}\n\nexport class HttpCore {\n private readonly baseUrl: string;\n private readonly token: string;\n private readonly timeoutMs: number;\n private readonly maxRetries: number;\n private readonly fetchImpl: FetchLike;\n\n constructor(config: HttpCoreConfig) {\n this.baseUrl = config.baseUrl.replace(/\\/+$/, '');\n this.token = config.token;\n this.timeoutMs = config.timeoutMs;\n this.maxRetries = config.maxRetries;\n this.fetchImpl = config.fetchImpl;\n }\n\n /**\n * Issue a request. On 2xx returns the tolerantly-parsed JSON body; on a non-2xx\n * envelope throws a base `TesseraError`. Retries network errors / 5xx / 429 up to\n * `maxRetries`; a per-attempt timeout is terminal (it means the call blew its budget).\n */\n async request<T = unknown>(\n method: HttpMethod,\n path: string,\n opts: RequestOptions = {},\n ): Promise<T> {\n const url = this.buildUrl(path, opts.query);\n // A rawBody is a pre-serialized string sent VERBATIM (the signed-bytes path); a `body` is an\n // object JSON-serialized here. rawBody wins when both are set. Either counts as \"having a body\"\n // (so Content-Type is set) — including an empty-string rawBody.\n const hasRawBody = opts.rawBody !== undefined;\n const hasBody = hasRawBody || opts.body !== undefined;\n const headers = this.buildHeaders(opts, hasBody);\n const body = hasRawBody ? opts.rawBody : opts.body !== undefined ? JSON.stringify(opts.body) : undefined;\n\n let attempt = 0;\n for (;;) {\n let response: Response;\n try {\n response = await this.dispatch(method, url, headers, body);\n } catch (err) {\n if (err instanceof TimeoutSignal) {\n throw new TesseraError({\n code: 'timeout',\n message: `Request to ${method} ${path} timed out after ${this.timeoutMs}ms.`,\n });\n }\n // Transport/network error — retriable while the budget remains.\n if (attempt < this.maxRetries) {\n await delay(backoffMs(attempt));\n attempt += 1;\n continue;\n }\n throw new TesseraError({\n code: 'network_error',\n message: err instanceof Error ? err.message : 'Network request failed.',\n details: err,\n });\n }\n\n // Retriable HTTP status (5xx / 429), while the budget remains. Honor Retry-After if present.\n if (isRetriableStatus(response.status) && attempt < this.maxRetries) {\n const retryAfterMs = parseRetryAfter(response.headers.get('Retry-After'));\n await delay(retryAfterMs ?? backoffMs(attempt));\n attempt += 1;\n continue;\n }\n\n const requestId = response.headers.get('X-Request-Id') ?? undefined;\n if (response.status >= 200 && response.status < 300) {\n return (await readJson(response)) as T;\n }\n\n // Non-2xx (a non-retriable 4xx, or a 5xx/429 with retries exhausted). The factory maps the\n // envelope `code` to its typed subclass, or the base error for an unknown code (never throws).\n const errorBody = await readJson(response);\n throw errorFromEnvelope(response.status, errorBody, requestId);\n }\n }\n\n /** One fetch attempt, raced against a per-attempt timeout that aborts the request. */\n private async dispatch(\n method: HttpMethod,\n url: string,\n headers: Headers,\n body: string | undefined,\n ): Promise<Response> {\n const controller = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => {\n controller.abort();\n reject(new TimeoutSignal());\n }, this.timeoutMs);\n });\n try {\n // Race so a fetch that ignores the abort signal still cannot outlive the budget.\n return await Promise.race([\n this.fetchImpl(url, { method, headers, body, signal: controller.signal }),\n timeout,\n ]);\n } finally {\n if (timer !== undefined) clearTimeout(timer);\n }\n }\n\n private buildUrl(path: string, query?: Record<string, QueryValue>): string {\n const rel = path.startsWith('/') ? path : `/${path}`;\n let url = `${this.baseUrl}${rel}`;\n if (query) {\n const params = new URLSearchParams();\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) params.set(key, String(value));\n }\n const qs = params.toString();\n if (qs) url += `?${qs}`;\n }\n return url;\n }\n\n private buildHeaders(opts: RequestOptions, hasBody: boolean): Headers {\n // Caller-supplied headers layer first; the SDK's own headers then take precedence.\n const headers = new Headers(opts.headers);\n headers.set('Authorization', `Bearer ${this.token}`);\n headers.set('Accept', 'application/json');\n if (hasBody) headers.set('Content-Type', 'application/json');\n if (opts.idempotencyKey) headers.set('Idempotency-Key', opts.idempotencyKey);\n return headers;\n }\n}\n\nfunction isRetriableStatus(status: number): boolean {\n return status === 429 || status >= 500;\n}\n\nfunction backoffMs(attempt: number): number {\n const base = 200;\n const cap = 20_000;\n return Math.min(cap, base * 2 ** attempt);\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** Parse a `Retry-After` header (delta-seconds or an HTTP date) into milliseconds. */\nfunction parseRetryAfter(value: string | null): number | undefined {\n if (!value) return undefined;\n const seconds = Number(value);\n if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);\n const at = Date.parse(value);\n if (!Number.isNaN(at)) return Math.max(0, at - Date.now());\n return undefined;\n}\n\n/** Read + JSON-parse a body tolerantly: an empty or non-JSON body yields `undefined`, never a throw. */\nasync function readJson(response: Response): Promise<unknown> {\n let text: string;\n try {\n text = await response.text();\n } catch {\n return undefined;\n }\n if (!text) return undefined;\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return undefined;\n }\n}\n","/**\n * @internal snake_case → camelCase key mapping for wire→public decoding. Not part of the\n * documented public surface; resources import it to translate API bodies into the SDK's own\n * camelCase public types (the wire is snake_case per `apps/api`; the SDK owns the public shapes).\n *\n * Structured reads (member profile/balance/status/history) map fields explicitly for precise\n * types; this generic transform is used only for OPEN variant records (a reward option's\n * `cost_model`) whose inner keys vary by kind and can't be enumerated.\n */\n\n/** Convert one snake_case (or kebab) key to camelCase. Leaves already-camel keys untouched. */\nexport function snakeToCamel(key: string): string {\n return key.replace(/[_-]+([a-z0-9])/g, (_match, ch: string) => ch.toUpperCase());\n}\n\n/**\n * Recursively camelCase every object KEY, leaving values (including string values like an enum\n * `kind`) intact. Arrays are mapped element-wise; primitives and `null` pass through unchanged.\n * Tolerant: an unexpected shape is returned as-is rather than throwing.\n */\nexport function deepCamelize(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(deepCamelize);\n if (value !== null && typeof value === 'object') {\n const out: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(value as Record<string, unknown>)) {\n out[snakeToCamel(key)] = deepCamelize(val);\n }\n return out;\n }\n return value;\n}\n","import { HttpCore } from '../http';\nimport { deepCamelize } from '../internal/camel';\n\n/**\n * The public rewards catalogue resource (`GET /v1/rewards`) — the ONE read a publishable key may\n * make (non-personalized: no balance, no PII). The SDK owns its public wire↔camelCase types; the\n * API body is snake_case (`apps/api` rewardsHandler + engine `toPublic`).\n */\n\n/** A reward option's redemption constraints (camelCased from the API `constraints` block). */\nexport interface RewardOptionConstraints {\n /** Minimum cart subtotal (minor units) required to redeem, or null for no minimum. */\n minSpendCents: number | null;\n /** Collection ids the option is scoped to (empty = unrestricted). */\n eligibleCollectionIds: string[];\n /** Product ids the option is scoped to (empty = unrestricted). */\n eligibleProductIds: string[];\n /** Whether this option may stack with another active redemption. */\n stackable: boolean;\n /** Per-customer redemption cap over a rolling window, or null for uncapped. */\n perCustomerCap: { count: number; window: string } | null;\n /** The tier key required to redeem, or null when ungated. */\n tierGate: string | null;\n /** Maximum discount value (minor units) the option may apply, or null for uncapped. */\n valueCapCents: number | null;\n}\n\n/** One public (storefront-safe) catalogue option. */\nexport interface RewardOption {\n id: string;\n /** Redemption type (e.g. `fixed_discount`, `percentage_discount`, `free_product`). */\n type: string;\n label: string;\n /** How the reward is fulfilled (e.g. `code`, `automatic`). */\n mechanism: string;\n /** Surfaces the option may be triggered on (e.g. `cart`, `checkout`). */\n triggerSurfaces: string[];\n /**\n * The cost model, an open variant record keyed by `kind` (`fixed_points` / `percentage` /\n * `points_per_unit_value` / `free`). Its inner keys are camelCased; kept loosely typed because\n * the shape varies by kind. `costPoints` is the flattened flat cost for the common case.\n */\n costModel: Record<string, unknown>;\n /** Flat point cost when known; null for variable (value-dependent) burn. */\n costPoints: number | null;\n constraints: RewardOptionConstraints;\n}\n\n/** The rewards catalogue: the merchant's points label + the list of options. */\nexport interface RewardsCatalogue {\n /** The merchant's display label for points (e.g. `Stars`); defaults server-side to `points`. */\n pointsLabel: string;\n data: RewardOption[];\n}\n\n// ── Wire types (snake_case, mirrors apps/api + engine PublicCatalogueOption) ──────────────────────\n\ninterface RewardOptionConstraintsWire {\n min_spend_cents: number | null;\n eligible_collection_ids: string[];\n eligible_product_ids: string[];\n stackable: boolean;\n per_customer_cap: { count: number; window: string } | null;\n tier_gate: string | null;\n value_cap_cents: number | null;\n}\n\ninterface RewardOptionWire {\n id: string;\n type: string;\n label: string;\n mechanism: string;\n trigger_surfaces: string[];\n cost_model: Record<string, unknown>;\n cost_points: number | null;\n constraints: RewardOptionConstraintsWire;\n}\n\ninterface RewardsCatalogueWire {\n points_label: string;\n data: RewardOptionWire[];\n}\n\nfunction toRewardOption(w: RewardOptionWire): RewardOption {\n const c = w.constraints;\n return {\n id: w.id,\n type: w.type,\n label: w.label,\n mechanism: w.mechanism,\n triggerSurfaces: w.trigger_surfaces,\n // The cost model is an open variant record — deep-camelCase its keys generically.\n costModel: deepCamelize(w.cost_model) as Record<string, unknown>,\n costPoints: w.cost_points,\n constraints: {\n minSpendCents: c.min_spend_cents,\n eligibleCollectionIds: c.eligible_collection_ids,\n eligibleProductIds: c.eligible_product_ids,\n stackable: c.stackable,\n perCustomerCap: c.per_customer_cap\n ? { count: c.per_customer_cap.count, window: c.per_customer_cap.window }\n : null,\n tierGate: c.tier_gate,\n valueCapCents: c.value_cap_cents,\n },\n };\n}\n\nexport class RewardsResource {\n constructor(private readonly http: HttpCore) {}\n\n /**\n * Fetch the public rewards catalogue. `keyScope: 'publishable'` — allowed with a publishable OR\n * a secret key (the one read a browser/publishable client may perform).\n */\n async list(): Promise<RewardsCatalogue> {\n const wire = await this.http.request<RewardsCatalogueWire>('GET', '/rewards', {\n keyScope: 'publishable',\n });\n return {\n pointsLabel: wire.points_label,\n data: (wire.data ?? []).map(toRewardOption),\n };\n }\n}\n","import type { KeyType } from '../client';\nimport { PublishableKeyForbiddenError } from '../errors';\n\n/**\n * @internal The pre-network secret-scope guard shared by every secret-only resource\n * (`customers` / `redemptions` / `events` / the member handle). Returns for a secret key; for a\n * publishable key it throws a {@link PublishableKeyForbiddenError} — before any transport work — with\n * the caller's operation-specific `message`. Centralizes the `publishable_key_forbidden` code + error\n * construction so the four call sites cannot drift. Not part of the documented public surface.\n */\nexport function assertSecretKey(keyType: KeyType, message: string): void {\n if (keyType === 'secret') return;\n throw new PublishableKeyForbiddenError({ code: 'publishable_key_forbidden', message });\n}\n","/**\n * @internal Request-body serialization helpers shared by the write resources. Not part of the\n * documented public surface.\n */\n\n/**\n * An ISO-8601 string or `Date` → an ISO string; `undefined` passes through unchanged. Every\n * resource that accepts a `string | Date` timestamp (`expiresAt` / `since` / `until` / `occurredAt`)\n * normalizes it through this one helper before it reaches the wire body.\n */\nexport function toIso(value: string | Date | undefined): string | undefined {\n if (value === undefined) return undefined;\n return value instanceof Date ? value.toISOString() : value;\n}\n","import type { KeyType } from '../client';\nimport { HttpCore, type QueryValue } from '../http';\nimport { assertSecretKey } from '../internal/scope';\nimport { toIso } from '../internal/serialize';\nimport type {\n PlaceRedemptionOptions,\n RedemptionHandle,\n RedemptionsResource,\n} from './redemptions';\n\n/**\n * The `(channel, externalId)`-keyed member handle — the ergonomic core of the read surface. Its\n * channel is BOUND (from the client's `resolveChannel()`), never a per-call arg, so the\n * \"`shopify` here, `shoify` there\" identity split is structurally impossible. All four reads are\n * SECRET-scope; a publishable-key client fails fast pre-network (better DX than a 403 round-trip).\n *\n * The SDK owns its public camelCase types; the API bodies are snake_case (`apps/api`). Structured\n * reads map fields explicitly (precise types + drift detection).\n */\n\n// ── Public types (camelCase) ──────────────────────────────────────────────────────────────────────\n\n/** The pseudonymous member profile (`GET /v1/customers/:channel/:externalId`). */\nexport interface MemberProfile {\n customerUuid: string;\n channel: string;\n externalId: string;\n /** ISO-8601 creation timestamp, or null when no customer row exists for the pair. */\n createdAt: string | null;\n}\n\n/** A member's balances across the two currencies + the redemption `available` (`.../balance`). */\nexport interface MemberBalance {\n customerUuid: string;\n /** The merchant's display label for points. */\n pointsLabel: string;\n /** Confirmed (spendable) points. */\n spendableBalance: number;\n /** Accruing points not yet matured (visible, not spendable). */\n pendingSpendable: number;\n /** Cumulative qualifying credits (the status-track currency). */\n qualifyingBalance: number;\n /** Points reserved by in-flight redemption holds. */\n activeHolds: number;\n /** Outstanding clawbacks (post-refund debt recovered on the next earn). */\n clawbacksOutstanding: number;\n /** `confirmed − activeHolds − clawbacksOutstanding` — what may be redeemed now (may be negative). */\n available: number;\n}\n\n/** A member's current tier (camelCased from the `current_tier` block). */\nexport interface MemberTier {\n key: string;\n name: string;\n /** The tier's earn multiplier. */\n earnMultiplier: number;\n /** Reward-eligibility tags carried on the tier (config round-tripped; enforcement is deferred). */\n rewardEligibility: string[];\n}\n\n/** Progress toward the next tier + the current hold threshold (camelCased `qualifying_progress`). */\nexport interface QualifyingProgress {\n /** The member's current qualifying total. */\n current: number;\n /** Qualifying needed to HOLD the current tier this period, or null when no tier applies. */\n holdThreshold: number | null;\n /** The next tier's key, or null at the top of the ladder / with no program. */\n nextTierKey: string | null;\n /** Qualifying needed to reach the next tier, or null when there is none. */\n nextQualifyThreshold: number | null;\n}\n\n/** A member's tier status + progress (`.../status`). Every field is null when no program applies. */\nexport interface MemberStatus {\n customerUuid: string;\n currentTier: MemberTier | null;\n effectiveTierKey: string | null;\n periodStart: string | null;\n periodEnd: string | null;\n requalificationState: string | null;\n graceDeadline: string | null;\n qualifyingProgress: QualifyingProgress;\n}\n\n/** One ledger history entry (`.../history` data item), camelCased. */\nexport interface HistoryEntry {\n id: string;\n /** entry type: earn | maturation | burn | expiry | adjustment | reversal | clawback. */\n type: string;\n currency: string;\n /** Signed minor units: positive = credit, negative = debit. */\n amount: number;\n state: string;\n /** Which external system triggered the entry, or null. */\n sourceChannel: string | null;\n /** The external object id (e.g. order id), or null. */\n sourceRef: string | null;\n /** The originating event id, or null. */\n sourceEventId: string | null;\n createdAt: string;\n}\n\n/** Page cursor metadata for a single history page. */\nexport interface HistoryPageInfo {\n /** The opaque cursor for the NEXT page, or null when there are no more pages. */\n nextCursor: string | null;\n hasMore: boolean;\n}\n\n/** One page of history: the entries + cursor info (what a single `await member.history()` yields). */\nexport interface HistoryPage {\n data: HistoryEntry[];\n page: HistoryPageInfo;\n}\n\n/** Options for a history read. `types` is joined into the API's comma-separated `type` param. */\nexport interface HistoryOptions {\n /** Page size (1–100). */\n limit?: number;\n /** An opaque cursor to start from (a prior page's `nextCursor`). */\n cursor?: string;\n /** Restrict to these entry types (joined into `?type=a,b`). */\n types?: string[];\n /** Only entries at/after this ISO-8601 timestamp (or a `Date`). */\n since?: string | Date;\n /** Only entries at/before this ISO-8601 timestamp (or a `Date`). */\n until?: string | Date;\n}\n\n/** Per-member overrides (currently only a channel override, validated vs the client allowlist). */\nexport interface MemberOptions {\n /** Override the client's bound channel for THIS handle (validated against the allowlist). */\n channel?: string;\n}\n\n/**\n * The `member.redeem(...)` input — a `place` request with the member's `externalId` + channel already\n * bound by the handle (so only the option + optional cost/eligibility inputs are passed here).\n */\nexport interface MemberRedeemInput {\n /** The reward option UUID to redeem. */\n optionId: string;\n /** Cart subtotal (minor units) — feeds min-spend eligibility + percentage/value cost resolution. */\n cartSubtotalCents?: number;\n /** An explicit requested discount value (minor units); `null` clears any prior request. */\n requestedValueCents?: number | null;\n /** When the hold should expire — an ISO-8601 string or a `Date`. */\n expiresAt?: string | Date;\n}\n\n// ── Wire types (snake_case, mirrors apps/api handlers) ────────────────────────────────────────────\n\ninterface MemberProfileWire {\n customer_uuid: string;\n channel: string;\n external_id: string;\n created_at: string | null;\n}\n\ninterface MemberBalanceWire {\n customer_uuid: string;\n points_label: string;\n spendable_balance: number;\n pending_spendable: number;\n qualifying_balance: number;\n active_holds: number;\n clawbacks_outstanding: number;\n available: number;\n}\n\ninterface MemberTierWire {\n key: string;\n name: string;\n earn_multiplier: number;\n reward_eligibility: string[];\n}\n\ninterface MemberStatusWire {\n customer_uuid: string;\n current_tier: MemberTierWire | null;\n effective_tier_key: string | null;\n period_start: string | null;\n period_end: string | null;\n requalification_state: string | null;\n grace_deadline: string | null;\n qualifying_progress: {\n current: number;\n hold_threshold: number | null;\n next_tier_key: string | null;\n next_qualify_threshold: number | null;\n };\n}\n\ninterface HistoryEntryWire {\n id: string;\n type: string;\n currency: string;\n amount: number;\n state: string;\n source_channel: string | null;\n source_ref: string | null;\n source_event_id: string | null;\n created_at: string;\n}\n\ninterface HistoryResponseWire {\n data: HistoryEntryWire[];\n page: { next_cursor: string | null; has_more: boolean };\n}\n\n// ── Mappers ───────────────────────────────────────────────────────────────────────────────────────\n\nfunction toProfile(w: MemberProfileWire): MemberProfile {\n return {\n customerUuid: w.customer_uuid,\n channel: w.channel,\n externalId: w.external_id,\n createdAt: w.created_at,\n };\n}\n\nfunction toBalance(w: MemberBalanceWire): MemberBalance {\n return {\n customerUuid: w.customer_uuid,\n pointsLabel: w.points_label,\n spendableBalance: w.spendable_balance,\n pendingSpendable: w.pending_spendable,\n qualifyingBalance: w.qualifying_balance,\n activeHolds: w.active_holds,\n clawbacksOutstanding: w.clawbacks_outstanding,\n available: w.available,\n };\n}\n\nfunction toStatus(w: MemberStatusWire): MemberStatus {\n const p = w.qualifying_progress;\n return {\n customerUuid: w.customer_uuid,\n currentTier: w.current_tier\n ? {\n key: w.current_tier.key,\n name: w.current_tier.name,\n earnMultiplier: w.current_tier.earn_multiplier,\n rewardEligibility: w.current_tier.reward_eligibility,\n }\n : null,\n effectiveTierKey: w.effective_tier_key,\n periodStart: w.period_start,\n periodEnd: w.period_end,\n requalificationState: w.requalification_state,\n graceDeadline: w.grace_deadline,\n qualifyingProgress: {\n current: p.current,\n holdThreshold: p.hold_threshold,\n nextTierKey: p.next_tier_key,\n nextQualifyThreshold: p.next_qualify_threshold,\n },\n };\n}\n\nfunction toHistoryEntry(w: HistoryEntryWire): HistoryEntry {\n return {\n id: w.id,\n type: w.type,\n currency: w.currency,\n amount: w.amount,\n state: w.state,\n sourceChannel: w.source_channel,\n sourceRef: w.source_ref,\n sourceEventId: w.source_event_id,\n createdAt: w.created_at,\n };\n}\n\n/**\n * The history pager. `member.history(opts)` returns one synchronously; it is BOTH:\n * - an async-iterable that auto-follows the cursor across every page (`for await (…)`), and\n * - awaitable (`PromiseLike`) to resolve the FIRST page as a {@link HistoryPage} with\n * `.data` / `.page`, for simple single-page use.\n * `for await` uses `Symbol.asyncIterator` (never `.then`), so the two forms never collide.\n */\nexport class HistoryPager implements AsyncIterable<HistoryEntry>, PromiseLike<HistoryPage> {\n constructor(private readonly fetchPage: (cursor?: string) => Promise<HistoryPage>) {}\n\n async *[Symbol.asyncIterator](): AsyncIterator<HistoryEntry> {\n let cursor: string | undefined;\n for (;;) {\n const page = await this.fetchPage(cursor);\n for (const entry of page.data) yield entry;\n const next = page.page.nextCursor;\n if (!page.page.hasMore || next === null || next === undefined) return;\n cursor = next;\n }\n }\n\n then<TResult1 = HistoryPage, TResult2 = never>(\n onfulfilled?: ((value: HistoryPage) => TResult1 | PromiseLike<TResult1>) | null,\n onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,\n ): PromiseLike<TResult1 | TResult2> {\n return this.fetchPage(undefined).then(onfulfilled, onrejected);\n }\n}\n\n/** Everything a {@link MemberHandle} needs from its parent client, passed explicitly (no cycle). */\nexport interface MemberDeps {\n http: HttpCore;\n keyType: KeyType;\n channel: string;\n externalId: string;\n /** The client's redemptions resource — powers `member.redeem(...)` (the write sugar). */\n redemptions: RedemptionsResource;\n}\n\nexport class MemberHandle {\n /** The channel bound to this handle (from the client's `resolveChannel()`). */\n readonly channel: string;\n /** The merchant-facing external id this handle reads. */\n readonly externalId: string;\n private readonly http: HttpCore;\n private readonly keyType: KeyType;\n private readonly redemptions: RedemptionsResource;\n\n constructor(deps: MemberDeps) {\n this.http = deps.http;\n this.keyType = deps.keyType;\n this.channel = deps.channel;\n this.externalId = deps.externalId;\n this.redemptions = deps.redemptions;\n }\n\n /** The `/customers/:channel/:externalId` path prefix (both segments URL-encoded). */\n private base(): string {\n return `/customers/${encodeURIComponent(this.channel)}/${encodeURIComponent(this.externalId)}`;\n }\n\n /** Fail fast pre-network when a secret-scope read is attempted with a publishable key. */\n private requireSecret(): void {\n assertSecretKey(\n this.keyType,\n 'This member read requires a secret key. The client is configured with a publishable key ' +\n '(catalogue-only). Construct the client with `secretKey` to read a member.',\n );\n }\n\n /** The pseudonymous member profile (identity + channel binding; no PII). */\n async get(): Promise<MemberProfile> {\n this.requireSecret();\n const wire = await this.http.request<MemberProfileWire>('GET', this.base(), {\n keyScope: 'secret',\n });\n return toProfile(wire);\n }\n\n /** The member's balances (spendable / pending / qualifying / holds / available). */\n async balance(): Promise<MemberBalance> {\n this.requireSecret();\n const wire = await this.http.request<MemberBalanceWire>('GET', `${this.base()}/balance`, {\n keyScope: 'secret',\n });\n return toBalance(wire);\n }\n\n /** The member's tier status + progress toward the next tier. */\n async status(): Promise<MemberStatus> {\n this.requireSecret();\n const wire = await this.http.request<MemberStatusWire>('GET', `${this.base()}/status`, {\n keyScope: 'secret',\n });\n return toStatus(wire);\n }\n\n /**\n * The member's points history. Returns a {@link HistoryPager}: iterate it with `for await` to\n * auto-follow the cursor across all pages, or `await` it for just the first {@link HistoryPage}\n * (`.data` / `.page`). `opts.cursor`, if given, is the starting page for the awaited-first-page\n * form; the iterator always starts fresh and follows the cursor from there.\n */\n history(opts: HistoryOptions = {}): HistoryPager {\n this.requireSecret();\n const baseQuery: Record<string, QueryValue> = {\n limit: opts.limit,\n type: opts.types && opts.types.length > 0 ? opts.types.join(',') : undefined,\n since: toIso(opts.since),\n until: toIso(opts.until),\n };\n const fetchPage = async (cursor?: string): Promise<HistoryPage> => {\n const wire = await this.http.request<HistoryResponseWire>('GET', `${this.base()}/history`, {\n keyScope: 'secret',\n query: { ...baseQuery, cursor: cursor ?? opts.cursor },\n });\n return {\n data: (wire.data ?? []).map(toHistoryEntry),\n page: {\n nextCursor: wire.page?.next_cursor ?? null,\n hasMore: wire.page?.has_more ?? false,\n },\n };\n };\n return new HistoryPager(fetchPage);\n }\n\n /**\n * Place a redemption hold for THIS member (`POST /v1/redemptions`) — sugar over\n * `client.redemptions.place(...)` with the handle's `externalId` + bound channel supplied for you.\n * Returns a {@link RedemptionHandle}: the placed-hold result plus `.finalize({ sourceChannel,\n * sourceRef })` / `.release()` bound to it, so the storefront flow chains cleanly. Secret-scope\n * (delegated to the redemptions resource); a publishable-key client rejects pre-network.\n */\n async redeem(\n input: MemberRedeemInput,\n opts?: PlaceRedemptionOptions,\n ): Promise<RedemptionHandle> {\n const result = await this.redemptions.place(\n {\n optionId: input.optionId,\n externalId: this.externalId,\n channel: this.channel,\n cartSubtotalCents: input.cartSubtotalCents,\n requestedValueCents: input.requestedValueCents,\n expiresAt: input.expiresAt,\n },\n opts,\n );\n return this.redemptions.toHandle(result);\n }\n}\n","import type { KeyType } from '../client';\nimport { HttpCore } from '../http';\nimport { assertSecretKey } from '../internal/scope';\n\n/**\n * The `client.customers` resource — the customer/identity WRITE + server-side PII surface\n * (`upsert` / `update` / `getPii` / `linkChannel` / `unlinkChannel`). Every endpoint here is\n * SECRET-scope: a publishable-key client fails fast pre-network (better DX than a 403 round-trip),\n * mirroring the read handle's guard.\n *\n * Channel discipline (design §5.2): the channel a customer lives on is validated through the\n * client's `resolveChannel()` (the `channels` allowlist choke point) — `upsert` resolves its\n * (optional) input channel to the bound default, and `linkChannel` validates its TARGET channel —\n * both BEFORE any request, so a typo'd channel can never split identity/ledger.\n *\n * The SDK owns its public camelCase types; the API bodies are snake_case (`apps/api` app.ts, the\n * `toProfileFields` / `piiToWire` helpers). Fields are mapped explicitly (precise types + drift\n * detection), and `null` is preserved as an intentional CLEAR (never dropped).\n */\n\n// ── Public types (camelCase) ────────────────────────────────────────────────────────────────────\n\n/** A per-channel marketing-consent state (`opted_in` / `opted_out` / `unset`). */\nexport type ConsentState = 'opted_in' | 'opted_out' | 'unset';\n\n/** Marketing-consent flags + when they last changed (camelCased from `marketing_consent`). */\nexport interface MarketingConsent {\n email?: ConsentState;\n sms?: ConsentState;\n /** ISO-8601 timestamp the consent last changed. */\n updatedAt?: string;\n}\n\n/**\n * The mutable PII field set shared by `upsert` and `update`. A `null` value CLEARS the field\n * (server-side); an ABSENT field is left untouched (merge semantics). Semantic validation\n * (email/birthday/locale) is enforced server-side and surfaces as a `ProfileValidationError` (422).\n */\nexport interface CustomerProfileFields {\n email?: string | null;\n name?: string | null;\n birthday?: string | null;\n locale?: string | null;\n socialHandles?: Record<string, string> | null;\n marketingConsent?: MarketingConsent | null;\n}\n\n/** The `upsert` input — the profile fields plus the REQUIRED `externalId` and an optional channel. */\nexport interface CustomerUpsertInput extends CustomerProfileFields {\n /** The merchant-facing external id keying this customer on its channel. Required. */\n externalId: string;\n /**\n * The channel this external id lives on. Resolved through the client's `resolveChannel()` — omit\n * to use the client's bound channel, or name one (validated against the `channels` allowlist,\n * throwing PRE-NETWORK on an out-of-vocabulary value).\n */\n channel?: string;\n}\n\n/** Per-call options for `upsert`. */\nexport interface CustomerUpsertOptions {\n /** Sets the `Idempotency-Key` header for a safe create-or-update retry. */\n idempotencyKey?: string;\n}\n\n/** The `upsert` result: the resolved customer + whether this call CREATED it (201) or updated it (200). */\nexport interface CustomerUpsertResult {\n customerUuid: string;\n channel: string;\n externalId: string;\n /** `true` when this call created the customer (HTTP 201); `false` on an update (HTTP 200). */\n created: boolean;\n /** ISO-8601 timestamp of the resulting row. */\n updatedAt: string;\n}\n\n/** Decrypted server-side PII (`getPii` / the `update` response). Absent keys were never set. */\nexport interface CustomerPii {\n customerUuid: string;\n email?: string;\n name?: string;\n birthday?: string;\n locale?: string;\n socialHandles?: Record<string, string>;\n marketingConsent?: MarketingConsent;\n /** ISO-8601 timestamp the identity row last changed. */\n updatedAt?: string;\n}\n\n/** The `linkChannel` input — the TARGET channel + external id to bind to an existing customer. */\nexport interface LinkChannelInput {\n /** The channel to link. Validated against the client's `channels` allowlist pre-network. */\n channel: string;\n externalId: string;\n}\n\n/** The `linkChannel` result. */\nexport interface LinkChannelResult {\n customerUuid: string;\n /** How the link was created (e.g. `manual`). */\n linkedVia: string;\n /** The resulting link status (e.g. `active`). */\n status: string;\n}\n\n/** The `unlinkChannel` result. */\nexport interface UnlinkChannelResult {\n /** The resulting link status (e.g. `unlinked`). */\n status: string;\n}\n\n// ── Wire types (snake_case, mirrors apps/api app.ts handlers + piiToWire) ────────────────────────\n\ninterface MarketingConsentWire {\n email?: ConsentState;\n sms?: ConsentState;\n updated_at?: string;\n}\n\ninterface CustomerUpsertResultWire {\n customer_uuid: string;\n channel: string;\n external_id: string;\n created: boolean;\n updated_at: string;\n}\n\ninterface CustomerPiiWire {\n customer_uuid: string;\n email?: string;\n name?: string;\n birthday?: string;\n locale?: string;\n social_handles?: Record<string, string>;\n marketing_consent?: MarketingConsentWire;\n updated_at?: string;\n}\n\ninterface LinkChannelResultWire {\n customer_uuid: string;\n linked_via: string;\n status: string;\n}\n\ninterface UnlinkChannelResultWire {\n status: string;\n}\n\n// ── Mappers (public camelCase → snake_case wire, and back) ───────────────────────────────────────\n\n/** camelCase consent → snake_case wire (only present keys included, mirroring the API). */\nfunction marketingConsentToWire(mc: MarketingConsent): MarketingConsentWire {\n const out: MarketingConsentWire = {};\n if (mc.email !== undefined) out.email = mc.email;\n if (mc.sms !== undefined) out.sms = mc.sms;\n if (mc.updatedAt !== undefined) out.updated_at = mc.updatedAt;\n return out;\n}\n\n/**\n * Serialize the mutable profile fields camelCase → snake_case. Mirrors the API's `toProfileFields`\n * in reverse: only present keys are emitted (absent = untouched), and `null` is preserved as an\n * explicit CLEAR — for both scalar fields and `marketingConsent`.\n */\nfunction profileFieldsToWire(fields: CustomerProfileFields): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n if (fields.email !== undefined) out.email = fields.email;\n if (fields.name !== undefined) out.name = fields.name;\n if (fields.birthday !== undefined) out.birthday = fields.birthday;\n if (fields.locale !== undefined) out.locale = fields.locale;\n if (fields.socialHandles !== undefined) out.social_handles = fields.socialHandles;\n if (fields.marketingConsent !== undefined) {\n out.marketing_consent =\n fields.marketingConsent === null ? null : marketingConsentToWire(fields.marketingConsent);\n }\n return out;\n}\n\n/** Decrypted PII wire → public camelCase, preserving key ABSENCE (omitted key = never set). */\nfunction toPii(w: CustomerPiiWire): CustomerPii {\n const out: CustomerPii = { customerUuid: w.customer_uuid };\n if (w.email !== undefined) out.email = w.email;\n if (w.name !== undefined) out.name = w.name;\n if (w.birthday !== undefined) out.birthday = w.birthday;\n if (w.locale !== undefined) out.locale = w.locale;\n if (w.social_handles !== undefined) out.socialHandles = w.social_handles;\n if (w.marketing_consent !== undefined) {\n const mc = w.marketing_consent;\n const consent: MarketingConsent = {};\n if (mc.email !== undefined) consent.email = mc.email;\n if (mc.sms !== undefined) consent.sms = mc.sms;\n if (mc.updated_at !== undefined) consent.updatedAt = mc.updated_at;\n out.marketingConsent = consent;\n }\n if (w.updated_at !== undefined) out.updatedAt = w.updated_at;\n return out;\n}\n\n/** Everything a {@link CustomersResource} needs from its parent client, passed explicitly (no cycle). */\nexport interface CustomersDeps {\n http: HttpCore;\n keyType: KeyType;\n /** The client's channel resolver — validates a channel against the `channels` allowlist. */\n resolveChannel: (channel?: string) => string;\n}\n\nexport class CustomersResource {\n private readonly http: HttpCore;\n private readonly keyType: KeyType;\n private readonly resolveChannel: (channel?: string) => string;\n\n constructor(deps: CustomersDeps) {\n this.http = deps.http;\n this.keyType = deps.keyType;\n this.resolveChannel = deps.resolveChannel;\n }\n\n /** Fail fast pre-network when a secret-scope write is attempted with a publishable key. */\n private requireSecret(): void {\n assertSecretKey(\n this.keyType,\n 'This customer operation requires a secret key. The client is configured with a ' +\n 'publishable key (catalogue-only). Construct the client with `secretKey`.',\n );\n }\n\n /**\n * Create-or-update a customer profile (`POST /v1/customers`). Returns `created: true` on a create\n * (201) or `false` on an update (200) — natural idempotency keys on the resolved `(channel,\n * externalId)`. The channel is resolved through the client (bound default when omitted, else\n * validated against the allowlist — a PRE-NETWORK throw on an out-of-vocabulary value). Pass\n * `opts.idempotencyKey` to make a retry safe.\n *\n * Not `async` so the channel-allowlist check throws SYNCHRONOUSLY before any transport work.\n */\n upsert(input: CustomerUpsertInput, opts: CustomerUpsertOptions = {}): Promise<CustomerUpsertResult> {\n const channel = this.resolveChannel(input.channel);\n return this.sendUpsert(channel, input, opts);\n }\n\n private async sendUpsert(\n channel: string,\n input: CustomerUpsertInput,\n opts: CustomerUpsertOptions,\n ): Promise<CustomerUpsertResult> {\n this.requireSecret();\n const body = {\n external_id: input.externalId,\n channel,\n ...profileFieldsToWire(input),\n };\n const wire = await this.http.request<CustomerUpsertResultWire>('POST', '/customers', {\n keyScope: 'secret',\n body,\n idempotencyKey: opts.idempotencyKey,\n });\n return {\n customerUuid: wire.customer_uuid,\n channel: wire.channel,\n externalId: wire.external_id,\n created: wire.created,\n updatedAt: wire.updated_at,\n };\n }\n\n /**\n * Partial-update a customer's PII (`PATCH /v1/customers/:ref`). `ref` is a customer UUID or an\n * external id (on the `api` channel). A `null` field CLEARS it; an absent field is untouched\n * (merge). Returns the decrypted PII (camelCase). 404 → `CustomerNotFoundError`.\n */\n async update(ref: string, fields: CustomerProfileFields): Promise<CustomerPii> {\n this.requireSecret();\n const wire = await this.http.request<CustomerPiiWire>(\n 'PATCH',\n `/customers/${encodeURIComponent(ref)}`,\n { keyScope: 'secret', body: profileFieldsToWire(fields) },\n );\n return toPii(wire);\n }\n\n /**\n * Read a customer's decrypted PII by UUID (`GET /v1/customers/:uuid`). Secret-only. 404 →\n * `CustomerNotFoundError` (no cross-tenant existence leak).\n */\n async getPii(uuid: string): Promise<CustomerPii> {\n this.requireSecret();\n const wire = await this.http.request<CustomerPiiWire>(\n 'GET',\n `/customers/${encodeURIComponent(uuid)}`,\n { keyScope: 'secret' },\n );\n return toPii(wire);\n }\n\n /**\n * Manually link a `(channel, externalId)` pair to an existing customer (`POST\n * /v1/customers/:uuid/links`). The TARGET channel is validated against the client's `channels`\n * allowlist PRE-NETWORK (a typo cannot split identity). 404 → `CustomerNotFoundError`.\n *\n * Not `async` so the channel-allowlist check throws SYNCHRONOUSLY before any transport work.\n */\n linkChannel(uuid: string, input: LinkChannelInput): Promise<LinkChannelResult> {\n const channel = this.resolveChannel(input.channel);\n return this.sendLink(uuid, channel, input.externalId);\n }\n\n private async sendLink(\n uuid: string,\n channel: string,\n externalId: string,\n ): Promise<LinkChannelResult> {\n this.requireSecret();\n const wire = await this.http.request<LinkChannelResultWire>(\n 'POST',\n `/customers/${encodeURIComponent(uuid)}/links`,\n { keyScope: 'secret', body: { channel, external_id: externalId } },\n );\n return {\n customerUuid: wire.customer_uuid,\n linkedVia: wire.linked_via,\n status: wire.status,\n };\n }\n\n /**\n * Manually unlink a `(channel, externalId)` pair (`DELETE\n * /v1/customers/:uuid/links/:channel/:externalId`). Idempotent. The `:uuid` scopes/authorizes the\n * call; the pair identifies the specific link to remove.\n */\n async unlinkChannel(\n uuid: string,\n channel: string,\n externalId: string,\n ): Promise<UnlinkChannelResult> {\n this.requireSecret();\n const wire = await this.http.request<UnlinkChannelResultWire>(\n 'DELETE',\n `/customers/${encodeURIComponent(uuid)}/links/${encodeURIComponent(channel)}/${encodeURIComponent(externalId)}`,\n { keyScope: 'secret' },\n );\n return { status: wire.status };\n }\n}\n","import type { KeyType } from '../client';\nimport { HttpCore } from '../http';\nimport { assertSecretKey } from '../internal/scope';\nimport { toIso } from '../internal/serialize';\n\n/**\n * The `client.redemptions` resource — the F16 burn-hold lifecycle over HTTP: `place` a hold →\n * `finalize` (permanent burn) or `release` (abandon, restoring the reserved points), plus `get` to\n * read a hold's status. Every endpoint is SECRET-scope (they write ledger state, never from a\n * browser): a publishable-key client fails fast pre-network (better DX than a 403 round-trip),\n * mirroring the customer write/read guards.\n *\n * Ledger correctness lives in the engine, not here — the SDK only CALLS the API. The engine's typed\n * eligibility/balance errors surface as the mapped taxonomy classes (`insufficient_balance` →\n * {@link InsufficientBalanceError} incl. `.shortfall`, `tier_gated`, `cap_reached`,\n * `option_not_eligible`, unknown option → `not_found`) via the shared `errorFromEnvelope` factory.\n *\n * Channel discipline (design §5.2): the channel the customer lives on is validated through the\n * client's `resolveChannel()` (the `channels` allowlist choke point) BEFORE any request, so a typo'd\n * channel can never resolve the wrong (or a split) customer.\n *\n * Idempotency (doc 02 §3): `POST /v1/redemptions` REQUIRES an `Idempotency-Key`. When the caller\n * does not supply one, `place` auto-generates a UUID and lets the HTTP core hold it constant across\n * the automatic retries of THAT call — a retried place never double-charges the hold. A\n * caller-supplied key is used verbatim (stable across the caller's own retries).\n *\n * The SDK owns its public camelCase types; the API bodies are snake_case (`apps/api` app.ts +\n * `placeRedemptionSchema` / `finalizeRedemptionSchema`). Fields are mapped explicitly (precise\n * types + drift detection).\n */\n\n// ── Public types (camelCase) ────────────────────────────────────────────────────────────────────\n\n/** The `place` input — the option + the member's external id, plus optional cost/eligibility inputs. */\nexport interface PlaceRedemptionInput {\n /** The reward option UUID to redeem. */\n optionId: string;\n /** The merchant-facing external id of the member redeeming (keyed on the resolved channel). */\n externalId: string;\n /**\n * The channel this external id lives on. Resolved through the client's `resolveChannel()` — omit\n * to use the client's bound channel, or name one (validated against the `channels` allowlist,\n * throwing PRE-NETWORK on an out-of-vocabulary value).\n */\n channel?: string;\n /** Cart subtotal (minor units) — feeds min-spend eligibility + percentage/value cost resolution. */\n cartSubtotalCents?: number;\n /** An explicit requested discount value (minor units); `null` clears any prior request. */\n requestedValueCents?: number | null;\n /** When the hold should expire — an ISO-8601 string or a `Date`. */\n expiresAt?: string | Date;\n}\n\n/** Per-call options for `place`. */\nexport interface PlaceRedemptionOptions {\n /**\n * The `Idempotency-Key`. When omitted, `place` auto-generates a UUID (held constant across the\n * automatic retries of this call). Supply your own to make a place stable across YOUR retries.\n */\n idempotencyKey?: string;\n}\n\n/** The placed-hold result (`POST /v1/redemptions`, state `held`). */\nexport interface RedemptionResult {\n redemptionId: string;\n customerUuid: string;\n optionId: string;\n /** The points reserved by the hold (minor points units). */\n amount: number;\n /** The resolved discount value (minor currency units) the redemption applies. */\n discountValueCents: number;\n /** How the reward is fulfilled (e.g. `code`, `automatic`). */\n mechanism: string;\n /** The hold state (`held` on placement). */\n state: string;\n /** When the hold expires (ISO-8601), or null when it does not. */\n expiresAt: string | null;\n /** ISO-8601 creation timestamp. */\n createdAt: string;\n}\n\n/** A redemption hold's status (`GET /v1/redemptions/:id`). */\nexport interface RedemptionStatus {\n redemptionId: string;\n /** The hold state (`held` | `finalized` | `released`). */\n state: string;\n amount: number;\n}\n\n/** The `finalize` input — the order that consumed the redemption (its burn provenance). */\nexport interface FinalizeRedemptionInput {\n /** Which external system consumed the redemption (e.g. `shopify`). */\n sourceChannel: string;\n /** The external object id (e.g. the order id) the burn is stamped with. */\n sourceRef: string;\n}\n\n/** The `finalize` result (permanent burn). */\nexport interface FinalizeRedemptionResult {\n redemptionId: string;\n state: 'finalized';\n}\n\n/** The `release` result (hold abandoned; reserved points restored). */\nexport interface ReleaseRedemptionResult {\n redemptionId: string;\n state: 'released';\n /** The member's `available` after the reserved points were restored. */\n available: number;\n}\n\n/**\n * A placed redemption augmented with `.finalize()` / `.release()` convenience methods bound to its\n * id — what `member.redeem(...)` returns so the storefront flow chains (`place → finalize|release`)\n * without threading the redemption id back through the resource.\n */\nexport interface RedemptionHandle extends RedemptionResult {\n /** Finalize this hold (permanent burn), stamped with the consuming order's provenance. */\n finalize(input: FinalizeRedemptionInput): Promise<FinalizeRedemptionResult>;\n /** Release this hold (abandon it; restore the reserved points). */\n release(): Promise<ReleaseRedemptionResult>;\n}\n\n// ── Wire types (snake_case, mirrors apps/api app.ts handlers) ────────────────────────────────────\n\ninterface RedemptionResultWire {\n redemption_id: string;\n customer_uuid: string;\n option_id: string;\n amount: number;\n discount_value_cents: number;\n mechanism: string;\n state: string;\n expires_at: string | null;\n created_at: string;\n}\n\ninterface RedemptionStatusWire {\n redemption_id: string;\n state: string;\n amount: number;\n}\n\ninterface FinalizeResultWire {\n redemption_id: string;\n state: string;\n}\n\ninterface ReleaseResultWire {\n redemption_id: string;\n state: string;\n available: number;\n}\n\n// ── Mappers ─────────────────────────────────────────────────────────────────────────────────────\n\nfunction toRedemptionResult(w: RedemptionResultWire): RedemptionResult {\n return {\n redemptionId: w.redemption_id,\n customerUuid: w.customer_uuid,\n optionId: w.option_id,\n amount: w.amount,\n discountValueCents: w.discount_value_cents,\n mechanism: w.mechanism,\n state: w.state,\n expiresAt: w.expires_at ?? null,\n createdAt: w.created_at,\n };\n}\n\n/** Everything a {@link RedemptionsResource} needs from its parent client, passed explicitly (no cycle). */\nexport interface RedemptionsDeps {\n http: HttpCore;\n keyType: KeyType;\n /** The client's channel resolver — validates a channel against the `channels` allowlist. */\n resolveChannel: (channel?: string) => string;\n}\n\nexport class RedemptionsResource {\n private readonly http: HttpCore;\n private readonly keyType: KeyType;\n private readonly resolveChannel: (channel?: string) => string;\n\n constructor(deps: RedemptionsDeps) {\n this.http = deps.http;\n this.keyType = deps.keyType;\n this.resolveChannel = deps.resolveChannel;\n }\n\n /** Fail fast pre-network when a secret-scope operation is attempted with a publishable key. */\n private requireSecret(): void {\n assertSecretKey(\n this.keyType,\n 'This redemption operation requires a secret key. The client is configured with a ' +\n 'publishable key (catalogue-only). Construct the client with `secretKey`.',\n );\n }\n\n /**\n * Place a redemption hold (`POST /v1/redemptions`). The channel is resolved through the client\n * (bound default when omitted, else validated against the allowlist — a PRE-NETWORK throw on an\n * out-of-vocabulary value). An `Idempotency-Key` is REQUIRED by the API; when `opts.idempotencyKey`\n * is absent the SDK auto-generates a UUID and holds it constant across this call's retries.\n *\n * Not `async` so the channel-allowlist check throws SYNCHRONOUSLY before any transport work.\n */\n place(input: PlaceRedemptionInput, opts: PlaceRedemptionOptions = {}): Promise<RedemptionResult> {\n const channel = this.resolveChannel(input.channel);\n return this.sendPlace(channel, input, opts);\n }\n\n private async sendPlace(\n channel: string,\n input: PlaceRedemptionInput,\n opts: PlaceRedemptionOptions,\n ): Promise<RedemptionResult> {\n this.requireSecret();\n const body: Record<string, unknown> = {\n option_id: input.optionId,\n customer_channel: channel,\n customer_external_id: input.externalId,\n };\n if (input.cartSubtotalCents !== undefined) body.cart_subtotal_cents = input.cartSubtotalCents;\n if (input.requestedValueCents !== undefined)\n body.requested_value_cents = input.requestedValueCents;\n if (input.expiresAt !== undefined) body.expires_at = toIso(input.expiresAt);\n\n // The API requires an Idempotency-Key. Auto-generate one when the caller passes none; the HTTP\n // core sets the header once and reuses it across every retry of THIS call, so a retried place\n // never double-charges the hold. A caller-supplied key is used verbatim.\n const idempotencyKey = opts.idempotencyKey ?? crypto.randomUUID();\n\n const wire = await this.http.request<RedemptionResultWire>('POST', '/redemptions', {\n keyScope: 'secret',\n body,\n idempotencyKey,\n });\n return toRedemptionResult(wire);\n }\n\n /**\n * Read a redemption hold's status (`GET /v1/redemptions/:id`). Tenant-scoped: 404 →\n * {@link RedemptionNotFoundError} (no cross-tenant existence leak).\n */\n async get(id: string): Promise<RedemptionStatus> {\n this.requireSecret();\n const wire = await this.http.request<RedemptionStatusWire>(\n 'GET',\n `/redemptions/${encodeURIComponent(id)}`,\n { keyScope: 'secret' },\n );\n return { redemptionId: wire.redemption_id, state: wire.state, amount: wire.amount };\n }\n\n /**\n * Finalize a hold — the permanent burn (`POST /v1/redemptions/:id/finalize`). Idempotent on the\n * hold: an already-finalized hold replays; finalizing a RELEASED (terminal) hold →\n * {@link HoldAlreadyReleasedError} (409). `sourceChannel`/`sourceRef` stamp the burn's provenance.\n */\n async finalize(id: string, input: FinalizeRedemptionInput): Promise<FinalizeRedemptionResult> {\n this.requireSecret();\n const wire = await this.http.request<FinalizeResultWire>(\n 'POST',\n `/redemptions/${encodeURIComponent(id)}/finalize`,\n {\n keyScope: 'secret',\n body: { source_channel: input.sourceChannel, source_ref: input.sourceRef },\n },\n );\n return { redemptionId: wire.redemption_id, state: 'finalized' };\n }\n\n /**\n * Release a hold — abandon it and restore the reserved points (`POST /v1/redemptions/:id/release`).\n * Idempotent. Returns the member's `available` after the restore.\n */\n async release(id: string): Promise<ReleaseRedemptionResult> {\n this.requireSecret();\n const wire = await this.http.request<ReleaseResultWire>(\n 'POST',\n `/redemptions/${encodeURIComponent(id)}/release`,\n { keyScope: 'secret' },\n );\n return { redemptionId: wire.redemption_id, state: 'released', available: wire.available };\n }\n\n /**\n * @internal Wrap a placed-hold result in a {@link RedemptionHandle} — the same fields plus\n * `.finalize()` / `.release()` bound to this resource + the redemption id. Consumed by\n * `member.redeem(...)`; not part of the documented public surface.\n */\n toHandle(result: RedemptionResult): RedemptionHandle {\n return {\n ...result,\n finalize: (input: FinalizeRedemptionInput) => this.finalize(result.redemptionId, input),\n release: () => this.release(result.redemptionId),\n };\n }\n}\n","/**\n * Client-side HMAC signer for inbound event ingestion (`POST /v1/events`).\n *\n * This is the exact mirror of the engine's verifier so a signed request is accepted byte-for-byte.\n * Source of truth: `packages/engine/src/crypto.ts` — `signEventPayload` / `signedPayload` /\n * `verifyEventSignature`. The scheme (do NOT change without changing the engine):\n *\n * - **Algorithm:** HMAC-SHA256, keyed on the per-tenant *connector* `signingSecret`\n * (`whsec_...`) — the credential separate from the bearer API key.\n * - **Canonical message:** the literal string `` `${timestamp}.${rawBody}` `` — the unix-seconds\n * timestamp, a single `.` delimiter, then the RAW request body byte-for-byte as sent. The nonce\n * is deliberately NOT part of the signed payload (it is an independent replay-defense header).\n * - **Encoding:** lowercase hex, presented with the scheme prefix `v1=`\n * (`X-Loyalty-Signature: v1=<hex>`).\n * - **Timestamp:** unix SECONDS as a string (the engine does `Number(timestamp)` and checks skew\n * against `Date.now()/1000`).\n * - **Nonce:** a fresh, opaque per-attempt value carried in `X-Loyalty-Nonce`; a verbatim resend\n * of the same nonce is a replay server-side.\n *\n * Runtime-self-contained: uses only Web Crypto (`globalThis.crypto.subtle` + `randomUUID`) and\n * `TextEncoder`, so it runs on Node 20+, edge/Workers, Deno, and Bun with zero runtime deps and no\n * `@loyalty/*` import. UTF-8 encoding via `TextEncoder` matches Node's default string encoding in\n * the engine's `createHmac(...).update(string)`, so multibyte bodies sign identically.\n */\n\n/** The signature scheme version prefix — matches the engine's `SIGNATURE_VERSION`. */\nexport const SIGNATURE_SCHEME = 'v1';\n\n/** Header carrying the `v1=<hex>` HMAC signature. */\nexport const SIGNATURE_HEADER = 'X-Loyalty-Signature';\n/** Header carrying the unix-seconds timestamp that is part of the signed payload. */\nexport const TIMESTAMP_HEADER = 'X-Loyalty-Timestamp';\n/** Header carrying the per-attempt replay nonce (not signed). */\nexport const NONCE_HEADER = 'X-Loyalty-Nonce';\n\nexport interface SignEventOptions {\n /**\n * The unix-SECONDS timestamp to sign with. A number is stringified; a string is used verbatim.\n * Defaults to `Math.floor(now() / 1000)`. Injectable so tests are deterministic.\n */\n timestamp?: string | number;\n /** The replay nonce. Defaults to `crypto.randomUUID()`. Injectable for deterministic tests. */\n nonce?: string;\n /** Millisecond clock used only to derive a default `timestamp`. Defaults to `Date.now`. */\n now?: () => number;\n}\n\n/** The three signing headers, ready to spread into a request's `headers`. */\nexport interface SignedEventHeaders {\n 'X-Loyalty-Signature': string;\n 'X-Loyalty-Timestamp': string;\n 'X-Loyalty-Nonce': string;\n}\n\nexport interface SignedEvent {\n /** `v1=<hex>` — the HMAC-SHA256 of `${timestamp}.${rawBody}` under the signing secret. */\n signature: string;\n /** The unix-seconds timestamp that was signed (echoed as `X-Loyalty-Timestamp`). */\n timestamp: string;\n /** The replay nonce (echoed as `X-Loyalty-Nonce`). */\n nonce: string;\n /** The complete set of signing headers to attach to the `POST /v1/events` request. */\n headers: SignedEventHeaders;\n}\n\n/** The canonical string the engine signs: `${timestamp}.${rawBody}` (timestamp, dot, raw body). */\nfunction canonicalMessage(timestamp: string, rawBody: string): string {\n return `${timestamp}.${rawBody}`;\n}\n\n/** Resolve the Web Crypto instance, failing loudly on a runtime that lacks it. */\nfunction webCrypto(): Crypto {\n const c = globalThis.crypto;\n if (!c || !c.subtle) {\n throw new Error(\n 'Web Crypto (globalThis.crypto.subtle) is unavailable in this runtime; @tesseraloyalty/sdk event ' +\n 'signing requires Node 20+, an edge/Workers runtime, Deno, or Bun.',\n );\n }\n return c;\n}\n\n/** Lowercase-hex encode raw bytes (matches Node's `digest('hex')`). */\nfunction toHex(bytes: ArrayBuffer): string {\n const view = new Uint8Array(bytes);\n let out = '';\n for (let i = 0; i < view.length; i += 1) {\n out += view[i].toString(16).padStart(2, '0');\n }\n return out;\n}\n\n/** HMAC-SHA256 of `message` under `secret`, both UTF-8 encoded, returned as lowercase hex. */\nasync function hmacSha256Hex(secret: string, message: string): Promise<string> {\n const encoder = new TextEncoder();\n const key = await webCrypto().subtle.importKey(\n 'raw',\n encoder.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n const signature = await webCrypto().subtle.sign('HMAC', key, encoder.encode(message));\n return toHex(signature);\n}\n\n/**\n * Sign a raw event body for `POST /v1/events`.\n *\n * @param rawBody The exact request body string that will be sent — sign and send the SAME bytes;\n * re-serializing after signing invalidates the signature.\n * @param signingSecret The per-tenant connector signing secret (`whsec_...`).\n * @param opts Injectable `timestamp` / `nonce` / `now` (defaults: unix-seconds now, a random UUID).\n * @returns The `v1=<hex>` signature plus the timestamp, nonce, and the ready-to-attach headers.\n */\nexport async function signEventBody(\n rawBody: string,\n signingSecret: string,\n opts: SignEventOptions = {},\n): Promise<SignedEvent> {\n const nowMs = opts.now ? opts.now() : Date.now();\n const timestamp =\n opts.timestamp !== undefined ? String(opts.timestamp) : String(Math.floor(nowMs / 1000));\n const nonce = opts.nonce ?? webCrypto().randomUUID();\n\n const hex = await hmacSha256Hex(signingSecret, canonicalMessage(timestamp, rawBody));\n const signature = `${SIGNATURE_SCHEME}=${hex}`;\n\n return {\n signature,\n timestamp,\n nonce,\n headers: {\n 'X-Loyalty-Signature': signature,\n 'X-Loyalty-Timestamp': timestamp,\n 'X-Loyalty-Nonce': nonce,\n },\n };\n}\n","import type { KeyType } from '../client';\nimport { TesseraError } from '../errors';\nimport { HttpCore } from '../http';\nimport { assertSecretKey } from '../internal/scope';\nimport { toIso } from '../internal/serialize';\nimport { signEventBody } from '../signing';\n\n/**\n * The `client.events` resource — the SIGNED, idempotent front door for inbound events\n * (`POST /v1/events`). A merchant/connector constructs an event envelope (a purchase or refund) and\n * the SDK HMAC-signs it and posts it; the engine authenticates the signature, dedupes, resolves the\n * customer, and enqueues earn/refund. Ledger correctness lives in the engine — this resource only\n * signs and transmits.\n *\n * ## The sign==sent invariant (the whole risk of this resource)\n * The signature covers the LITERAL request body bytes (`${timestamp}.${rawBody}`, mirrored from\n * `packages/engine/src/crypto.ts`). So the bytes signed MUST equal the bytes sent — a re-serialized\n * body (key reordering / whitespace) invalidates the signature and the API 401s. This resource\n * guarantees byte-identity structurally:\n * 1. Serialize the wire payload to a raw JSON string EXACTLY ONCE (`JSON.stringify`).\n * 2. Sign THAT string (`signEventBody(rawBody, signingSecret)`).\n * 3. Transmit THAT SAME string verbatim via the HTTP core's `rawBody` path — which never\n * re-serializes (contrast the object `body` path, which `JSON.stringify`s) — with the three\n * returned signing headers.\n * The one string produced in (1) is the one thing signed and the one thing sent; there is no second\n * serialization anywhere in between.\n *\n * ## Guards (both PRE-NETWORK)\n * - SECRET-scope: signed ingestion is a server-to-server write; a publishable-key client fails fast\n * (mirrors the customer/redemption write guards).\n * - `signingSecret` REQUIRED: signing needs the per-tenant connector secret (`whsec_...`), which is\n * SEPARATE from the bearer API key. A client constructed without one throws a specific config\n * error before any request — never a silent unsigned POST that the API would only reject as a 401.\n *\n * The SDK owns its public camelCase types; the API request body is snake_case (`ingestEventSchema` in\n * `packages/shared`, parsed by `apps/api` `POST /v1/events`). Fields are mapped explicitly; the\n * top-level wire is `.strict()`, so only known keys are emitted. `amount` is intentionally open (the\n * engine reads the known bases and strips the rest), so extra breakdown fields are forwarded verbatim.\n */\n\n// ── Public types (camelCase) ────────────────────────────────────────────────────────────────────\n\n/**\n * The event types the operations API accepts at its boundary. Phase 1 INGESTS `purchase` and\n * `refund` (others resolve to an `unsupported_event_type` 422 → {@link UnsupportedEventTypeError});\n * the wider vocabulary is typed here for forward-compatibility with the server schema.\n */\nexport type EventType = 'purchase' | 'refund' | 'referral' | 'review' | 'profile';\n\n/**\n * The monetary breakdown for an event. The named fields are what the engine reads (purchase earn\n * bases; refund `refundedBase`/`orderBase`, minor units). It is deliberately OPEN — connectors\n * legitimately attach extra breakdown fields (tax/shipping/discounts/fees) which the engine strips;\n * any extra key is forwarded verbatim (use the wire snake_case name for a field you want preserved).\n */\nexport interface EventAmount {\n /** Gross order total, minor units. */\n gross?: number;\n /** Order total net of tax + shipping, minor units. */\n netOfTaxShipping?: number;\n /** Order total after discounts, minor units. */\n finalAfterDiscounts?: number;\n /** ISO-4217 currency code (3 letters). */\n currency?: string;\n /** Refund events: this refund's amount, minor units (same denomination as the earn base). */\n refundedBase?: number;\n /** Refund events: the ORIGINAL order's earn base, minor units (pinned on the first refund). */\n orderBase?: number;\n /** Extra breakdown fields, forwarded verbatim; the engine reads only the known bases. */\n [extra: string]: number | string | undefined;\n}\n\n/**\n * The event envelope a merchant/connector constructs to ingest. `type` + the core provenance\n * (`channel`, `externalId`, `sourceRef`, `idempotencyKey`, `occurredAt`) are always required; the\n * rest are type-specific (purchase: `trigger`/`orderRef`/`amount` earn bases; refund:\n * `orderRef`/`amount.refundedBase`/`amount.orderBase`). `(channel, externalId)` resolves the\n * pseudonymous customer server-side (never a bare external id, invariant #1).\n */\nexport interface IngestEventInput {\n /** The event type (`purchase` | `refund` in Phase 1). */\n type: EventType;\n /** The channel this event originated on (also the customer-resolution + provenance axis). */\n channel: string;\n /** The merchant-facing external id keying the customer on `channel`. */\n externalId: string;\n /** The external object id (e.g. the order id) — the business-dedup axis + ledger provenance. */\n sourceRef: string;\n /**\n * The idempotency key (namespace the channel in, e.g. `shopify:ord_5512-purchase`). This value in\n * the SIGNED BODY is the canonical dedup key; the `Idempotency-Key` header, when sent, must echo it.\n */\n idempotencyKey: string;\n /** When the event occurred — an ISO-8601 string or a `Date` (serialized to ISO on the wire). */\n occurredAt: string | Date;\n /** Optional defense-in-depth tenant id; when present it MUST equal the authenticated key's tenant. */\n tenant?: string;\n /** The earn trigger (e.g. `orders/paid`), captured for the Earn Engine. */\n trigger?: string;\n /** The originating order id (required for a `refund`; maps a refund to the original accrual). */\n orderRef?: string;\n /** The monetary breakdown (earn bases for a purchase; refund/order bases for a refund). */\n amount?: EventAmount;\n /** Optional email carried for identity resolution (feature 27). */\n email?: string;\n /** Optional phone carried for identity resolution (feature 27). */\n phone?: string;\n}\n\n/** Per-call options for `ingest`. */\nexport interface IngestEventOptions {\n /**\n * Sets the `Idempotency-Key` HEADER. Optional: the canonical dedup key is the SIGNED BODY's\n * `idempotencyKey`. When provided it MUST equal that body value (the API rejects a mismatch with\n * `invalid_payload`); use it only to make transport-layer replay explicit.\n */\n idempotencyKey?: string;\n}\n\n/**\n * The result of ingesting an event (`POST /v1/events`). `status` is `accepted` on the first sighting\n * (HTTP 201) or `duplicate` on a deduped redelivery (HTTP 200). On a duplicate, `customerCreated` and\n * `enqueued` are `false` (no new customer, no new earn job).\n */\nexport interface IngestEventResult {\n eventId: string;\n customerId: string;\n channel: string;\n sourceRef: string;\n type: string;\n /** `accepted` (201, first sighting) | `duplicate` (200, deduped redelivery). */\n status: 'accepted' | 'duplicate';\n /** `true` when Identity Resolution minted a new customer for this event. */\n customerCreated: boolean;\n /** How the customer was linked (`channel` today; email/phone match once feature 27 lands). */\n linkedVia: string;\n /** `true` when this call enqueued an earn job (only on `accepted`; `false` on `duplicate`). */\n enqueued: boolean;\n}\n\n// ── Wire types (snake_case, mirrors apps/api POST /v1/events + ingestEventSchema) ─────────────────\n\ninterface IngestEventResultWire {\n event_id: string;\n customer_id: string;\n channel: string;\n source_ref: string;\n type: string;\n status: 'accepted' | 'duplicate';\n customer_created: boolean;\n linked_via: string;\n enqueued: boolean;\n}\n\n// ── Mappers (public camelCase → snake_case wire, and back) ───────────────────────────────────────\n\n/** The named camelCase `amount` keys → their snake_case wire names. Unlisted keys pass through verbatim. */\nconst AMOUNT_KEY_MAP: Record<string, string> = {\n gross: 'gross',\n netOfTaxShipping: 'net_of_tax_shipping',\n finalAfterDiscounts: 'final_after_discounts',\n currency: 'currency',\n refundedBase: 'refunded_base',\n orderBase: 'order_base',\n};\n\n/**\n * Serialize `amount` camelCase → snake_case. Known keys are renamed; any EXTRA key is forwarded\n * verbatim (the engine's `amount` is non-strict and reads only the known bases). `undefined` values\n * are dropped so `JSON.stringify` never emits them.\n */\nfunction amountToWire(amount: EventAmount): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(amount)) {\n if (value === undefined) continue;\n out[AMOUNT_KEY_MAP[key] ?? key] = value;\n }\n return out;\n}\n\n/**\n * Build the snake_case wire body. The top-level `ingestEventSchema` is `.strict()`, so ONLY the known\n * keys are emitted (explicit mapping, no passthrough) — an out-of-vocabulary top-level key would 422.\n * Optional keys are included only when present, so `JSON.stringify` produces a minimal canonical body.\n */\nfunction toWireBody(input: IngestEventInput): Record<string, unknown> {\n const wire: Record<string, unknown> = {\n type: input.type,\n channel: input.channel,\n external_id: input.externalId,\n source_ref: input.sourceRef,\n idempotency_key: input.idempotencyKey,\n occurred_at: toIso(input.occurredAt),\n };\n if (input.tenant !== undefined) wire.tenant = input.tenant;\n if (input.trigger !== undefined) wire.trigger = input.trigger;\n if (input.orderRef !== undefined) wire.order_ref = input.orderRef;\n if (input.amount !== undefined) wire.amount = amountToWire(input.amount);\n if (input.email !== undefined) wire.email = input.email;\n if (input.phone !== undefined) wire.phone = input.phone;\n return wire;\n}\n\nfunction toResult(w: IngestEventResultWire): IngestEventResult {\n return {\n eventId: w.event_id,\n customerId: w.customer_id,\n channel: w.channel,\n sourceRef: w.source_ref,\n type: w.type,\n status: w.status,\n customerCreated: w.customer_created,\n linkedVia: w.linked_via,\n enqueued: w.enqueued,\n };\n}\n\n/** Everything an {@link EventsResource} needs from its parent client, passed explicitly (no cycle). */\nexport interface EventsDeps {\n http: HttpCore;\n keyType: KeyType;\n /** The per-tenant connector signing secret (`whsec_...`); `undefined` when not configured. */\n signingSecret: string | undefined;\n}\n\nexport class EventsResource {\n private readonly http: HttpCore;\n private readonly keyType: KeyType;\n private readonly signingSecret: string | undefined;\n\n constructor(deps: EventsDeps) {\n this.http = deps.http;\n this.keyType = deps.keyType;\n this.signingSecret = deps.signingSecret;\n }\n\n /** Fail fast pre-network when signed ingestion is attempted with a publishable key. */\n private requireSecret(): void {\n assertSecretKey(\n this.keyType,\n 'Event ingestion requires a secret key. The client is configured with a publishable key ' +\n '(catalogue-only). Construct the client with `secretKey`.',\n );\n }\n\n /**\n * Fail fast pre-network when no signing secret is configured — signing is impossible without the\n * per-tenant connector secret (`whsec_...`), which is SEPARATE from the bearer API key. Throwing\n * here (rather than sending an unsigned body the API would 401) gives a clear, specific diagnosis.\n */\n private requireSigningSecret(): string {\n if (this.signingSecret) return this.signingSecret;\n throw new TesseraError({\n code: 'config_error',\n message:\n 'Event ingestion requires a `signingSecret` (the per-tenant connector HMAC secret, ' +\n '`whsec_...`) — it is separate from the API key. Construct the client with `signingSecret`.',\n });\n }\n\n /**\n * Ingest a signed event (`POST /v1/events`). SECRET-scope + requires a `signingSecret` (both fail\n * fast pre-network). Serializes the payload to a raw JSON string ONCE, HMAC-signs THAT string, and\n * transmits THE SAME string verbatim with the `X-Loyalty-Signature`/`-Timestamp`/`-Nonce` headers —\n * so the bytes signed are byte-identical to the bytes sent (see the resource doc). Returns the\n * ingestion result mapped to camelCase (`accepted` 201 / `duplicate` 200).\n */\n async ingest(input: IngestEventInput, opts: IngestEventOptions = {}): Promise<IngestEventResult> {\n this.requireSecret();\n const signingSecret = this.requireSigningSecret();\n\n // (1) Serialize the wire payload EXACTLY ONCE. This one string is both what we sign and what we\n // send — there is no second serialization between here and the socket.\n const rawBody = JSON.stringify(toWireBody(input));\n // (2) Sign the literal string. `signed.headers` carries v1=<hex> + the timestamp it signed + nonce.\n const signed = await signEventBody(rawBody, signingSecret);\n\n // (3) Transmit the SAME string verbatim via the rawBody path (never re-serialized), with the\n // signing headers and — only when the caller supplied one — the Idempotency-Key header.\n const wire = await this.http.request<IngestEventResultWire>('POST', '/events', {\n keyScope: 'secret',\n rawBody,\n // Spread into a fresh object literal so the fixed-key `SignedEventHeaders` interface widens to\n // the transport's `Record<string, string>` header bag.\n headers: { ...signed.headers },\n idempotencyKey: opts.idempotencyKey,\n });\n return toResult(wire);\n }\n}\n","import { TesseraError } from './errors';\nimport { HttpCore, type FetchLike } from './http';\nimport { RewardsResource } from './resources/rewards';\nimport { MemberHandle, type MemberOptions } from './resources/members';\nimport { CustomersResource } from './resources/customers';\nimport { RedemptionsResource } from './resources/redemptions';\nimport { EventsResource } from './resources/events';\n\n/**\n * `TesseraClient` — the SDK entry point. It validates + resolves configuration\n * (exactly one of secret/publishable key → the bearer token), binds the channel,\n * holds the signing / transport settings later tickets consume, and wires the HTTP core.\n *\n * ## Channel binding (the typo-split defense — design §5.2)\n * Customer-scoped operations take NO per-call `channel`: it is bound once, here, so\n * the \"`'shopify'` here, `'shoify'` there\" identity/ledger split is structurally\n * impossible. Three layers, all enforced by this class:\n * 1. **Bound channel** — set once (default `'api'`); `withChannel(x)` derives a\n * channel-scoped client for the rare legitimate multi-channel case.\n * 2. **`channels` allowlist** — if provided, the bound channel, every `withChannel`\n * target, and every `resolveChannel(target)` (used later by `linkChannel`) must\n * be a member, else an immediate PRE-NETWORK throw listing the known channels.\n * 3. **TS union** — the client is generic over the channel literal union `Ch`,\n * inferred from a `channels: [...] as const`, so a typo fails to COMPILE.\n *\n * The publishable-vs-secret per-endpoint guard is still deferred (a later ticket);\n * the resource groups and the member handle land later and read the bound channel\n * via `resolveChannel()`.\n */\n\nexport type KeyType = 'secret' | 'publishable';\n\n/** The production `/v1` API base — the default. Overridable via `baseUrl` (tests/demo point at a local URL). */\nexport const DEFAULT_BASE_URL = 'https://api.usetessera.io/v1';\nexport const DEFAULT_TIMEOUT_MS = 30_000;\nexport const DEFAULT_MAX_RETRIES = 2;\nexport const DEFAULT_CHANNEL = 'api';\n\n/**\n * Configuration for {@link TesseraClient}. Generic over the channel literal union\n * `Ch`, which is inferred from a `channels: [...] as const` allowlist so that a typo\n * in `channel` fails to compile. When `channels` is omitted, `Ch` widens to `string`\n * (any channel is accepted).\n */\nexport interface TesseraClientConfig<Ch extends string = string> {\n /** The secret bearer key — unlocks every operation. Provide exactly one of secret/publishable. */\n secretKey?: string;\n /** The publishable bearer key — catalogue-only. Provide exactly one of secret/publishable. */\n publishableKey?: string;\n /** The per-tenant connector HMAC secret (events signing, TER-87). Never equal to the bearer key. */\n signingSecret?: string;\n /**\n * The channel bound to customer-scoped operations. Defaults to `'api'`. Must be a\n * member of `channels` when that allowlist is provided. Wrapped in `NoInfer` so the\n * channel union is driven ONLY by `channels` — a lone `channel` never narrows `Ch`.\n */\n channel?: NoInfer<Ch>;\n /**\n * Optional channel vocabulary. When provided it is enforced two ways: at runtime\n * (an out-of-list channel throws pre-network) and, via `as const`, at compile time\n * (`Ch` narrows to the literal union so `withChannel('typo')` is a type error).\n */\n channels?: readonly Ch[];\n /** The `/v1` base URL. Defaults to the production constant; overridable for local/dev. */\n baseUrl?: string;\n /** Per-request timeout in milliseconds. Default 30000. */\n timeoutMs?: number;\n /** Maximum retries on retriable failures (network / 5xx / 429). Default 2. */\n maxRetries?: number;\n /** A custom `fetch` implementation. Defaults to `globalThis.fetch`. */\n fetch?: FetchLike;\n}\n\nexport class TesseraClient<Ch extends string = string> {\n /** Whether this client authenticates with a secret or publishable key. */\n readonly keyType: KeyType;\n /** The channel bound to customer-scoped operations (default `'api'`). */\n readonly channel: Ch;\n /** The optional channel allowlist. When set, every channel used must be a member. */\n readonly channels?: readonly Ch[];\n /** The per-tenant events signing secret, if configured (used by TER-87). */\n readonly signingSecret?: string;\n /** The resolved `/v1` base URL. */\n readonly baseUrl: string;\n /** The per-request timeout in milliseconds. */\n readonly timeoutMs: number;\n /** The maximum retry count on retriable failures. */\n readonly maxRetries: number;\n /**\n * @internal The shared HTTP transport. Resource groups (later tickets) issue their\n * requests through this; it is not part of the documented public surface.\n */\n readonly http: HttpCore;\n\n constructor(config: TesseraClientConfig<Ch>) {\n const hasSecret = nonEmpty(config.secretKey);\n const hasPublishable = nonEmpty(config.publishableKey);\n if (hasSecret === hasPublishable) {\n throw new TesseraError({\n code: 'config_error',\n message: hasSecret\n ? 'Provide exactly one of `secretKey` or `publishableKey` — not both.'\n : 'A `secretKey` or `publishableKey` is required to construct a TesseraClient.',\n });\n }\n this.keyType = hasSecret ? 'secret' : 'publishable';\n const token = (hasSecret ? config.secretKey : config.publishableKey) as string;\n\n this.signingSecret = config.signingSecret;\n this.channels = config.channels;\n\n // Bind + enforce the channel BEFORE any transport wiring, so an out-of-vocabulary\n // channel is a pre-network throw. The default `'api'` is validated too: an allowlist\n // that omits `'api'` requires the caller to name a channel explicitly.\n const channel = (config.channel ?? DEFAULT_CHANNEL) as Ch;\n assertChannelAllowed(channel, this.channels);\n this.channel = channel;\n\n this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;\n this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;\n\n const fetchImpl: FetchLike | undefined = config.fetch ?? globalThis.fetch;\n if (!fetchImpl) {\n throw new TesseraError({\n code: 'config_error',\n message:\n 'No global `fetch` is available in this runtime. Pass a `fetch` implementation in the TesseraClient config.',\n });\n }\n\n this.http = new HttpCore({\n baseUrl: this.baseUrl,\n token,\n timeoutMs: this.timeoutMs,\n maxRetries: this.maxRetries,\n fetchImpl,\n });\n }\n\n /**\n * Derive a NEW client bound to `channel`, for the rare legitimate multi-channel\n * case. It SHARES this client's auth, transport (the very same {@link HttpCore}\n * instance — never rebuilt), signing secret, and `channels` allowlist; the original\n * is never mutated. Throws pre-network (before any request) if `channel` is outside\n * the allowlist. `channel` is typed against `Ch`, so a typo fails to compile.\n */\n withChannel(channel: Ch): TesseraClient<Ch> {\n assertChannelAllowed(channel, this.channels);\n // Clone off the prototype so the shared HTTP core / auth are reused verbatim —\n // reconstructing via `new` would rebuild the transport and re-run key validation.\n const scoped = Object.create(TesseraClient.prototype) as Mutable<TesseraClient<Ch>>;\n scoped.keyType = this.keyType;\n scoped.channel = channel;\n scoped.channels = this.channels;\n scoped.signingSecret = this.signingSecret;\n scoped.baseUrl = this.baseUrl;\n scoped.timeoutMs = this.timeoutMs;\n scoped.maxRetries = this.maxRetries;\n scoped.http = this.http;\n return scoped as TesseraClient<Ch>;\n }\n\n /**\n * The public rewards catalogue resource (`GET /v1/rewards`). Allowed with a publishable OR a\n * secret key — the one read a storefront/browser client may make. A fresh (stateless) resource\n * is returned per access so channel-scoped clones (`withChannel`) inherit it for free.\n */\n get rewards(): RewardsResource {\n return new RewardsResource(this.http);\n }\n\n /**\n * The customer/identity WRITE + server-side PII resource (`upsert` / `update` / `getPii` /\n * `linkChannel` / `unlinkChannel`). Every operation is secret-scope; a publishable-key client\n * fails fast pre-network. A fresh (stateless) resource is returned per access, wired with the\n * shared transport + this client's channel resolver so `withChannel` clones inherit it correctly.\n */\n get customers(): CustomersResource {\n return new CustomersResource({\n http: this.http,\n keyType: this.keyType,\n resolveChannel: (channel?: string) => this.resolveChannel(channel),\n });\n }\n\n /**\n * The redemption burn-hold lifecycle resource (`place` / `get` / `finalize` / `release`). Every\n * operation is secret-scope; a publishable-key client fails fast pre-network. A fresh (stateless)\n * resource is returned per access, wired with the shared transport + this client's channel\n * resolver so `withChannel` clones inherit it correctly.\n */\n get redemptions(): RedemptionsResource {\n return new RedemptionsResource({\n http: this.http,\n keyType: this.keyType,\n resolveChannel: (channel?: string) => this.resolveChannel(channel),\n });\n }\n\n /**\n * The signed event-ingestion resource (`ingest` → `POST /v1/events`). Every ingestion is\n * SECRET-scope AND requires a `signingSecret` (the per-tenant connector HMAC secret, separate from\n * the bearer key) — both fail fast pre-network. A fresh (stateless) resource is returned per access,\n * wired with the shared transport + this client's signing secret so `withChannel` clones inherit it.\n */\n get events(): EventsResource {\n return new EventsResource({\n http: this.http,\n keyType: this.keyType,\n signingSecret: this.signingSecret,\n });\n }\n\n /**\n * Open a {@link MemberHandle} for a member identified by `externalId` on this client's bound\n * channel — the ergonomic `(channel, externalId)`-keyed read + redeem surface (`get` / `balance` /\n * `status` / `history` / `redeem`). The channel comes from {@link resolveChannel}: omit\n * `opts.channel` to use the bound channel, or pass `opts.channel` to override it (validated against\n * the `channels` allowlist, throwing PRE-NETWORK on an out-of-vocabulary value). All member\n * operations are secret-scope; a publishable-key client fails fast when one is attempted.\n */\n member(externalId: string, opts?: MemberOptions): MemberHandle {\n const channel = this.resolveChannel(opts?.channel);\n return new MemberHandle({\n http: this.http,\n keyType: this.keyType,\n channel,\n externalId,\n redemptions: this.redemptions,\n });\n }\n\n /**\n * @internal Resolve the channel for a customer-scoped operation. Called with no\n * argument it returns the bound channel (the member reads / redeem path). Called\n * with a `channel` (e.g. the target of `linkChannel`) it validates that value\n * against the `channels` allowlist and returns it — throwing pre-network on an\n * out-of-vocabulary channel. Kept internal: resource groups consume it; it is not\n * part of the documented public surface.\n */\n resolveChannel(channel?: string): string {\n if (channel === undefined) return this.channel;\n assertChannelAllowed(channel, this.channels);\n return channel;\n }\n}\n\nfunction nonEmpty(value: string | undefined): value is string {\n return typeof value === 'string' && value.length > 0;\n}\n\n/** Strips `readonly` so the prototype-clone in `withChannel` can assign the fields. */\ntype Mutable<T> = { -readonly [K in keyof T]: T[K] };\n\n/**\n * Enforce the `channels` allowlist: a no-op when no allowlist is configured or when\n * `channel` is a member; otherwise a pre-network `TesseraError` naming the offending\n * value and the known vocabulary. The single choke point for all three call sites\n * (constructor, `withChannel`, `resolveChannel`).\n */\nfunction assertChannelAllowed(channel: string, allowlist: readonly string[] | undefined): void {\n if (!allowlist || allowlist.includes(channel)) return;\n throw new TesseraError({\n code: 'config_error',\n message:\n `Channel \"${channel}\" is not in this client's configured \\`channels\\` allowlist. ` +\n `Known channels: ${allowlist.join(', ')}.`,\n });\n}\n","/**\n * `@tesseraloyalty/sdk` — the official TypeScript SDK for the Tessera loyalty platform's\n * public operations API (`/v1`).\n *\n * This entry point exports the client, the error taxonomy, the public config types, the READ\n * resources (the rewards catalogue + the `member(externalId)` handle), the `customers` identity/PII\n * resource, the `redemptions` burn-hold lifecycle (`place`/`get`/`finalize`/`release` +\n * `member.redeem`), the `events` signed-ingestion resource (`ingest` → `POST /v1/events`), and the\n * `signEventBody` HMAC signer.\n *\n * Runtime-self-contained: it uses only global Web standards (`fetch` + friends), so\n * it runs on Node 20+, edge/Workers, Deno, and Bun with zero runtime dependencies.\n */\n\nexport { TesseraClient } from './client';\nexport type { TesseraClientConfig, KeyType } from './client';\n\n// Read resources (TER-89): the public rewards catalogue + the member(externalId) read handle.\nexport { RewardsResource } from './resources/rewards';\nexport type {\n RewardsCatalogue,\n RewardOption,\n RewardOptionConstraints,\n} from './resources/rewards';\nexport { MemberHandle, HistoryPager } from './resources/members';\nexport type {\n MemberOptions,\n MemberRedeemInput,\n MemberProfile,\n MemberBalance,\n MemberTier,\n MemberStatus,\n QualifyingProgress,\n HistoryEntry,\n HistoryPage,\n HistoryPageInfo,\n HistoryOptions,\n} from './resources/members';\n\n// Redemption burn-hold lifecycle resource (TER-92): place / get / finalize / release + member.redeem.\nexport { RedemptionsResource } from './resources/redemptions';\nexport type {\n PlaceRedemptionInput,\n PlaceRedemptionOptions,\n RedemptionResult,\n RedemptionStatus,\n RedemptionHandle,\n FinalizeRedemptionInput,\n FinalizeRedemptionResult,\n ReleaseRedemptionResult,\n} from './resources/redemptions';\n\n// Signed event ingestion resource (TER-91): events.ingest → POST /v1/events (HMAC-signed body).\nexport { EventsResource } from './resources/events';\nexport type {\n EventType,\n EventAmount,\n IngestEventInput,\n IngestEventOptions,\n IngestEventResult,\n} from './resources/events';\n\n// Customer/identity write + PII resource (TER-90): upsert / update / getPii / link / unlink.\nexport { CustomersResource } from './resources/customers';\nexport type {\n ConsentState,\n MarketingConsent,\n CustomerProfileFields,\n CustomerUpsertInput,\n CustomerUpsertOptions,\n CustomerUpsertResult,\n CustomerPii,\n LinkChannelInput,\n LinkChannelResult,\n UnlinkChannelResult,\n} from './resources/customers';\n\nexport {\n // Base + factory + map\n TesseraError,\n errorFromEnvelope,\n CODE_TO_ERROR,\n // Typed subclasses (catch by type; keyed off the envelope `code`)\n ValidationError,\n UnauthorizedError,\n SignatureInvalidError,\n ReplayDetectedError,\n ForbiddenError,\n TierGatedError,\n PublishableKeyForbiddenError,\n NotFoundError,\n CustomerNotFoundError,\n RedemptionNotFoundError,\n ConflictError,\n IdempotencyConflictError,\n VersionConflictError,\n CapReachedError,\n NotStackableError,\n HoldAlreadyReleasedError,\n CustomerErasedError,\n PayloadTooLargeError,\n InsufficientBalanceError,\n OptionNotEligibleError,\n InvalidPayloadError,\n UnsupportedEventTypeError,\n ProfileValidationError,\n RateLimitedError,\n InternalError,\n NotImplementedError,\n} from './errors';\nexport type { TesseraErrorInit } from './errors';\n\nexport type { FetchLike } from './http';\n\nexport {\n signEventBody,\n SIGNATURE_SCHEME,\n SIGNATURE_HEADER,\n TIMESTAMP_HEADER,\n NONCE_HEADER,\n} from './signing';\nexport type { SignEventOptions, SignedEvent, SignedEventHeaders } from './signing';\n\n/** The published version of this SDK. Kept in sync with `package.json` at release. */\nexport const VERSION = '0.1.0';\n\n/** The major version of the Tessera operations API this SDK targets. */\nexport const API_VERSION = 'v1';\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tesseraloyalty/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Official TypeScript SDK for the Tessera loyalty platform operations API (/v1).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public",
|
|
9
|
+
"provenance": true
|
|
10
|
+
},
|
|
11
|
+
"main": "./dist/index.cjs",
|
|
12
|
+
"module": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js",
|
|
18
|
+
"require": "./dist/index.cjs"
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsup",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"lint": "eslint .",
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"test:integration": "vitest run --config vitest.integration.config.ts --passWithNoTests"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@loyalty/api": "workspace:*",
|
|
37
|
+
"@loyalty/db": "workspace:*",
|
|
38
|
+
"@loyalty/engine": "workspace:*",
|
|
39
|
+
"tsup": "^8",
|
|
40
|
+
"vitest": "^3"
|
|
41
|
+
}
|
|
42
|
+
}
|