@scrawn/core 0.0.2 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +40 -0
  2. package/dist/config.d.ts +17 -2
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +9 -2
  5. package/dist/config.js.map +1 -1
  6. package/dist/core/auth/apiKeyAuth.d.ts +4 -13
  7. package/dist/core/auth/apiKeyAuth.d.ts.map +1 -1
  8. package/dist/core/auth/apiKeyAuth.js +12 -17
  9. package/dist/core/auth/apiKeyAuth.js.map +1 -1
  10. package/dist/core/auth/baseAuth.d.ts +14 -36
  11. package/dist/core/auth/baseAuth.d.ts.map +1 -1
  12. package/dist/core/auth/baseAuth.js +0 -6
  13. package/dist/core/auth/baseAuth.js.map +1 -1
  14. package/dist/core/errors/index.d.ts +192 -0
  15. package/dist/core/errors/index.d.ts.map +1 -0
  16. package/dist/core/errors/index.js +280 -0
  17. package/dist/core/errors/index.js.map +1 -0
  18. package/dist/core/grpc/callContext.d.ts +18 -0
  19. package/dist/core/grpc/callContext.d.ts.map +1 -0
  20. package/dist/core/grpc/callContext.js +35 -0
  21. package/dist/core/grpc/callContext.js.map +1 -0
  22. package/dist/core/grpc/client.d.ts +13 -123
  23. package/dist/core/grpc/client.d.ts.map +1 -1
  24. package/dist/core/grpc/client.js +23 -131
  25. package/dist/core/grpc/client.js.map +1 -1
  26. package/dist/core/grpc/index.d.ts +5 -3
  27. package/dist/core/grpc/index.d.ts.map +1 -1
  28. package/dist/core/grpc/index.js +4 -2
  29. package/dist/core/grpc/index.js.map +1 -1
  30. package/dist/core/grpc/requestBuilder.d.ts +12 -113
  31. package/dist/core/grpc/requestBuilder.d.ts.map +1 -1
  32. package/dist/core/grpc/requestBuilder.js +36 -126
  33. package/dist/core/grpc/requestBuilder.js.map +1 -1
  34. package/dist/core/grpc/streamRequestBuilder.d.ts +13 -0
  35. package/dist/core/grpc/streamRequestBuilder.d.ts.map +1 -0
  36. package/dist/core/grpc/streamRequestBuilder.js +60 -0
  37. package/dist/core/grpc/streamRequestBuilder.js.map +1 -0
  38. package/dist/core/grpc/types.d.ts +5 -52
  39. package/dist/core/grpc/types.d.ts.map +1 -1
  40. package/dist/core/grpc/types.js +0 -7
  41. package/dist/core/grpc/types.js.map +1 -1
  42. package/dist/core/pricing/builders.d.ts +157 -0
  43. package/dist/core/pricing/builders.d.ts.map +1 -0
  44. package/dist/core/pricing/builders.js +218 -0
  45. package/dist/core/pricing/builders.js.map +1 -0
  46. package/dist/core/pricing/index.d.ts +30 -0
  47. package/dist/core/pricing/index.d.ts.map +1 -0
  48. package/dist/core/pricing/index.js +32 -0
  49. package/dist/core/pricing/index.js.map +1 -0
  50. package/dist/core/pricing/resolve.d.ts +39 -0
  51. package/dist/core/pricing/resolve.d.ts.map +1 -0
  52. package/dist/core/pricing/resolve.js +50 -0
  53. package/dist/core/pricing/resolve.js.map +1 -0
  54. package/dist/core/pricing/serialize.d.ts +55 -0
  55. package/dist/core/pricing/serialize.d.ts.map +1 -0
  56. package/dist/core/pricing/serialize.js +127 -0
  57. package/dist/core/pricing/serialize.js.map +1 -0
  58. package/dist/core/pricing/types.d.ts +122 -0
  59. package/dist/core/pricing/types.d.ts.map +1 -0
  60. package/dist/core/pricing/types.js +17 -0
  61. package/dist/core/pricing/types.js.map +1 -0
  62. package/dist/core/pricing/validate.d.ts +56 -0
  63. package/dist/core/pricing/validate.d.ts.map +1 -0
  64. package/dist/core/pricing/validate.js +162 -0
  65. package/dist/core/pricing/validate.js.map +1 -0
  66. package/dist/core/scrawn.d.ts +218 -17
  67. package/dist/core/scrawn.d.ts.map +1 -1
  68. package/dist/core/scrawn.js +469 -71
  69. package/dist/core/scrawn.js.map +1 -1
  70. package/dist/core/types/auth.d.ts +1 -1
  71. package/dist/core/types/event.d.ts +182 -18
  72. package/dist/core/types/event.d.ts.map +1 -1
  73. package/dist/core/types/event.js +133 -5
  74. package/dist/core/types/event.js.map +1 -1
  75. package/dist/gen/auth/v1/auth_grpc_pb.d.ts +3 -0
  76. package/dist/gen/auth/v1/auth_grpc_pb.js +45 -0
  77. package/dist/gen/auth/v1/auth_pb.d.ts +63 -57
  78. package/dist/gen/auth/v1/auth_pb.js +471 -86
  79. package/dist/gen/data/v1/data_grpc_pb.d.ts +5 -0
  80. package/dist/gen/data/v1/data_grpc_pb.js +44 -0
  81. package/dist/gen/data/v1/data_pb.d.ts +254 -0
  82. package/dist/gen/data/v1/data_pb.js +1530 -0
  83. package/dist/gen/event/v1/event_grpc_pb.d.ts +3 -0
  84. package/dist/gen/event/v1/event_grpc_pb.js +79 -0
  85. package/dist/gen/event/v1/event_pb.d.ts +273 -100
  86. package/dist/gen/event/v1/event_pb.js +1862 -138
  87. package/dist/gen/package.json +3 -0
  88. package/dist/gen/payment/v1/payment_grpc_pb.d.ts +3 -0
  89. package/dist/gen/payment/v1/payment_grpc_pb.js +45 -0
  90. package/dist/gen/payment/v1/payment_pb.d.ts +43 -35
  91. package/dist/gen/payment/v1/payment_pb.js +321 -59
  92. package/dist/gen/query/v1/query_grpc_pb.d.ts +5 -0
  93. package/dist/gen/query/v1/query_grpc_pb.js +44 -0
  94. package/dist/gen/query/v1/query_pb.d.ts +359 -0
  95. package/dist/gen/query/v1/query_pb.js +2327 -0
  96. package/dist/index.d.ts +19 -10
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +20 -10
  99. package/dist/index.js.map +1 -1
  100. package/dist/utils/forkAsyncIterable.d.ts +13 -0
  101. package/dist/utils/forkAsyncIterable.d.ts.map +1 -0
  102. package/dist/utils/forkAsyncIterable.js +78 -0
  103. package/dist/utils/forkAsyncIterable.js.map +1 -0
  104. package/dist/utils/logger.d.ts.map +1 -1
  105. package/dist/utils/logger.js +19 -19
  106. package/dist/utils/logger.js.map +1 -1
  107. package/dist/utils/pathMatcher.js +5 -5
  108. package/package.json +19 -15
  109. package/dist/gen/auth/v1/auth_connect.d.ts +0 -22
  110. package/dist/gen/auth/v1/auth_connect.d.ts.map +0 -1
  111. package/dist/gen/auth/v1/auth_connect.js +0 -26
  112. package/dist/gen/auth/v1/auth_connect.js.map +0 -1
  113. package/dist/gen/auth/v1/auth_pb.d.ts.map +0 -1
  114. package/dist/gen/auth/v1/auth_pb.js.map +0 -1
  115. package/dist/gen/event/v1/event_connect.d.ts +0 -22
  116. package/dist/gen/event/v1/event_connect.d.ts.map +0 -1
  117. package/dist/gen/event/v1/event_connect.js +0 -26
  118. package/dist/gen/event/v1/event_connect.js.map +0 -1
  119. package/dist/gen/event/v1/event_pb.d.ts.map +0 -1
  120. package/dist/gen/event/v1/event_pb.js.map +0 -1
  121. package/dist/gen/payment/v1/payment_connect.d.ts +0 -22
  122. package/dist/gen/payment/v1/payment_connect.d.ts.map +0 -1
  123. package/dist/gen/payment/v1/payment_connect.js +0 -26
  124. package/dist/gen/payment/v1/payment_connect.js.map +0 -1
  125. package/dist/gen/payment/v1/payment_pb.d.ts.map +0 -1
  126. package/dist/gen/payment/v1/payment_pb.js.map +0 -1
@@ -1,31 +1,58 @@
1
- import { ApiKeyAuth } from './auth/apiKeyAuth.js';
2
- import { ScrawnLogger } from '../utils/logger.js';
3
- import { matchPath } from '../utils/pathMatcher.js';
4
- import { EventPayloadSchema } from './types/event.js';
5
- import { GrpcClient } from './grpc/index.js';
6
- import { EventService } from '../gen/event/v1/event_connect.js';
7
- import { EventType, SDKCallType, SDKCall } from '../gen/event/v1/event_pb.js';
8
- import { PaymentService } from '../gen/payment/v1/payment_connect.js';
9
- const log = new ScrawnLogger('Scrawn');
1
+ import { ApiKeyAuth } from "./auth/apiKeyAuth.js";
2
+ import { ScrawnLogger } from "../utils/logger.js";
3
+ import { matchPath } from "../utils/pathMatcher.js";
4
+ import { forkAsyncIterable } from "../utils/forkAsyncIterable.js";
5
+ import { EventPayloadSchema, AITokenUsagePayloadSchema, } from "./types/event.js";
6
+ import { GrpcClient } from "./grpc/index.js";
7
+ import { EventServiceClient } from "../gen/event/v1/event_grpc_pb.js";
8
+ import { RegisterEventRequest, StreamEventRequest, EventType, SDKCallType, SDKCall, AITokenUsage, } from "../gen/event/v1/event_pb.js";
9
+ import { PaymentServiceClient } from "../gen/payment/v1/payment_grpc_pb.js";
10
+ import { CreateCheckoutLinkRequest, } from "../gen/payment/v1/payment_pb.js";
11
+ import { ScrawnConfigError, ScrawnValidationError, convertGrpcError, isScrawnError, } from "./errors/index.js";
12
+ import { serializeExpr, resolveTokens, prettyPrintExpr, tag as _tag } from "./pricing/index.js";
13
+ import { ScrawnConfig } from "../config.js";
14
+ const log = new ScrawnLogger("Scrawn");
10
15
  /**
11
16
  * Main SDK class for Scrawn billing infrastructure.
12
17
  *
13
18
  * Manages authentication, event tracking, and credential caching.
14
19
  * All event consumption methods are available directly on the SDK instance.
15
20
  *
21
+ * @typeParam TTags - Union of valid tag names for compile-time type checking
22
+ *
16
23
  * @example
17
24
  * ```typescript
18
- * import { Scrawn } from '@scrawn/core';
25
+ * import { createScrawn } from '@scrawn/core';
19
26
  *
20
- * // Initialize SDK
21
- * const scrawn = new Scrawn({ apiKey: process.env.SCRAWN_KEY });
22
- * await scrawn.init();
27
+ * const biller = createScrawn({
28
+ * apiKey: process.env.SCRAWN_KEY,
29
+ * baseURL: 'http://localhost:8069',
30
+ * tags: ["PREMIUM_CALL", "EXTRA_FEE"] as const,
31
+ * });
23
32
  *
24
- * // Track SDK calls
25
- * await scrawn.sdkCallEventConsumer({ userId: 'u123', debitAmount: 3 });
33
+ * // Tags are compile-time checked
34
+ * biller.sdkCallEventConsumer({ userId: 'u123', debitTag: 'PREMIUM_FEATURE' });
35
+ * // biller.sdkCallEventConsumer({ userId: 'u123', debitTag: 'UNKNOWN' }); // Type error!
26
36
  * ```
27
37
  */
28
38
  export class Scrawn {
39
+ /** Public access to the gRPC client for use by other packages (e.g. @scrawn/analytics) */
40
+ get grpc() {
41
+ return this.grpcClient;
42
+ }
43
+ /** API key used for authorizing gRPC calls */
44
+ get apikey() {
45
+ return this.apiKey;
46
+ }
47
+ notifyEventConsumerError(error, onError) {
48
+ const converted = isScrawnError(error) ? error : convertGrpcError(error);
49
+ onError?.(converted);
50
+ return converted;
51
+ }
52
+ notifyValidationError(error, onError) {
53
+ onError?.(error);
54
+ return error;
55
+ }
29
56
  /**
30
57
  * Creates a new Scrawn SDK instance.
31
58
  *
@@ -51,15 +78,60 @@ export class Scrawn {
51
78
  */
52
79
  this.credCache = new Map();
53
80
  try {
81
+ // Validate configuration
82
+ if (!config.apiKey || typeof config.apiKey !== "string") {
83
+ throw new ScrawnConfigError("API key is required and must be a string", {
84
+ details: { provided: typeof config.apiKey },
85
+ });
86
+ }
87
+ if (!config.baseURL || typeof config.baseURL !== "string") {
88
+ throw new ScrawnConfigError("baseURL is required and must be a string", {
89
+ details: { provided: typeof config.baseURL },
90
+ });
91
+ }
54
92
  this.apiKey = config.apiKey;
55
- this.grpcClient = new GrpcClient(config.baseURL);
56
- this.registerAuthMethod('api', new ApiKeyAuth(this.apiKey));
93
+ this.grpcClient = new GrpcClient(this.parseURLToTarget(config.baseURL), { secure: config.secure ?? true, credentials: config.credentials });
94
+ this.registerAuthMethod("api", new ApiKeyAuth(this.apiKey));
57
95
  }
58
96
  catch (error) {
59
- log.error('Failed to initialize Scrawn SDK');
97
+ log.error("Failed to initialize Scrawn SDK");
60
98
  throw error;
61
99
  }
62
100
  }
101
+ parseURLToTarget(baseURL) {
102
+ if (baseURL.includes("://")) {
103
+ const url = new URL(baseURL);
104
+ return `${url.hostname}:${url.port || ScrawnConfig.grpc.defaultPort}`;
105
+ }
106
+ return baseURL.includes(":")
107
+ ? baseURL
108
+ : `${baseURL}:${ScrawnConfig.grpc.defaultPort}`;
109
+ }
110
+ /**
111
+ * Create a type-safe tag reference.
112
+ *
113
+ * Only tag names known to this biller instance are accepted at compile time.
114
+ * Tag values are resolved to cent amounts by the backend at runtime.
115
+ *
116
+ * @param name - The tag name (must be one of the known tags for this instance)
117
+ * @returns A TagExpr referencing the named tag
118
+ * @throws PricingExpressionError at runtime if name format is invalid
119
+ *
120
+ * @example
121
+ * ```typescript
122
+ * const expr = mul(biller.tag("PREMIUM_CALL"), 3);
123
+ * ```
124
+ */
125
+ tag(name) {
126
+ return _tag(name);
127
+ }
128
+ expr(value) {
129
+ return {
130
+ _expr: typeof value === "string"
131
+ ? { kind: "exprRef", name: value }
132
+ : value,
133
+ };
134
+ }
63
135
  /**
64
136
  * Register an authentication method with the SDK.
65
137
  *
@@ -102,7 +174,9 @@ export class Scrawn {
102
174
  // Get fresh creds from auth method
103
175
  const auth = this.authMethods.get(authMethodName);
104
176
  if (!auth) {
105
- throw new Error(`No auth method registered: ${authMethodName}`);
177
+ throw new ScrawnConfigError(`No auth method registered: ${authMethodName}`, {
178
+ details: { requestedMethod: authMethodName },
179
+ });
106
180
  }
107
181
  const creds = await auth.getCreds();
108
182
  this.credCache.set(authMethodName, creds);
@@ -116,32 +190,74 @@ export class Scrawn {
116
190
  *
117
191
  * @param payload - The SDK call data to track
118
192
  * @param payload.userId - Unique identifier of the user making the call
119
- * @param payload.debitAmount - Amount to debit from the user's account
120
- * @returns A promise that resolves when the event is tracked
121
- * @throws Error if payload validation fails
193
+ * @param payload.debitAmount - (Optional) Direct amount in cents to debit from the user's account
194
+ * @param payload.debitTag - (Optional) Named price tag for backend-managed pricing
195
+ * @param payload.debitExpr - (Optional) Pricing expression for complex calculations
196
+ * @param options - Optional configuration
197
+ * @param options.onError - Optional callback for handling validation or gRPC errors
198
+ * @returns A promise that resolves when the event is tracked or returns early on error
122
199
  *
123
200
  * @example
124
201
  * ```typescript
202
+ * import { add, mul, tag } from '@scrawn/core';
203
+ *
204
+ * // Using direct amount (500 cents = $5.00)
205
+ * await scrawn.sdkCallEventConsumer({
206
+ * userId: 'user_abc123',
207
+ * debitAmount: 500
208
+ * });
209
+ *
210
+ * // Using price tag
211
+ * await scrawn.sdkCallEventConsumer({
212
+ * userId: 'user_abc123',
213
+ * debitTag: 'PREMIUM_FEATURE'
214
+ * });
215
+ *
216
+ * // Using pricing expression: (PREMIUM_CALL * 3) + EXTRA_FEE + 250 cents
125
217
  * await scrawn.sdkCallEventConsumer({
126
218
  * userId: 'user_abc123',
127
- * debitAmount: 10
219
+ * debitExpr: add(mul(tag('PREMIUM_CALL'), 3), tag('EXTRA_FEE'), 250)
128
220
  * });
129
221
  * ```
130
222
  */
131
- async sdkCallEventConsumer(payload) {
132
- const validationResult = EventPayloadSchema.safeParse(payload);
223
+ async sdkCallEventConsumer(payload, options) {
224
+ const rawPayload = {
225
+ userId: payload.userId,
226
+ debitAmount: payload.debitAmount,
227
+ debitTag: payload.debitTag,
228
+ debitExpr: payload.debitExpr?._expr,
229
+ };
230
+ const validationResult = EventPayloadSchema.safeParse(rawPayload);
133
231
  if (!validationResult.success) {
134
- const errors = validationResult.error.issues.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
232
+ const errors = validationResult.error.issues
233
+ .map((e) => `${e.path.join(".")}: ${e.message}`)
234
+ .join(", ");
135
235
  log.error(`Invalid payload for sdkCallEventConsumer: ${errors}`);
136
- throw new Error(`Payload validation failed: ${errors}`); // TODO: for error shit implement the callback shit
236
+ const error = new ScrawnValidationError("Payload validation failed", {
237
+ details: {
238
+ errors: validationResult.error.issues.map((e) => ({
239
+ field: e.path.join("."),
240
+ message: e.message,
241
+ })),
242
+ },
243
+ });
244
+ this.notifyValidationError(error, options?.onError);
245
+ return;
246
+ }
247
+ try {
248
+ await this.consumeEvent(validationResult.data, "api", "SDK_CALL");
249
+ }
250
+ catch (error) {
251
+ log.error(`Failed to track sdkCallEventConsumer event: ${error instanceof Error ? error.message : "Unknown error"}`);
252
+ this.notifyEventConsumerError(error, options?.onError);
253
+ return;
137
254
  }
138
- return this.consumeEvent(validationResult.data, 'api', 'SDK_CALL'); // TODO: change this event type when jaydeep changes it in backend
139
255
  }
140
256
  /**
141
257
  * Create an Express-compatible middleware for tracking API endpoint usage.
142
258
  *
143
259
  * This middleware automatically tracks requests to your API endpoints for billing purposes.
144
- * You provide an extractor function that determines the userId and debitAmount from each request.
260
+ * You provide an extractor function that determines the userId and debit info (amount or tag) from each request.
145
261
  * Optionally, you can provide a whitelist array to only track specific endpoints,
146
262
  * or a blacklist array to exclude specific endpoints from tracking.
147
263
  *
@@ -157,6 +273,7 @@ export class Scrawn {
157
273
  * Takes precedence over blacklist. If omitted, all requests will be tracked.
158
274
  * @param config.blacklist - Optional array of endpoint patterns to exclude. Same wildcard support as whitelist.
159
275
  * Only applies to endpoints not in the whitelist.
276
+ * @param config.onError - Optional callback for handling validation or gRPC errors
160
277
  *
161
278
  * @returns Express-compatible middleware function
162
279
  *
@@ -192,17 +309,17 @@ export class Scrawn {
192
309
  middlewareEventConsumer(config) {
193
310
  return async (req, res, next) => {
194
311
  try {
195
- const requestPath = req.path || req.url || '';
312
+ const requestPath = req.path || req.url || "";
196
313
  // Check whitelist first (takes precedence)
197
314
  if (config.whitelist && config.whitelist.length > 0) {
198
- const isWhitelisted = config.whitelist.some(pattern => matchPath(requestPath, pattern));
315
+ const isWhitelisted = config.whitelist.some((pattern) => matchPath(requestPath, pattern));
199
316
  if (!isWhitelisted) {
200
317
  return next();
201
318
  }
202
319
  }
203
320
  // Then check blacklist
204
321
  if (config.blacklist && config.blacklist.length > 0) {
205
- const isBlacklisted = config.blacklist.some(pattern => matchPath(requestPath, pattern));
322
+ const isBlacklisted = config.blacklist.some((pattern) => matchPath(requestPath, pattern));
206
323
  if (isBlacklisted) {
207
324
  return next();
208
325
  }
@@ -213,22 +330,40 @@ export class Scrawn {
213
330
  log.warn(`Extractor returned null for path: ${requestPath}. Skipping event tracking.`);
214
331
  return next();
215
332
  }
216
- const validationResult = EventPayloadSchema.safeParse(extractedPayload);
333
+ const rawPayload = {
334
+ userId: extractedPayload.userId,
335
+ debitAmount: extractedPayload.debitAmount,
336
+ debitTag: extractedPayload.debitTag,
337
+ debitExpr: extractedPayload.debitExpr?._expr,
338
+ };
339
+ const validationResult = EventPayloadSchema.safeParse(rawPayload);
217
340
  if (!validationResult.success) {
218
- const errors = validationResult.error.issues.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
219
- log.error(`Invalid payload extracted in middlewareEventConsumer: ${errors}`); // TODO: for error shit implement the callback shit
341
+ const errors = validationResult.error.issues
342
+ .map((e) => `${e.path.join(".")}: ${e.message}`)
343
+ .join(", ");
344
+ log.error(`Invalid payload extracted in middlewareEventConsumer: ${errors}`);
345
+ const error = new ScrawnValidationError("Payload validation failed", {
346
+ details: {
347
+ errors: validationResult.error.issues.map((e) => ({
348
+ field: e.path.join("."),
349
+ message: e.message,
350
+ })),
351
+ },
352
+ });
353
+ this.notifyValidationError(error, config.onError);
220
354
  return next();
221
355
  }
222
- this.consumeEvent(validationResult.data, 'api', 'MIDDLEWARE_CALL')
223
- .catch(error => {
356
+ this.consumeEvent(validationResult.data, "api", "MIDDLEWARE_CALL").catch((error) => {
224
357
  log.error(`Failed to track middleware event: ${error.message}`);
225
- }); // TODO: for error shit implement the callback shit
358
+ this.notifyEventConsumerError(error, config.onError);
359
+ });
226
360
  next();
227
361
  }
228
362
  catch (error) {
229
- log.error(`Error in middlewareEventConsumer: ${error instanceof Error ? error.message : 'Unknown error'}`);
363
+ log.error(`Error in middlewareEventConsumer: ${error instanceof Error ? error.message : "Unknown error"}`);
364
+ this.notifyEventConsumerError(error, config.onError);
230
365
  next();
231
- } // TODO: for error shit implement the callback shit
366
+ }
232
367
  };
233
368
  }
234
369
  /**
@@ -250,25 +385,29 @@ export class Scrawn {
250
385
  */
251
386
  async collectPayment(userId) {
252
387
  // Validate input
253
- if (!userId || typeof userId !== 'string' || userId.trim().length === 0) {
254
- log.error('Invalid userId provided to collectPayment');
255
- throw new Error('userId must be a non-empty string');
388
+ if (!userId || typeof userId !== "string" || userId.trim().length === 0) {
389
+ log.error("Invalid userId provided to collectPayment");
390
+ throw new ScrawnValidationError("userId must be a non-empty string", {
391
+ details: { provided: typeof userId },
392
+ });
256
393
  }
257
394
  // Get credentials for authentication
258
- const creds = await this.getCredsFor('api');
395
+ const creds = await this.getCredsFor("api");
259
396
  try {
260
397
  log.info(`Creating checkout link for user: ${userId}`);
398
+ const request = new CreateCheckoutLinkRequest();
399
+ request.setUserid(userId);
261
400
  const response = await this.grpcClient
262
- .newCall(PaymentService, 'createCheckoutLink')
263
- .addHeader('Authorization', `Bearer ${creds.apiKey}`)
264
- .addPayload({ userId })
401
+ .newCall(PaymentServiceClient, "createCheckoutLink")
402
+ .addMetadata("authorization", `Bearer ${creds.apiKey}`)
403
+ .addPayload(request)
265
404
  .request();
266
- log.info(`Checkout link created successfully: ${response.checkoutLink}`);
267
- return response.checkoutLink;
405
+ log.info(`Checkout link created successfully: ${response.getCheckoutlink()}`);
406
+ return response.getCheckoutlink();
268
407
  }
269
408
  catch (error) {
270
- log.error(`Failed to create checkout link: ${error instanceof Error ? error.message : 'Unknown error'}`);
271
- throw error;
409
+ log.error(`Failed to create checkout link: ${error instanceof Error ? error.message : "Unknown error"}`);
410
+ throw convertGrpcError(error);
272
411
  }
273
412
  }
274
413
  /**
@@ -290,42 +429,301 @@ export class Scrawn {
290
429
  */
291
430
  async consumeEvent(payload, authMethodName, eventType) {
292
431
  const auth = this.authMethods.get(authMethodName);
293
- if (!auth)
294
- throw new Error(`No auth registered for type ${authMethodName}`);
432
+ if (!auth) {
433
+ throw new ScrawnConfigError(`No auth registered for type ${authMethodName}`, {
434
+ details: { requestedAuth: authMethodName },
435
+ });
436
+ }
295
437
  // Run pre-hook if exists
296
438
  if (auth.preRun)
297
439
  await auth.preRun();
298
440
  // Get creds (from cache or fresh)
299
441
  const creds = await this.getCredsFor(authMethodName);
300
442
  // Map event type to SDKCallType
301
- const sdkCallType = eventType === 'SDK_CALL'
302
- ? SDKCallType.RAW
303
- : SDKCallType.MIDDLEWARE_CALL;
443
+ const sdkCallType = eventType === "SDK_CALL" ? SDKCallType.RAW : SDKCallType.MIDDLEWARE_CALL;
304
444
  try {
305
445
  log.info(`Ingesting event (type: ${eventType}) with creds: ${JSON.stringify(creds)}, payload: ${JSON.stringify(payload)}`);
446
+ // Build debit field based on which debit option is provided
447
+ let debitField;
448
+ if (payload.debitAmount !== undefined) {
449
+ debitField = { case: "amount", value: payload.debitAmount };
450
+ }
451
+ else if (payload.debitTag !== undefined) {
452
+ debitField = { case: "tag", value: payload.debitTag };
453
+ }
454
+ else {
455
+ // debitExpr is defined (validated by schema)
456
+ const serialized = serializeExpr(payload.debitExpr);
457
+ log.debug(`Serialized pricing expression: ${serialized}\n${prettyPrintExpr(payload.debitExpr)}`);
458
+ debitField = {
459
+ case: "expr",
460
+ value: serialized,
461
+ };
462
+ }
463
+ const sdkCall = new SDKCall();
464
+ sdkCall.setSdkcalltype(sdkCallType);
465
+ if (debitField.case === "amount") {
466
+ sdkCall.setAmount(debitField.value);
467
+ }
468
+ else if (debitField.case === "tag") {
469
+ sdkCall.setTag(debitField.value);
470
+ }
471
+ else {
472
+ sdkCall.setExpr(debitField.value);
473
+ }
474
+ const request = new RegisterEventRequest();
475
+ request.setType(EventType.SDK_CALL);
476
+ request.setUserid(payload.userId);
477
+ request.setSdkcall(sdkCall);
306
478
  const response = await this.grpcClient
307
- .newCall(EventService, 'registerEvent')
308
- .addHeader('Authorization', `Bearer ${creds.apiKey}`)
309
- .addPayload({
310
- type: EventType.SDK_CALL,
311
- userId: payload.userId,
312
- data: {
313
- case: 'sdkCall',
314
- value: new SDKCall({
315
- sdkCallType: sdkCallType,
316
- debitAmount: payload.debitAmount,
317
- }),
318
- },
319
- })
479
+ .newCall(EventServiceClient, "registerEvent")
480
+ .addMetadata("authorization", `Bearer ${creds.apiKey}`)
481
+ .addPayload(request)
320
482
  .request();
321
483
  log.info(`Event registered successfully: ${JSON.stringify(response)}`);
322
484
  }
323
485
  catch (error) {
324
- log.error(`Failed to register event: ${error instanceof Error ? error.message : 'Unknown error'}`);
325
- throw error;
486
+ log.error(`Failed to register event: ${error instanceof Error ? error.message : "Unknown error"}`);
487
+ throw convertGrpcError(error);
326
488
  }
327
489
  if (auth.postRun)
328
490
  await auth.postRun();
329
491
  }
492
+ /**
493
+ * Stream AI token usage events to the Scrawn backend.
494
+ *
495
+ * Consumes an async iterable of AI token usage payloads and streams them
496
+ * to the backend for billing tracking. This is designed for real-time
497
+ * AI token tracking where usage is reported as tokens are consumed.
498
+ *
499
+ * The streaming is non-blocking: the iterable is consumed in the background
500
+ * and streamed to the server without blocking the caller's code path.
501
+ *
502
+ * When `return: true`, the stream is forked internally - one fork goes to
503
+ * billing (non-blocking), and another is returned to the caller for streaming
504
+ * to the user.
505
+ *
506
+ * @param stream - An async iterable of AI token usage payloads
507
+ * @param config - Optional configuration object
508
+ * @param config.return - If true, returns a forked stream alongside the response promise
509
+ * @param config.onError - Optional callback for handling validation or gRPC errors
510
+ * @returns Depends on config.return:
511
+ * - false/undefined: Promise<StreamEventResponse | undefined>
512
+ * - true: { response: Promise<StreamEventResponse | undefined>, stream: AsyncIterable<AITokenUsagePayload> }
513
+ *
514
+ * @example
515
+ * ```typescript
516
+ * // Fire-and-forget mode (default)
517
+ * async function* tokenUsageStream() {
518
+ * yield {
519
+ * userId: 'user_abc123',
520
+ * model: 'gpt-4',
521
+ * inputTokens: 100,
522
+ * outputTokens: 50,
523
+ * inputDebit: { amount: 0.003 },
524
+ * outputDebit: { amount: 0.006 }
525
+ * };
526
+ * }
527
+ *
528
+ * const response = await scrawn.aiTokenStreamConsumer(tokenUsageStream());
529
+ * if (response) {
530
+ * console.log(`Processed ${response.getEventsprocessed()} events`);
531
+ * }
532
+ *
533
+ * // Return mode - stream to user while billing
534
+ * const { response, stream } = await scrawn.aiTokenStreamConsumer(
535
+ * tokenUsageStream(),
536
+ * { return: true }
537
+ * );
538
+ *
539
+ * for await (const token of stream) {
540
+ * // Stream to user
541
+ * }
542
+ *
543
+ * const result = await response;
544
+ * if (!result) return;
545
+ * ```
546
+ */
547
+ // fallow-ignore-next-line unused-class-member
548
+ async aiTokenStreamConsumer(stream, config) {
549
+ const onError = config?.onError;
550
+ // Get credentials for authentication
551
+ const creds = await this.getCredsFor("api");
552
+ // If return mode, fork the stream
553
+ if (config?.return === true) {
554
+ const [billingStream, userStream] = forkAsyncIterable(stream);
555
+ // Transform billing stream and send to backend (non-blocking)
556
+ const transformedStream = this.transformAITokenStream(billingStream, onError);
557
+ const responsePromise = (async () => {
558
+ try {
559
+ log.info("Starting AI token usage stream (return mode)");
560
+ const response = await this.grpcClient
561
+ .newStreamCall(EventServiceClient, "streamEvents")
562
+ .addMetadata("authorization", `Bearer ${creds.apiKey}`)
563
+ .stream(transformedStream);
564
+ log.info(`AI token stream completed: ${response.getEventsprocessed()} events processed`);
565
+ return response;
566
+ }
567
+ catch (error) {
568
+ log.error(`Failed to stream AI token usage: ${error instanceof Error ? error.message : "Unknown error"}`);
569
+ this.notifyEventConsumerError(error, onError);
570
+ return undefined;
571
+ }
572
+ })();
573
+ return { response: responsePromise, stream: userStream };
574
+ }
575
+ // Default: fire-and-forget mode
576
+ const transformedStream = this.transformAITokenStream(stream, onError);
577
+ try {
578
+ log.info("Starting AI token usage stream");
579
+ const response = await this.grpcClient
580
+ .newStreamCall(EventServiceClient, "streamEvents")
581
+ .addMetadata("authorization", `Bearer ${creds.apiKey}`)
582
+ .stream(transformedStream);
583
+ log.info(`AI token stream completed: ${response.getEventsprocessed()} events processed`);
584
+ return response;
585
+ }
586
+ catch (error) {
587
+ log.error(`Failed to stream AI token usage: ${error instanceof Error ? error.message : "Unknown error"}`);
588
+ this.notifyEventConsumerError(error, onError);
589
+ return undefined;
590
+ }
591
+ }
592
+ /**
593
+ * Transform user-provided AI token usage payloads into StreamEventRequest format.
594
+ *
595
+ * Validates each payload and maps it to the gRPC request format.
596
+ * Invalid payloads are logged and skipped.
597
+ *
598
+ * @param stream - The user's async iterable of AITokenUsagePayload
599
+ * @returns An async iterable of StreamEventRequest payloads
600
+ * @internal
601
+ */
602
+ async *transformAITokenStream(stream, onError) {
603
+ for await (const payload of stream) {
604
+ // Unwrap ScrawnExpr before Zod validation
605
+ const rawPayload = {
606
+ userId: payload.userId,
607
+ model: payload.model,
608
+ inputTokens: payload.inputTokens,
609
+ outputTokens: payload.outputTokens,
610
+ inputDebit: {
611
+ amount: payload.inputDebit.amount,
612
+ tag: payload.inputDebit.tag,
613
+ expr: payload.inputDebit.expr?._expr,
614
+ },
615
+ outputDebit: {
616
+ amount: payload.outputDebit.amount,
617
+ tag: payload.outputDebit.tag,
618
+ expr: payload.outputDebit.expr?._expr,
619
+ },
620
+ };
621
+ // Validate each payload
622
+ const validationResult = AITokenUsagePayloadSchema.safeParse(rawPayload);
623
+ if (!validationResult.success) {
624
+ const errors = validationResult.error.issues
625
+ .map((e) => `${e.path.join(".")}: ${e.message}`)
626
+ .join(", ");
627
+ log.error(`Invalid AI token usage payload, skipping: ${errors}`);
628
+ const error = new ScrawnValidationError("AI token usage payload validation failed", {
629
+ details: {
630
+ errors: validationResult.error.issues.map((e) => ({
631
+ field: e.path.join("."),
632
+ message: e.message,
633
+ })),
634
+ },
635
+ });
636
+ this.notifyValidationError(error, onError);
637
+ continue;
638
+ }
639
+ const validated = validationResult.data;
640
+ // Token context for resolving inputTokens()/outputTokens() placeholders
641
+ const tokenContext = {
642
+ inputTokens: validated.inputTokens,
643
+ outputTokens: validated.outputTokens,
644
+ };
645
+ // Build input debit field (amount, tag, or expr)
646
+ let inputDebit;
647
+ if (validated.inputDebit.amount !== undefined) {
648
+ inputDebit = {
649
+ case: "inputAmount",
650
+ value: validated.inputDebit.amount,
651
+ };
652
+ }
653
+ else if (validated.inputDebit.tag !== undefined) {
654
+ inputDebit = {
655
+ case: "inputTag",
656
+ value: validated.inputDebit.tag,
657
+ };
658
+ }
659
+ else {
660
+ const resolved = resolveTokens(validated.inputDebit.expr, tokenContext);
661
+ const serialized = serializeExpr(resolved);
662
+ log.debug(`Resolved input debit expression (inputTokens=${validated.inputTokens}): ${serialized}\n${prettyPrintExpr(resolved)}`);
663
+ inputDebit = {
664
+ case: "inputExpr",
665
+ value: serialized,
666
+ };
667
+ }
668
+ // Build output debit field (amount, tag, or expr)
669
+ let outputDebit;
670
+ if (validated.outputDebit.amount !== undefined) {
671
+ outputDebit = {
672
+ case: "outputAmount",
673
+ value: validated.outputDebit.amount,
674
+ };
675
+ }
676
+ else if (validated.outputDebit.tag !== undefined) {
677
+ outputDebit = {
678
+ case: "outputTag",
679
+ value: validated.outputDebit.tag,
680
+ };
681
+ }
682
+ else {
683
+ const resolved = resolveTokens(validated.outputDebit.expr, tokenContext);
684
+ const serialized = serializeExpr(resolved);
685
+ log.debug(`Resolved output debit expression (outputTokens=${validated.outputTokens}): ${serialized}\n${prettyPrintExpr(resolved)}`);
686
+ outputDebit = {
687
+ case: "outputExpr",
688
+ value: serialized,
689
+ };
690
+ }
691
+ const aiTokenUsage = new AITokenUsage();
692
+ aiTokenUsage.setModel(validated.model);
693
+ aiTokenUsage.setInputtokens(validated.inputTokens);
694
+ aiTokenUsage.setOutputtokens(validated.outputTokens);
695
+ if (inputDebit.case === "inputAmount") {
696
+ aiTokenUsage.setInputamount(inputDebit.value);
697
+ }
698
+ else if (inputDebit.case === "inputTag") {
699
+ aiTokenUsage.setInputtag(inputDebit.value);
700
+ }
701
+ else {
702
+ aiTokenUsage.setInputexpr(inputDebit.value);
703
+ }
704
+ if (outputDebit.case === "outputAmount") {
705
+ aiTokenUsage.setOutputamount(outputDebit.value);
706
+ }
707
+ else if (outputDebit.case === "outputTag") {
708
+ aiTokenUsage.setOutputtag(outputDebit.value);
709
+ }
710
+ else {
711
+ aiTokenUsage.setOutputexpr(outputDebit.value);
712
+ }
713
+ const request = new StreamEventRequest();
714
+ request.setType(EventType.AI_TOKEN_USAGE);
715
+ request.setUserid(validated.userId);
716
+ request.setAitokenusage(aiTokenUsage);
717
+ yield request;
718
+ }
719
+ }
720
+ }
721
+ export function createScrawn(config) {
722
+ return new Scrawn({
723
+ apiKey: config.apiKey,
724
+ baseURL: config.baseURL,
725
+ secure: config.secure,
726
+ credentials: config.credentials,
727
+ });
330
728
  }
331
729
  //# sourceMappingURL=scrawn.js.map