@traceten/sdk-node 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.
package/dist/client.js ADDED
@@ -0,0 +1,320 @@
1
+ /**
2
+ * The buffered Traceten client.
3
+ *
4
+ * `page()` and `track()` split at the API surface (two endpoints, two payload
5
+ * shapes) but unify in one buffered client with a single flush that drains both
6
+ * internal queues. See DESIGN.md §3–§4.
7
+ */
8
+ import { Transport, chunk } from "./queue.js";
9
+ import { normalizeCurrency, normalizeHost, toIsoTimestamp, validateConversionName, validateConversionSessionId, validateEventName, validateEventSessionId, validateGoalName, validateReferrer, validateAmount, validateApiKey, validateProvider, validateSiteId, validateTransactionId, normalizePaymentCurrency, validateUrl, validateUserAgent, validateValueCents, validateVisitorId, } from "./validate.js";
10
+ const DEFAULT_FLUSH_AT = 50;
11
+ const MAX_FLUSH_AT = 50;
12
+ const MIN_FLUSH_AT = 1;
13
+ const DEFAULT_FLUSH_INTERVAL_MS = 5000;
14
+ const DEFAULT_MAX_RETRIES = 3;
15
+ const DEFAULT_TIMEOUT_MS = 10000;
16
+ /** Node process signals we install best-effort flush handlers for. */
17
+ const EXIT_SIGNALS = ["SIGTERM", "SIGINT"];
18
+ function clamp(value, min, max) {
19
+ return Math.min(max, Math.max(min, value));
20
+ }
21
+ /** Validate an optional positive-number config field. */
22
+ function positiveOr(value, fallback, name) {
23
+ if (value === undefined)
24
+ return fallback;
25
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
26
+ throw new TypeError(`Traceten: ${name} must be a positive number`);
27
+ }
28
+ return value;
29
+ }
30
+ export class Client {
31
+ siteId;
32
+ host;
33
+ flushAt;
34
+ transport;
35
+ onError;
36
+ events = [];
37
+ conversions = [];
38
+ timer;
39
+ inflight = new Set();
40
+ exitHooks = [];
41
+ closed = false;
42
+ constructor(options) {
43
+ if (options === null || typeof options !== "object") {
44
+ throw new TypeError("Traceten: Client requires an options object");
45
+ }
46
+ this.siteId = validateSiteId(options.siteId);
47
+ this.host = normalizeHost(options.host);
48
+ // Validated BEFORE anything else is set up: an integration missing its key
49
+ // would otherwise start, buffer, flush, and 401 every batch forever while
50
+ // looking perfectly healthy from the outside.
51
+ const apiKey = validateApiKey(options.apiKey);
52
+ this.flushAt = clamp(options.flushAt ?? DEFAULT_FLUSH_AT, MIN_FLUSH_AT, MAX_FLUSH_AT);
53
+ this.onError = options.onError ?? (() => undefined);
54
+ const flushInterval = positiveOr(options.flushInterval, DEFAULT_FLUSH_INTERVAL_MS, "flushInterval");
55
+ const maxRetries = options.maxRetries === undefined
56
+ ? DEFAULT_MAX_RETRIES
57
+ : (() => {
58
+ if (typeof options.maxRetries !== "number" ||
59
+ !Number.isInteger(options.maxRetries) ||
60
+ options.maxRetries < 0) {
61
+ throw new TypeError("Traceten: maxRetries must be a non-negative integer");
62
+ }
63
+ return options.maxRetries;
64
+ })();
65
+ const timeoutMs = positiveOr(options.timeoutMs, DEFAULT_TIMEOUT_MS, "timeoutMs");
66
+ this.transport = new Transport({
67
+ host: this.host,
68
+ apiKey,
69
+ maxRetries,
70
+ timeoutMs,
71
+ onError: this.onError,
72
+ });
73
+ // Background flush timer — unref'd so it never keeps the process alive.
74
+ this.timer = setInterval(() => {
75
+ void this.flush();
76
+ }, flushInterval);
77
+ this.timer.unref();
78
+ if (options.flushOnExit ?? true) {
79
+ this.registerExitHooks();
80
+ }
81
+ }
82
+ /**
83
+ * Enqueue a pageview (`/v1/server/events`). Returns immediately; never throws on
84
+ * network problems. Throws synchronously on programmer errors (bad url,
85
+ * bad eventName, bad visitorId).
86
+ */
87
+ page(props) {
88
+ this.assertOpen();
89
+ if (props === null || typeof props !== "object") {
90
+ throw new TypeError("Traceten: page() requires a props object");
91
+ }
92
+ const url = validateUrl(props.url);
93
+ const eventName = validateEventName(props.eventName ?? "pageview");
94
+ const referrer = validateReferrer(props.referrer ?? "");
95
+ const ts = toIsoTimestamp(props.timestamp);
96
+ const event = { siteId: this.siteId, ts, url, referrer, eventName };
97
+ if (props.sessionId !== undefined) {
98
+ event.sessionId = validateEventSessionId(props.sessionId);
99
+ }
100
+ if (props.visitorId !== undefined) {
101
+ event.vid = validateVisitorId(props.visitorId);
102
+ }
103
+ if (props.userAgent !== undefined) {
104
+ event.ua = validateUserAgent(props.userAgent);
105
+ }
106
+ this.events.push(event);
107
+ this.maybeFlush();
108
+ }
109
+ /**
110
+ * Enqueue a conversion (`/v1/server/conversions`). `opts.visitorId` is REQUIRED —
111
+ * omitting it is a programmer error and throws synchronously (a conversion
112
+ * cannot be attributed without it). Returns immediately otherwise.
113
+ */
114
+ track(name, opts) {
115
+ this.assertOpen();
116
+ const eventName = validateConversionName(name);
117
+ if (opts === null || typeof opts !== "object") {
118
+ throw new TypeError("Traceten: track() requires an options object with a visitorId");
119
+ }
120
+ if (opts.visitorId === undefined) {
121
+ throw new TypeError("Traceten: track() requires opts.visitorId — conversions cannot attribute without it");
122
+ }
123
+ const visitorId = validateVisitorId(opts.visitorId);
124
+ const timestamp = toIsoTimestamp(opts.timestamp);
125
+ const conversion = {
126
+ event_name: eventName,
127
+ site_id: this.siteId,
128
+ timestamp,
129
+ visitor_id: visitorId,
130
+ properties: opts.properties ?? {},
131
+ };
132
+ if (opts.sessionId !== undefined) {
133
+ conversion.session_id = validateConversionSessionId(opts.sessionId);
134
+ }
135
+ if (opts.valueCents !== undefined) {
136
+ conversion.value_cents = validateValueCents(opts.valueCents);
137
+ }
138
+ if (opts.currency !== undefined) {
139
+ conversion.currency = normalizeCurrency(opts.currency);
140
+ }
141
+ this.conversions.push(conversion);
142
+ this.maybeFlush();
143
+ }
144
+ /**
145
+ * Enqueue a goal completion. Identical to {@link Client.track} in every
146
+ * respect except the name check: the reserved Stripe/Shopify names are
147
+ * refused, because a goal that collides with one silently corrupts the site's
148
+ * revenue funnel.
149
+ *
150
+ * A goal IS a custom event — this is the same `/v1/server/conversions` call
151
+ * under a name that matches what the dashboard calls it.
152
+ *
153
+ * @example
154
+ * ```ts
155
+ * traceten.goal("demo_booked", { visitorId, properties: { plan: "pro" } });
156
+ * ```
157
+ */
158
+ goal(name, opts) {
159
+ this.track(validateGoalName(name), opts);
160
+ }
161
+ /**
162
+ * Record a payment from ANY payment processor (`POST /v1/server/payments`).
163
+ *
164
+ * ⚠️ The only method here that is AWAITED and that can reject. `page()` and
165
+ * `track()` are buffered and fire-and-forget because a lost pageview is a lost
166
+ * pageview; a lost payment is missing revenue, and the endpoint's whole point
167
+ * is the idempotency answer it returns — which a queue could not give back.
168
+ *
169
+ * Safe to call again with the same `transactionId`: the server deduplicates
170
+ * durably on `(site, provider, transactionId)` and answers `"duplicate"`
171
+ * rather than recording a second payment.
172
+ *
173
+ * `settlementAmount`/`settlementCurrency` are an optional fallback: your
174
+ * processor's own conversion of the payment into a currency Traceten can
175
+ * price, used only when `currency` itself isn't one Traceten prices
176
+ * natively. Set both or neither — one without the other is dropped.
177
+ *
178
+ * @example
179
+ * ```ts
180
+ * const res = await traceten.payment({
181
+ * transactionId: charge.id,
182
+ * amount: 49.99,
183
+ * currency: "USD",
184
+ * provider: "dodo",
185
+ * email: customer.email,
186
+ * });
187
+ * // res.status === "recorded"
188
+ * ```
189
+ */
190
+ async payment(props) {
191
+ this.assertOpen();
192
+ if (props === null || typeof props !== "object") {
193
+ throw new TypeError("Traceten: payment() requires a props object");
194
+ }
195
+ const payment = {
196
+ site_id: this.siteId,
197
+ transaction_id: validateTransactionId(props.transactionId),
198
+ amount: validateAmount(props.amount),
199
+ currency: normalizePaymentCurrency(props.currency),
200
+ };
201
+ if (props.provider !== undefined)
202
+ payment.provider = validateProvider(props.provider);
203
+ if (props.visitorId !== undefined)
204
+ payment.visitor_id = validateVisitorId(props.visitorId);
205
+ if (props.email !== undefined)
206
+ payment.email = props.email;
207
+ if (props.customerId !== undefined)
208
+ payment.customer_id = props.customerId;
209
+ if (props.renewal !== undefined)
210
+ payment.renewal = props.renewal;
211
+ if (props.refunded !== undefined)
212
+ payment.refunded = props.refunded;
213
+ if (props.isFreeTrial !== undefined)
214
+ payment.is_free_trial = props.isFreeTrial;
215
+ if (props.timestamp !== undefined)
216
+ payment.timestamp = toIsoTimestamp(props.timestamp);
217
+ // Fallback fields — never sent unless BOTH are present. One without the
218
+ // other is not a valid settlement figure and the server would reject it.
219
+ if (props.settlementAmount !== undefined && props.settlementCurrency !== undefined) {
220
+ payment.settlement_amount = validateAmount(props.settlementAmount);
221
+ payment.settlement_currency = normalizePaymentCurrency(props.settlementCurrency);
222
+ }
223
+ return this.transport.sendPayment(payment);
224
+ }
225
+ /**
226
+ * Drain both queues now. Resolves once every in-flight request settles.
227
+ * Never rejects — delivery failures surface via `onError`.
228
+ */
229
+ async flush() {
230
+ // Snapshot + clear synchronously so concurrent enqueues aren't lost or
231
+ // double-sent. JS is single-threaded, so nothing runs between these lines.
232
+ const events = this.events;
233
+ this.events = [];
234
+ const conversions = this.conversions;
235
+ this.conversions = [];
236
+ // Send sequentially, not via Promise.all: firing one request per queued
237
+ // conversion concurrently would open N connections at once and, against the
238
+ // Worker's per-site / per-IP rate limits (100–200 req/min), trigger a 429
239
+ // storm that drops revenue events after retries. Matches the Go/Python SDKs.
240
+ for (const batch of chunk(events, this.flushAt)) {
241
+ await this.launch(this.transport.sendEvents(batch));
242
+ }
243
+ for (const conversion of conversions) {
244
+ await this.launch(this.transport.sendConversion(conversion));
245
+ }
246
+ }
247
+ /**
248
+ * Flush, stop the background timer, and remove exit hooks. Idempotent — safe
249
+ * to call more than once. After close, `page()`/`track()` throw.
250
+ */
251
+ async close() {
252
+ if (this.closed)
253
+ return;
254
+ this.closed = true;
255
+ if (this.timer !== undefined) {
256
+ clearInterval(this.timer);
257
+ this.timer = undefined;
258
+ }
259
+ for (const { event, handler } of this.exitHooks) {
260
+ process.removeListener(event, handler);
261
+ }
262
+ this.exitHooks = [];
263
+ await this.flush();
264
+ // Await any batches launched by a prior auto-/timer-flush still in flight.
265
+ await Promise.all([...this.inflight]);
266
+ }
267
+ /** Alias for {@link Client.close} — matches the cross-language `shutdown()`. */
268
+ shutdown() {
269
+ return this.close();
270
+ }
271
+ // ── internals ──────────────────────────────────────────────────────────────
272
+ assertOpen() {
273
+ if (this.closed) {
274
+ throw new Error("Traceten: client is closed; construct a new Client to send more events");
275
+ }
276
+ }
277
+ /** Trigger a flush when either queue has reached the flush threshold. */
278
+ maybeFlush() {
279
+ if (this.events.length >= this.flushAt || this.conversions.length >= this.flushAt) {
280
+ void this.flush();
281
+ }
282
+ }
283
+ /** Track an in-flight send so `close()` can await it, then self-remove. */
284
+ launch(promise) {
285
+ const tracked = promise.finally(() => {
286
+ this.inflight.delete(tracked);
287
+ });
288
+ this.inflight.add(tracked);
289
+ return tracked;
290
+ }
291
+ /**
292
+ * Register best-effort flush-on-exit hooks.
293
+ *
294
+ * `beforeExit` fires when the loop would otherwise empty — we flush and let
295
+ * the pending I/O keep the process alive until it settles. For `SIGTERM` /
296
+ * `SIGINT` we flush, then re-exit ONLY if we were the sole handler (otherwise
297
+ * the host app owns the shutdown decision and forcing `process.exit` would
298
+ * stomp its own handlers / exit code).
299
+ */
300
+ registerExitHooks() {
301
+ const beforeExit = () => {
302
+ void this.flush();
303
+ };
304
+ process.once("beforeExit", beforeExit);
305
+ this.exitHooks.push({ event: "beforeExit", handler: beforeExit });
306
+ for (const signal of EXIT_SIGNALS) {
307
+ const soleHandler = process.listenerCount(signal) === 0;
308
+ const handler = () => {
309
+ void this.flush().finally(() => {
310
+ if (soleHandler) {
311
+ process.exit(0);
312
+ }
313
+ });
314
+ };
315
+ process.once(signal, handler);
316
+ this.exitHooks.push({ event: signal, handler });
317
+ }
318
+ }
319
+ }
320
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `@traceten/sdk-node` — the Traceten server-side SDK for Node.js.
3
+ *
4
+ * Send AI-traffic pageviews (`page`) and revenue/conversion events (`track`)
5
+ * from your backend over authenticated, ad-blocker-resistant HTTP. See
6
+ * `API.md` for the quickstart and full reference.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { Client } from "@traceten/sdk-node";
11
+ *
12
+ * const traceten = new Client({ siteId: "ttid_7Rb4TrC1dTbnD8w3s1TS12", host: "https://ingest.traceten.com" });
13
+ * traceten.page({ url: "https://acme.com/pricing", visitorId });
14
+ * traceten.track("subscription_started", { visitorId, valueCents: 4900, currency: "usd" });
15
+ * await traceten.close(); // flush on shutdown
16
+ * ```
17
+ */
18
+ export { Client } from "./client.js";
19
+ export type { ClientOptions, PageProps, PaymentProps, PaymentResult, PaymentStatus, TrackOptions, OnError, EventWire, ConversionWire, PaymentWire, } from "./types.js";
20
+ export { verifyWebhook, WebhookVerificationError, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, WEBHOOK_REPLAY_TOLERANCE_SECONDS, WEBHOOK_EVENT_TYPES, ATTRIBUTION_MODELS, } from "./webhook.js";
21
+ export type { WebhookEvent, WebhookEventType, VerifyWebhookOptions, AiSessionClassifiedData, ConversionAttributedData, WebhookPingData, AttributionModel, AttributionModelBreakdown, } from "./webhook.js";
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EACV,aAAa,EACb,SAAS,EACT,YAAY,EACZ,aAAa,EACb,aAAa,EACb,YAAY,EACZ,OAAO,EACP,SAAS,EACT,cAAc,EACd,WAAW,GACZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,aAAa,EACb,wBAAwB,EACxB,wBAAwB,EACxB,wBAAwB,EACxB,gCAAgC,EAChC,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,YAAY,EACZ,gBAAgB,EAChB,oBAAoB,EACpB,uBAAuB,EACvB,wBAAwB,EACxB,eAAe,EACf,gBAAgB,EAChB,yBAAyB,GAC1B,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `@traceten/sdk-node` — the Traceten server-side SDK for Node.js.
3
+ *
4
+ * Send AI-traffic pageviews (`page`) and revenue/conversion events (`track`)
5
+ * from your backend over authenticated, ad-blocker-resistant HTTP. See
6
+ * `API.md` for the quickstart and full reference.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { Client } from "@traceten/sdk-node";
11
+ *
12
+ * const traceten = new Client({ siteId: "ttid_7Rb4TrC1dTbnD8w3s1TS12", host: "https://ingest.traceten.com" });
13
+ * traceten.page({ url: "https://acme.com/pricing", visitorId });
14
+ * traceten.track("subscription_started", { visitorId, valueCents: 4900, currency: "usd" });
15
+ * await traceten.close(); // flush on shutdown
16
+ * ```
17
+ */
18
+ export { Client } from "./client.js";
19
+ export { verifyWebhook, WebhookVerificationError, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, WEBHOOK_REPLAY_TOLERANCE_SECONDS, WEBHOOK_EVENT_TYPES, ATTRIBUTION_MODELS, } from "./webhook.js";
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAcrC,OAAO,EACL,aAAa,EACb,wBAAwB,EACxB,wBAAwB,EACxB,wBAAwB,EACxB,gCAAgC,EAChC,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,cAAc,CAAC"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Transport layer for the Traceten Node SDK.
3
+ *
4
+ * Owns the "one HTTP request, with retry" primitive that both queues share.
5
+ * Batching/chunking lives in the {@link Client}; this module only knows how to
6
+ * deliver a single already-shaped body to a single endpoint, retrying on
7
+ * transient failures per DESIGN.md §4.
8
+ *
9
+ * Retry policy:
10
+ * - 2xx → success.
11
+ * - 5xx or 429 or network/timeout → retryable; exponential backoff with full
12
+ * jitter (`base 200ms * 2^attempt`), up to
13
+ * `maxRetries` retries.
14
+ * - other 4xx → the payload is malformed; DROP it and call
15
+ * `onError`. Retrying a 400 forever is a
16
+ * self-DoS.
17
+ */
18
+ import type { ConversionWire, EventWire, OnError, PaymentResult, PaymentWire } from "./types.js";
19
+ export interface TransportOptions {
20
+ host: string;
21
+ /** REQUIRED — `/v1/server/*` returns 401 without it. */
22
+ apiKey: string;
23
+ maxRetries: number;
24
+ timeoutMs: number;
25
+ onError: OnError;
26
+ }
27
+ /** Split `items` into contiguous chunks of at most `size`. */
28
+ export declare function chunk<T>(items: readonly T[], size: number): T[][];
29
+ export declare class Transport {
30
+ private readonly eventsUrl;
31
+ private readonly conversionsUrl;
32
+ private readonly paymentsUrl;
33
+ private readonly headers;
34
+ private readonly maxRetries;
35
+ private readonly timeoutMs;
36
+ private readonly onError;
37
+ constructor(opts: TransportOptions);
38
+ /** Send one `/v1/server/events` batch envelope. Never throws. */
39
+ sendEvents(events: EventWire[]): Promise<void>;
40
+ /** Send one `/v1/server/conversions` body (one event per request). Never throws. */
41
+ sendConversion(conversion: ConversionWire): Promise<void>;
42
+ /**
43
+ * Send one `/v1/server/payments` body and RETURN the outcome (#794).
44
+ *
45
+ * Unlike the two above, this one is awaited and its result surfaced: the
46
+ * endpoint's whole contract is the idempotency answer, so swallowing it would
47
+ * discard the one thing the caller asked for.
48
+ *
49
+ * Retrying is safe for the same reason — the server deduplicates on
50
+ * `(site, provider, transaction_id)` durably, so a retried 5xx cannot become a
51
+ * second payment.
52
+ *
53
+ * @throws when the payment was NOT accepted after every retry. `payment()` is
54
+ * the only SDK call that rejects, because a silently dropped payment is
55
+ * missing revenue rather than a missing pageview.
56
+ */
57
+ sendPayment(payment: PaymentWire): Promise<PaymentResult>;
58
+ /** One attempt that also reads the JSON body, for the payments endpoint. */
59
+ private attemptJson;
60
+ /**
61
+ * POST `body` to `url`, retrying transient failures. Resolves once the
62
+ * request either succeeds or is permanently given up on (after calling
63
+ * `onError`). Deliberately never rejects — `page()`/`track()`/`flush()` must
64
+ * not throw on network problems.
65
+ */
66
+ private deliver;
67
+ /** A single HTTP attempt, classified into an {@link Outcome}. */
68
+ private attempt;
69
+ /** Invoke the user `onError` hook, swallowing any error it throws. */
70
+ private reportError;
71
+ }
72
+ //# sourceMappingURL=queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.d.ts","sourceRoot":"","sources":["../src/queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAKjG,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,8DAA8D;AAC9D,wBAAgB,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,EAAE,EAAE,CAMjE;AAuBD,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IACjD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;gBAEtB,IAAI,EAAE,gBAAgB;IAoBlC,iEAAiE;IAC3D,UAAU,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAKpD,oFAAoF;IAC9E,cAAc,CAAC,UAAU,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/D;;;;;;;;;;;;;;OAcG;IACG,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC;IAsB/D,4EAA4E;YAC9D,WAAW;IAgDzB;;;;;OAKG;YACW,OAAO;IAmBrB,iEAAiE;YACnD,OAAO;IAwCrB,sEAAsE;IACtE,OAAO,CAAC,WAAW;CAOpB"}