@xmodeai/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.
@@ -0,0 +1,286 @@
1
+ //#region src/types.d.ts
2
+ /**
3
+ * Public wire types for the xMode API.
4
+ *
5
+ * These are hand-written and deliberately decoupled from the server's internal
6
+ * schemas so that:
7
+ * - the published package carries ZERO runtime dependencies (no zod), and
8
+ * - new server capabilities never break older SDK versions (forward-compat).
9
+ *
10
+ * Forward-compat rules applied below:
11
+ * - "enum-ish" fields the caller SENDS (model, size) are widened with
12
+ * `| (string & {})` so any future alias compiles while keeping autocomplete.
13
+ * - response unions keep literal `status` branches so `switch (record.status)`
14
+ * narrows cleanly; unknown statuses are handled by BEHAVIOR (polling treats
15
+ * anything outside the terminal set as "in progress"), never by throwing.
16
+ *
17
+ * The `src/__contract__/shared-contract.ts` file statically checks these types
18
+ * against the server's zod schemas, so drift is caught at type-check time.
19
+ */
20
+ type KnownImageModel = 'xSD4.5';
21
+ type KnownVideoModel = 'xW2.7S';
22
+ /** Image model alias. Known values autocomplete; any future alias also compiles. */
23
+ type ImageModel = KnownImageModel | (string & {});
24
+ /** Video model alias. Known values autocomplete; any future alias also compiles. */
25
+ type VideoModel = KnownVideoModel | (string & {});
26
+ type SizePreset = '2K' | '4K';
27
+ /** Either a preset (`2K`, `4K`) or explicit `WxH` pixels (e.g. `2048x2048`). */
28
+ type ImageSize = SizePreset | (string & {});
29
+ type AspectRatio = 'auto' | 'square' | 'landscape' | 'portrait';
30
+ type ResponseFormat = 'url' | 'b64_json';
31
+ type VideoResolution = '720p' | '1080p';
32
+ type KnownStatus = 'queued' | 'processing' | 'succeeded' | 'failed' | 'expired';
33
+ /** Widened status for filters and forward-compat reads. */
34
+ type Status = KnownStatus | (string & {});
35
+ interface CreateGenerationInput {
36
+ /** Email of YOUR end-user who triggered this generation. Stored lowercase server-side. */
37
+ endUserEmail: string;
38
+ /** English text prompt. 1–8000 chars. */
39
+ prompt: string;
40
+ /** Public model alias, e.g. `'xSD4.5'`. */
41
+ model: ImageModel;
42
+ /** Reference image(s): a single HTTPS URL or an array of 1–10 URLs. */
43
+ references?: string | string[];
44
+ /** Output dimensions: preset (`2K`/`4K`) or explicit `WxH`. Defaults to `2K`. */
45
+ size?: ImageSize;
46
+ /** Aspect-ratio shortcut. Combined with a preset `size`, resolves to explicit pixels. */
47
+ aspectRatio?: AspectRatio;
48
+ /** How many images to generate, 1–15. Each is billed separately. */
49
+ n?: number;
50
+ /** Determinism seed. Omit for a random seed (returned in the record). */
51
+ seed?: number;
52
+ /** Add a small watermark. Defaults to false. */
53
+ watermark?: boolean;
54
+ /** Prompt-adherence strength, 1–20 (typical 5–10). */
55
+ guidanceScale?: number;
56
+ /** `'url'` (signed CDN URLs, default) or `'b64_json'` (inline base64). */
57
+ responseFormat?: ResponseFormat;
58
+ /** `'auto'` to produce a connected series (requires `sequentialOptions.maxImages`). */
59
+ sequential?: 'auto' | 'disabled';
60
+ /** Series length when `sequential: 'auto'`. */
61
+ sequentialOptions?: {
62
+ maxImages: number;
63
+ };
64
+ /** HTTPS URL we POST to once this generation reaches a terminal status. */
65
+ webhookUrl?: string;
66
+ }
67
+ interface ImageRecord {
68
+ /** Stable image id. */
69
+ id: string;
70
+ /** Signed URL, valid ~23h. Absent when `error` is set. */
71
+ url?: string;
72
+ /** Present only when the image is unavailable. Mutually exclusive with `url`. */
73
+ error?: {
74
+ code: 'storage_failed' | (string & {});
75
+ };
76
+ /** Actual dimensions returned by the model. */
77
+ size?: string;
78
+ /** When the signed URL stops working (ISO 8601). */
79
+ expiresAt: string;
80
+ }
81
+ interface GenerationCost {
82
+ xTokens: number;
83
+ }
84
+ interface GenerationError {
85
+ /** Stable error code, e.g. `content_policy`, `provider_error`. */
86
+ code: string;
87
+ /** Human-readable message, safe to surface to end-users. */
88
+ message: string;
89
+ }
90
+ interface GenerationRecordBase {
91
+ requestId: string;
92
+ model: string;
93
+ prompt: string;
94
+ referencesCount: number;
95
+ endUserEmail: string;
96
+ createdAt: string;
97
+ }
98
+ interface QueuedGenerationRecord extends GenerationRecordBase {
99
+ status: 'queued';
100
+ /** Path to GET to poll for status. */
101
+ pollUrl: string;
102
+ }
103
+ interface ProcessingGenerationRecord extends GenerationRecordBase {
104
+ status: 'processing';
105
+ }
106
+ interface SucceededGenerationRecord extends GenerationRecordBase {
107
+ status: 'succeeded';
108
+ images: ImageRecord[];
109
+ cost: GenerationCost;
110
+ finishedAt: string;
111
+ }
112
+ interface FailedGenerationRecord extends GenerationRecordBase {
113
+ status: 'failed';
114
+ error: GenerationError;
115
+ finishedAt: string;
116
+ }
117
+ interface ExpiredGenerationRecord extends GenerationRecordBase {
118
+ status: 'expired';
119
+ finishedAt: string;
120
+ }
121
+ /**
122
+ * Discriminated by `status`. Returned by `generations.get`, the items of
123
+ * `generations.list`, and the outbound webhook body. At runtime `status` may
124
+ * carry a value newer than this SDK version knows — handle the `default` branch.
125
+ */
126
+ type GenerationRecord = QueuedGenerationRecord | ProcessingGenerationRecord | SucceededGenerationRecord | FailedGenerationRecord | ExpiredGenerationRecord;
127
+ interface GenerationListParams {
128
+ page?: number;
129
+ pageSize?: number;
130
+ endUserEmail?: string;
131
+ status?: Status;
132
+ /** ISO 8601 — only generations created strictly after this instant. */
133
+ createdAfter?: string;
134
+ }
135
+ interface GenerationListResponse {
136
+ items: GenerationRecord[];
137
+ page: number;
138
+ pageSize: number;
139
+ hasMore: boolean;
140
+ }
141
+ /**
142
+ * Body for `POST /v1/videos` with `model: 'xW2.7S'`. Other video models, when
143
+ * added, may carry different params — send those via `client.request()` until
144
+ * a typed method ships.
145
+ */
146
+ interface CreateVideoInput {
147
+ endUserEmail: string;
148
+ model: VideoModel;
149
+ prompt: string;
150
+ /** Input image to animate (HTTPS URL). */
151
+ image: string;
152
+ negative_prompt?: string;
153
+ /** Optional audio URL (HTTPS) to drive motion. */
154
+ audio_url?: string;
155
+ /** `'720p'` or `'1080p'`. Defaults to `'1080p'`. */
156
+ resolution?: VideoResolution;
157
+ /** Seconds, 2–15. Defaults to 5. Billed per second. */
158
+ duration?: number;
159
+ /** Auto-expand the prompt before generation. Defaults to true. */
160
+ prompt_extend?: boolean;
161
+ /** Determinism seed, 0–2147483647, or null for auto. */
162
+ seed?: number | null;
163
+ webhookUrl?: string;
164
+ }
165
+ interface VideoRecordItem {
166
+ id: string;
167
+ url?: string;
168
+ error?: {
169
+ code: 'storage_failed' | (string & {});
170
+ };
171
+ duration?: number;
172
+ resolution?: VideoResolution;
173
+ expiresAt: string;
174
+ }
175
+ interface VideoCost {
176
+ xTokens: number;
177
+ }
178
+ interface VideoError {
179
+ code: string;
180
+ message: string;
181
+ }
182
+ interface VideoRecordBase {
183
+ requestId: string;
184
+ model: string;
185
+ prompt: string;
186
+ endUserEmail: string;
187
+ createdAt: string;
188
+ }
189
+ interface QueuedVideoRecord extends VideoRecordBase {
190
+ status: 'queued';
191
+ pollUrl: string;
192
+ }
193
+ interface ProcessingVideoRecord extends VideoRecordBase {
194
+ status: 'processing';
195
+ }
196
+ interface SucceededVideoRecord extends VideoRecordBase {
197
+ status: 'succeeded';
198
+ videos: VideoRecordItem[];
199
+ cost: VideoCost;
200
+ finishedAt: string;
201
+ }
202
+ interface FailedVideoRecord extends VideoRecordBase {
203
+ status: 'failed';
204
+ error: VideoError;
205
+ finishedAt: string;
206
+ }
207
+ interface ExpiredVideoRecord extends VideoRecordBase {
208
+ status: 'expired';
209
+ finishedAt: string;
210
+ }
211
+ type VideoRecord = QueuedVideoRecord | ProcessingVideoRecord | SucceededVideoRecord | FailedVideoRecord | ExpiredVideoRecord;
212
+ interface BalanceResponse {
213
+ xTokens: number;
214
+ updatedAt: string;
215
+ }
216
+ type BalanceLedgerType = 'debit' | 'credit';
217
+ type BalanceLedgerReason = 'generation' | 'refund' | 'topup' | 'admin_adjustment';
218
+ interface BalanceLedgerEntry {
219
+ id: string;
220
+ type: BalanceLedgerType;
221
+ amount: number;
222
+ reason: BalanceLedgerReason;
223
+ reference: string;
224
+ balanceAfter: number;
225
+ createdAt: string;
226
+ }
227
+ interface BalanceHistoryParams {
228
+ page?: number;
229
+ pageSize?: number;
230
+ }
231
+ interface BalanceHistoryResponse {
232
+ items: BalanceLedgerEntry[];
233
+ page: number;
234
+ pageSize: number;
235
+ hasMore: boolean;
236
+ }
237
+ interface EndUserRecord {
238
+ email: string;
239
+ createdAt: string;
240
+ lastActiveAt: string;
241
+ totalRequests: number;
242
+ totalXTokensSpent: number;
243
+ /** When the owner blocked this end-user (ISO 8601), or null when active. */
244
+ blockedAt: string | null;
245
+ }
246
+ interface EndUserListParams {
247
+ page?: number;
248
+ pageSize?: number;
249
+ /** Filter to only blocked (`true`) or only active (`false`) end-users. */
250
+ blocked?: boolean;
251
+ /** Case-insensitive substring match on email. */
252
+ email?: string;
253
+ }
254
+ interface EndUserListResponse {
255
+ items: EndUserRecord[];
256
+ page: number;
257
+ pageSize: number;
258
+ hasMore: boolean;
259
+ }
260
+ interface BlockEndUserResponse {
261
+ email: string;
262
+ blockedAt: string;
263
+ }
264
+ interface UnblockEndUserResponse {
265
+ email: string;
266
+ blockedAt: null;
267
+ }
268
+ interface DeleteEndUserResponse {
269
+ email: string;
270
+ deleted: true;
271
+ removedRequests: number;
272
+ removedObjects: number;
273
+ }
274
+ type KnownApiErrorCode = 'validation_error' | 'unauthorized' | 'forbidden' | 'not_found' | 'conflict' | 'rate_limited' | 'payment_required' | 'content_policy' | 'provider_timeout' | 'provider_error' | 'internal_error' | 'account_blocked';
275
+ /** Widened so a server-added code never breaks error handling in older SDKs. */
276
+ type ApiErrorCode = KnownApiErrorCode | (string & {});
277
+ interface ApiErrorBody {
278
+ error: {
279
+ code: ApiErrorCode;
280
+ message: string;
281
+ fields?: Record<string, string>;
282
+ };
283
+ }
284
+ //#endregion
285
+ export { KnownImageModel as A, SucceededGenerationRecord as B, GenerationListParams as C, ImageRecord as D, ImageModel as E, QueuedGenerationRecord as F, VideoModel as G, UnblockEndUserResponse as H, QueuedVideoRecord as I, VideoResolution as J, VideoRecord as K, ResponseFormat as L, KnownVideoModel as M, ProcessingGenerationRecord as N, ImageSize as O, ProcessingVideoRecord as P, SizePreset as R, GenerationError as S, GenerationRecord as T, VideoCost as U, SucceededVideoRecord as V, VideoError as W, ExpiredGenerationRecord as _, BalanceHistoryResponse as a, FailedVideoRecord as b, BalanceLedgerType as c, CreateGenerationInput as d, CreateVideoInput as f, EndUserRecord as g, EndUserListResponse as h, BalanceHistoryParams as i, KnownStatus as j, KnownApiErrorCode as k, BalanceResponse as l, EndUserListParams as m, ApiErrorCode as n, BalanceLedgerEntry as o, DeleteEndUserResponse as p, VideoRecordItem as q, AspectRatio as r, BalanceLedgerReason as s, ApiErrorBody as t, BlockEndUserResponse as u, ExpiredVideoRecord as v, GenerationListResponse as w, GenerationCost as x, FailedGenerationRecord as y, Status as z };
286
+ //# sourceMappingURL=types-BLVayOx3.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-BLVayOx3.d.ts","names":[],"sources":["../src/types.ts"],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/webhooks.ts
3
+ var WebhookVerificationError = class extends Error {
4
+ reason;
5
+ constructor(message, reason) {
6
+ super(message);
7
+ this.name = "WebhookVerificationError";
8
+ this.reason = reason;
9
+ }
10
+ };
11
+ const encoder = new TextEncoder();
12
+ const DEFAULT_TOLERANCE_SECONDS = 300;
13
+ function getHeader(headers, name) {
14
+ if (typeof Headers !== "undefined" && headers instanceof Headers) return headers.get(name);
15
+ const target = name.toLowerCase();
16
+ for (const [key, value] of Object.entries(headers)) if (key.toLowerCase() === target) {
17
+ if (Array.isArray(value)) return value[0] ?? null;
18
+ return value ?? null;
19
+ }
20
+ return null;
21
+ }
22
+ function hexToBytes(hex) {
23
+ if (hex.length === 0 || hex.length % 2 !== 0 || /[^0-9a-fA-F]/.test(hex)) return null;
24
+ const bytes = new Uint8Array(hex.length / 2);
25
+ for (let i = 0; i < bytes.length; i++) bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
26
+ return bytes;
27
+ }
28
+ /**
29
+ * Verify a webhook delivery and parse its body. Resolves with the typed event,
30
+ * or throws {@link WebhookVerificationError} (with a `reason`) on any failure.
31
+ *
32
+ * ```ts
33
+ * import { verifyWebhook } from '@xmodeai/sdk/webhooks';
34
+ * const { event } = await verifyWebhook({ payload: rawBody, headers: req.headers, secret });
35
+ * ```
36
+ */
37
+ async function verifyWebhook(options) {
38
+ const { payload, headers, secret } = options;
39
+ const tolerance = options.tolerance ?? DEFAULT_TOLERANCE_SECONDS;
40
+ const now = options.now ?? Date.now;
41
+ const signatureHeader = getHeader(headers, "x-xmode-signature");
42
+ const timestampHeader = getHeader(headers, "x-xmode-timestamp");
43
+ if (!signatureHeader || !timestampHeader) throw new WebhookVerificationError("Missing X-XMode-Signature or X-XMode-Timestamp header.", "missing_header");
44
+ const timestamp = Number(timestampHeader);
45
+ if (!Number.isFinite(timestamp)) throw new WebhookVerificationError("Malformed X-XMode-Timestamp header.", "malformed");
46
+ if (Math.abs(now() / 1e3 - timestamp) > tolerance) throw new WebhookVerificationError("Webhook timestamp is outside the allowed tolerance.", "timestamp_out_of_tolerance");
47
+ const candidates = signatureHeader.split(/[,\s]+/).filter((part) => part.startsWith("v1=")).map((part) => part.slice(3));
48
+ if (candidates.length === 0) throw new WebhookVerificationError("No v1 signature found in header.", "malformed");
49
+ const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
50
+ name: "HMAC",
51
+ hash: "SHA-256"
52
+ }, false, ["verify"]);
53
+ const message = encoder.encode(`${timestampHeader}.${payload}`);
54
+ let valid = false;
55
+ for (const hex of candidates) {
56
+ const signatureBytes = hexToBytes(hex);
57
+ if (!signatureBytes) continue;
58
+ if (await crypto.subtle.verify("HMAC", key, signatureBytes, message)) {
59
+ valid = true;
60
+ break;
61
+ }
62
+ }
63
+ if (!valid) throw new WebhookVerificationError("Webhook signature verification failed.", "invalid_signature");
64
+ let event;
65
+ try {
66
+ event = JSON.parse(payload);
67
+ } catch {
68
+ throw new WebhookVerificationError("Webhook payload is not valid JSON.", "malformed");
69
+ }
70
+ return {
71
+ event,
72
+ requestId: getHeader(headers, "x-xmode-request-id"),
73
+ timestamp
74
+ };
75
+ }
76
+ /** True when the event is a video record (image records carry `referencesCount`). */
77
+ function isVideoEvent(event) {
78
+ return !("referencesCount" in event);
79
+ }
80
+ /** True when the event is an image generation record. */
81
+ function isGenerationEvent(event) {
82
+ return "referencesCount" in event;
83
+ }
84
+ //#endregion
85
+ exports.WebhookVerificationError = WebhookVerificationError;
86
+ exports.isGenerationEvent = isGenerationEvent;
87
+ exports.isVideoEvent = isVideoEvent;
88
+ exports.verifyWebhook = verifyWebhook;
89
+
90
+ //# sourceMappingURL=webhooks.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.cjs","names":[],"sources":["../src/webhooks.ts"],"sourcesContent":["/**\n * Webhook verification — published at the `@xmodeai/sdk/webhooks` subpath.\n *\n * Uses Web Crypto (`crypto.subtle`) only, so it runs unchanged on Node 18+,\n * Deno, Bun, Cloudflare Workers and other edge runtimes without `node:crypto`.\n *\n * Outbound signature contract (what xMode sends):\n * X-XMode-Signature: v1=<hex(HMAC_SHA256(secret, \"<timestamp>.<rawBody>\"))>\n * X-XMode-Timestamp: unix seconds when the payload was built\n * X-XMode-Request-Id: requestId (handy for client-side dedup)\n */\nimport type { GenerationRecord, VideoRecord } from './types';\n\nexport type WebhookVerificationReason =\n | 'missing_header'\n | 'invalid_signature'\n | 'timestamp_out_of_tolerance'\n | 'malformed';\n\nexport class WebhookVerificationError extends Error {\n readonly reason: WebhookVerificationReason;\n constructor(message: string, reason: WebhookVerificationReason) {\n super(message);\n this.name = 'WebhookVerificationError';\n this.reason = reason;\n }\n}\n\n/** The webhook body is the same shape as `generations.get` / `videos.get`. */\nexport type XModeWebhookEvent = GenerationRecord | VideoRecord;\n\nexport type HeadersLike = Headers | Record<string, string | string[] | undefined>;\n\nexport interface VerifyWebhookOptions {\n /** The RAW request body string. Must NOT be a re-serialized object. */\n payload: string;\n /** Request headers, as a `Headers` object or a plain record. */\n headers: HeadersLike;\n /** Your account webhook signing secret. */\n secret: string;\n /** Allowed clock skew in seconds. Defaults to 300 (5 minutes). */\n tolerance?: number;\n /** Clock injection for tests; returns ms since epoch. Defaults to `Date.now`. */\n now?: () => number;\n}\n\nexport interface VerifiedWebhook {\n event: XModeWebhookEvent;\n requestId: string | null;\n timestamp: number;\n}\n\nconst encoder = new TextEncoder();\nconst DEFAULT_TOLERANCE_SECONDS = 300;\n\nfunction getHeader(headers: HeadersLike, name: string): string | null {\n if (typeof Headers !== 'undefined' && headers instanceof Headers) {\n return headers.get(name);\n }\n const target = name.toLowerCase();\n for (const [key, value] of Object.entries(headers)) {\n if (key.toLowerCase() === target) {\n if (Array.isArray(value)) return value[0] ?? null;\n return value ?? null;\n }\n }\n return null;\n}\n\nfunction hexToBytes(hex: string): Uint8Array | null {\n if (hex.length === 0 || hex.length % 2 !== 0 || /[^0-9a-fA-F]/.test(hex)) return null;\n const bytes = new Uint8Array(hex.length / 2);\n for (let i = 0; i < bytes.length; i++) {\n bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);\n }\n return bytes;\n}\n\n/**\n * Verify a webhook delivery and parse its body. Resolves with the typed event,\n * or throws {@link WebhookVerificationError} (with a `reason`) on any failure.\n *\n * ```ts\n * import { verifyWebhook } from '@xmodeai/sdk/webhooks';\n * const { event } = await verifyWebhook({ payload: rawBody, headers: req.headers, secret });\n * ```\n */\nexport async function verifyWebhook(options: VerifyWebhookOptions): Promise<VerifiedWebhook> {\n const { payload, headers, secret } = options;\n const tolerance = options.tolerance ?? DEFAULT_TOLERANCE_SECONDS;\n const now = options.now ?? Date.now;\n\n const signatureHeader = getHeader(headers, 'x-xmode-signature');\n const timestampHeader = getHeader(headers, 'x-xmode-timestamp');\n if (!signatureHeader || !timestampHeader) {\n throw new WebhookVerificationError(\n 'Missing X-XMode-Signature or X-XMode-Timestamp header.',\n 'missing_header',\n );\n }\n\n const timestamp = Number(timestampHeader);\n if (!Number.isFinite(timestamp)) {\n throw new WebhookVerificationError('Malformed X-XMode-Timestamp header.', 'malformed');\n }\n if (Math.abs(now() / 1000 - timestamp) > tolerance) {\n throw new WebhookVerificationError(\n 'Webhook timestamp is outside the allowed tolerance.',\n 'timestamp_out_of_tolerance',\n );\n }\n\n // Header is `v1=<hex>`; tolerate several space/comma-separated entries (rotation).\n const candidates = signatureHeader\n .split(/[,\\s]+/)\n .filter((part) => part.startsWith('v1='))\n .map((part) => part.slice(3));\n if (candidates.length === 0) {\n throw new WebhookVerificationError('No v1 signature found in header.', 'malformed');\n }\n\n const key = await crypto.subtle.importKey(\n 'raw',\n encoder.encode(secret) as BufferSource,\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['verify'],\n );\n // Sign over the RAW timestamp header, not the parsed number, so verification\n // never depends on the server using a canonical integer representation.\n const message = encoder.encode(`${timestampHeader}.${payload}`) as BufferSource;\n\n let valid = false;\n for (const hex of candidates) {\n const signatureBytes = hexToBytes(hex);\n if (!signatureBytes) continue;\n // crypto.subtle.verify is constant-time internally.\n if (await crypto.subtle.verify('HMAC', key, signatureBytes as BufferSource, message)) {\n valid = true;\n break;\n }\n }\n if (!valid) {\n throw new WebhookVerificationError('Webhook signature verification failed.', 'invalid_signature');\n }\n\n let event: XModeWebhookEvent;\n try {\n event = JSON.parse(payload) as XModeWebhookEvent;\n } catch {\n throw new WebhookVerificationError('Webhook payload is not valid JSON.', 'malformed');\n }\n\n return { event, requestId: getHeader(headers, 'x-xmode-request-id'), timestamp };\n}\n\n/** True when the event is a video record (image records carry `referencesCount`). */\nexport function isVideoEvent(event: XModeWebhookEvent): event is VideoRecord {\n return !('referencesCount' in event);\n}\n\n/** True when the event is an image generation record. */\nexport function isGenerationEvent(event: XModeWebhookEvent): event is GenerationRecord {\n return 'referencesCount' in event;\n}\n"],"mappings":";;AAmBA,IAAa,2BAAb,cAA8C,MAAM;CAClD;CACA,YAAY,SAAiB,QAAmC;EAC9D,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,SAAS;CAChB;AACF;AA0BA,MAAM,UAAU,IAAI,YAAY;AAChC,MAAM,4BAA4B;AAElC,SAAS,UAAU,SAAsB,MAA6B;CACpE,IAAI,OAAO,YAAY,eAAe,mBAAmB,SACvD,OAAO,QAAQ,IAAI,IAAI;CAEzB,MAAM,SAAS,KAAK,YAAY;CAChC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,GAC/C,IAAI,IAAI,YAAY,MAAM,QAAQ;EAChC,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,MAAM,MAAM;EAC7C,OAAO,SAAS;CAClB;CAEF,OAAO;AACT;AAEA,SAAS,WAAW,KAAgC;CAClD,IAAI,IAAI,WAAW,KAAK,IAAI,SAAS,MAAM,KAAK,eAAe,KAAK,GAAG,GAAG,OAAO;CACjF,MAAM,QAAQ,IAAI,WAAW,IAAI,SAAS,CAAC;CAC3C,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAChC,MAAM,KAAK,OAAO,SAAS,IAAI,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE;CAE5D,OAAO;AACT;;;;;;;;;;AAWA,eAAsB,cAAc,SAAyD;CAC3F,MAAM,EAAE,SAAS,SAAS,WAAW;CACrC,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAM,MAAM,QAAQ,OAAO,KAAK;CAEhC,MAAM,kBAAkB,UAAU,SAAS,mBAAmB;CAC9D,MAAM,kBAAkB,UAAU,SAAS,mBAAmB;CAC9D,IAAI,CAAC,mBAAmB,CAAC,iBACvB,MAAM,IAAI,yBACR,0DACA,gBACF;CAGF,MAAM,YAAY,OAAO,eAAe;CACxC,IAAI,CAAC,OAAO,SAAS,SAAS,GAC5B,MAAM,IAAI,yBAAyB,uCAAuC,WAAW;CAEvF,IAAI,KAAK,IAAI,IAAI,IAAI,MAAO,SAAS,IAAI,WACvC,MAAM,IAAI,yBACR,uDACA,4BACF;CAIF,MAAM,aAAa,gBAChB,MAAM,QAAQ,CAAC,CACf,QAAQ,SAAS,KAAK,WAAW,KAAK,CAAC,CAAC,CACxC,KAAK,SAAS,KAAK,MAAM,CAAC,CAAC;CAC9B,IAAI,WAAW,WAAW,GACxB,MAAM,IAAI,yBAAyB,oCAAoC,WAAW;CAGpF,MAAM,MAAM,MAAM,OAAO,OAAO,UAC9B,OACA,QAAQ,OAAO,MAAM,GACrB;EAAE,MAAM;EAAQ,MAAM;CAAU,GAChC,OACA,CAAC,QAAQ,CACX;CAGA,MAAM,UAAU,QAAQ,OAAO,GAAG,gBAAgB,GAAG,SAAS;CAE9D,IAAI,QAAQ;CACZ,KAAK,MAAM,OAAO,YAAY;EAC5B,MAAM,iBAAiB,WAAW,GAAG;EACrC,IAAI,CAAC,gBAAgB;EAErB,IAAI,MAAM,OAAO,OAAO,OAAO,QAAQ,KAAK,gBAAgC,OAAO,GAAG;GACpF,QAAQ;GACR;EACF;CACF;CACA,IAAI,CAAC,OACH,MAAM,IAAI,yBAAyB,0CAA0C,mBAAmB;CAGlG,IAAI;CACJ,IAAI;EACF,QAAQ,KAAK,MAAM,OAAO;CAC5B,QAAQ;EACN,MAAM,IAAI,yBAAyB,sCAAsC,WAAW;CACtF;CAEA,OAAO;EAAE;EAAO,WAAW,UAAU,SAAS,oBAAoB;EAAG;CAAU;AACjF;;AAGA,SAAgB,aAAa,OAAgD;CAC3E,OAAO,EAAE,qBAAqB;AAChC;;AAGA,SAAgB,kBAAkB,OAAqD;CACrF,OAAO,qBAAqB;AAC9B"}
@@ -0,0 +1,45 @@
1
+ import { K as VideoRecord, T as GenerationRecord } from "./types-BLVayOx3.cjs";
2
+
3
+ //#region src/webhooks.d.ts
4
+ type WebhookVerificationReason = 'missing_header' | 'invalid_signature' | 'timestamp_out_of_tolerance' | 'malformed';
5
+ declare class WebhookVerificationError extends Error {
6
+ readonly reason: WebhookVerificationReason;
7
+ constructor(message: string, reason: WebhookVerificationReason);
8
+ }
9
+ /** The webhook body is the same shape as `generations.get` / `videos.get`. */
10
+ type XModeWebhookEvent = GenerationRecord | VideoRecord;
11
+ type HeadersLike = Headers | Record<string, string | string[] | undefined>;
12
+ interface VerifyWebhookOptions {
13
+ /** The RAW request body string. Must NOT be a re-serialized object. */
14
+ payload: string;
15
+ /** Request headers, as a `Headers` object or a plain record. */
16
+ headers: HeadersLike;
17
+ /** Your account webhook signing secret. */
18
+ secret: string;
19
+ /** Allowed clock skew in seconds. Defaults to 300 (5 minutes). */
20
+ tolerance?: number;
21
+ /** Clock injection for tests; returns ms since epoch. Defaults to `Date.now`. */
22
+ now?: () => number;
23
+ }
24
+ interface VerifiedWebhook {
25
+ event: XModeWebhookEvent;
26
+ requestId: string | null;
27
+ timestamp: number;
28
+ }
29
+ /**
30
+ * Verify a webhook delivery and parse its body. Resolves with the typed event,
31
+ * or throws {@link WebhookVerificationError} (with a `reason`) on any failure.
32
+ *
33
+ * ```ts
34
+ * import { verifyWebhook } from '@xmodeai/sdk/webhooks';
35
+ * const { event } = await verifyWebhook({ payload: rawBody, headers: req.headers, secret });
36
+ * ```
37
+ */
38
+ declare function verifyWebhook(options: VerifyWebhookOptions): Promise<VerifiedWebhook>;
39
+ /** True when the event is a video record (image records carry `referencesCount`). */
40
+ declare function isVideoEvent(event: XModeWebhookEvent): event is VideoRecord;
41
+ /** True when the event is an image generation record. */
42
+ declare function isGenerationEvent(event: XModeWebhookEvent): event is GenerationRecord;
43
+ //#endregion
44
+ export { HeadersLike, VerifiedWebhook, VerifyWebhookOptions, WebhookVerificationError, WebhookVerificationReason, XModeWebhookEvent, isGenerationEvent, isVideoEvent, verifyWebhook };
45
+ //# sourceMappingURL=webhooks.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.d.cts","names":[],"sources":["../src/webhooks.ts"],"mappings":";;;KAaY,yBAAA;AAAA,cAMC,wBAAA,SAAiC,KAAA;EAAA,SACnC,MAAA,EAAQ,yBAAA;cACL,OAAA,UAAiB,MAAA,EAAQ,yBAAA;AAAA;;KAQ3B,iBAAA,GAAoB,gBAAA,GAAmB,WAAW;AAAA,KAElD,WAAA,GAAc,OAAA,GAAU,MAAM;AAAA,UAEzB,oBAAA;EAZsB;EAcrC,OAAA;EAd8D;EAgB9D,OAAA,EAAS,WAAW;EARV;EAUV,MAAA;;EAEA,SAAA;EAZ4D;EAc5D,GAAA;AAAA;AAAA,UAGe,eAAA;EACf,KAAA,EAAO,iBAAiB;EACxB,SAAA;EACA,SAAA;AAAA;;;;;;;;;;iBAsCoB,aAAA,CAAc,OAAA,EAAS,oBAAA,GAAuB,OAAA,CAAQ,eAAA;AAzC5E;AAAA,iBA+GgB,YAAA,CAAa,KAAA,EAAO,iBAAA,GAAoB,KAAA,IAAS,WAAW;;iBAK5D,iBAAA,CAAkB,KAAA,EAAO,iBAAA,GAAoB,KAAA,IAAS,gBAAgB"}
@@ -0,0 +1,45 @@
1
+ import { K as VideoRecord, T as GenerationRecord } from "./types-BLVayOx3.js";
2
+
3
+ //#region src/webhooks.d.ts
4
+ type WebhookVerificationReason = 'missing_header' | 'invalid_signature' | 'timestamp_out_of_tolerance' | 'malformed';
5
+ declare class WebhookVerificationError extends Error {
6
+ readonly reason: WebhookVerificationReason;
7
+ constructor(message: string, reason: WebhookVerificationReason);
8
+ }
9
+ /** The webhook body is the same shape as `generations.get` / `videos.get`. */
10
+ type XModeWebhookEvent = GenerationRecord | VideoRecord;
11
+ type HeadersLike = Headers | Record<string, string | string[] | undefined>;
12
+ interface VerifyWebhookOptions {
13
+ /** The RAW request body string. Must NOT be a re-serialized object. */
14
+ payload: string;
15
+ /** Request headers, as a `Headers` object or a plain record. */
16
+ headers: HeadersLike;
17
+ /** Your account webhook signing secret. */
18
+ secret: string;
19
+ /** Allowed clock skew in seconds. Defaults to 300 (5 minutes). */
20
+ tolerance?: number;
21
+ /** Clock injection for tests; returns ms since epoch. Defaults to `Date.now`. */
22
+ now?: () => number;
23
+ }
24
+ interface VerifiedWebhook {
25
+ event: XModeWebhookEvent;
26
+ requestId: string | null;
27
+ timestamp: number;
28
+ }
29
+ /**
30
+ * Verify a webhook delivery and parse its body. Resolves with the typed event,
31
+ * or throws {@link WebhookVerificationError} (with a `reason`) on any failure.
32
+ *
33
+ * ```ts
34
+ * import { verifyWebhook } from '@xmodeai/sdk/webhooks';
35
+ * const { event } = await verifyWebhook({ payload: rawBody, headers: req.headers, secret });
36
+ * ```
37
+ */
38
+ declare function verifyWebhook(options: VerifyWebhookOptions): Promise<VerifiedWebhook>;
39
+ /** True when the event is a video record (image records carry `referencesCount`). */
40
+ declare function isVideoEvent(event: XModeWebhookEvent): event is VideoRecord;
41
+ /** True when the event is an image generation record. */
42
+ declare function isGenerationEvent(event: XModeWebhookEvent): event is GenerationRecord;
43
+ //#endregion
44
+ export { HeadersLike, VerifiedWebhook, VerifyWebhookOptions, WebhookVerificationError, WebhookVerificationReason, XModeWebhookEvent, isGenerationEvent, isVideoEvent, verifyWebhook };
45
+ //# sourceMappingURL=webhooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.d.ts","names":[],"sources":["../src/webhooks.ts"],"mappings":";;;KAaY,yBAAA;AAAA,cAMC,wBAAA,SAAiC,KAAA;EAAA,SACnC,MAAA,EAAQ,yBAAA;cACL,OAAA,UAAiB,MAAA,EAAQ,yBAAA;AAAA;;KAQ3B,iBAAA,GAAoB,gBAAA,GAAmB,WAAW;AAAA,KAElD,WAAA,GAAc,OAAA,GAAU,MAAM;AAAA,UAEzB,oBAAA;EAZsB;EAcrC,OAAA;EAd8D;EAgB9D,OAAA,EAAS,WAAW;EARV;EAUV,MAAA;;EAEA,SAAA;EAZ4D;EAc5D,GAAA;AAAA;AAAA,UAGe,eAAA;EACf,KAAA,EAAO,iBAAiB;EACxB,SAAA;EACA,SAAA;AAAA;;;;;;;;;;iBAsCoB,aAAA,CAAc,OAAA,EAAS,oBAAA,GAAuB,OAAA,CAAQ,eAAA;AAzC5E;AAAA,iBA+GgB,YAAA,CAAa,KAAA,EAAO,iBAAA,GAAoB,KAAA,IAAS,WAAW;;iBAK5D,iBAAA,CAAkB,KAAA,EAAO,iBAAA,GAAoB,KAAA,IAAS,gBAAgB"}
@@ -0,0 +1,86 @@
1
+ //#region src/webhooks.ts
2
+ var WebhookVerificationError = class extends Error {
3
+ reason;
4
+ constructor(message, reason) {
5
+ super(message);
6
+ this.name = "WebhookVerificationError";
7
+ this.reason = reason;
8
+ }
9
+ };
10
+ const encoder = new TextEncoder();
11
+ const DEFAULT_TOLERANCE_SECONDS = 300;
12
+ function getHeader(headers, name) {
13
+ if (typeof Headers !== "undefined" && headers instanceof Headers) return headers.get(name);
14
+ const target = name.toLowerCase();
15
+ for (const [key, value] of Object.entries(headers)) if (key.toLowerCase() === target) {
16
+ if (Array.isArray(value)) return value[0] ?? null;
17
+ return value ?? null;
18
+ }
19
+ return null;
20
+ }
21
+ function hexToBytes(hex) {
22
+ if (hex.length === 0 || hex.length % 2 !== 0 || /[^0-9a-fA-F]/.test(hex)) return null;
23
+ const bytes = new Uint8Array(hex.length / 2);
24
+ for (let i = 0; i < bytes.length; i++) bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
25
+ return bytes;
26
+ }
27
+ /**
28
+ * Verify a webhook delivery and parse its body. Resolves with the typed event,
29
+ * or throws {@link WebhookVerificationError} (with a `reason`) on any failure.
30
+ *
31
+ * ```ts
32
+ * import { verifyWebhook } from '@xmodeai/sdk/webhooks';
33
+ * const { event } = await verifyWebhook({ payload: rawBody, headers: req.headers, secret });
34
+ * ```
35
+ */
36
+ async function verifyWebhook(options) {
37
+ const { payload, headers, secret } = options;
38
+ const tolerance = options.tolerance ?? DEFAULT_TOLERANCE_SECONDS;
39
+ const now = options.now ?? Date.now;
40
+ const signatureHeader = getHeader(headers, "x-xmode-signature");
41
+ const timestampHeader = getHeader(headers, "x-xmode-timestamp");
42
+ if (!signatureHeader || !timestampHeader) throw new WebhookVerificationError("Missing X-XMode-Signature or X-XMode-Timestamp header.", "missing_header");
43
+ const timestamp = Number(timestampHeader);
44
+ if (!Number.isFinite(timestamp)) throw new WebhookVerificationError("Malformed X-XMode-Timestamp header.", "malformed");
45
+ if (Math.abs(now() / 1e3 - timestamp) > tolerance) throw new WebhookVerificationError("Webhook timestamp is outside the allowed tolerance.", "timestamp_out_of_tolerance");
46
+ const candidates = signatureHeader.split(/[,\s]+/).filter((part) => part.startsWith("v1=")).map((part) => part.slice(3));
47
+ if (candidates.length === 0) throw new WebhookVerificationError("No v1 signature found in header.", "malformed");
48
+ const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
49
+ name: "HMAC",
50
+ hash: "SHA-256"
51
+ }, false, ["verify"]);
52
+ const message = encoder.encode(`${timestampHeader}.${payload}`);
53
+ let valid = false;
54
+ for (const hex of candidates) {
55
+ const signatureBytes = hexToBytes(hex);
56
+ if (!signatureBytes) continue;
57
+ if (await crypto.subtle.verify("HMAC", key, signatureBytes, message)) {
58
+ valid = true;
59
+ break;
60
+ }
61
+ }
62
+ if (!valid) throw new WebhookVerificationError("Webhook signature verification failed.", "invalid_signature");
63
+ let event;
64
+ try {
65
+ event = JSON.parse(payload);
66
+ } catch {
67
+ throw new WebhookVerificationError("Webhook payload is not valid JSON.", "malformed");
68
+ }
69
+ return {
70
+ event,
71
+ requestId: getHeader(headers, "x-xmode-request-id"),
72
+ timestamp
73
+ };
74
+ }
75
+ /** True when the event is a video record (image records carry `referencesCount`). */
76
+ function isVideoEvent(event) {
77
+ return !("referencesCount" in event);
78
+ }
79
+ /** True when the event is an image generation record. */
80
+ function isGenerationEvent(event) {
81
+ return "referencesCount" in event;
82
+ }
83
+ //#endregion
84
+ export { WebhookVerificationError, isGenerationEvent, isVideoEvent, verifyWebhook };
85
+
86
+ //# sourceMappingURL=webhooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.js","names":[],"sources":["../src/webhooks.ts"],"sourcesContent":["/**\n * Webhook verification — published at the `@xmodeai/sdk/webhooks` subpath.\n *\n * Uses Web Crypto (`crypto.subtle`) only, so it runs unchanged on Node 18+,\n * Deno, Bun, Cloudflare Workers and other edge runtimes without `node:crypto`.\n *\n * Outbound signature contract (what xMode sends):\n * X-XMode-Signature: v1=<hex(HMAC_SHA256(secret, \"<timestamp>.<rawBody>\"))>\n * X-XMode-Timestamp: unix seconds when the payload was built\n * X-XMode-Request-Id: requestId (handy for client-side dedup)\n */\nimport type { GenerationRecord, VideoRecord } from './types';\n\nexport type WebhookVerificationReason =\n | 'missing_header'\n | 'invalid_signature'\n | 'timestamp_out_of_tolerance'\n | 'malformed';\n\nexport class WebhookVerificationError extends Error {\n readonly reason: WebhookVerificationReason;\n constructor(message: string, reason: WebhookVerificationReason) {\n super(message);\n this.name = 'WebhookVerificationError';\n this.reason = reason;\n }\n}\n\n/** The webhook body is the same shape as `generations.get` / `videos.get`. */\nexport type XModeWebhookEvent = GenerationRecord | VideoRecord;\n\nexport type HeadersLike = Headers | Record<string, string | string[] | undefined>;\n\nexport interface VerifyWebhookOptions {\n /** The RAW request body string. Must NOT be a re-serialized object. */\n payload: string;\n /** Request headers, as a `Headers` object or a plain record. */\n headers: HeadersLike;\n /** Your account webhook signing secret. */\n secret: string;\n /** Allowed clock skew in seconds. Defaults to 300 (5 minutes). */\n tolerance?: number;\n /** Clock injection for tests; returns ms since epoch. Defaults to `Date.now`. */\n now?: () => number;\n}\n\nexport interface VerifiedWebhook {\n event: XModeWebhookEvent;\n requestId: string | null;\n timestamp: number;\n}\n\nconst encoder = new TextEncoder();\nconst DEFAULT_TOLERANCE_SECONDS = 300;\n\nfunction getHeader(headers: HeadersLike, name: string): string | null {\n if (typeof Headers !== 'undefined' && headers instanceof Headers) {\n return headers.get(name);\n }\n const target = name.toLowerCase();\n for (const [key, value] of Object.entries(headers)) {\n if (key.toLowerCase() === target) {\n if (Array.isArray(value)) return value[0] ?? null;\n return value ?? null;\n }\n }\n return null;\n}\n\nfunction hexToBytes(hex: string): Uint8Array | null {\n if (hex.length === 0 || hex.length % 2 !== 0 || /[^0-9a-fA-F]/.test(hex)) return null;\n const bytes = new Uint8Array(hex.length / 2);\n for (let i = 0; i < bytes.length; i++) {\n bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);\n }\n return bytes;\n}\n\n/**\n * Verify a webhook delivery and parse its body. Resolves with the typed event,\n * or throws {@link WebhookVerificationError} (with a `reason`) on any failure.\n *\n * ```ts\n * import { verifyWebhook } from '@xmodeai/sdk/webhooks';\n * const { event } = await verifyWebhook({ payload: rawBody, headers: req.headers, secret });\n * ```\n */\nexport async function verifyWebhook(options: VerifyWebhookOptions): Promise<VerifiedWebhook> {\n const { payload, headers, secret } = options;\n const tolerance = options.tolerance ?? DEFAULT_TOLERANCE_SECONDS;\n const now = options.now ?? Date.now;\n\n const signatureHeader = getHeader(headers, 'x-xmode-signature');\n const timestampHeader = getHeader(headers, 'x-xmode-timestamp');\n if (!signatureHeader || !timestampHeader) {\n throw new WebhookVerificationError(\n 'Missing X-XMode-Signature or X-XMode-Timestamp header.',\n 'missing_header',\n );\n }\n\n const timestamp = Number(timestampHeader);\n if (!Number.isFinite(timestamp)) {\n throw new WebhookVerificationError('Malformed X-XMode-Timestamp header.', 'malformed');\n }\n if (Math.abs(now() / 1000 - timestamp) > tolerance) {\n throw new WebhookVerificationError(\n 'Webhook timestamp is outside the allowed tolerance.',\n 'timestamp_out_of_tolerance',\n );\n }\n\n // Header is `v1=<hex>`; tolerate several space/comma-separated entries (rotation).\n const candidates = signatureHeader\n .split(/[,\\s]+/)\n .filter((part) => part.startsWith('v1='))\n .map((part) => part.slice(3));\n if (candidates.length === 0) {\n throw new WebhookVerificationError('No v1 signature found in header.', 'malformed');\n }\n\n const key = await crypto.subtle.importKey(\n 'raw',\n encoder.encode(secret) as BufferSource,\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['verify'],\n );\n // Sign over the RAW timestamp header, not the parsed number, so verification\n // never depends on the server using a canonical integer representation.\n const message = encoder.encode(`${timestampHeader}.${payload}`) as BufferSource;\n\n let valid = false;\n for (const hex of candidates) {\n const signatureBytes = hexToBytes(hex);\n if (!signatureBytes) continue;\n // crypto.subtle.verify is constant-time internally.\n if (await crypto.subtle.verify('HMAC', key, signatureBytes as BufferSource, message)) {\n valid = true;\n break;\n }\n }\n if (!valid) {\n throw new WebhookVerificationError('Webhook signature verification failed.', 'invalid_signature');\n }\n\n let event: XModeWebhookEvent;\n try {\n event = JSON.parse(payload) as XModeWebhookEvent;\n } catch {\n throw new WebhookVerificationError('Webhook payload is not valid JSON.', 'malformed');\n }\n\n return { event, requestId: getHeader(headers, 'x-xmode-request-id'), timestamp };\n}\n\n/** True when the event is a video record (image records carry `referencesCount`). */\nexport function isVideoEvent(event: XModeWebhookEvent): event is VideoRecord {\n return !('referencesCount' in event);\n}\n\n/** True when the event is an image generation record. */\nexport function isGenerationEvent(event: XModeWebhookEvent): event is GenerationRecord {\n return 'referencesCount' in event;\n}\n"],"mappings":";AAmBA,IAAa,2BAAb,cAA8C,MAAM;CAClD;CACA,YAAY,SAAiB,QAAmC;EAC9D,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,SAAS;CAChB;AACF;AA0BA,MAAM,UAAU,IAAI,YAAY;AAChC,MAAM,4BAA4B;AAElC,SAAS,UAAU,SAAsB,MAA6B;CACpE,IAAI,OAAO,YAAY,eAAe,mBAAmB,SACvD,OAAO,QAAQ,IAAI,IAAI;CAEzB,MAAM,SAAS,KAAK,YAAY;CAChC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,GAC/C,IAAI,IAAI,YAAY,MAAM,QAAQ;EAChC,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,MAAM,MAAM;EAC7C,OAAO,SAAS;CAClB;CAEF,OAAO;AACT;AAEA,SAAS,WAAW,KAAgC;CAClD,IAAI,IAAI,WAAW,KAAK,IAAI,SAAS,MAAM,KAAK,eAAe,KAAK,GAAG,GAAG,OAAO;CACjF,MAAM,QAAQ,IAAI,WAAW,IAAI,SAAS,CAAC;CAC3C,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAChC,MAAM,KAAK,OAAO,SAAS,IAAI,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE;CAE5D,OAAO;AACT;;;;;;;;;;AAWA,eAAsB,cAAc,SAAyD;CAC3F,MAAM,EAAE,SAAS,SAAS,WAAW;CACrC,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAM,MAAM,QAAQ,OAAO,KAAK;CAEhC,MAAM,kBAAkB,UAAU,SAAS,mBAAmB;CAC9D,MAAM,kBAAkB,UAAU,SAAS,mBAAmB;CAC9D,IAAI,CAAC,mBAAmB,CAAC,iBACvB,MAAM,IAAI,yBACR,0DACA,gBACF;CAGF,MAAM,YAAY,OAAO,eAAe;CACxC,IAAI,CAAC,OAAO,SAAS,SAAS,GAC5B,MAAM,IAAI,yBAAyB,uCAAuC,WAAW;CAEvF,IAAI,KAAK,IAAI,IAAI,IAAI,MAAO,SAAS,IAAI,WACvC,MAAM,IAAI,yBACR,uDACA,4BACF;CAIF,MAAM,aAAa,gBAChB,MAAM,QAAQ,CAAC,CACf,QAAQ,SAAS,KAAK,WAAW,KAAK,CAAC,CAAC,CACxC,KAAK,SAAS,KAAK,MAAM,CAAC,CAAC;CAC9B,IAAI,WAAW,WAAW,GACxB,MAAM,IAAI,yBAAyB,oCAAoC,WAAW;CAGpF,MAAM,MAAM,MAAM,OAAO,OAAO,UAC9B,OACA,QAAQ,OAAO,MAAM,GACrB;EAAE,MAAM;EAAQ,MAAM;CAAU,GAChC,OACA,CAAC,QAAQ,CACX;CAGA,MAAM,UAAU,QAAQ,OAAO,GAAG,gBAAgB,GAAG,SAAS;CAE9D,IAAI,QAAQ;CACZ,KAAK,MAAM,OAAO,YAAY;EAC5B,MAAM,iBAAiB,WAAW,GAAG;EACrC,IAAI,CAAC,gBAAgB;EAErB,IAAI,MAAM,OAAO,OAAO,OAAO,QAAQ,KAAK,gBAAgC,OAAO,GAAG;GACpF,QAAQ;GACR;EACF;CACF;CACA,IAAI,CAAC,OACH,MAAM,IAAI,yBAAyB,0CAA0C,mBAAmB;CAGlG,IAAI;CACJ,IAAI;EACF,QAAQ,KAAK,MAAM,OAAO;CAC5B,QAAQ;EACN,MAAM,IAAI,yBAAyB,sCAAsC,WAAW;CACtF;CAEA,OAAO;EAAE;EAAO,WAAW,UAAU,SAAS,oBAAoB;EAAG;CAAU;AACjF;;AAGA,SAAgB,aAAa,OAAgD;CAC3E,OAAO,EAAE,qBAAqB;AAChC;;AAGA,SAAgB,kBAAkB,OAAqD;CACrF,OAAO,qBAAqB;AAC9B"}