@customyai/customy-sdk 0.1.0 → 0.3.2

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,452 @@
1
+ import { warnDeprecated, CustomySdkError, CustomyProductClient } from './chunk-25MIDL7U.mjs';
2
+ import { createCustomy as createCustomy$1, CustomySdkError as CustomySdkError$1 } from '@customyai/sdk';
3
+ import { CustomySend } from '@customyai/send-sdk';
4
+ import { CustomyLinks } from '@customyai/links-sdk';
5
+
6
+ // src/vendor/sdk-data.ts
7
+ var CUSTOMY_DATA_SDK_VERSION = "0.3.1";
8
+ var DataSdkError = class extends Error {
9
+ constructor(message, statusCode, response) {
10
+ super(message);
11
+ this.statusCode = statusCode;
12
+ this.response = response;
13
+ this.name = "DataSdkError";
14
+ }
15
+ statusCode;
16
+ response;
17
+ };
18
+ var CustomyCustomerDataClient = class {
19
+ constructor(config) {
20
+ this.config = config;
21
+ if (!config.collectUrl || !config.writeKey && !config.accessToken) {
22
+ throw new Error("collectUrl and writeKey (or accessToken) are required");
23
+ }
24
+ const fetchImpl = config.fetchImpl ?? globalThis.fetch;
25
+ if (!fetchImpl) throw new Error("fetch is required. Pass fetchImpl in non-browser runtimes.");
26
+ this.fetchImpl = fetchImpl;
27
+ this.maxRetries = config.maxRetries ?? 3;
28
+ this.retryBaseMs = config.retryBaseMs ?? 250;
29
+ this.timeoutMs = config.timeoutMs ?? 1e4;
30
+ this.maxBatchSize = Math.min(1e3, Math.max(1, config.maxBatchSize ?? 100));
31
+ this.maxQueueSize = Math.max(1, config.maxQueueSize ?? 1e4);
32
+ this.redactFields = new Set(config.redactFields ?? []);
33
+ this.now = config.now ?? (() => /* @__PURE__ */ new Date());
34
+ this.idFactory = config.idFactory ?? randomId;
35
+ }
36
+ config;
37
+ fetchImpl;
38
+ maxRetries;
39
+ retryBaseMs;
40
+ timeoutMs;
41
+ maxBatchSize;
42
+ maxQueueSize;
43
+ redactFields;
44
+ now;
45
+ idFactory;
46
+ queue = [];
47
+ flushing = false;
48
+ inFlightCount = 0;
49
+ event(input) {
50
+ rejectCustomerDataTenantFields(input);
51
+ let normalized = {
52
+ ...input,
53
+ messageId: input.messageId ?? this.idFactory(),
54
+ timestamp: normalizeDate(input.timestamp ?? this.now()),
55
+ schemaVersion: input.schemaVersion ?? "1.0",
56
+ properties: input.properties ?? {},
57
+ traits: input.traits ?? {},
58
+ context: {
59
+ ...input.context,
60
+ library: {
61
+ name: "./sdk-data",
62
+ version: CUSTOMY_DATA_SDK_VERSION
63
+ }
64
+ },
65
+ consent: input.consent ?? {}
66
+ };
67
+ validateCustomerDataEvent(normalized);
68
+ normalized = redactCustomerDataEvent(normalized, this.redactFields);
69
+ if (this.config.beforeSend) {
70
+ const candidate = this.config.beforeSend(cloneCustomerDataEvent(normalized));
71
+ if (!candidate) throw new Error("Event blocked by beforeSend");
72
+ rejectCustomerDataTenantFields(candidate);
73
+ normalized = redactCustomerDataEvent(candidate, this.redactFields);
74
+ validateCustomerDataEvent(normalized);
75
+ }
76
+ return normalized;
77
+ }
78
+ enqueue(input) {
79
+ if (this.queue.length + this.inFlightCount >= this.maxQueueSize) {
80
+ throw new Error("Customer data queue is full");
81
+ }
82
+ const normalized = this.event(input);
83
+ if (this.queue.length + this.inFlightCount >= this.maxQueueSize) {
84
+ throw new Error("Customer data queue is full");
85
+ }
86
+ this.queue.push(normalized);
87
+ return this.queue.length;
88
+ }
89
+ async send(input) {
90
+ return this.request("event", this.event(input));
91
+ }
92
+ async track(event, properties, identity) {
93
+ return this.send({ type: "track", event, properties, ...identity });
94
+ }
95
+ async identify(traits, identity) {
96
+ return this.send({ type: "identify", traits, ...identity });
97
+ }
98
+ async group(traits, identity) {
99
+ return this.send({ type: "group", traits, ...identity });
100
+ }
101
+ async page(properties, identity) {
102
+ return this.send({ type: "page", properties, ...identity });
103
+ }
104
+ async screen(properties, identity) {
105
+ return this.send({ type: "screen", properties, ...identity });
106
+ }
107
+ async alias(userId, previousId, options = {}) {
108
+ return this.send({
109
+ type: "alias",
110
+ userId,
111
+ anonymousId: previousId,
112
+ properties: { previousId },
113
+ ...options
114
+ });
115
+ }
116
+ async flush() {
117
+ if (this.flushing) throw new Error("A customer data flush is already in progress");
118
+ if (this.queue.length === 0) {
119
+ return {
120
+ accepted: 0,
121
+ deduplicated: 0,
122
+ quarantined: 0,
123
+ results: []
124
+ };
125
+ }
126
+ this.flushing = true;
127
+ const pending = this.queue;
128
+ this.inFlightCount = pending.length;
129
+ this.queue = [];
130
+ const aggregate = {
131
+ accepted: 0,
132
+ deduplicated: 0,
133
+ quarantined: 0,
134
+ results: []
135
+ };
136
+ try {
137
+ for (let offset = 0; offset < pending.length; offset += this.maxBatchSize) {
138
+ const batch = pending.slice(offset, offset + this.maxBatchSize);
139
+ const response = await this.request("batch", {
140
+ batch
141
+ });
142
+ aggregate.accepted += Number(response.accepted ?? 0);
143
+ aggregate.deduplicated += Number(response.deduplicated ?? 0);
144
+ aggregate.quarantined += Number(response.quarantined ?? 0);
145
+ aggregate.results.push(...response.results ?? []);
146
+ }
147
+ this.flushing = false;
148
+ this.inFlightCount = 0;
149
+ return aggregate;
150
+ } catch (error) {
151
+ this.queue = [...pending, ...this.queue];
152
+ this.flushing = false;
153
+ this.inFlightCount = 0;
154
+ const normalized = error instanceof Error ? error : new Error(String(error));
155
+ this.config.onError?.(normalized, pending);
156
+ throw normalized;
157
+ }
158
+ }
159
+ async request(path, body) {
160
+ let lastError;
161
+ for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) {
162
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
163
+ const timer = controller ? setTimeout(() => controller.abort(), this.timeoutMs) : void 0;
164
+ try {
165
+ const response = await this.fetchImpl(`${this.config.collectUrl.replace(/\/$/, "")}/v1/collect/${path}`, {
166
+ method: "POST",
167
+ headers: {
168
+ "content-type": "application/json",
169
+ ...this.config.accessToken ? { authorization: `Bearer ${await this.config.accessToken()}` } : { "x-write-key": this.config.writeKey }
170
+ },
171
+ body: JSON.stringify(body),
172
+ signal: controller?.signal
173
+ });
174
+ const text = await response.text();
175
+ const parsed = text ? safeJson(text) : {};
176
+ if (!response.ok) {
177
+ throw new DataSdkError(`Customy customer data collection failed with HTTP ${response.status}`, response.status, parsed);
178
+ }
179
+ validateCustomerDataAcknowledgement(
180
+ path,
181
+ parsed,
182
+ path === "batch" ? body.batch.length : 1,
183
+ response.status
184
+ );
185
+ return parsed;
186
+ } catch (error) {
187
+ lastError = error instanceof Error ? error : new Error(String(error));
188
+ if (!isRetryableError(lastError) || attempt >= this.maxRetries) throw lastError;
189
+ await sleep(this.retryBaseMs * Math.pow(2, attempt));
190
+ } finally {
191
+ if (timer) clearTimeout(timer);
192
+ }
193
+ }
194
+ throw lastError ?? new Error("Unknown customer data collection failure");
195
+ }
196
+ };
197
+ function isRecord(value) {
198
+ return value !== null && typeof value === "object" && !Array.isArray(value);
199
+ }
200
+ function collectionOutcome(value) {
201
+ if (!isRecord(value)) return void 0;
202
+ const hasId = (id) => typeof id === "string" && id.trim().length > 0;
203
+ if (value.quarantined === true) {
204
+ return value.accepted === false && value.deduplicated === false && hasId(value.quarantineId) ? "quarantined" : void 0;
205
+ }
206
+ if (value.quarantined !== void 0 && value.quarantined !== false) return void 0;
207
+ if (!hasId(value.eventId)) return void 0;
208
+ if (value.accepted === true && value.deduplicated === false) return "accepted";
209
+ if (value.accepted === false && value.deduplicated === true) return "deduplicated";
210
+ return void 0;
211
+ }
212
+ function validateCustomerDataAcknowledgement(path, value, expectedCount, status) {
213
+ const reject = () => {
214
+ throw new DataSdkError("Customy customer data collection returned an invalid acknowledgement", status);
215
+ };
216
+ if (path === "event") {
217
+ const outcome = collectionOutcome(value);
218
+ if (outcome !== "accepted" && outcome !== "deduplicated") reject();
219
+ return;
220
+ }
221
+ if (!isRecord(value) || !Array.isArray(value.results) || value.results.length !== expectedCount) return reject();
222
+ const counts = { accepted: 0, deduplicated: 0, quarantined: 0 };
223
+ for (const result of value.results) {
224
+ const outcome = collectionOutcome(result);
225
+ if (!outcome) return reject();
226
+ counts[outcome] += 1;
227
+ }
228
+ for (const key of ["accepted", "deduplicated", "quarantined"]) {
229
+ if (!Number.isSafeInteger(value[key]) || value[key] !== counts[key]) reject();
230
+ }
231
+ }
232
+ var CUSTOMER_DATA_EVENT_TYPES = /* @__PURE__ */ new Set([
233
+ "track",
234
+ "identify",
235
+ "group",
236
+ "page",
237
+ "screen",
238
+ "alias"
239
+ ]);
240
+ var FORBIDDEN_CUSTOMER_DATA_TENANT_FIELDS = [
241
+ "tenantId",
242
+ "organizationId",
243
+ "projectId",
244
+ "environmentId"
245
+ ];
246
+ function validateCustomerDataEvent(event) {
247
+ if (!CUSTOMER_DATA_EVENT_TYPES.has(event.type)) {
248
+ throw new Error("Type must be track, identify, group, page, screen or alias");
249
+ }
250
+ if (!event.userId && !event.anonymousId && !event.groupId) {
251
+ throw new Error("At least one userId, anonymousId or groupId is required");
252
+ }
253
+ if (event.type === "track" && !event.event) {
254
+ throw new Error("Track calls require an event name");
255
+ }
256
+ }
257
+ function rejectCustomerDataTenantFields(event) {
258
+ const payload = event;
259
+ const forbidden = FORBIDDEN_CUSTOMER_DATA_TENANT_FIELDS.filter(
260
+ (key) => Object.prototype.hasOwnProperty.call(payload, key)
261
+ );
262
+ if (forbidden.length > 0) {
263
+ throw new Error(
264
+ `Tenant scope is derived from the write key; forbidden fields: ${forbidden.join(", ")}`
265
+ );
266
+ }
267
+ }
268
+ function cloneCustomerDataEvent(event) {
269
+ return JSON.parse(JSON.stringify(event));
270
+ }
271
+ function redactCustomerDataEvent(event, fields) {
272
+ return redactCustomerDataValue(event, fields);
273
+ }
274
+ function redactCustomerDataValue(value, fields) {
275
+ if (value instanceof Date) return value.toISOString();
276
+ if (Array.isArray(value)) {
277
+ return value.map((entry) => redactCustomerDataValue(entry, fields));
278
+ }
279
+ if (value && typeof value === "object") {
280
+ return Object.fromEntries(
281
+ Object.entries(value).map(([key, entry]) => [
282
+ key,
283
+ fields.has(key) ? "[REDACTED]" : redactCustomerDataValue(entry, fields)
284
+ ])
285
+ );
286
+ }
287
+ return value;
288
+ }
289
+ function normalizeDate(value) {
290
+ return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
291
+ }
292
+ function isRetryableError(error) {
293
+ if (error instanceof DataSdkError) {
294
+ return !error.statusCode || error.statusCode >= 500 || error.statusCode === 429;
295
+ }
296
+ return true;
297
+ }
298
+ function sleep(ms) {
299
+ return new Promise((resolve) => setTimeout(resolve, ms));
300
+ }
301
+ function randomId() {
302
+ if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID();
303
+ if (globalThis.crypto?.getRandomValues) {
304
+ const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
305
+ bytes[6] = bytes[6] & 15 | 64;
306
+ bytes[8] = bytes[8] & 63 | 128;
307
+ const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0"));
308
+ return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
309
+ }
310
+ return `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
311
+ }
312
+ function safeJson(text) {
313
+ try {
314
+ return JSON.parse(text);
315
+ } catch {
316
+ return { raw: text };
317
+ }
318
+ }
319
+
320
+ // src/vendor/app-usage.ts
321
+ var AppUsageError = class extends Error {
322
+ constructor(status, code, message) {
323
+ super(message);
324
+ this.status = status;
325
+ this.code = code;
326
+ this.name = "AppUsageError";
327
+ }
328
+ status;
329
+ code;
330
+ };
331
+ var AppUsageClient = class {
332
+ constructor(config) {
333
+ this.config = config;
334
+ if (!config?.baseUrl || typeof config.accessToken !== "function") throw new Error("AppUsageClient: baseUrl and accessToken are required");
335
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
336
+ this.fetchImpl = config.fetch ?? globalThis.fetch;
337
+ this.timeoutMs = config.timeoutMs ?? 1e4;
338
+ }
339
+ config;
340
+ baseUrl;
341
+ fetchImpl;
342
+ timeoutMs;
343
+ /** Reporta hasta 100 eventos de consumo en una llamada. */
344
+ async report(events) {
345
+ if (events.length === 0 || events.length > 100) throw new Error("AppUsageClient: between 1 and 100 events per call");
346
+ const response = await this.fetchImpl(`${this.baseUrl}/v1/apps/usage`, {
347
+ method: "POST",
348
+ headers: { authorization: `Bearer ${await this.config.accessToken()}`, "content-type": "application/json", accept: "application/json" },
349
+ body: JSON.stringify({
350
+ events: events.map((event) => ({
351
+ meter: event.meter,
352
+ quantity: event.quantity,
353
+ idempotencyKey: event.idempotencyKey,
354
+ ...event.occurredAt ? { occurredAt: new Date(event.occurredAt).toISOString() } : {}
355
+ }))
356
+ }),
357
+ signal: AbortSignal.timeout(this.timeoutMs)
358
+ });
359
+ const body = await response.json().catch(() => ({}));
360
+ if (!response.ok) throw new AppUsageError(response.status, String(body.error ?? `HTTP_${response.status}`), String(body.message ?? "Usage report failed"));
361
+ return body;
362
+ }
363
+ };
364
+ function legacyPlatformError(error) {
365
+ if (!(error instanceof CustomySdkError$1)) return error;
366
+ const { code, status } = error;
367
+ let message = null;
368
+ if (code === "SDK_DISCOVERY_FAILED") message = status ? `CUSTOMY_DISCOVERY_FAILED: ${status}` : "CUSTOMY_DISCOVERY_FAILED";
369
+ else if (code === "SDK_DISCOVERY_INVALID") message = "CUSTOMY_DISCOVERY_INVALID";
370
+ else if (code === "SDK_ISSUER_INVALID") message = "CUSTOMY_ACCESS_ISSUER_INVALID";
371
+ else if (code.startsWith("SDK_MACHINE_TOKEN_") && status > 0) {
372
+ const reason = code === "SDK_MACHINE_TOKEN_FAILED" ? "" : code.slice("SDK_MACHINE_TOKEN_".length).toLowerCase();
373
+ message = `CUSTOMY_MACHINE_TOKEN_FAILED: ${status} ${reason}`.trim();
374
+ } else if (code === "SDK_MACHINE_TOKEN_FAILED") message = "CUSTOMY_MACHINE_TOKEN_FAILED";
375
+ else if (code.startsWith("SDK_MACHINE_")) message = `CUSTOMY_${code.slice(4)}`;
376
+ if (!message) return error;
377
+ const legacy = new Error(message);
378
+ legacy.cause = error;
379
+ return legacy;
380
+ }
381
+
382
+ // src/server.ts
383
+ async function createCustomy(options) {
384
+ if (!options.clientId || !options.clientSecret) throw new Error("CUSTOMY_MACHINE_CREDENTIALS_REQUIRED");
385
+ warnDeprecated("@customyai/customy-sdk", "use createCustomy from @customyai/sdk (same options; billing.report becomes billing.usage.report).");
386
+ const shared = await createCustomy$1({
387
+ issuer: options.issuer,
388
+ clientId: options.clientId,
389
+ clientSecret: options.clientSecret,
390
+ fetch: options.fetch,
391
+ scopes: options.scopes
392
+ }).catch((error) => {
393
+ throw legacyPlatformError(error);
394
+ });
395
+ const platform = shared.platform;
396
+ const clients = /* @__PURE__ */ new Map();
397
+ function entry(product) {
398
+ const found = platform.products[product];
399
+ if (!found) throw new Error(`CUSTOMY_PRODUCT_NOT_DISCOVERED: ${product}`);
400
+ return found;
401
+ }
402
+ const providers = /* @__PURE__ */ new Map();
403
+ function token(product) {
404
+ entry(product);
405
+ let provider = providers.get(product);
406
+ if (!provider) {
407
+ const source = shared.token(product);
408
+ provider = () => Promise.resolve().then(() => source()).catch((error) => {
409
+ throw legacyPlatformError(error);
410
+ });
411
+ providers.set(product, provider);
412
+ }
413
+ return provider;
414
+ }
415
+ function once(key, build) {
416
+ if (!clients.has(key)) clients.set(key, build());
417
+ return clients.get(key);
418
+ }
419
+ return {
420
+ platform,
421
+ token,
422
+ product(product) {
423
+ if (!platform.products[product]) throw new CustomySdkError(product, 0, "SDK_PRODUCT_NOT_DISCOVERED");
424
+ return once(`product:${product}`, () => new CustomyProductClient(product, {
425
+ baseUrl: entry(product).baseUrl,
426
+ accessToken: token(product),
427
+ fetch: options.fetch,
428
+ timeoutMs: options.timeoutMs
429
+ }));
430
+ },
431
+ get send() {
432
+ return once("send", () => new CustomySend(token("send"), { baseUrl: entry("send").baseUrl, fetch: options.fetch, timeoutMs: options.timeoutMs }));
433
+ },
434
+ get links() {
435
+ return once("links", () => new CustomyLinks({ apiKey: token("links"), baseUrl: entry("links").baseUrl, fetch: options.fetch, timeoutMs: options.timeoutMs }));
436
+ },
437
+ get data() {
438
+ return once("data", () => new CustomyCustomerDataClient({
439
+ ...options.data,
440
+ collectUrl: entry("data").baseUrl,
441
+ accessToken: token("data"),
442
+ fetchImpl: options.fetch,
443
+ timeoutMs: options.timeoutMs
444
+ }));
445
+ },
446
+ get billing() {
447
+ return once("billing", () => new AppUsageClient({ baseUrl: entry("billing").baseUrl, accessToken: token("billing"), fetch: options.fetch, timeoutMs: options.timeoutMs }));
448
+ }
449
+ };
450
+ }
451
+
452
+ export { createCustomy };
package/package.json CHANGED
@@ -1,42 +1,87 @@
1
1
  {
2
2
  "name": "@customyai/customy-sdk",
3
- "version": "0.1.0",
4
- "description": "TypeScript umbrella SDK for all Customy products and platform services",
5
- "main": "./dist/index.js",
3
+ "version": "0.3.2",
4
+ "description": "Deprecated: use @customyai/sdk. Adapter of the Customy umbrella SDK (createCustomy, portfolio clients) kept for one major cycle.",
5
+ "license": "MIT",
6
+ "author": "Customy <hello@customy.ai>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/customyco/customy-js.git",
10
+ "directory": "packages/customy-sdk"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/customyco/customy-js/issues"
14
+ },
15
+ "keywords": [
16
+ "customy",
17
+ "ai",
18
+ "agent",
19
+ "sdk",
20
+ "api",
21
+ "client"
22
+ ],
23
+ "type": "module",
24
+ "main": "./dist/index.cjs",
6
25
  "module": "./dist/index.mjs",
7
- "types": "./dist/index.d.ts",
26
+ "types": "./dist/index.d.cts",
8
27
  "exports": {
9
28
  ".": {
10
- "types": "./dist/index.d.ts",
11
- "import": "./dist/index.mjs",
12
- "require": "./dist/index.js"
29
+ "import": {
30
+ "types": "./dist/index.d.ts",
31
+ "default": "./dist/index.mjs"
32
+ },
33
+ "require": {
34
+ "types": "./dist/index.d.cts",
35
+ "default": "./dist/index.cjs"
36
+ }
37
+ },
38
+ "./server": {
39
+ "import": {
40
+ "types": "./dist/server.d.ts",
41
+ "default": "./dist/server.mjs"
42
+ },
43
+ "require": {
44
+ "types": "./dist/server.d.cts",
45
+ "default": "./dist/server.cjs"
46
+ }
47
+ },
48
+ "./package.json": "./package.json"
49
+ },
50
+ "typesVersions": {
51
+ "*": {
52
+ "server": [
53
+ "./dist/server.d.ts"
54
+ ]
13
55
  }
14
56
  },
15
57
  "files": [
16
- "dist"
58
+ "dist",
59
+ "README.md",
60
+ "LICENSE",
61
+ "CHANGELOG.md"
17
62
  ],
63
+ "engines": {
64
+ "node": ">=20"
65
+ },
18
66
  "scripts": {
19
- "build": "tsup src/index.ts --format cjs,esm --dts --clean",
20
- "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
21
- "typecheck": "tsc --noEmit"
67
+ "build": "rm -rf dist && tsup",
68
+ "typecheck": "tsc --noEmit",
69
+ "test": "vitest run"
70
+ },
71
+ "dependencies": {
72
+ "@customyai/customy-access": "^0.8.2",
73
+ "@customyai/links-sdk": "^0.3.0",
74
+ "@customyai/sdk": "^0.1.1",
75
+ "@customyai/send-sdk": "^1.3.0"
22
76
  },
23
77
  "devDependencies": {
78
+ "typescript": "^5.5.0",
79
+ "vitest": "^4.1.11",
24
80
  "tsup": "^8.0.0",
25
- "typescript": "^5.5.0"
26
- },
27
- "keywords": [
28
- "customy",
29
- "ai",
30
- "agent",
31
- "sdk",
32
- "api",
33
- "client"
34
- ],
35
- "license": "MIT",
36
- "repository": {
37
- "type": "git",
38
- "url": "https://github.com/customyco/customynew",
39
- "directory": "packages/sdk"
81
+ "@types/node": "^20.14.2"
40
82
  },
41
- "author": "Customy <hello@customy.ai>"
83
+ "publishConfig": {
84
+ "access": "public",
85
+ "provenance": true
86
+ }
42
87
  }