@chaja/sdk-node 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,19 @@
1
- # Temporary Holding Version
1
+ # @chaja/sdk-node
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Error tracking for Node.js 22+, reporting to a self-hosted [Chajá](https://github.com/emiliodominguez/chaja) server.
4
+
5
+ ```sh
6
+ pnpm add @chaja/sdk-node
7
+ ```
8
+
9
+ ```ts
10
+ import { expressErrorHandler, init } from "@chaja/sdk-node";
11
+
12
+ init({ endpoint: process.env.CHAJA_URL!, key: process.env.CHAJA_KEY!, service: "api", release: process.env.RELEASE });
13
+
14
+ app.use(expressErrorHandler());
15
+ ```
16
+
17
+ Uncaught exceptions and unhandled rejections are reported with source context. Events that cannot be delivered can be kept on
18
+ disk (`offlineDirectory`) and resent on the next start. For Hono, Next.js route handlers and other Fetch-API servers, use
19
+ `captureRequestError(error, request)`. All options: [docs/sdk.md](https://github.com/emiliodominguez/chaja/blob/main/docs/sdk.md).
@@ -0,0 +1,162 @@
1
+ import { BreadcrumbInput, CaptureContext, CaptureContext as CaptureContext$1, Client, Client as Client$1, ClientOptions, ClientOptions as ClientOptions$1 } from "@chaja/sdk-core";
2
+ //#region ../protocol/src/wire.d.ts
3
+ type AttributeScalar = string | number | boolean;
4
+ type AttributeValue = AttributeScalar | AttributeScalar[];
5
+ type Attributes = Record<string, AttributeValue>;
6
+ interface RequestInfo {
7
+ url?: string;
8
+ method?: string;
9
+ headers?: Record<string, string>;
10
+ }
11
+ //#endregion
12
+ //#region src/index.d.ts
13
+ export interface NodeOptions extends ClientOptions$1 {
14
+ /**
15
+ * Capture `uncaughtException` and `unhandledRejection`. Default true.
16
+ */
17
+ processHandlers?: boolean;
18
+ /**
19
+ * Exit with code 1 after reporting an uncaught exception (Node's default behaviour). Default true.
20
+ */
21
+ exitOnUncaught?: boolean;
22
+ /**
23
+ * Directory where envelopes that could not be delivered are kept and retried on the next start.
24
+ */
25
+ offlineDirectory?: string;
26
+ /**
27
+ * Lines of source code around in-app frames. Default 5; 0 disables.
28
+ */
29
+ contextLines?: number;
30
+ /**
31
+ * HTTP client (tests inject one). Defaults to global fetch.
32
+ */
33
+ fetch?: typeof fetch;
34
+ }
35
+ export interface NodeClient extends Client$1 {
36
+ close(timeoutMs?: number): Promise<boolean>;
37
+ /**
38
+ * The HTTP client the SDK sends with.
39
+ */
40
+ readonly fetch: typeof fetch;
41
+ }
42
+ /**
43
+ * Start the Node SDK.
44
+ *
45
+ * @param options - SDK options.
46
+ * @returns The client; calling `init` again replaces it.
47
+ */
48
+ export declare function init(options: NodeOptions): NodeClient;
49
+ /**
50
+ * The parts of an Express, Fastify or `http.IncomingMessage` request the SDK reads.
51
+ */
52
+ export interface NodeRequestLike {
53
+ method?: string;
54
+ url?: string;
55
+ originalUrl?: string;
56
+ headers?: Record<string, string | string[] | undefined>;
57
+ }
58
+ /**
59
+ * Request details from a Node-style request object, without the body.
60
+ *
61
+ * @param request - Incoming request.
62
+ * @returns Request info.
63
+ */
64
+ export declare function requestInfo(request: NodeRequestLike): RequestInfo;
65
+ export type ExpressErrorHandler = (error: unknown, request: NodeRequestLike, response: unknown, next: (error?: unknown) => void) => void;
66
+ /**
67
+ * Express error middleware: reports the error with request details, then passes it on.
68
+ *
69
+ * @param client - Client; defaults to the one from `init`.
70
+ * @returns Express error handler.
71
+ */
72
+ export declare function expressErrorHandler(client?: Client$1): ExpressErrorHandler;
73
+ /**
74
+ * Report an error from a Fetch-API style request (Hono, Next.js route handlers, Bun, Deno).
75
+ *
76
+ * @param error - Thrown value.
77
+ * @param request - Web request.
78
+ * @param context - Extra context.
79
+ * @returns Event id, if sent.
80
+ */
81
+ export declare function captureRequestError(error: unknown, request: Request, context?: CaptureContext$1): string | undefined;
82
+ /**
83
+ * Capture an error with the client from `init`.
84
+ *
85
+ * @param error - Thrown value.
86
+ * @param context - Extra context.
87
+ * @returns Event id, if sent.
88
+ */
89
+ export declare function captureException(error: unknown, context?: CaptureContext$1): string | undefined;
90
+ /**
91
+ * Capture a message with the client from `init`.
92
+ *
93
+ * @param message - Message.
94
+ * @param level - Level.
95
+ * @param context - Extra context.
96
+ * @returns Event id, if sent.
97
+ */
98
+ export declare function captureMessage(message: string, level?: Parameters<Client$1["captureMessage"]>[1], context?: CaptureContext$1): string | undefined;
99
+ export interface ServerEvent {
100
+ /**
101
+ * Your id for the person who did it, the same one the browser SDK passes to `identify`.
102
+ */
103
+ distinctId: string;
104
+ /**
105
+ * Event name, such as `subscription_renewed`.
106
+ */
107
+ name: string;
108
+ properties?: Attributes;
109
+ /**
110
+ * Person properties to set.
111
+ */
112
+ set?: Attributes;
113
+ /**
114
+ * Person properties to set only when the person does not have them yet.
115
+ */
116
+ setOnce?: Attributes;
117
+ }
118
+ /**
119
+ * Record a product analytics event for a person with the client from `init`. Servers act for many people, so every event
120
+ * names its person.
121
+ *
122
+ * @param event - Person, name and properties.
123
+ * @returns Event id, if sent.
124
+ * @example
125
+ * capture({ distinctId: user.id, name: "subscription_renewed", properties: { plan: "pro" } });
126
+ */
127
+ export declare function capture(event: ServerEvent): string | undefined;
128
+ /**
129
+ * Send queued events.
130
+ *
131
+ * @param timeoutMs - Maximum wait.
132
+ * @returns Whether everything was sent.
133
+ */
134
+ export declare function flush(timeoutMs?: number): Promise<boolean>;
135
+ /**
136
+ * The client created by `init`, if any.
137
+ *
138
+ * @returns Current client.
139
+ */
140
+ export declare function getClient(): NodeClient | undefined;
141
+ /**
142
+ * Feature flags for a person, with the client from `init`, cached for 30 seconds per person and properties.
143
+ *
144
+ * @param distinctId - Your id for the person.
145
+ * @param properties - Properties the flags' conditions may test, on top of what product analytics knows about them.
146
+ * @returns Flag values: `false`, `true` or a variant key; empty when the server cannot be reached.
147
+ * @example
148
+ * const flags = await getFeatureFlags(user.id, { plan: user.plan });
149
+ * if (flags["new-checkout"]) { ... }
150
+ */
151
+ export declare function getFeatureFlags(distinctId: string, properties?: Attributes): Promise<Record<string, boolean | string>>;
152
+ /**
153
+ * Whether a flag is on for a person (a variant counts as on), with the client from `init`.
154
+ *
155
+ * @param key - Flag key.
156
+ * @param distinctId - Your id for the person.
157
+ * @param properties - Properties for the flag's conditions.
158
+ * @returns True when on.
159
+ */
160
+ export declare function isFeatureEnabled(key: string, distinctId: string, properties?: Attributes): Promise<boolean>;
161
+ //#endregion
162
+ export type { BreadcrumbInput, CaptureContext, Client, ClientOptions };
package/dist/index.mjs ADDED
@@ -0,0 +1,414 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { SDK_VERSION, createClient, parseRetryAfter } from "@chaja/sdk-core";
6
+ //#region src/index.ts
7
+ const SOURCE_CACHE_LIMIT = 100;
8
+ const MAX_LINE = 300;
9
+ let current;
10
+ /**
11
+ * Shorten long source lines.
12
+ *
13
+ * @param text - Source line.
14
+ * @returns Clipped line.
15
+ */
16
+ function clip(text) {
17
+ return text.slice(0, MAX_LINE);
18
+ }
19
+ /**
20
+ * Whether a scheduled handle is a Node timer.
21
+ *
22
+ * @param handle - Handle from `schedule`.
23
+ * @returns True for timers.
24
+ */
25
+ function isTimer(handle) {
26
+ return typeof handle === "object" && handle !== null && "unref" in handle;
27
+ }
28
+ /**
29
+ * Read source lines for frames, caching files and skipping dependencies.
30
+ *
31
+ * @param lines - Context lines per side.
32
+ * @returns Frame enricher.
33
+ */
34
+ function sourceContext(lines) {
35
+ const cache = /* @__PURE__ */ new Map();
36
+ /**
37
+ * Lines of a file, or null when unreadable.
38
+ *
39
+ * @param filename - Absolute path or file URL.
40
+ * @returns Lines.
41
+ */
42
+ function read(filename) {
43
+ if (cache.has(filename)) return cache.get(filename) ?? null;
44
+ let content = null;
45
+ try {
46
+ const path = filename.startsWith("file://") ? fileURLToPath(filename) : filename;
47
+ content = readFileSync(path, "utf8").split(/\r?\n/u);
48
+ } catch {
49
+ content = null;
50
+ }
51
+ if (cache.size >= SOURCE_CACHE_LIMIT) cache.clear();
52
+ cache.set(filename, content);
53
+ return content;
54
+ }
55
+ return function withContext(frame) {
56
+ const { filename, lineno } = frame;
57
+ if (lines <= 0 || !filename || !lineno || filename.includes("node_modules") || filename.startsWith("node:") || !/^(?:\/|file:\/\/|[a-z]:\\)/iu.test(filename)) return frame;
58
+ const source = read(filename);
59
+ const line = source?.[lineno - 1];
60
+ if (!source || line === void 0) return frame;
61
+ return {
62
+ ...frame,
63
+ contextLine: clip(line),
64
+ preContext: source.slice(Math.max(0, lineno - 1 - lines), lineno - 1).map(clip),
65
+ postContext: source.slice(lineno, lineno + lines).map(clip)
66
+ };
67
+ };
68
+ }
69
+ /**
70
+ * Whether a frame is application code.
71
+ *
72
+ * @param frame - Frame.
73
+ * @returns In-app flag.
74
+ */
75
+ function inApp(frame) {
76
+ const filename = frame.filename ?? "";
77
+ return !filename.includes("node_modules") && !filename.startsWith("node:") && !filename.startsWith("internal/");
78
+ }
79
+ /**
80
+ * Start the Node SDK.
81
+ *
82
+ * @param options - SDK options.
83
+ * @returns The client; calling `init` again replaces it.
84
+ */
85
+ function init(options) {
86
+ current?.close(0);
87
+ const send = options.fetch ?? fetch;
88
+ const envelopeUrl = `${options.endpoint.replace(/\/+$/u, "")}/api/v1/envelope`;
89
+ const withContext = sourceContext(options.contextLines ?? 5);
90
+ const cleanups = [];
91
+ const transport = { async send(body) {
92
+ try {
93
+ const response = await send(envelopeUrl, {
94
+ method: "POST",
95
+ headers: {
96
+ "content-type": "application/json",
97
+ "x-chaja-key": options.key
98
+ },
99
+ body,
100
+ signal: AbortSignal.timeout(1e4)
101
+ });
102
+ const retry = parseRetryAfter(response.headers.get("retry-after"));
103
+ return {
104
+ status: response.status,
105
+ ...retry === void 0 ? {} : { retryAfterSeconds: retry }
106
+ };
107
+ } catch {
108
+ return { status: 0 };
109
+ }
110
+ } };
111
+ const client = createClient(options, {
112
+ sdk: {
113
+ name: "chaja.javascript.node",
114
+ version: SDK_VERSION
115
+ },
116
+ platform: "node",
117
+ transport,
118
+ eventId() {
119
+ return randomUUID().replaceAll("-", "");
120
+ },
121
+ now: Date.now,
122
+ random: Math.random,
123
+ schedule(callback, delayMs) {
124
+ const handle = setTimeout(callback, delayMs);
125
+ handle.unref();
126
+ return handle;
127
+ },
128
+ cancel(handle) {
129
+ if (isTimer(handle)) clearTimeout(handle);
130
+ },
131
+ enrich(item) {
132
+ return {
133
+ ...item,
134
+ ...item.exceptions ? { exceptions: item.exceptions.map(function exception(entry) {
135
+ return {
136
+ ...entry,
137
+ frames: entry.frames.map(function frame(value) {
138
+ return withContext({
139
+ ...value,
140
+ inApp: value.inApp ?? inApp(value)
141
+ });
142
+ })
143
+ };
144
+ }) } : {},
145
+ attributes: {
146
+ "process.runtime.version": process.version,
147
+ "host.arch": process.arch,
148
+ "os.type": process.platform,
149
+ ...item.attributes
150
+ }
151
+ };
152
+ },
153
+ ...options.offlineDirectory ? { onUndeliverable: persist(options.offlineDirectory) } : {}
154
+ });
155
+ if (options.processHandlers !== false) {
156
+ /**
157
+ * Report an uncaught exception, flush and exit like Node would.
158
+ *
159
+ * @param error - Uncaught error.
160
+ */
161
+ function onUncaught(error) {
162
+ client.captureException(error, {
163
+ level: "fatal",
164
+ mechanism: {
165
+ type: "uncaughtException",
166
+ handled: false
167
+ }
168
+ });
169
+ if (options.exitOnUncaught !== false) client.flush(2e3).finally(function exit() {
170
+ process.exit(1);
171
+ });
172
+ }
173
+ /**
174
+ * Report an unhandled promise rejection.
175
+ *
176
+ * @param reason - Rejection reason.
177
+ */
178
+ function onRejection(reason) {
179
+ client.captureException(reason, { mechanism: {
180
+ type: "unhandledRejection",
181
+ handled: false
182
+ } });
183
+ }
184
+ process.on("uncaughtException", onUncaught);
185
+ process.on("unhandledRejection", onRejection);
186
+ cleanups.push(function remove() {
187
+ process.off("uncaughtException", onUncaught);
188
+ process.off("unhandledRejection", onRejection);
189
+ });
190
+ }
191
+ if (options.offlineDirectory) replay(options.offlineDirectory, transport);
192
+ const instance = {
193
+ ...client,
194
+ fetch: send,
195
+ async close(timeoutMs = 2e3) {
196
+ for (const cleanup of cleanups.splice(0)) cleanup();
197
+ if (current === instance) current = void 0;
198
+ return client.flush(timeoutMs);
199
+ }
200
+ };
201
+ current = instance;
202
+ return instance;
203
+ }
204
+ /**
205
+ * Persist undeliverable envelopes as files.
206
+ *
207
+ * @param directory - Offline directory.
208
+ * @returns Callback for the queue.
209
+ */
210
+ function persist(directory) {
211
+ return function save(body) {
212
+ try {
213
+ mkdirSync(directory, { recursive: true });
214
+ writeFileSync(join(directory, `${Date.now()}-${randomUUID()}.json`), body);
215
+ } catch {}
216
+ };
217
+ }
218
+ /**
219
+ * Resend envelopes saved by a previous run, deleting the ones the server accepted or rejected for good.
220
+ *
221
+ * @param directory - Offline directory.
222
+ * @param transport - Transport.
223
+ */
224
+ function replay(directory, transport) {
225
+ if (!existsSync(directory)) return;
226
+ (async function resend() {
227
+ for (const name of readdirSync(directory).filter(isEnvelopeFile).toSorted()) {
228
+ const path = join(directory, name);
229
+ const result = await transport.send(readFileSync(path, "utf8"));
230
+ if (result.status === 0 || result.status === 429 || result.status >= 500) return;
231
+ rmSync(path, { force: true });
232
+ }
233
+ })();
234
+ }
235
+ /**
236
+ * Whether a file name looks like a saved envelope.
237
+ *
238
+ * @param name - File name.
239
+ * @returns True for `.json` files.
240
+ */
241
+ function isEnvelopeFile(name) {
242
+ return name.endsWith(".json");
243
+ }
244
+ /**
245
+ * Request details from a Node-style request object, without the body.
246
+ *
247
+ * @param request - Incoming request.
248
+ * @returns Request info.
249
+ */
250
+ function requestInfo(request) {
251
+ const headers = {};
252
+ for (const [key, value] of Object.entries(request.headers ?? {})) if (value !== void 0) headers[key] = Array.isArray(value) ? value.join(", ") : value;
253
+ return {
254
+ ...request.method ? { method: request.method } : {},
255
+ ...request.originalUrl ?? request.url ? { url: request.originalUrl ?? request.url } : {},
256
+ headers
257
+ };
258
+ }
259
+ /**
260
+ * Express error middleware: reports the error with request details, then passes it on.
261
+ *
262
+ * @param client - Client; defaults to the one from `init`.
263
+ * @returns Express error handler.
264
+ */
265
+ function expressErrorHandler(client) {
266
+ return function chajaErrorHandler(error, request, _response, next) {
267
+ (client ?? current)?.captureException(error, {
268
+ request: requestInfo(request),
269
+ mechanism: {
270
+ type: "express",
271
+ handled: false
272
+ }
273
+ });
274
+ next(error);
275
+ };
276
+ }
277
+ /**
278
+ * Report an error from a Fetch-API style request (Hono, Next.js route handlers, Bun, Deno).
279
+ *
280
+ * @param error - Thrown value.
281
+ * @param request - Web request.
282
+ * @param context - Extra context.
283
+ * @returns Event id, if sent.
284
+ */
285
+ function captureRequestError(error, request, context = {}) {
286
+ const headers = {};
287
+ request.headers.forEach(function copy(value, key) {
288
+ headers[key] = value;
289
+ });
290
+ return current?.captureException(error, {
291
+ ...context,
292
+ request: {
293
+ method: request.method,
294
+ url: request.url,
295
+ headers
296
+ },
297
+ mechanism: context.mechanism ?? {
298
+ type: "request",
299
+ handled: false
300
+ }
301
+ });
302
+ }
303
+ /**
304
+ * Capture an error with the client from `init`.
305
+ *
306
+ * @param error - Thrown value.
307
+ * @param context - Extra context.
308
+ * @returns Event id, if sent.
309
+ */
310
+ function captureException(error, context) {
311
+ return current?.captureException(error, context);
312
+ }
313
+ /**
314
+ * Capture a message with the client from `init`.
315
+ *
316
+ * @param message - Message.
317
+ * @param level - Level.
318
+ * @param context - Extra context.
319
+ * @returns Event id, if sent.
320
+ */
321
+ function captureMessage(message, level, context) {
322
+ return current?.captureMessage(message, level, context);
323
+ }
324
+ /**
325
+ * Record a product analytics event for a person with the client from `init`. Servers act for many people, so every event
326
+ * names its person.
327
+ *
328
+ * @param event - Person, name and properties.
329
+ * @returns Event id, if sent.
330
+ * @example
331
+ * capture({ distinctId: user.id, name: "subscription_renewed", properties: { plan: "pro" } });
332
+ */
333
+ function capture(event) {
334
+ return current?.capture(event.name, event.properties, {
335
+ distinctId: event.distinctId,
336
+ ...event.set ? { set: event.set } : {},
337
+ ...event.setOnce ? { setOnce: event.setOnce } : {}
338
+ });
339
+ }
340
+ /**
341
+ * Send queued events.
342
+ *
343
+ * @param timeoutMs - Maximum wait.
344
+ * @returns Whether everything was sent.
345
+ */
346
+ async function flush(timeoutMs) {
347
+ return current ? current.flush(timeoutMs) : true;
348
+ }
349
+ /**
350
+ * The client created by `init`, if any.
351
+ *
352
+ * @returns Current client.
353
+ */
354
+ function getClient() {
355
+ return current;
356
+ }
357
+ const FLAG_CACHE_MS = 3e4;
358
+ const flagCache = /* @__PURE__ */ new Map();
359
+ /**
360
+ * Feature flags for a person, with the client from `init`, cached for 30 seconds per person and properties.
361
+ *
362
+ * @param distinctId - Your id for the person.
363
+ * @param properties - Properties the flags' conditions may test, on top of what product analytics knows about them.
364
+ * @returns Flag values: `false`, `true` or a variant key; empty when the server cannot be reached.
365
+ * @example
366
+ * const flags = await getFeatureFlags(user.id, { plan: user.plan });
367
+ * if (flags["new-checkout"]) { ... }
368
+ */
369
+ async function getFeatureFlags(distinctId, properties = {}) {
370
+ const client = current;
371
+ if (!client) return {};
372
+ const cacheKey = JSON.stringify([distinctId, properties]);
373
+ const cached = flagCache.get(cacheKey);
374
+ if (cached && Date.now() - cached.at < FLAG_CACHE_MS) return cached.flags;
375
+ try {
376
+ const response = await client.fetch(`${client.options.endpoint.replace(/\/+$/u, "")}/api/v1/flags`, {
377
+ method: "POST",
378
+ headers: {
379
+ "content-type": "application/json",
380
+ "x-chaja-key": client.options.key
381
+ },
382
+ body: JSON.stringify({
383
+ distinctId,
384
+ properties
385
+ }),
386
+ signal: AbortSignal.timeout(5e3)
387
+ });
388
+ const body = response.ok ? await response.json() : void 0;
389
+ const raw = typeof body === "object" && body !== null && "flags" in body && typeof body.flags === "object" && body.flags !== null ? body.flags : {};
390
+ const flags = {};
391
+ for (const [key, value] of Object.entries(raw)) if (typeof value === "boolean" || typeof value === "string") flags[key] = value;
392
+ flagCache.set(cacheKey, {
393
+ at: Date.now(),
394
+ flags
395
+ });
396
+ return flags;
397
+ } catch {
398
+ return cached?.flags ?? {};
399
+ }
400
+ }
401
+ /**
402
+ * Whether a flag is on for a person (a variant counts as on), with the client from `init`.
403
+ *
404
+ * @param key - Flag key.
405
+ * @param distinctId - Your id for the person.
406
+ * @param properties - Properties for the flag's conditions.
407
+ * @returns True when on.
408
+ */
409
+ async function isFeatureEnabled(key, distinctId, properties = {}) {
410
+ const value = (await getFeatureFlags(distinctId, properties))[key];
411
+ return value !== void 0 && value !== false;
412
+ }
413
+ //#endregion
414
+ export { capture, captureException, captureMessage, captureRequestError, expressErrorHandler, flush, getClient, getFeatureFlags, init, isFeatureEnabled, requestInfo };
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "@chaja/sdk-node",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Chajá error tracking for Node.js: process handlers, source context, offline buffer, Express and Fetch helpers.",
5
+ "keywords": [
6
+ "chaja",
7
+ "error-tracking",
8
+ "observability",
9
+ "node",
10
+ "sdk"
11
+ ],
12
+ "homepage": "https://github.com/emiliodominguez/chaja#readme",
13
+ "bugs": "https://github.com/emiliodominguez/chaja/issues",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/emiliodominguez/chaja.git",
17
+ "directory": "packages/sdk-node"
18
+ },
19
+ "type": "module",
20
+ "nx": {
21
+ "tags": [
22
+ "scope:sdk",
23
+ "type:sdk"
24
+ ]
25
+ },
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.mts",
29
+ "default": "./dist/index.mjs"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "sideEffects": false,
36
+ "devDependencies": {
37
+ "@chaja/protocol": "0.0.0",
38
+ "tsdown": "0.23.0"
39
+ },
40
+ "dependencies": {
41
+ "@chaja/sdk-core": "0.1.0"
42
+ },
43
+ "engines": {
44
+ "node": ">=22"
45
+ },
46
+ "scripts": {
47
+ "build": "tsdown src/index.ts --format esm --dts --clean --out-dir dist",
48
+ "typecheck": "tsc --noEmit -p ."
49
+ }
6
50
  }