@drawbridge/drawbridge-utils 0.0.79 → 0.0.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai.cjs +31 -8
- package/dist/ai.d.cts +35 -4
- package/dist/ai.d.ts +35 -4
- package/dist/ai.js +21 -8
- package/dist/billing.cjs +24 -4
- package/dist/billing.d.cts +51 -3
- package/dist/billing.d.ts +51 -3
- package/dist/billing.js +23 -4
- package/dist/transactions.cjs +0 -4
- package/dist/transactions.d.cts +14 -17
- package/dist/transactions.d.ts +14 -17
- package/dist/transactions.js +0 -4
- package/package.json +2 -2
package/dist/ai.cjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
1
2
|
var __defProp = Object.defineProperty;
|
|
2
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
4
6
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
7
|
var __export = (target, all) => {
|
|
6
8
|
for (var name in all)
|
|
@@ -14,6 +16,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
14
16
|
}
|
|
15
17
|
return to;
|
|
16
18
|
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
17
27
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
28
|
|
|
19
29
|
// lib/ai.js
|
|
@@ -91,9 +101,6 @@ var insertTransaction = async ({
|
|
|
91
101
|
session,
|
|
92
102
|
...rest
|
|
93
103
|
}) => {
|
|
94
|
-
var _a;
|
|
95
|
-
const beforeRaw = (_a = user == null ? void 0 : user.balance) == null ? void 0 : _a[type];
|
|
96
|
-
const balance = typeof beforeRaw === "number" ? { before: beforeRaw, after: beforeRaw + amount } : void 0;
|
|
97
104
|
const trace = (0, import_drawbridge_telemetry.currentTraceId)();
|
|
98
105
|
await db.create({
|
|
99
106
|
authenticated: user,
|
|
@@ -105,7 +112,6 @@ var insertTransaction = async ({
|
|
|
105
112
|
category,
|
|
106
113
|
source,
|
|
107
114
|
amount,
|
|
108
|
-
...balance && { balance },
|
|
109
115
|
...trace && { trace },
|
|
110
116
|
...rest,
|
|
111
117
|
...stripeInvoiceId && { stripeInvoiceId },
|
|
@@ -251,6 +257,13 @@ var ai = {
|
|
|
251
257
|
bill: ({ db, user, model, usage, tools }) => (user == null ? void 0 : user.id) ? () => billRequest({ db, user, model, usage, tools }) : () => Promise.resolve()
|
|
252
258
|
};
|
|
253
259
|
|
|
260
|
+
// lib/fetch.js
|
|
261
|
+
var import_qs = __toESM(require("qs"), 1);
|
|
262
|
+
var isTransientError = (error) => {
|
|
263
|
+
const status = error == null ? void 0 : error.status;
|
|
264
|
+
return !status || status === 429 || status >= 500;
|
|
265
|
+
};
|
|
266
|
+
|
|
254
267
|
// lib/ai.js
|
|
255
268
|
var google_client = new import_genai.GoogleGenAI({
|
|
256
269
|
apiKey: process.env.GOOGLE_GEMINI_API_KEY
|
|
@@ -260,6 +273,16 @@ var googleCircuit = circuit({
|
|
|
260
273
|
threshold: 3,
|
|
261
274
|
timeout: 6e4
|
|
262
275
|
});
|
|
276
|
+
var withRetries = async (fn, { attempts = 3, delay = 500 } = {}) => {
|
|
277
|
+
for (let attempt = 1; ; attempt++) {
|
|
278
|
+
try {
|
|
279
|
+
return await fn();
|
|
280
|
+
} catch (error) {
|
|
281
|
+
if (attempt >= attempts || !isTransientError(error)) throw error;
|
|
282
|
+
await new Promise((resolve) => setTimeout(resolve, delay * attempt));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
};
|
|
263
286
|
var models = {
|
|
264
287
|
image: "gemini-2.5-flash-image",
|
|
265
288
|
text: "gemini-3.5-flash"
|
|
@@ -293,14 +316,14 @@ var google = {
|
|
|
293
316
|
...refs.map((ref) => ({ inlineData: { data: ref.data, mimeType: ref.mimeType } })),
|
|
294
317
|
{ text: prompt.join("\n") }
|
|
295
318
|
];
|
|
296
|
-
const response = await googleCircuit(() => google_client.models.generateContent({
|
|
319
|
+
const response = await googleCircuit(() => withRetries(() => google_client.models.generateContent({
|
|
297
320
|
model,
|
|
298
321
|
contents: { parts },
|
|
299
322
|
config: {
|
|
300
323
|
...config,
|
|
301
324
|
responseModalities: ["Image"]
|
|
302
325
|
}
|
|
303
|
-
}));
|
|
326
|
+
})));
|
|
304
327
|
const content = (_f = (_e = (_d = (_c = (_b = (_a = response == null ? void 0 : response.candidates) == null ? void 0 : _a[0]) == null ? void 0 : _b.content) == null ? void 0 : _c.parts) == null ? void 0 : _d[0]) == null ? void 0 : _e.inlineData) == null ? void 0 : _f.data;
|
|
305
328
|
if (!content) {
|
|
306
329
|
throw new Error("Could not generate from prompt. Please try again with a different prompt.");
|
|
@@ -321,14 +344,14 @@ var google = {
|
|
|
321
344
|
}) => {
|
|
322
345
|
const model = models.text;
|
|
323
346
|
const contents = parts ? { parts } : images.length ? { parts: [...images.map((image) => ({ inlineData: { data: image.data, mimeType: image.mimeType } })), { text: prompt.join("\n") }] } : prompt.join("\n");
|
|
324
|
-
const response = await googleCircuit(() => google_client.models.generateContent({
|
|
347
|
+
const response = await googleCircuit(() => withRetries(() => google_client.models.generateContent({
|
|
325
348
|
config: {
|
|
326
349
|
...config,
|
|
327
350
|
responseModalities: ["Text"]
|
|
328
351
|
},
|
|
329
352
|
contents,
|
|
330
353
|
model
|
|
331
|
-
}));
|
|
354
|
+
})));
|
|
332
355
|
const content = response == null ? void 0 : response.text;
|
|
333
356
|
if (!content) {
|
|
334
357
|
throw new Error("Could not generate from prompt. Please try again with a different prompt.");
|
package/dist/ai.d.cts
CHANGED
|
@@ -2,9 +2,11 @@ import { GoogleGenAI } from '@google/genai';
|
|
|
2
2
|
import { circuit } from './circuit.cjs';
|
|
3
3
|
import { ai } from './billing.cjs';
|
|
4
4
|
export { MARKUP } from './billing.cjs';
|
|
5
|
+
import { isTransientError } from './fetch.cjs';
|
|
5
6
|
import './transactions.cjs';
|
|
6
7
|
import '@drawbridge/drawbridge-telemetry';
|
|
7
8
|
import './usage.cjs';
|
|
9
|
+
import 'qs';
|
|
8
10
|
|
|
9
11
|
// AI request entry point. Wraps the Google GenAI client with a per-process
|
|
10
12
|
// circuit breaker, owns the per-model pricing table, and atomically debits
|
|
@@ -35,6 +37,35 @@ const googleCircuit = circuit({
|
|
|
35
37
|
timeout : 60000
|
|
36
38
|
});
|
|
37
39
|
|
|
40
|
+
// Bounded retry for the raw model call. A single transient failure — undici
|
|
41
|
+
// "fetch failed" on a resource-starved pod, a 429, a 5xx — must not lose the
|
|
42
|
+
// generation step OR count toward the breaker: three of those trip it and
|
|
43
|
+
// hard-fail every AI call for the next 60s. Runs INSIDE the googleCircuit
|
|
44
|
+
// callback so the breaker only counts calls that failed after retries were
|
|
45
|
+
// exhausted, and an OPEN circuit still fails fast without retrying.
|
|
46
|
+
// isTransientError treats status-less errors as transient, which is correct
|
|
47
|
+
// here: the wrapped fn is exactly one SDK call, and the GenAI SDK reports
|
|
48
|
+
// network-level failures as plain Errors with no `.status`.
|
|
49
|
+
const withRetries = async ( fn, { attempts = 3, delay = 500 } = {} ) => {
|
|
50
|
+
|
|
51
|
+
for( let attempt = 1; ; attempt++ ){
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
|
|
55
|
+
return await fn();
|
|
56
|
+
|
|
57
|
+
} catch ( error ) {
|
|
58
|
+
|
|
59
|
+
if( attempt >= attempts || ! isTransientError( error ) ) throw error;
|
|
60
|
+
|
|
61
|
+
await new Promise( ( resolve ) => setTimeout( resolve, delay * attempt ) );
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
};
|
|
68
|
+
|
|
38
69
|
// gemini-2.5-flash was retired by Google (generateContent → 404 "no longer
|
|
39
70
|
// available"), taking down every generation. gemini-3.5-flash is the current
|
|
40
71
|
// stable flash successor. The image model is a distinct model and still live,
|
|
@@ -89,14 +120,14 @@ const google = {
|
|
|
89
120
|
{ text : prompt.join( '\n' ) }
|
|
90
121
|
];
|
|
91
122
|
|
|
92
|
-
const response = await googleCircuit( () => google_client.models.generateContent({
|
|
123
|
+
const response = await googleCircuit( () => withRetries( () => google_client.models.generateContent({
|
|
93
124
|
model,
|
|
94
125
|
contents : { parts },
|
|
95
126
|
config : {
|
|
96
127
|
...config,
|
|
97
128
|
responseModalities : [ 'Image' ]
|
|
98
129
|
}
|
|
99
|
-
}) );
|
|
130
|
+
}) ) );
|
|
100
131
|
|
|
101
132
|
const content = response?.candidates?.[ 0 ]?.content?.parts?.[ 0 ]?.inlineData?.data;
|
|
102
133
|
|
|
@@ -143,14 +174,14 @@ const google = {
|
|
|
143
174
|
? { parts : [ ...images.map( ( image ) => ({ inlineData : { data : image.data, mimeType : image.mimeType } }) ), { text : prompt.join( '\n' ) } ] }
|
|
144
175
|
: prompt.join( '\n' );
|
|
145
176
|
|
|
146
|
-
const response = await googleCircuit( () => google_client.models.generateContent({
|
|
177
|
+
const response = await googleCircuit( () => withRetries( () => google_client.models.generateContent({
|
|
147
178
|
config : {
|
|
148
179
|
...config,
|
|
149
180
|
responseModalities : [ 'Text' ]
|
|
150
181
|
},
|
|
151
182
|
contents,
|
|
152
183
|
model
|
|
153
|
-
}) );
|
|
184
|
+
}) ) );
|
|
154
185
|
|
|
155
186
|
const content = response?.text;
|
|
156
187
|
|
package/dist/ai.d.ts
CHANGED
|
@@ -2,9 +2,11 @@ import { GoogleGenAI } from '@google/genai';
|
|
|
2
2
|
import { circuit } from './circuit.js';
|
|
3
3
|
import { ai } from './billing.js';
|
|
4
4
|
export { MARKUP } from './billing.js';
|
|
5
|
+
import { isTransientError } from './fetch.js';
|
|
5
6
|
import './transactions.js';
|
|
6
7
|
import '@drawbridge/drawbridge-telemetry';
|
|
7
8
|
import './usage.js';
|
|
9
|
+
import 'qs';
|
|
8
10
|
|
|
9
11
|
// AI request entry point. Wraps the Google GenAI client with a per-process
|
|
10
12
|
// circuit breaker, owns the per-model pricing table, and atomically debits
|
|
@@ -35,6 +37,35 @@ const googleCircuit = circuit({
|
|
|
35
37
|
timeout : 60000
|
|
36
38
|
});
|
|
37
39
|
|
|
40
|
+
// Bounded retry for the raw model call. A single transient failure — undici
|
|
41
|
+
// "fetch failed" on a resource-starved pod, a 429, a 5xx — must not lose the
|
|
42
|
+
// generation step OR count toward the breaker: three of those trip it and
|
|
43
|
+
// hard-fail every AI call for the next 60s. Runs INSIDE the googleCircuit
|
|
44
|
+
// callback so the breaker only counts calls that failed after retries were
|
|
45
|
+
// exhausted, and an OPEN circuit still fails fast without retrying.
|
|
46
|
+
// isTransientError treats status-less errors as transient, which is correct
|
|
47
|
+
// here: the wrapped fn is exactly one SDK call, and the GenAI SDK reports
|
|
48
|
+
// network-level failures as plain Errors with no `.status`.
|
|
49
|
+
const withRetries = async ( fn, { attempts = 3, delay = 500 } = {} ) => {
|
|
50
|
+
|
|
51
|
+
for( let attempt = 1; ; attempt++ ){
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
|
|
55
|
+
return await fn();
|
|
56
|
+
|
|
57
|
+
} catch ( error ) {
|
|
58
|
+
|
|
59
|
+
if( attempt >= attempts || ! isTransientError( error ) ) throw error;
|
|
60
|
+
|
|
61
|
+
await new Promise( ( resolve ) => setTimeout( resolve, delay * attempt ) );
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
};
|
|
68
|
+
|
|
38
69
|
// gemini-2.5-flash was retired by Google (generateContent → 404 "no longer
|
|
39
70
|
// available"), taking down every generation. gemini-3.5-flash is the current
|
|
40
71
|
// stable flash successor. The image model is a distinct model and still live,
|
|
@@ -89,14 +120,14 @@ const google = {
|
|
|
89
120
|
{ text : prompt.join( '\n' ) }
|
|
90
121
|
];
|
|
91
122
|
|
|
92
|
-
const response = await googleCircuit( () => google_client.models.generateContent({
|
|
123
|
+
const response = await googleCircuit( () => withRetries( () => google_client.models.generateContent({
|
|
93
124
|
model,
|
|
94
125
|
contents : { parts },
|
|
95
126
|
config : {
|
|
96
127
|
...config,
|
|
97
128
|
responseModalities : [ 'Image' ]
|
|
98
129
|
}
|
|
99
|
-
}) );
|
|
130
|
+
}) ) );
|
|
100
131
|
|
|
101
132
|
const content = response?.candidates?.[ 0 ]?.content?.parts?.[ 0 ]?.inlineData?.data;
|
|
102
133
|
|
|
@@ -143,14 +174,14 @@ const google = {
|
|
|
143
174
|
? { parts : [ ...images.map( ( image ) => ({ inlineData : { data : image.data, mimeType : image.mimeType } }) ), { text : prompt.join( '\n' ) } ] }
|
|
144
175
|
: prompt.join( '\n' );
|
|
145
176
|
|
|
146
|
-
const response = await googleCircuit( () => google_client.models.generateContent({
|
|
177
|
+
const response = await googleCircuit( () => withRetries( () => google_client.models.generateContent({
|
|
147
178
|
config : {
|
|
148
179
|
...config,
|
|
149
180
|
responseModalities : [ 'Text' ]
|
|
150
181
|
},
|
|
151
182
|
contents,
|
|
152
183
|
model
|
|
153
|
-
}) );
|
|
184
|
+
}) ) );
|
|
154
185
|
|
|
155
186
|
const content = response?.text;
|
|
156
187
|
|
package/dist/ai.js
CHANGED
|
@@ -67,9 +67,6 @@ var insertTransaction = async ({
|
|
|
67
67
|
session,
|
|
68
68
|
...rest
|
|
69
69
|
}) => {
|
|
70
|
-
var _a;
|
|
71
|
-
const beforeRaw = (_a = user == null ? void 0 : user.balance) == null ? void 0 : _a[type];
|
|
72
|
-
const balance = typeof beforeRaw === "number" ? { before: beforeRaw, after: beforeRaw + amount } : void 0;
|
|
73
70
|
const trace = currentTraceId();
|
|
74
71
|
await db.create({
|
|
75
72
|
authenticated: user,
|
|
@@ -81,7 +78,6 @@ var insertTransaction = async ({
|
|
|
81
78
|
category,
|
|
82
79
|
source,
|
|
83
80
|
amount,
|
|
84
|
-
...balance && { balance },
|
|
85
81
|
...trace && { trace },
|
|
86
82
|
...rest,
|
|
87
83
|
...stripeInvoiceId && { stripeInvoiceId },
|
|
@@ -227,6 +223,13 @@ var ai = {
|
|
|
227
223
|
bill: ({ db, user, model, usage, tools }) => (user == null ? void 0 : user.id) ? () => billRequest({ db, user, model, usage, tools }) : () => Promise.resolve()
|
|
228
224
|
};
|
|
229
225
|
|
|
226
|
+
// lib/fetch.js
|
|
227
|
+
import qs from "qs";
|
|
228
|
+
var isTransientError = (error) => {
|
|
229
|
+
const status = error == null ? void 0 : error.status;
|
|
230
|
+
return !status || status === 429 || status >= 500;
|
|
231
|
+
};
|
|
232
|
+
|
|
230
233
|
// lib/ai.js
|
|
231
234
|
var google_client = new GoogleGenAI({
|
|
232
235
|
apiKey: process.env.GOOGLE_GEMINI_API_KEY
|
|
@@ -236,6 +239,16 @@ var googleCircuit = circuit({
|
|
|
236
239
|
threshold: 3,
|
|
237
240
|
timeout: 6e4
|
|
238
241
|
});
|
|
242
|
+
var withRetries = async (fn, { attempts = 3, delay = 500 } = {}) => {
|
|
243
|
+
for (let attempt = 1; ; attempt++) {
|
|
244
|
+
try {
|
|
245
|
+
return await fn();
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (attempt >= attempts || !isTransientError(error)) throw error;
|
|
248
|
+
await new Promise((resolve) => setTimeout(resolve, delay * attempt));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
239
252
|
var models = {
|
|
240
253
|
image: "gemini-2.5-flash-image",
|
|
241
254
|
text: "gemini-3.5-flash"
|
|
@@ -269,14 +282,14 @@ var google = {
|
|
|
269
282
|
...refs.map((ref) => ({ inlineData: { data: ref.data, mimeType: ref.mimeType } })),
|
|
270
283
|
{ text: prompt.join("\n") }
|
|
271
284
|
];
|
|
272
|
-
const response = await googleCircuit(() => google_client.models.generateContent({
|
|
285
|
+
const response = await googleCircuit(() => withRetries(() => google_client.models.generateContent({
|
|
273
286
|
model,
|
|
274
287
|
contents: { parts },
|
|
275
288
|
config: {
|
|
276
289
|
...config,
|
|
277
290
|
responseModalities: ["Image"]
|
|
278
291
|
}
|
|
279
|
-
}));
|
|
292
|
+
})));
|
|
280
293
|
const content = (_f = (_e = (_d = (_c = (_b = (_a = response == null ? void 0 : response.candidates) == null ? void 0 : _a[0]) == null ? void 0 : _b.content) == null ? void 0 : _c.parts) == null ? void 0 : _d[0]) == null ? void 0 : _e.inlineData) == null ? void 0 : _f.data;
|
|
281
294
|
if (!content) {
|
|
282
295
|
throw new Error("Could not generate from prompt. Please try again with a different prompt.");
|
|
@@ -297,14 +310,14 @@ var google = {
|
|
|
297
310
|
}) => {
|
|
298
311
|
const model = models.text;
|
|
299
312
|
const contents = parts ? { parts } : images.length ? { parts: [...images.map((image) => ({ inlineData: { data: image.data, mimeType: image.mimeType } })), { text: prompt.join("\n") }] } : prompt.join("\n");
|
|
300
|
-
const response = await googleCircuit(() => google_client.models.generateContent({
|
|
313
|
+
const response = await googleCircuit(() => withRetries(() => google_client.models.generateContent({
|
|
301
314
|
config: {
|
|
302
315
|
...config,
|
|
303
316
|
responseModalities: ["Text"]
|
|
304
317
|
},
|
|
305
318
|
contents,
|
|
306
319
|
model
|
|
307
|
-
}));
|
|
320
|
+
})));
|
|
308
321
|
const content = response == null ? void 0 : response.text;
|
|
309
322
|
if (!content) {
|
|
310
323
|
throw new Error("Could not generate from prompt. Please try again with a different prompt.");
|
package/dist/billing.cjs
CHANGED
|
@@ -22,6 +22,7 @@ __export(billing_exports, {
|
|
|
22
22
|
MARKUP: () => MARKUP,
|
|
23
23
|
action: () => action,
|
|
24
24
|
ai: () => ai,
|
|
25
|
+
balance: () => balance,
|
|
25
26
|
grant: () => grant,
|
|
26
27
|
scrape: () => scrape
|
|
27
28
|
});
|
|
@@ -42,9 +43,6 @@ var insertTransaction = async ({
|
|
|
42
43
|
session,
|
|
43
44
|
...rest
|
|
44
45
|
}) => {
|
|
45
|
-
var _a;
|
|
46
|
-
const beforeRaw = (_a = user == null ? void 0 : user.balance) == null ? void 0 : _a[type];
|
|
47
|
-
const balance = typeof beforeRaw === "number" ? { before: beforeRaw, after: beforeRaw + amount } : void 0;
|
|
48
46
|
const trace = (0, import_drawbridge_telemetry.currentTraceId)();
|
|
49
47
|
await db.create({
|
|
50
48
|
authenticated: user,
|
|
@@ -56,7 +54,6 @@ var insertTransaction = async ({
|
|
|
56
54
|
category,
|
|
57
55
|
source,
|
|
58
56
|
amount,
|
|
59
|
-
...balance && { balance },
|
|
60
57
|
...trace && { trace },
|
|
61
58
|
...rest,
|
|
62
59
|
...stripeInvoiceId && { stripeInvoiceId },
|
|
@@ -341,6 +338,28 @@ var scrape = {
|
|
|
341
338
|
}
|
|
342
339
|
}
|
|
343
340
|
};
|
|
341
|
+
var balance = async ({ db, user, type }) => {
|
|
342
|
+
const userId = user && user.id || user;
|
|
343
|
+
if (!db || !userId || !type) return 0;
|
|
344
|
+
const [checkpoint] = await db.aggregate({
|
|
345
|
+
collection: "balanceCheckpoint",
|
|
346
|
+
pipeline: [
|
|
347
|
+
{ $match: { user: userId, type } },
|
|
348
|
+
{ $sort: { _id: -1 } },
|
|
349
|
+
{ $limit: 1 }
|
|
350
|
+
]
|
|
351
|
+
}).catch(() => []);
|
|
352
|
+
const match = { user: userId, type };
|
|
353
|
+
if (checkpoint == null ? void 0 : checkpoint.throughId) match._id = { $gt: checkpoint.throughId };
|
|
354
|
+
const [summed] = await db.aggregate({
|
|
355
|
+
collection: "transaction",
|
|
356
|
+
pipeline: [
|
|
357
|
+
{ $match: match },
|
|
358
|
+
{ $group: { _id: null, total: { $sum: "$amount" } } }
|
|
359
|
+
]
|
|
360
|
+
});
|
|
361
|
+
return ((checkpoint == null ? void 0 : checkpoint.value) || 0) + ((summed == null ? void 0 : summed.total) || 0);
|
|
362
|
+
};
|
|
344
363
|
var grant = {
|
|
345
364
|
// System-issued credit (signup welcome, migration catch-up). Pass `session`
|
|
346
365
|
// to enroll the credit in the caller's transaction.
|
|
@@ -471,6 +490,7 @@ var action = {
|
|
|
471
490
|
MARKUP,
|
|
472
491
|
action,
|
|
473
492
|
ai,
|
|
493
|
+
balance,
|
|
474
494
|
grant,
|
|
475
495
|
scrape
|
|
476
496
|
});
|
package/dist/billing.d.cts
CHANGED
|
@@ -318,10 +318,58 @@ const scrape = {
|
|
|
318
318
|
|
|
319
319
|
};
|
|
320
320
|
|
|
321
|
+
// ─── Balance (derived from the ledger) ──────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
// The AI credit balance is DERIVED, never stored — so it cannot drift from the
|
|
324
|
+
// ledger. balance = the newest checkpoint's cumulative value + Σ amount of the
|
|
325
|
+
// rows after it (credits +, debits −); with no checkpoint it sums the whole
|
|
326
|
+
// ledger. Checkpoints (`balanceCheckpoint`) are immutable rollup rows written by
|
|
327
|
+
// drawbridge-sync's nightly job to bound the scan on long-lived accounts — this
|
|
328
|
+
// reader tolerates their absence and falls back to the full sum. The
|
|
329
|
+
// { user:1, type:1, _id:1, amount:1 } index on `transaction` keeps both the full
|
|
330
|
+
// and post-checkpoint sums index-only.
|
|
331
|
+
//
|
|
332
|
+
// `user` accepts an id string or a user object (uses `.id`). `db` is any
|
|
333
|
+
// @drawbridge/mongodb controller (uses `.aggregate`). Returns integer cents;
|
|
334
|
+
// MAY be negative when a generation over-spent past zero — callers clamp the
|
|
335
|
+
// DISPLAY to 0, while the credit gate treats `<= 0` as "no credits".
|
|
336
|
+
const balance = async ({ db, user, type }) => {
|
|
337
|
+
|
|
338
|
+
const userId = ( user && user.id ) || user;
|
|
339
|
+
|
|
340
|
+
if( ! db || ! userId || ! type ) return 0;
|
|
341
|
+
|
|
342
|
+
// Newest immutable checkpoint: its `value` is the cumulative balance through
|
|
343
|
+
// `throughId`, so we only sum rows inserted after it. Missing collection / no
|
|
344
|
+
// checkpoint yet → [] and we sum the whole ledger.
|
|
345
|
+
const [ checkpoint ] = await db.aggregate({
|
|
346
|
+
collection : 'balanceCheckpoint',
|
|
347
|
+
pipeline : [
|
|
348
|
+
{ $match : { user : userId, type } },
|
|
349
|
+
{ $sort : { _id : -1 } },
|
|
350
|
+
{ $limit : 1 }
|
|
351
|
+
]
|
|
352
|
+
}).catch( () => [] );
|
|
353
|
+
|
|
354
|
+
const match = { user : userId, type };
|
|
355
|
+
if( checkpoint?.throughId ) match._id = { $gt : checkpoint.throughId };
|
|
356
|
+
|
|
357
|
+
const [ summed ] = await db.aggregate({
|
|
358
|
+
collection : 'transaction',
|
|
359
|
+
pipeline : [
|
|
360
|
+
{ $match : match },
|
|
361
|
+
{ $group : { _id : null, total : { $sum : '$amount' } } }
|
|
362
|
+
]
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
return ( checkpoint?.value || 0 ) + ( summed?.total || 0 );
|
|
366
|
+
|
|
367
|
+
};
|
|
368
|
+
|
|
321
369
|
// ─── Grants (credit rail) ──────────────────────────────────────────────────────
|
|
322
370
|
|
|
323
|
-
// Promotional credits to a user's AI balance
|
|
324
|
-
//
|
|
371
|
+
// Promotional credits to a user's AI balance — append-only rows the derived
|
|
372
|
+
// `balance` sums. `source` captures who acted.
|
|
325
373
|
const grant = {
|
|
326
374
|
|
|
327
375
|
// System-issued credit (signup welcome, migration catch-up). Pass `session`
|
|
@@ -497,4 +545,4 @@ const action = {
|
|
|
497
545
|
|
|
498
546
|
};
|
|
499
547
|
|
|
500
|
-
export { MARKUP, action, ai, grant, scrape };
|
|
548
|
+
export { MARKUP, action, ai, balance, grant, scrape };
|
package/dist/billing.d.ts
CHANGED
|
@@ -318,10 +318,58 @@ const scrape = {
|
|
|
318
318
|
|
|
319
319
|
};
|
|
320
320
|
|
|
321
|
+
// ─── Balance (derived from the ledger) ──────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
// The AI credit balance is DERIVED, never stored — so it cannot drift from the
|
|
324
|
+
// ledger. balance = the newest checkpoint's cumulative value + Σ amount of the
|
|
325
|
+
// rows after it (credits +, debits −); with no checkpoint it sums the whole
|
|
326
|
+
// ledger. Checkpoints (`balanceCheckpoint`) are immutable rollup rows written by
|
|
327
|
+
// drawbridge-sync's nightly job to bound the scan on long-lived accounts — this
|
|
328
|
+
// reader tolerates their absence and falls back to the full sum. The
|
|
329
|
+
// { user:1, type:1, _id:1, amount:1 } index on `transaction` keeps both the full
|
|
330
|
+
// and post-checkpoint sums index-only.
|
|
331
|
+
//
|
|
332
|
+
// `user` accepts an id string or a user object (uses `.id`). `db` is any
|
|
333
|
+
// @drawbridge/mongodb controller (uses `.aggregate`). Returns integer cents;
|
|
334
|
+
// MAY be negative when a generation over-spent past zero — callers clamp the
|
|
335
|
+
// DISPLAY to 0, while the credit gate treats `<= 0` as "no credits".
|
|
336
|
+
const balance = async ({ db, user, type }) => {
|
|
337
|
+
|
|
338
|
+
const userId = ( user && user.id ) || user;
|
|
339
|
+
|
|
340
|
+
if( ! db || ! userId || ! type ) return 0;
|
|
341
|
+
|
|
342
|
+
// Newest immutable checkpoint: its `value` is the cumulative balance through
|
|
343
|
+
// `throughId`, so we only sum rows inserted after it. Missing collection / no
|
|
344
|
+
// checkpoint yet → [] and we sum the whole ledger.
|
|
345
|
+
const [ checkpoint ] = await db.aggregate({
|
|
346
|
+
collection : 'balanceCheckpoint',
|
|
347
|
+
pipeline : [
|
|
348
|
+
{ $match : { user : userId, type } },
|
|
349
|
+
{ $sort : { _id : -1 } },
|
|
350
|
+
{ $limit : 1 }
|
|
351
|
+
]
|
|
352
|
+
}).catch( () => [] );
|
|
353
|
+
|
|
354
|
+
const match = { user : userId, type };
|
|
355
|
+
if( checkpoint?.throughId ) match._id = { $gt : checkpoint.throughId };
|
|
356
|
+
|
|
357
|
+
const [ summed ] = await db.aggregate({
|
|
358
|
+
collection : 'transaction',
|
|
359
|
+
pipeline : [
|
|
360
|
+
{ $match : match },
|
|
361
|
+
{ $group : { _id : null, total : { $sum : '$amount' } } }
|
|
362
|
+
]
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
return ( checkpoint?.value || 0 ) + ( summed?.total || 0 );
|
|
366
|
+
|
|
367
|
+
};
|
|
368
|
+
|
|
321
369
|
// ─── Grants (credit rail) ──────────────────────────────────────────────────────
|
|
322
370
|
|
|
323
|
-
// Promotional credits to a user's AI balance
|
|
324
|
-
//
|
|
371
|
+
// Promotional credits to a user's AI balance — append-only rows the derived
|
|
372
|
+
// `balance` sums. `source` captures who acted.
|
|
325
373
|
const grant = {
|
|
326
374
|
|
|
327
375
|
// System-issued credit (signup welcome, migration catch-up). Pass `session`
|
|
@@ -497,4 +545,4 @@ const action = {
|
|
|
497
545
|
|
|
498
546
|
};
|
|
499
547
|
|
|
500
|
-
export { MARKUP, action, ai, grant, scrape };
|
|
548
|
+
export { MARKUP, action, ai, balance, grant, scrape };
|
package/dist/billing.js
CHANGED
|
@@ -13,9 +13,6 @@ var insertTransaction = async ({
|
|
|
13
13
|
session,
|
|
14
14
|
...rest
|
|
15
15
|
}) => {
|
|
16
|
-
var _a;
|
|
17
|
-
const beforeRaw = (_a = user == null ? void 0 : user.balance) == null ? void 0 : _a[type];
|
|
18
|
-
const balance = typeof beforeRaw === "number" ? { before: beforeRaw, after: beforeRaw + amount } : void 0;
|
|
19
16
|
const trace = currentTraceId();
|
|
20
17
|
await db.create({
|
|
21
18
|
authenticated: user,
|
|
@@ -27,7 +24,6 @@ var insertTransaction = async ({
|
|
|
27
24
|
category,
|
|
28
25
|
source,
|
|
29
26
|
amount,
|
|
30
|
-
...balance && { balance },
|
|
31
27
|
...trace && { trace },
|
|
32
28
|
...rest,
|
|
33
29
|
...stripeInvoiceId && { stripeInvoiceId },
|
|
@@ -312,6 +308,28 @@ var scrape = {
|
|
|
312
308
|
}
|
|
313
309
|
}
|
|
314
310
|
};
|
|
311
|
+
var balance = async ({ db, user, type }) => {
|
|
312
|
+
const userId = user && user.id || user;
|
|
313
|
+
if (!db || !userId || !type) return 0;
|
|
314
|
+
const [checkpoint] = await db.aggregate({
|
|
315
|
+
collection: "balanceCheckpoint",
|
|
316
|
+
pipeline: [
|
|
317
|
+
{ $match: { user: userId, type } },
|
|
318
|
+
{ $sort: { _id: -1 } },
|
|
319
|
+
{ $limit: 1 }
|
|
320
|
+
]
|
|
321
|
+
}).catch(() => []);
|
|
322
|
+
const match = { user: userId, type };
|
|
323
|
+
if (checkpoint == null ? void 0 : checkpoint.throughId) match._id = { $gt: checkpoint.throughId };
|
|
324
|
+
const [summed] = await db.aggregate({
|
|
325
|
+
collection: "transaction",
|
|
326
|
+
pipeline: [
|
|
327
|
+
{ $match: match },
|
|
328
|
+
{ $group: { _id: null, total: { $sum: "$amount" } } }
|
|
329
|
+
]
|
|
330
|
+
});
|
|
331
|
+
return ((checkpoint == null ? void 0 : checkpoint.value) || 0) + ((summed == null ? void 0 : summed.total) || 0);
|
|
332
|
+
};
|
|
315
333
|
var grant = {
|
|
316
334
|
// System-issued credit (signup welcome, migration catch-up). Pass `session`
|
|
317
335
|
// to enroll the credit in the caller's transaction.
|
|
@@ -441,6 +459,7 @@ export {
|
|
|
441
459
|
MARKUP,
|
|
442
460
|
action,
|
|
443
461
|
ai,
|
|
462
|
+
balance,
|
|
444
463
|
grant,
|
|
445
464
|
scrape
|
|
446
465
|
};
|
package/dist/transactions.cjs
CHANGED
|
@@ -37,9 +37,6 @@ var insertTransaction = async ({
|
|
|
37
37
|
session,
|
|
38
38
|
...rest
|
|
39
39
|
}) => {
|
|
40
|
-
var _a;
|
|
41
|
-
const beforeRaw = (_a = user == null ? void 0 : user.balance) == null ? void 0 : _a[type];
|
|
42
|
-
const balance = typeof beforeRaw === "number" ? { before: beforeRaw, after: beforeRaw + amount } : void 0;
|
|
43
40
|
const trace = (0, import_drawbridge_telemetry.currentTraceId)();
|
|
44
41
|
await db.create({
|
|
45
42
|
authenticated: user,
|
|
@@ -51,7 +48,6 @@ var insertTransaction = async ({
|
|
|
51
48
|
category,
|
|
52
49
|
source,
|
|
53
50
|
amount,
|
|
54
|
-
...balance && { balance },
|
|
55
51
|
...trace && { trace },
|
|
56
52
|
...rest,
|
|
57
53
|
...stripeInvoiceId && { stripeInvoiceId },
|
package/dist/transactions.d.cts
CHANGED
|
@@ -2,12 +2,13 @@ import { currentTraceId } from '@drawbridge/drawbridge-telemetry';
|
|
|
2
2
|
|
|
3
3
|
// Ledger transaction insert helpers — no balance writes happen here.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// `
|
|
8
|
-
// (
|
|
9
|
-
//
|
|
10
|
-
//
|
|
5
|
+
// The `transaction` collection is the SINGLE SOURCE OF TRUTH for balance:
|
|
6
|
+
// there is no stored balance field anywhere. The balance is DERIVED by summing
|
|
7
|
+
// a user's rows on read (see `balance` in billing.js) — credits (+) and debits
|
|
8
|
+
// (−) — so it can never drift from the ledger. credit()/debit() only append
|
|
9
|
+
// rows; drawbridge-sync's stream/transaction.js reacts to each insert to fire
|
|
10
|
+
// low/depleted notifications and emit `account.update` (it no longer maintains
|
|
11
|
+
// a balance).
|
|
11
12
|
//
|
|
12
13
|
// import { credit, debit } from '@drawbridge/drawbridge-utils/transactions';
|
|
13
14
|
//
|
|
@@ -21,12 +22,9 @@ import { currentTraceId } from '@drawbridge/drawbridge-telemetry';
|
|
|
21
22
|
// `db` is the wrapper returned by @drawbridge/mongodb — credit/debit
|
|
22
23
|
// duck-type against its `.create` interface so this module has no hard dep.
|
|
23
24
|
//
|
|
24
|
-
// `user` is the user the transaction is being recorded against
|
|
25
|
-
//
|
|
26
|
-
// row
|
|
27
|
-
// for support / dispute resolution. When `balance.<type>` isn't on the user
|
|
28
|
-
// (fresh signup, webhook-initiated row), the snapshot is omitted, which is
|
|
29
|
-
// the honest representation of "no prior view to record".
|
|
25
|
+
// `user` is the user the transaction is being recorded against (stored as
|
|
26
|
+
// `user.id`). No per-row balance snapshot is stamped — the running balance at
|
|
27
|
+
// any row is recoverable by summing the ledger up to it.
|
|
30
28
|
|
|
31
29
|
const insertTransaction = async ({
|
|
32
30
|
db,
|
|
@@ -42,10 +40,10 @@ const insertTransaction = async ({
|
|
|
42
40
|
...rest
|
|
43
41
|
}) => {
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
// No balance snapshot is stamped: the balance is DERIVED from this ledger
|
|
44
|
+
// (billing.balance sums it on read), so the running balance at any row is
|
|
45
|
+
// recoverable by summing up to that row — a stored before/after would be
|
|
46
|
+
// redundant and, now that user.balance is gone, has no cheap source anyway.
|
|
49
47
|
|
|
50
48
|
// Pull the active Sentry trace ID so every credit/debit auto-correlates
|
|
51
49
|
// to the originating HTTP request or BullMQ job — useful for grouping
|
|
@@ -65,7 +63,6 @@ const insertTransaction = async ({
|
|
|
65
63
|
category,
|
|
66
64
|
source,
|
|
67
65
|
amount,
|
|
68
|
-
...( balance && { balance }),
|
|
69
66
|
...( trace && { trace }),
|
|
70
67
|
...rest,
|
|
71
68
|
...( stripeInvoiceId && { stripeInvoiceId }),
|
package/dist/transactions.d.ts
CHANGED
|
@@ -2,12 +2,13 @@ import { currentTraceId } from '@drawbridge/drawbridge-telemetry';
|
|
|
2
2
|
|
|
3
3
|
// Ledger transaction insert helpers — no balance writes happen here.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// `
|
|
8
|
-
// (
|
|
9
|
-
//
|
|
10
|
-
//
|
|
5
|
+
// The `transaction` collection is the SINGLE SOURCE OF TRUTH for balance:
|
|
6
|
+
// there is no stored balance field anywhere. The balance is DERIVED by summing
|
|
7
|
+
// a user's rows on read (see `balance` in billing.js) — credits (+) and debits
|
|
8
|
+
// (−) — so it can never drift from the ledger. credit()/debit() only append
|
|
9
|
+
// rows; drawbridge-sync's stream/transaction.js reacts to each insert to fire
|
|
10
|
+
// low/depleted notifications and emit `account.update` (it no longer maintains
|
|
11
|
+
// a balance).
|
|
11
12
|
//
|
|
12
13
|
// import { credit, debit } from '@drawbridge/drawbridge-utils/transactions';
|
|
13
14
|
//
|
|
@@ -21,12 +22,9 @@ import { currentTraceId } from '@drawbridge/drawbridge-telemetry';
|
|
|
21
22
|
// `db` is the wrapper returned by @drawbridge/mongodb — credit/debit
|
|
22
23
|
// duck-type against its `.create` interface so this module has no hard dep.
|
|
23
24
|
//
|
|
24
|
-
// `user` is the user the transaction is being recorded against
|
|
25
|
-
//
|
|
26
|
-
// row
|
|
27
|
-
// for support / dispute resolution. When `balance.<type>` isn't on the user
|
|
28
|
-
// (fresh signup, webhook-initiated row), the snapshot is omitted, which is
|
|
29
|
-
// the honest representation of "no prior view to record".
|
|
25
|
+
// `user` is the user the transaction is being recorded against (stored as
|
|
26
|
+
// `user.id`). No per-row balance snapshot is stamped — the running balance at
|
|
27
|
+
// any row is recoverable by summing the ledger up to it.
|
|
30
28
|
|
|
31
29
|
const insertTransaction = async ({
|
|
32
30
|
db,
|
|
@@ -42,10 +40,10 @@ const insertTransaction = async ({
|
|
|
42
40
|
...rest
|
|
43
41
|
}) => {
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
// No balance snapshot is stamped: the balance is DERIVED from this ledger
|
|
44
|
+
// (billing.balance sums it on read), so the running balance at any row is
|
|
45
|
+
// recoverable by summing up to that row — a stored before/after would be
|
|
46
|
+
// redundant and, now that user.balance is gone, has no cheap source anyway.
|
|
49
47
|
|
|
50
48
|
// Pull the active Sentry trace ID so every credit/debit auto-correlates
|
|
51
49
|
// to the originating HTTP request or BullMQ job — useful for grouping
|
|
@@ -65,7 +63,6 @@ const insertTransaction = async ({
|
|
|
65
63
|
category,
|
|
66
64
|
source,
|
|
67
65
|
amount,
|
|
68
|
-
...( balance && { balance }),
|
|
69
66
|
...( trace && { trace }),
|
|
70
67
|
...rest,
|
|
71
68
|
...( stripeInvoiceId && { stripeInvoiceId }),
|
package/dist/transactions.js
CHANGED
|
@@ -13,9 +13,6 @@ var insertTransaction = async ({
|
|
|
13
13
|
session,
|
|
14
14
|
...rest
|
|
15
15
|
}) => {
|
|
16
|
-
var _a;
|
|
17
|
-
const beforeRaw = (_a = user == null ? void 0 : user.balance) == null ? void 0 : _a[type];
|
|
18
|
-
const balance = typeof beforeRaw === "number" ? { before: beforeRaw, after: beforeRaw + amount } : void 0;
|
|
19
16
|
const trace = currentTraceId();
|
|
20
17
|
await db.create({
|
|
21
18
|
authenticated: user,
|
|
@@ -27,7 +24,6 @@ var insertTransaction = async ({
|
|
|
27
24
|
category,
|
|
28
25
|
source,
|
|
29
26
|
amount,
|
|
30
|
-
...balance && { balance },
|
|
31
27
|
...trace && { trace },
|
|
32
28
|
...rest,
|
|
33
29
|
...stripeInvoiceId && { stripeInvoiceId },
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"type": "module",
|
|
3
3
|
"dependencies": {
|
|
4
4
|
"@drawbridge/drawbridge-agents": "0.0.10",
|
|
5
|
-
"@drawbridge/drawbridge-telemetry": "0.0.
|
|
5
|
+
"@drawbridge/drawbridge-telemetry": "0.0.15",
|
|
6
6
|
"@google/genai": "1.30.0",
|
|
7
7
|
"axios": "1.16.0",
|
|
8
8
|
"currency-codes": "2.2.0",
|
|
@@ -164,5 +164,5 @@
|
|
|
164
164
|
"test": "node --test test/"
|
|
165
165
|
},
|
|
166
166
|
"types": "dist/index.d.ts",
|
|
167
|
-
"version": "0.0.
|
|
167
|
+
"version": "0.0.81"
|
|
168
168
|
}
|