@ozura/elements 0.1.0-beta.7 → 1.0.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.
Files changed (50) hide show
  1. package/README.md +1121 -887
  2. package/dist/frame/element-frame.js +77 -57
  3. package/dist/frame/element-frame.js.map +1 -1
  4. package/dist/frame/tokenizer-frame.html +1 -1
  5. package/dist/frame/tokenizer-frame.js +206 -85
  6. package/dist/frame/tokenizer-frame.js.map +1 -1
  7. package/dist/oz-elements.esm.js +806 -230
  8. package/dist/oz-elements.esm.js.map +1 -1
  9. package/dist/oz-elements.umd.js +806 -229
  10. package/dist/oz-elements.umd.js.map +1 -1
  11. package/dist/react/frame/tokenizerFrame.d.ts +32 -0
  12. package/dist/react/index.cjs.js +957 -218
  13. package/dist/react/index.cjs.js.map +1 -1
  14. package/dist/react/index.esm.js +954 -219
  15. package/dist/react/index.esm.js.map +1 -1
  16. package/dist/react/react/index.d.ts +148 -6
  17. package/dist/react/sdk/OzElement.d.ts +34 -3
  18. package/dist/react/sdk/OzVault.d.ts +68 -4
  19. package/dist/react/sdk/errors.d.ts +9 -0
  20. package/dist/react/sdk/index.d.ts +29 -0
  21. package/dist/react/server/index.d.ts +168 -10
  22. package/dist/react/types/index.d.ts +65 -16
  23. package/dist/react/utils/appearance.d.ts +9 -0
  24. package/dist/react/utils/cardUtils.d.ts +14 -0
  25. package/dist/react/utils/uuid.d.ts +12 -0
  26. package/dist/server/frame/tokenizerFrame.d.ts +32 -0
  27. package/dist/server/index.cjs.js +598 -67
  28. package/dist/server/index.cjs.js.map +1 -1
  29. package/dist/server/index.esm.js +596 -68
  30. package/dist/server/index.esm.js.map +1 -1
  31. package/dist/server/sdk/OzElement.d.ts +34 -3
  32. package/dist/server/sdk/OzVault.d.ts +68 -4
  33. package/dist/server/sdk/errors.d.ts +9 -0
  34. package/dist/server/sdk/index.d.ts +29 -0
  35. package/dist/server/server/index.d.ts +168 -10
  36. package/dist/server/types/index.d.ts +65 -16
  37. package/dist/server/utils/appearance.d.ts +9 -0
  38. package/dist/server/utils/cardUtils.d.ts +14 -0
  39. package/dist/server/utils/uuid.d.ts +12 -0
  40. package/dist/types/frame/tokenizerFrame.d.ts +32 -0
  41. package/dist/types/sdk/OzElement.d.ts +34 -3
  42. package/dist/types/sdk/OzVault.d.ts +68 -4
  43. package/dist/types/sdk/errors.d.ts +9 -0
  44. package/dist/types/sdk/index.d.ts +29 -0
  45. package/dist/types/server/index.d.ts +168 -10
  46. package/dist/types/types/index.d.ts +65 -16
  47. package/dist/types/utils/appearance.d.ts +9 -0
  48. package/dist/types/utils/cardUtils.d.ts +14 -0
  49. package/dist/types/utils/uuid.d.ts +12 -0
  50. package/package.json +7 -4
@@ -10,7 +10,7 @@ import { ElementType, BankElementType, ElementOptions, VaultOptions, TokenizeOpt
10
10
  * const vault = await OzVault.create({
11
11
  * pubKey: 'pk_live_...',
12
12
  * fetchWaxKey: async (sessionId) => {
13
- * // Call your backend — which calls ozura.mintWaxKey() from oz-elements/server
13
+ * // Call your backend — which calls ozura.mintWaxKey() from @ozura/elements/server
14
14
  * const { waxKey } = await fetch('/api/mint-wax', {
15
15
  * method: 'POST',
16
16
  * body: JSON.stringify({ sessionId }),
@@ -41,17 +41,25 @@ export declare class OzVault {
41
41
  private completionState;
42
42
  private tokenizerFrame;
43
43
  private tokenizerWindow;
44
- private tokenizerName;
45
44
  private tokenizerReady;
46
45
  private _tokenizing;
47
46
  private _destroyed;
47
+ private _tokenizeSuccessCount;
48
+ private _maxTokenizeCalls;
48
49
  private boundHandleMessage;
49
50
  private _pendingMount;
51
+ private _storedFetchWaxKey;
52
+ private _waxRefreshing;
53
+ private _onWaxRefresh;
54
+ private _onReady;
50
55
  private loadErrorTimeoutId;
56
+ private _hiddenAt;
57
+ private boundHandleVisibility;
51
58
  /**
52
59
  * Internal constructor — use `OzVault.create()` instead.
53
60
  * The constructor mounts the tokenizer iframe immediately so it can start
54
61
  * loading in parallel while `fetchWaxKey` is being awaited.
62
+ * @internal
55
63
  */
56
64
  private constructor();
57
65
  /**
@@ -69,15 +77,39 @@ export declare class OzVault {
69
77
  * The returned vault is ready to create elements immediately. `createToken()`
70
78
  * additionally requires `vault.isReady` (tokenizer iframe loaded).
71
79
  *
72
- * @throws {OzError} if `fetchWaxKey` throws or returns an empty string.
80
+ * @throws {OzError} if `fetchWaxKey` throws, returns a non-string value, or returns an empty/whitespace-only string.
73
81
  */
74
- static create(options: VaultOptions): Promise<OzVault>;
82
+ static create(options: VaultOptions, signal?: AbortSignal): Promise<OzVault>;
75
83
  /**
76
84
  * True once the hidden tokenizer iframe has loaded and signalled ready.
77
85
  * Use this to gate the pay button when building custom UIs without React.
78
86
  * React consumers should use the `ready` value returned by `useOzElements()`.
87
+ *
88
+ * Once `true`, remains `true` for the lifetime of this vault instance.
89
+ * It only reverts to `false` after `vault.destroy()` is called, at which
90
+ * point the vault is unusable and a new instance must be created.
91
+ *
92
+ * @remarks
93
+ * This tracks **tokenizer readiness only** — it says nothing about whether
94
+ * the individual element iframes (card number, CVV, etc.) have loaded.
95
+ * A vault can be `isReady === true` while elements are still mounting.
96
+ * To gate a submit button correctly in vanilla JS, wait for every element's
97
+ * `'ready'` event in addition to this flag. In React, use the `ready` value
98
+ * from `useOzElements()` instead, which combines both checks automatically.
79
99
  */
80
100
  get isReady(): boolean;
101
+ /**
102
+ * Number of successful tokenize calls made against the current wax key.
103
+ *
104
+ * Resets to `0` each time the wax key is refreshed (proactively or reactively).
105
+ * Useful in vanilla JS integrations to display "attempts remaining" UI.
106
+ * In React, use `tokenizeCount` from `useOzElements()` instead.
107
+ *
108
+ * @example
109
+ * const remaining = 3 - vault.tokenizeCount;
110
+ * payButton.textContent = remaining > 0 ? `Pay (${remaining} attempts left)` : 'Pay';
111
+ */
112
+ get tokenizeCount(): number;
81
113
  /**
82
114
  * Creates a new OzElement of the given type. Call `.mount(selector)` on the
83
115
  * returned element to attach it to the DOM.
@@ -129,6 +161,17 @@ export declare class OzVault {
129
161
  * unmounts (e.g. in React's useEffect cleanup or a SPA route change).
130
162
  */
131
163
  destroy(): void;
164
+ /**
165
+ * Proactively re-mints the wax key when the page becomes visible again after
166
+ * a long idle period. Wax keys have a fixed TTL (~30 minutes); a user who
167
+ * leaves the tab in the background and returns could have an expired key.
168
+ * Rather than waiting for a failed tokenization to trigger the reactive
169
+ * refresh path, this pre-empts the failure when the vault is idle.
170
+ *
171
+ * Threshold: 20 minutes hidden. Chosen to be comfortably inside the ~30m TTL
172
+ * while avoiding spurious refreshes for brief tab-switches.
173
+ */
174
+ private handleVisibilityChange;
132
175
  private mountTokenizerFrame;
133
176
  private handleMessage;
134
177
  /**
@@ -138,5 +181,26 @@ export declare class OzVault {
138
181
  */
139
182
  private handleElementChange;
140
183
  private handleTokenizerMessage;
184
+ /**
185
+ * Returns true when an OZ_TOKEN_ERROR should trigger a wax key refresh.
186
+ *
187
+ * Primary path: vault returns 401/403 → errorCode 'auth'.
188
+ * Defensive path: vault returns 400 → errorCode 'validation', but the raw
189
+ * message contains wax-key-specific language (consumed, expired, invalid key,
190
+ * etc.). This avoids a hard dependency on the vault returning a unified HTTP
191
+ * status for consumed-key vs expired-key failures — both should refresh.
192
+ *
193
+ * Deliberately excludes 'network', 'timeout', and 'server' codes (transient
194
+ * errors are already retried in fetchWithRetry) and 'unknown' (too broad).
195
+ */
196
+ private isRefreshableAuthError;
197
+ /**
198
+ * Re-mints the wax key using the stored fetchWaxKey callback and updates
199
+ * the tokenizer with the new key. Used for transparent auto-refresh when
200
+ * the vault returns an auth error on tokenization.
201
+ *
202
+ * Only one refresh runs at a time — concurrent retries share the same promise.
203
+ */
204
+ private refreshWaxKey;
141
205
  private sendToTokenizer;
142
206
  }
@@ -10,6 +10,8 @@
10
10
  * errorMapping.ts so the same error strings produce the same user-facing copy.
11
11
  */
12
12
  export type OzErrorCode = 'network' | 'timeout' | 'auth' | 'validation' | 'server' | 'config' | 'unknown';
13
+ /** Returns true and narrows to OzErrorCode when `value` is a valid member of the union. */
14
+ export declare function isOzErrorCode(value: unknown): value is OzErrorCode;
13
15
  export declare class OzError extends Error {
14
16
  /** The raw error string returned by the vault or cardSale API, if available. */
15
17
  readonly raw: string;
@@ -52,5 +54,12 @@ export declare function normalizeBankVaultError(raw: string): string;
52
54
  * Falls back to the original string when it's under 100 characters, or to a
53
55
  * generic message for long/opaque server errors — matching checkout's fallback
54
56
  * behaviour exactly.
57
+ *
58
+ * **Trade-off:** Short unrecognised strings (e.g. processor codes like
59
+ * `"PROC_TIMEOUT"`) are passed through verbatim. This intentionally mirrors
60
+ * checkout so the same raw Pay API errors produce the same user-facing text on
61
+ * both surfaces. If the Pay API ever returns internal codes that should never
62
+ * reach the UI, the fix belongs in the Pay API error normalisation layer rather
63
+ * than here.
55
64
  */
56
65
  export declare function normalizeCardSaleError(raw: string): string;
@@ -1,5 +1,34 @@
1
1
  export { OzVault } from './OzVault';
2
2
  export { OzElement } from './OzElement';
3
3
  export { OzError, normalizeVaultError, normalizeBankVaultError, normalizeCardSaleError } from './errors';
4
+ /**
5
+ * Creates a ready-to-use `fetchWaxKey` callback for `OzVault.create()` and `<OzElements>`.
6
+ *
7
+ * Calls your backend mint endpoint with `{ sessionId }` and returns the wax key string.
8
+ * Throws on non-OK responses or a missing `waxKey` field so the vault can surface the
9
+ * error through its normal error path.
10
+ *
11
+ * Each call enforces a 10-second per-attempt timeout. On a pure network-level
12
+ * failure (connection refused, DNS failure, etc.) the call is retried once after
13
+ * 750ms before throwing. HTTP errors (4xx/5xx) are never retried — they indicate
14
+ * an endpoint misconfiguration or an invalid key, not a transient failure.
15
+ *
16
+ * The mint endpoint is typically the one-line `createMintWaxHandler` / `createMintWaxMiddleware`
17
+ * from `@ozura/elements/server`.
18
+ *
19
+ * @param mintUrl - Absolute or relative URL of your wax-key mint endpoint, e.g. `'/api/mint-wax'`.
20
+ *
21
+ * @example
22
+ * // Vanilla JS
23
+ * const vault = await OzVault.create({
24
+ * pubKey: 'pk_live_...',
25
+ * fetchWaxKey: createFetchWaxKey('/api/mint-wax'),
26
+ * });
27
+ *
28
+ * @example
29
+ * // React
30
+ * <OzElements pubKey="pk_live_..." fetchWaxKey={createFetchWaxKey('/api/mint-wax')}>
31
+ */
32
+ export declare function createFetchWaxKey(mintUrl: string): (sessionId: string) => Promise<string>;
4
33
  export type { OzErrorCode } from './errors';
5
34
  export type { ElementType, BankElementType, ElementOptions, ElementStyleConfig, ElementStyle, ElementChangeEvent, VaultOptions, TokenizeOptions, BankTokenizeOptions, TokenResponse, BankTokenResponse, CardMetadata, BankAccountMetadata, FontSource, CssFontSource, CustomFontSource, BillingDetails, BillingAddress, CardSaleRequest, CardSaleResponseData, CardSaleApiResponse, Appearance, AppearanceVariables, OzTheme, TransactionQueryParams, TransactionQueryPagination, TransactionQueryResponse, TransactionType, CardTransactionType, AchTransactionType, CryptoTransactionType, TransactionBase, CardTransactionData, AchTransactionData, CryptoTransactionData, TransactionData, } from '../types';
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @example
8
8
  * ```ts
9
- * import { Ozura } from 'oz-elements/server';
9
+ * import { Ozura } from '@ozura/elements/server';
10
10
  *
11
11
  * const ozura = new Ozura({
12
12
  * merchantId: process.env.MERCHANT_ID!,
@@ -58,6 +58,37 @@ export interface MintWaxKeyResult {
58
58
  /** Seconds until the wax key expires. Typically 1800 (30 min). */
59
59
  expiresInSeconds: number;
60
60
  }
61
+ /**
62
+ * Options for {@link Ozura.mintWaxKey}.
63
+ */
64
+ export interface MintWaxKeyOptions {
65
+ /**
66
+ * SDK-generated session UUID forwarded from the `fetchWaxKey` callback.
67
+ * Stored by the vault for correlation and audit — not used for authentication.
68
+ */
69
+ tokenizationSessionId?: string;
70
+ /**
71
+ * Maximum number of tokenize calls this wax key will accept before the vault
72
+ * marks it as consumed. Once consumed, further tokenize attempts return an
73
+ * auth/validation error and the client SDK automatically re-mints a fresh key.
74
+ *
75
+ * Keep this value small (3–5) to limit the blast radius if a key is intercepted.
76
+ * Set the same value in `VaultOptions.maxTokenizeCalls` so the client SDK can
77
+ * proactively refresh before hitting the wall, avoiding a user-visible delay.
78
+ *
79
+ * @default 3
80
+ */
81
+ maxTokenizeCalls?: number;
82
+ /**
83
+ * Maximum number of proxy calls this wax key will accept.
84
+ * Proxy calls are distinct from tokenize calls — they cover non-tokenize vault
85
+ * operations. Leave `undefined` to use the vault's built-in default.
86
+ *
87
+ * In most integrations you only need `maxTokenizeCalls`; set this only if you
88
+ * are explicitly using vault proxy endpoints.
89
+ */
90
+ maxProxyCalls?: number;
91
+ }
61
92
  export interface CardSaleInput {
62
93
  /** From TokenResponse.token (frontend SDK). */
63
94
  token: string;
@@ -118,27 +149,34 @@ export declare class Ozura {
118
149
  */
119
150
  listTransactions(input?: ListTransactionsInput): Promise<ListTransactionsResult>;
120
151
  /**
121
- * Mint a short-lived wax key from the vault.
152
+ * Mint a short-lived, use-limited wax key from the vault.
122
153
  *
123
154
  * Call this server-side to implement the `fetchWaxKey` callback required by
124
155
  * `OzVault.create()` on the frontend. The wax key replaces the vault secret
125
156
  * on every browser tokenize call — the secret never leaves your server.
126
157
  *
127
- * The `tokenizationSessionId` passed to `fetchWaxKey` by the SDK should be
128
- * forwarded here for correlation; it is stored by the vault alongside the
129
- * wax key but is not used for authentication.
158
+ * **Use limits:** by default each wax key accepts up to 3 tokenize calls
159
+ * (`maxTokenizeCalls: 3`). After that the vault marks the key as consumed and
160
+ * the client SDK transparently re-mints. Keep `maxTokenizeCalls` in sync with
161
+ * `VaultOptions.maxTokenizeCalls` so the SDK can proactively refresh before
162
+ * hitting the limit rather than waiting for a rejection.
163
+ *
164
+ * **Session correlation:** the `tokenizationSessionId` forwarded from the SDK's
165
+ * `fetchWaxKey` callback should be passed here so the vault can correlate the
166
+ * key with the checkout session in its audit log.
130
167
  *
131
168
  * @example
132
169
  * // Next.js API route
133
170
  * export async function POST(req: Request) {
134
171
  * const { sessionId } = await req.json();
135
- * const { waxKey } = await ozura.mintWaxKey({ tokenizationSessionId: sessionId });
172
+ * const { waxKey } = await ozura.mintWaxKey({
173
+ * tokenizationSessionId: sessionId,
174
+ * maxTokenizeCalls: 3,
175
+ * });
136
176
  * return Response.json({ waxKey });
137
177
  * }
138
178
  */
139
- mintWaxKey(options?: {
140
- tokenizationSessionId?: string;
141
- }): Promise<MintWaxKeyResult>;
179
+ mintWaxKey(options?: MintWaxKeyOptions): Promise<MintWaxKeyResult>;
142
180
  /**
143
181
  * Revoke a previously minted wax key.
144
182
  *
@@ -163,11 +201,19 @@ export declare class Ozura {
163
201
  private post;
164
202
  private getRaw;
165
203
  private handleResponse;
204
+ /**
205
+ * Parses a Pay API response JSON and throws `OzuraError` on HTTP errors or
206
+ * `success: false` payloads. Used by both `getRaw` and `handleResponse` to
207
+ * avoid duplicating the error-mapping logic.
208
+ */
209
+ private parseApiJson;
166
210
  }
167
211
  /** Minimal response shape required by {@link createMintWaxMiddleware}. */
168
212
  interface NodeLikeResponse {
169
213
  json(data: unknown): void;
170
214
  status(code: number): NodeLikeResponse;
215
+ /** Present on Node/Express `ServerResponse` — used for 429 `Retry-After`. */
216
+ setHeader?(name: string, value: string | number): void;
171
217
  }
172
218
  /**
173
219
  * Creates a ready-to-use Fetch API route handler for minting wax keys.
@@ -219,6 +265,7 @@ export declare function createMintWaxHandler(ozura: Ozura): (req: Request) => Pr
219
265
  */
220
266
  export declare function createMintWaxMiddleware(ozura: Ozura): (req: {
221
267
  body?: unknown;
268
+ headers?: unknown;
222
269
  }, res: NodeLikeResponse) => Promise<void>;
223
270
  /**
224
271
  * Extract the client IP address from a server request object.
@@ -229,12 +276,123 @@ export declare function createMintWaxMiddleware(ozura: Ozura): (req: {
229
276
  * (Cloudflare) → `x-forwarded-for` (reverse proxy) → `x-real-ip` →
230
277
  * `socket.remoteAddress` → `"0.0.0.0"`.
231
278
  *
279
+ * **Proxy trust requirements — read before deploying:**
280
+ *
281
+ * - **`x-forwarded-for` / `x-real-ip`** are HTTP headers that any client can
282
+ * set arbitrarily. They are only trustworthy when your server sits behind a
283
+ * reverse proxy (nginx, AWS ALB, Cloudflare, etc.) that strips and rewrites
284
+ * those headers. If your Node.js process is directly internet-accessible,
285
+ * an attacker can spoof any IP value and bypass payment-processor
286
+ * IP-based fraud checks.
287
+ * - **Express `req.ip`** resolves through `X-Forwarded-For` only when
288
+ * `app.set('trust proxy', true)` (or a specific proxy count/subnet) is
289
+ * configured. Without `trust proxy`, `req.ip` returns the direct socket
290
+ * address (your load-balancer's IP, not the client's).
291
+ * - **`cf-connecting-ip`** is only trustworthy when Cloudflare is genuinely
292
+ * in front of your server. Without Cloudflare, any client can send this
293
+ * header with a fabricated value.
294
+ *
295
+ * In all cases, ensure your infrastructure strips untrusted forwarding headers
296
+ * before they reach your application.
297
+ *
232
298
  * @example
233
- * // Express / Fastify
299
+ * // Express requires app.set('trust proxy', true) behind a proxy
234
300
  * clientIpAddress: getClientIp(req)
235
301
  *
236
302
  * // Next.js App Router
237
303
  * clientIpAddress: getClientIp(req)
238
304
  */
239
305
  export declare function getClientIp(req: Record<string, unknown>): string;
306
+ /**
307
+ * Options for {@link createCardSaleHandler} and {@link createCardSaleMiddleware}.
308
+ *
309
+ * The only required option is `getAmount` — it must return the authoritative
310
+ * transaction amount from **your own** database or session. Never read the
311
+ * amount from the request body without validating it against your records;
312
+ * a malicious client could otherwise send an arbitrarily low amount.
313
+ */
314
+ export interface CardSaleHandlerOptions {
315
+ /**
316
+ * Return the authoritative amount for this transaction as a decimal string
317
+ * (e.g. `"49.00"`). Source this from your own database or session — never
318
+ * trust the value the client sends in the request body.
319
+ *
320
+ * Receives the parsed request body so you can forward an `orderId` or
321
+ * similar identifier to look up the correct amount.
322
+ *
323
+ * @example
324
+ * getAmount: async (body) => {
325
+ * const order = await db.orders.findById(body.orderId as string);
326
+ * return order.total;
327
+ * }
328
+ */
329
+ getAmount: (body: Record<string, unknown>) => string | Promise<string>;
330
+ /**
331
+ * Return the ISO 4217 currency code for this transaction. Default: `"USD"`.
332
+ */
333
+ getCurrency?: (body: Record<string, unknown>) => string | Promise<string>;
334
+ }
335
+ /**
336
+ * Creates a ready-to-use Fetch API route handler for charging a tokenized card.
337
+ *
338
+ * Drop-in for Next.js App Router, Cloudflare Workers, Vercel Edge, and any
339
+ * runtime built on the standard Web API `Request` / `Response`.
340
+ *
341
+ * The handler reads `{ token, cvcSession, billing }` from the JSON request body,
342
+ * resolves the amount via `options.getAmount()`, calls `ozura.cardSale()`, and
343
+ * returns `{ transactionId, amount, cardLastFour, cardBrand }` on success.
344
+ * On error it returns `{ error }` with a normalized, user-facing message and
345
+ * an appropriate HTTP status.
346
+ *
347
+ * `clientIpAddress` is extracted automatically from the request headers.
348
+ *
349
+ * @example
350
+ * // app/api/charge/route.ts (Next.js App Router)
351
+ * import { Ozura, createCardSaleHandler } from '@ozura/elements/server';
352
+ *
353
+ * const ozura = new Ozura({ merchantId: '...', apiKey: '...', vaultKey: '...' });
354
+ *
355
+ * export const POST = createCardSaleHandler(ozura, {
356
+ * getAmount: async (body) => {
357
+ * const order = await db.orders.findById(body.orderId as string);
358
+ * return order.total;
359
+ * },
360
+ * });
361
+ */
362
+ export declare function createCardSaleHandler(ozura: Ozura, options: CardSaleHandlerOptions): (req: Request) => Promise<Response>;
363
+ /**
364
+ * Creates a ready-to-use Express / Connect middleware for charging a tokenized card.
365
+ *
366
+ * Requires `express.json()` (or equivalent body-parser) to be registered before
367
+ * this middleware so `req.body` is available.
368
+ *
369
+ * The middleware reads `{ token, cvcSession, billing }` from `req.body`, resolves
370
+ * the amount via `options.getAmount()`, calls `ozura.cardSale()`, and sends
371
+ * `{ transactionId, amount, cardLastFour, cardBrand }` on success.
372
+ * On error it sends `{ error }` with a normalized, user-facing message and an
373
+ * appropriate HTTP status.
374
+ *
375
+ * `clientIpAddress` is extracted automatically from the request object.
376
+ *
377
+ * @example
378
+ * // Express
379
+ * import express from 'express';
380
+ * import { Ozura, createCardSaleMiddleware } from '@ozura/elements/server';
381
+ *
382
+ * const app = express();
383
+ * const ozura = new Ozura({ merchantId: '...', apiKey: '...', vaultKey: '...' });
384
+ *
385
+ * app.use(express.json());
386
+ * app.post('/api/charge', createCardSaleMiddleware(ozura, {
387
+ * getAmount: async (body) => {
388
+ * const order = await db.orders.findById(body.orderId as string);
389
+ * return order.total;
390
+ * },
391
+ * }));
392
+ */
393
+ export declare function createCardSaleMiddleware(ozura: Ozura, options: CardSaleHandlerOptions): (req: {
394
+ body?: unknown;
395
+ headers?: unknown;
396
+ }, res: NodeLikeResponse) => Promise<void>;
240
397
  export type { BillingDetails, CardSaleResponseData, TransactionQueryPagination, TransactionType, TransactionBase, CardTransactionData, AchTransactionData, CryptoTransactionData, TransactionData, } from '../types';
398
+ export { normalizeCardSaleError } from '../sdk/errors';
@@ -5,6 +5,9 @@ export interface ElementStyle {
5
5
  fontWeight?: string;
6
6
  fontStyle?: string;
7
7
  fontVariant?: string;
8
+ fontSmoothing?: string;
9
+ webkitFontSmoothing?: string;
10
+ mozOsxFontSmoothing?: string;
8
11
  letterSpacing?: string;
9
12
  lineHeight?: string;
10
13
  textAlign?: string;
@@ -55,8 +58,6 @@ export interface ElementStyle {
55
58
  minHeight?: string;
56
59
  maxHeight?: string;
57
60
  transition?: string;
58
- /** Index signature for forward-compatibility with future CSS properties. */
59
- [key: string]: string | undefined;
60
61
  }
61
62
  export interface ElementStyleConfig {
62
63
  /** Default styles applied to the input. */
@@ -74,7 +75,8 @@ export interface ElementOptions {
74
75
  style?: ElementStyleConfig;
75
76
  placeholder?: string;
76
77
  disabled?: boolean;
77
- /** How long to wait (ms) for the iframe to load before emitting `loaderror`. Default: 10 000. */
78
+ /** How long to wait (ms) for the iframe to load before emitting `loaderror`. Default: 10 000.
79
+ * Only takes effect when this element's `onLoadError` option is provided or when `VaultOptions.onLoadError` is set. */
78
80
  loadTimeoutMs?: number;
79
81
  }
80
82
  export interface ElementChangeEvent {
@@ -118,7 +120,7 @@ export interface AppearanceVariables {
118
120
  colorText?: string;
119
121
  /** Input background color. Maps to `base.backgroundColor`. */
120
122
  colorBackground?: string;
121
- /** Primary accent color. Maps to `focus.caretColor` and `focus.color`. */
123
+ /** Primary accent color. Maps to `focus.caretColor` and `base.caretColor`. */
122
124
  colorPrimary?: string;
123
125
  /** Error/invalid state text color. Maps to `invalid.color`. */
124
126
  colorDanger?: string;
@@ -162,7 +164,7 @@ export interface VaultOptions {
162
164
  * It replaces the vault secret on every browser tokenize call — the secret
163
165
  * never leaves your server.
164
166
  *
165
- * **Server-side (recommended):** Use `ozura.mintWaxKey()` from `oz-elements/server`:
167
+ * **Server-side (recommended):** Use `ozura.mintWaxKey()` from `@ozura/elements/server`:
166
168
  * @example
167
169
  * // Your API route (e.g. Next.js App Router):
168
170
  * // POST /api/mint-wax
@@ -205,8 +207,40 @@ export interface VaultOptions {
205
207
  * Use this to show a fallback UI (e.g. "Unable to load payment fields").
206
208
  */
207
209
  onLoadError?: () => void;
208
- /** How long to wait (ms) for the tokenizer iframe before calling `onLoadError`. Default: 10 000. */
210
+ /** How long to wait (ms) for the tokenizer iframe before calling `onLoadError`. Default: 10 000.
211
+ * Only takes effect when `onLoadError` is also provided — setting this without `onLoadError` has no effect. */
209
212
  loadTimeoutMs?: number;
213
+ /**
214
+ * Called when the SDK silently re-mints the wax key during a tokenization
215
+ * attempt (expiry or consumption). The `createToken()` / `createBankToken()`
216
+ * promise stays pending until the refresh completes and the retry resolves.
217
+ * Use this to show a loading indicator while the re-mint round trip is in flight.
218
+ */
219
+ onWaxRefresh?: () => void;
220
+ /**
221
+ * Maximum number of tokenize calls each minted wax key accepts before the
222
+ * vault marks it as consumed. Should match the `maxTokenizeCalls` passed to
223
+ * `ozura.mintWaxKey()` on your server (both default to `3`).
224
+ *
225
+ * The SDK uses this value to proactively refresh the wax key after it has been
226
+ * fully consumed — before the next `createToken()` call is made — so users
227
+ * never experience a delay from a reactive re-mint. If the values are out of
228
+ * sync the reactive refresh path still catches consumption errors; this is
229
+ * purely an optimisation.
230
+ *
231
+ * @default 3
232
+ */
233
+ maxTokenizeCalls?: number;
234
+ /**
235
+ * Called once when the tokenizer iframe has loaded and is ready to accept
236
+ * tokenization requests. Useful in vanilla JS to re-evaluate submit-button
237
+ * readiness when the tokenizer becomes ready after all element iframes have
238
+ * already loaded.
239
+ *
240
+ * In React, use `useOzElements().ready` instead — it combines both tokenizer
241
+ * and element readiness automatically.
242
+ */
243
+ onReady?: () => void;
210
244
  /**
211
245
  * Global appearance configuration. Applies preset themes and/or variable
212
246
  * overrides to all elements created by this vault. Per-element `style`
@@ -278,8 +312,8 @@ export interface BankTokenizeOptions {
278
312
  export interface BankAccountMetadata {
279
313
  /** Last 4 digits of the account number. */
280
314
  last4: string;
281
- /** US routing number (9 digits). */
282
- routingNumber: string;
315
+ /** Last 4 digits of the routing number sufficient for display and reconciliation. */
316
+ routingNumberLast4: string;
283
317
  }
284
318
  /** Non-sensitive card metadata returned alongside the token. */
285
319
  export interface CardMetadata {
@@ -294,6 +328,16 @@ export interface CardMetadata {
294
328
  }
295
329
  export interface TokenResponse {
296
330
  token: string;
331
+ /**
332
+ * CVC session token returned by the vault alongside the card token.
333
+ *
334
+ * Required by the Ozura Pay API `cardSale` endpoint. If this is absent it
335
+ * means the vault is not configured to return CVC sessions — `createToken()`
336
+ * will reject with an `OZ_TOKEN_ERROR` before this response is ever returned
337
+ * to the caller. In practice, this field is always present on successful
338
+ * tokenization; the optional type reflects only that the field is not part
339
+ * of the tokenized card data itself.
340
+ */
297
341
  cvcSession?: string;
298
342
  /** Non-sensitive card metadata — available immediately without a cardSale call. */
299
343
  card?: CardMetadata;
@@ -368,12 +412,12 @@ export interface CardSaleResponseData {
368
412
  amount: string;
369
413
  /** ISO 4217 currency code, e.g. "USD". */
370
414
  currency: string;
371
- /** Surcharge amount applied, e.g. "0.00". */
372
- surchargeAmount: string;
415
+ /** Surcharge amount applied, e.g. "1.50". Absent when no surcharge was configured. */
416
+ surchargeAmount?: string;
373
417
  /** Sales tax calculated by the Pay API based on billing address. */
374
418
  salesTaxAmount?: string;
375
- /** Tip amount applied, e.g. "0.00". */
376
- tipAmount: string;
419
+ /** Tip amount applied, e.g. "5.00". Absent when no tip was included in the request. */
420
+ tipAmount?: string;
377
421
  /** Last four digits of the card number. */
378
422
  cardLastFour: string;
379
423
  /** First six digits of the card number (BIN). */
@@ -408,12 +452,17 @@ export interface CardSaleResponseData {
408
452
  * Full response envelope from the Ozura Pay API cardSale endpoint.
409
453
  *
410
454
  * On success: `{ success: true, data: CardSaleResponseData }`
411
- * On failure: HTTP 4xx/5xx with `{ error: string }` — pass `error` to
412
- * `normalizeCardSaleError()` to get a user-facing message.
455
+ * On failure: HTTP 4xx/5xx with `{ success: false, error: string }` — pass
456
+ * `error` to `normalizeCardSaleError()` to get a user-facing message.
457
+ *
458
+ * Note: the server SDK `Ozura.cardSale()` throws `OzuraError` on failure rather
459
+ * than returning this shape. This type is only needed when calling the Pay API
460
+ * directly via `fetch`.
413
461
  */
414
462
  export interface CardSaleApiResponse {
415
463
  success: boolean;
416
- data: CardSaleResponseData;
464
+ data?: CardSaleResponseData;
465
+ error?: string;
417
466
  }
418
467
  /**
419
468
  * Transaction types returned by the Pay API transQuery endpoint.
@@ -539,7 +588,7 @@ export interface TransactionQueryResponse {
539
588
  data: TransactionData[];
540
589
  pagination: TransactionQueryPagination;
541
590
  }
542
- export type OzMessageType = 'OZ_FRAME_READY' | 'OZ_INIT' | 'OZ_UPDATE' | 'OZ_CLEAR' | 'OZ_CHANGE' | 'OZ_FOCUS' | 'OZ_BLUR' | 'OZ_RESIZE' | 'OZ_SET_TOKENIZER_NAME' | 'OZ_BEGIN_COLLECT' | 'OZ_FIELD_VALUE' | 'OZ_TOKENIZE' | 'OZ_TOKEN_RESULT' | 'OZ_TOKEN_ERROR' | 'OZ_FOCUS_REQUEST' | 'OZ_BLUR_REQUEST' | 'OZ_SET_CVV_LENGTH' | 'OZ_BANK_TOKENIZE' | 'OZ_BANK_TOKEN_RESULT';
591
+ export type OzMessageType = 'OZ_FRAME_READY' | 'OZ_INIT' | 'OZ_UPDATE' | 'OZ_CLEAR' | 'OZ_CHANGE' | 'OZ_FOCUS' | 'OZ_BLUR' | 'OZ_RESIZE' | 'OZ_BEGIN_COLLECT' | 'OZ_FIELD_VALUE' | 'OZ_TOKENIZE' | 'OZ_TOKEN_RESULT' | 'OZ_TOKEN_ERROR' | 'OZ_FOCUS_REQUEST' | 'OZ_BLUR_REQUEST' | 'OZ_SET_CVV_LENGTH' | 'OZ_BANK_TOKENIZE' | 'OZ_BANK_TOKEN_RESULT' | 'OZ_TOKENIZE_CANCEL';
543
592
  export interface OzMessage {
544
593
  __oz: true;
545
594
  vaultId: string;
@@ -4,6 +4,15 @@ import type { Appearance, ElementStyleConfig } from '../types';
4
4
  * Resolution order: theme defaults → variable overrides.
5
5
  * The returned config is then used as the "base appearance" that
6
6
  * per-element `style` overrides merge on top of.
7
+ *
8
+ * @remarks
9
+ * - `appearance: undefined` → no styles injected (element iframes use their
10
+ * own minimal built-in defaults).
11
+ * - `appearance: {}` or `appearance: { variables: {...} }` without an explicit
12
+ * `theme` → the `'default'` theme is used as the base. Omitting `theme`
13
+ * does NOT mean "no theme" — it means `theme: 'default'`. To opt out of
14
+ * the preset themes entirely, use per-element `style` overrides without
15
+ * passing an `appearance` prop at all.
7
16
  */
8
17
  export declare function resolveAppearance(appearance?: Appearance): ElementStyleConfig | undefined;
9
18
  /**
@@ -21,6 +21,20 @@ export interface CardValidationResult {
21
21
  valid: boolean;
22
22
  error?: string;
23
23
  }
24
+ /**
25
+ * Canonical card number validation: length check then Luhn.
26
+ *
27
+ * **Not called by `elementFrame.ts` at runtime.** The frame needs `complete`
28
+ * and `valid` as separate signals for field UX (show completion indicator
29
+ * before running Luhn), so it implements both checks inline via `isComplete()`
30
+ * and `isValid()`. Those methods MUST stay logically in sync with this
31
+ * function — any change to length bounds or error text here must be mirrored
32
+ * there, and vice-versa.
33
+ *
34
+ * Used by the unit test suite as the authoritative spec for the algorithm.
35
+ *
36
+ * @internal
37
+ */
24
38
  export declare function validateCardNumber(digits: string, brand: CardBrand | string): CardValidationResult;
25
39
  export interface ExpiryValidationResult {
26
40
  complete: boolean;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Generates a RFC 4122 v4 UUID.
3
+ *
4
+ * Uses `crypto.randomUUID()` when available (Chrome 92+, Firefox 95+,
5
+ * Safari 15.4+, Node 14.17+) and falls back to `crypto.getRandomValues()`
6
+ * for older environments (Safari 14, some embedded WebViews, older Node).
7
+ *
8
+ * Both paths use the same CSPRNG — the difference is only in API surface.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare function uuid(): string;
@@ -19,3 +19,35 @@
19
19
  * Exported for unit testing only — not part of the public SDK API.
20
20
  */
21
21
  export declare function isAllowedVaultOrigin(apiUrl: string): boolean;
22
+ /** Exported for unit testing only — not part of the public SDK API. */
23
+ export declare class TokenizerFrame {
24
+ private vaultId;
25
+ private hostOrigin;
26
+ /** Wax key delivered once via OZ_INIT. Used for all tokenize calls. */
27
+ private waxKey;
28
+ private pending;
29
+ private pendingBank;
30
+ constructor();
31
+ private onMessage;
32
+ /**
33
+ * Shared vault POST helper. Handles origin guard, header construction,
34
+ * the fetchWithRetry call, token extraction, empty-token error, and catch.
35
+ *
36
+ * Returns the raw response data augmented with a guaranteed non-empty
37
+ * `token` string, or `null` if an error was already reported via
38
+ * postMessage to `target`.
39
+ */
40
+ private postToVault;
41
+ private tokenize;
42
+ private tokenizeBank;
43
+ private static classifyHttpStatus;
44
+ /**
45
+ * Fetches with a 30s timeout and a single automatic retry for network
46
+ * errors only. 4xx and 5xx responses are not retried — 4xx inputs won't
47
+ * succeed on a second attempt, and 5xx may have already been processed
48
+ * server-side (idempotency cannot be guaranteed for the vault tokenize POST).
49
+ *
50
+ * Throws an Error with an `errorCode` property for downstream classification.
51
+ */
52
+ private fetchWithRetry;
53
+ }