@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.
- package/README.md +40 -0
- package/dist/config.d.ts +17 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -2
- package/dist/config.js.map +1 -1
- package/dist/core/auth/apiKeyAuth.d.ts +4 -13
- package/dist/core/auth/apiKeyAuth.d.ts.map +1 -1
- package/dist/core/auth/apiKeyAuth.js +12 -17
- package/dist/core/auth/apiKeyAuth.js.map +1 -1
- package/dist/core/auth/baseAuth.d.ts +14 -36
- package/dist/core/auth/baseAuth.d.ts.map +1 -1
- package/dist/core/auth/baseAuth.js +0 -6
- package/dist/core/auth/baseAuth.js.map +1 -1
- package/dist/core/errors/index.d.ts +192 -0
- package/dist/core/errors/index.d.ts.map +1 -0
- package/dist/core/errors/index.js +280 -0
- package/dist/core/errors/index.js.map +1 -0
- package/dist/core/grpc/callContext.d.ts +18 -0
- package/dist/core/grpc/callContext.d.ts.map +1 -0
- package/dist/core/grpc/callContext.js +35 -0
- package/dist/core/grpc/callContext.js.map +1 -0
- package/dist/core/grpc/client.d.ts +13 -123
- package/dist/core/grpc/client.d.ts.map +1 -1
- package/dist/core/grpc/client.js +23 -131
- package/dist/core/grpc/client.js.map +1 -1
- package/dist/core/grpc/index.d.ts +5 -3
- package/dist/core/grpc/index.d.ts.map +1 -1
- package/dist/core/grpc/index.js +4 -2
- package/dist/core/grpc/index.js.map +1 -1
- package/dist/core/grpc/requestBuilder.d.ts +12 -113
- package/dist/core/grpc/requestBuilder.d.ts.map +1 -1
- package/dist/core/grpc/requestBuilder.js +36 -126
- package/dist/core/grpc/requestBuilder.js.map +1 -1
- package/dist/core/grpc/streamRequestBuilder.d.ts +13 -0
- package/dist/core/grpc/streamRequestBuilder.d.ts.map +1 -0
- package/dist/core/grpc/streamRequestBuilder.js +60 -0
- package/dist/core/grpc/streamRequestBuilder.js.map +1 -0
- package/dist/core/grpc/types.d.ts +5 -52
- package/dist/core/grpc/types.d.ts.map +1 -1
- package/dist/core/grpc/types.js +0 -7
- package/dist/core/grpc/types.js.map +1 -1
- package/dist/core/pricing/builders.d.ts +157 -0
- package/dist/core/pricing/builders.d.ts.map +1 -0
- package/dist/core/pricing/builders.js +218 -0
- package/dist/core/pricing/builders.js.map +1 -0
- package/dist/core/pricing/index.d.ts +30 -0
- package/dist/core/pricing/index.d.ts.map +1 -0
- package/dist/core/pricing/index.js +32 -0
- package/dist/core/pricing/index.js.map +1 -0
- package/dist/core/pricing/resolve.d.ts +39 -0
- package/dist/core/pricing/resolve.d.ts.map +1 -0
- package/dist/core/pricing/resolve.js +50 -0
- package/dist/core/pricing/resolve.js.map +1 -0
- package/dist/core/pricing/serialize.d.ts +55 -0
- package/dist/core/pricing/serialize.d.ts.map +1 -0
- package/dist/core/pricing/serialize.js +127 -0
- package/dist/core/pricing/serialize.js.map +1 -0
- package/dist/core/pricing/types.d.ts +122 -0
- package/dist/core/pricing/types.d.ts.map +1 -0
- package/dist/core/pricing/types.js +17 -0
- package/dist/core/pricing/types.js.map +1 -0
- package/dist/core/pricing/validate.d.ts +56 -0
- package/dist/core/pricing/validate.d.ts.map +1 -0
- package/dist/core/pricing/validate.js +162 -0
- package/dist/core/pricing/validate.js.map +1 -0
- package/dist/core/scrawn.d.ts +218 -17
- package/dist/core/scrawn.d.ts.map +1 -1
- package/dist/core/scrawn.js +469 -71
- package/dist/core/scrawn.js.map +1 -1
- package/dist/core/types/auth.d.ts +1 -1
- package/dist/core/types/event.d.ts +182 -18
- package/dist/core/types/event.d.ts.map +1 -1
- package/dist/core/types/event.js +133 -5
- package/dist/core/types/event.js.map +1 -1
- package/dist/gen/auth/v1/auth_grpc_pb.d.ts +3 -0
- package/dist/gen/auth/v1/auth_grpc_pb.js +45 -0
- package/dist/gen/auth/v1/auth_pb.d.ts +63 -57
- package/dist/gen/auth/v1/auth_pb.js +471 -86
- package/dist/gen/data/v1/data_grpc_pb.d.ts +5 -0
- package/dist/gen/data/v1/data_grpc_pb.js +44 -0
- package/dist/gen/data/v1/data_pb.d.ts +254 -0
- package/dist/gen/data/v1/data_pb.js +1530 -0
- package/dist/gen/event/v1/event_grpc_pb.d.ts +3 -0
- package/dist/gen/event/v1/event_grpc_pb.js +79 -0
- package/dist/gen/event/v1/event_pb.d.ts +273 -100
- package/dist/gen/event/v1/event_pb.js +1862 -138
- package/dist/gen/package.json +3 -0
- package/dist/gen/payment/v1/payment_grpc_pb.d.ts +3 -0
- package/dist/gen/payment/v1/payment_grpc_pb.js +45 -0
- package/dist/gen/payment/v1/payment_pb.d.ts +43 -35
- package/dist/gen/payment/v1/payment_pb.js +321 -59
- package/dist/gen/query/v1/query_grpc_pb.d.ts +5 -0
- package/dist/gen/query/v1/query_grpc_pb.js +44 -0
- package/dist/gen/query/v1/query_pb.d.ts +359 -0
- package/dist/gen/query/v1/query_pb.js +2327 -0
- package/dist/index.d.ts +19 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -10
- package/dist/index.js.map +1 -1
- package/dist/utils/forkAsyncIterable.d.ts +13 -0
- package/dist/utils/forkAsyncIterable.d.ts.map +1 -0
- package/dist/utils/forkAsyncIterable.js +78 -0
- package/dist/utils/forkAsyncIterable.js.map +1 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +19 -19
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/pathMatcher.js +5 -5
- package/package.json +19 -15
- package/dist/gen/auth/v1/auth_connect.d.ts +0 -22
- package/dist/gen/auth/v1/auth_connect.d.ts.map +0 -1
- package/dist/gen/auth/v1/auth_connect.js +0 -26
- package/dist/gen/auth/v1/auth_connect.js.map +0 -1
- package/dist/gen/auth/v1/auth_pb.d.ts.map +0 -1
- package/dist/gen/auth/v1/auth_pb.js.map +0 -1
- package/dist/gen/event/v1/event_connect.d.ts +0 -22
- package/dist/gen/event/v1/event_connect.d.ts.map +0 -1
- package/dist/gen/event/v1/event_connect.js +0 -26
- package/dist/gen/event/v1/event_connect.js.map +0 -1
- package/dist/gen/event/v1/event_pb.d.ts.map +0 -1
- package/dist/gen/event/v1/event_pb.js.map +0 -1
- package/dist/gen/payment/v1/payment_connect.d.ts +0 -22
- package/dist/gen/payment/v1/payment_connect.d.ts.map +0 -1
- package/dist/gen/payment/v1/payment_connect.js +0 -26
- package/dist/gen/payment/v1/payment_connect.js.map +0 -1
- package/dist/gen/payment/v1/payment_pb.d.ts.map +0 -1
- package/dist/gen/payment/v1/payment_pb.js.map +0 -1
package/dist/core/scrawn.js
CHANGED
|
@@ -1,31 +1,58 @@
|
|
|
1
|
-
import { ApiKeyAuth } from
|
|
2
|
-
import { ScrawnLogger } from
|
|
3
|
-
import { matchPath } from
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
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 {
|
|
25
|
+
* import { createScrawn } from '@scrawn/core';
|
|
19
26
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
-
* //
|
|
25
|
-
*
|
|
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(
|
|
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(
|
|
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
|
|
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 -
|
|
120
|
-
* @
|
|
121
|
-
* @
|
|
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
|
-
*
|
|
219
|
+
* debitExpr: add(mul(tag('PREMIUM_CALL'), 3), tag('EXTRA_FEE'), 250)
|
|
128
220
|
* });
|
|
129
221
|
* ```
|
|
130
222
|
*/
|
|
131
|
-
async sdkCallEventConsumer(payload) {
|
|
132
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
219
|
-
|
|
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,
|
|
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
|
-
|
|
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 :
|
|
363
|
+
log.error(`Error in middlewareEventConsumer: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
364
|
+
this.notifyEventConsumerError(error, config.onError);
|
|
230
365
|
next();
|
|
231
|
-
}
|
|
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 !==
|
|
254
|
-
log.error(
|
|
255
|
-
throw new
|
|
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(
|
|
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(
|
|
263
|
-
.
|
|
264
|
-
.addPayload(
|
|
401
|
+
.newCall(PaymentServiceClient, "createCheckoutLink")
|
|
402
|
+
.addMetadata("authorization", `Bearer ${creds.apiKey}`)
|
|
403
|
+
.addPayload(request)
|
|
265
404
|
.request();
|
|
266
|
-
log.info(`Checkout link created successfully: ${response.
|
|
267
|
-
return response.
|
|
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 :
|
|
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
|
|
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 ===
|
|
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(
|
|
308
|
-
.
|
|
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 :
|
|
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
|