@drawbridge/drawbridge-utils 0.0.27 → 0.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,187 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // transactions.js
20
+ var transactions_exports = {};
21
+ __export(transactions_exports, {
22
+ InsufficientCreditsError: () => InsufficientCreditsError,
23
+ credit: () => credit,
24
+ debit: () => debit
25
+ });
26
+ module.exports = __toCommonJS(transactions_exports);
27
+ var recordTransaction = async ({
28
+ db,
29
+ authenticated,
30
+ userId,
31
+ category,
32
+ tags,
33
+ amount,
34
+ balance,
35
+ provider,
36
+ stripeInvoiceId,
37
+ stripeEventId,
38
+ session
39
+ }) => {
40
+ await db.create({
41
+ authenticated,
42
+ collection: "transaction",
43
+ data: {
44
+ userId,
45
+ billable: {
46
+ item: "ai"
47
+ },
48
+ category,
49
+ tags,
50
+ amount,
51
+ balance,
52
+ ...provider && { provider },
53
+ ...stripeInvoiceId && { stripeInvoiceId },
54
+ ...stripeEventId && { stripeEventId }
55
+ },
56
+ ...session && { options: { session } }
57
+ });
58
+ };
59
+ var credit = async ({
60
+ db,
61
+ authenticated,
62
+ userId,
63
+ amount,
64
+ category,
65
+ tags = [],
66
+ stripeInvoiceId,
67
+ stripeEventId,
68
+ session
69
+ }) => {
70
+ var _a, _b, _c;
71
+ if (!Number.isInteger(amount) || amount <= 0) {
72
+ throw new Error(`credit() requires a positive integer amount, got ${amount}`);
73
+ }
74
+ const result = await db.update({
75
+ authenticated,
76
+ collection: "user",
77
+ query: {
78
+ id: userId
79
+ },
80
+ data: {
81
+ $inc: {
82
+ "balance.ai": amount
83
+ }
84
+ },
85
+ ...session && { options: { session } }
86
+ });
87
+ const after = ((_b = (_a = result == null ? void 0 : result.value) == null ? void 0 : _a.balance) == null ? void 0 : _b.ai) ?? ((_c = result == null ? void 0 : result.balance) == null ? void 0 : _c.ai);
88
+ if (typeof after !== "number") {
89
+ throw new Error(`credit() could not read updated balance for user ${userId}`);
90
+ }
91
+ const before = after - amount;
92
+ await recordTransaction({
93
+ db,
94
+ authenticated,
95
+ userId,
96
+ category,
97
+ tags,
98
+ amount,
99
+ balance: {
100
+ before,
101
+ after
102
+ },
103
+ stripeInvoiceId,
104
+ stripeEventId,
105
+ session
106
+ });
107
+ return {
108
+ balance: {
109
+ before,
110
+ after
111
+ }
112
+ };
113
+ };
114
+ var InsufficientCreditsError = class extends Error {
115
+ constructor(message = "Insufficient AI credits") {
116
+ super(message);
117
+ this.name = "InsufficientCreditsError";
118
+ this.status = 402;
119
+ }
120
+ };
121
+ var debit = async ({
122
+ db,
123
+ authenticated,
124
+ userId,
125
+ amount,
126
+ category,
127
+ tags = [],
128
+ provider,
129
+ stripeInvoiceId,
130
+ stripeEventId,
131
+ session
132
+ }) => {
133
+ var _a, _b, _c;
134
+ if (!Number.isInteger(amount) || amount <= 0) {
135
+ throw new Error(`debit() requires a positive integer amount, got ${amount}`);
136
+ }
137
+ const result = await db.update({
138
+ authenticated,
139
+ collection: "user",
140
+ query: {
141
+ id: userId,
142
+ "balance.ai": {
143
+ $gte: amount
144
+ }
145
+ },
146
+ data: {
147
+ $inc: {
148
+ "balance.ai": -amount
149
+ }
150
+ },
151
+ ...session && { options: { session } }
152
+ });
153
+ const after = ((_b = (_a = result == null ? void 0 : result.value) == null ? void 0 : _a.balance) == null ? void 0 : _b.ai) ?? ((_c = result == null ? void 0 : result.balance) == null ? void 0 : _c.ai);
154
+ if (typeof after !== "number") {
155
+ throw new InsufficientCreditsError();
156
+ }
157
+ const before = after + amount;
158
+ await recordTransaction({
159
+ db,
160
+ authenticated,
161
+ userId,
162
+ category,
163
+ tags,
164
+ amount: -amount,
165
+ balance: {
166
+ before,
167
+ after
168
+ },
169
+ provider,
170
+ stripeInvoiceId,
171
+ stripeEventId,
172
+ session
173
+ });
174
+ return {
175
+ amount: -amount,
176
+ balance: {
177
+ before,
178
+ after
179
+ }
180
+ };
181
+ };
182
+ // Annotate the CommonJS export names for ESM import in node:
183
+ 0 && (module.exports = {
184
+ InsufficientCreditsError,
185
+ credit,
186
+ debit
187
+ });
@@ -0,0 +1,223 @@
1
+ // Generic ledger primitives — authoritative balance for user.balance.ai lives
2
+ // in Mongo, and every change to that balance is recorded in the `transaction`
3
+ // collection. Domain-specific helpers (e.g., LLM pricing in ./ai.js) call into
4
+ // credit/debit with pre-computed amounts.
5
+ //
6
+ // import { credit, debit } from '@drawbridge/drawbridge-utils/transactions';
7
+ //
8
+ // await credit({ db, authenticated, userId, amount: 500,
9
+ // category: 'promotional', tags: [ 'welcome' ] });
10
+ //
11
+ // await debit({ db, authenticated, userId, amount: 9,
12
+ // category: 'usage', tags: [ 'google', 'gemini-2.5-flash' ],
13
+ // provider: { name, model, tokens, snapshot } });
14
+ //
15
+ // `db` is the wrapper returned by @drawbridge/mongodb — credit/debit duck-type
16
+ // against its `.create`, `.update` interface so this module has no hard dep.
17
+ //
18
+ // Both helpers accept an optional `session` so the caller can enroll them in
19
+ // a larger Mongo transaction. When session is omitted, the balance update +
20
+ // transaction insert run as two separate atomic ops (acceptable: balance is
21
+ // always correct, worst case is a missing ledger row on a Mongo blip between
22
+ // the two writes).
23
+
24
+ // Internal: write a transaction row. The user.balance.ai update happens in
25
+ // the callers (credit/debit) so we can keep the atomic gate-and-decrement
26
+ // contract.
27
+ const recordTransaction = async ({
28
+ db,
29
+ authenticated,
30
+ userId,
31
+ category,
32
+ tags,
33
+ amount,
34
+ balance,
35
+ provider,
36
+ stripeInvoiceId,
37
+ stripeEventId,
38
+ session
39
+ }) => {
40
+
41
+ await db.create({
42
+ authenticated,
43
+ collection : 'transaction',
44
+ data : {
45
+ userId,
46
+ billable : {
47
+ item : 'ai'
48
+ },
49
+ category,
50
+ tags,
51
+ amount,
52
+ balance,
53
+ ...( provider && { provider }),
54
+ ...( stripeInvoiceId && { stripeInvoiceId }),
55
+ ...( stripeEventId && { stripeEventId })
56
+ },
57
+ ...( session && { options : { session } })
58
+ });
59
+
60
+ };
61
+
62
+ // Additive: grant credits to a user. Used by welcome flow, top-up webhook,
63
+ // admin promo, refunds. If stripeEventId is provided, the unique sparse index
64
+ // on the transaction collection makes the insert idempotent against webhook
65
+ // retries — second call no-ops at the Mongo layer.
66
+ const credit = async ({
67
+ db,
68
+ authenticated,
69
+ userId,
70
+ amount,
71
+ category,
72
+ tags = [],
73
+ stripeInvoiceId,
74
+ stripeEventId,
75
+ session
76
+ }) => {
77
+
78
+ if( !Number.isInteger( amount ) || amount <= 0 ){
79
+
80
+ throw new Error( `credit() requires a positive integer amount, got ${ amount }` );
81
+
82
+ }
83
+
84
+ const result = await db.update({
85
+ authenticated,
86
+ collection : 'user',
87
+ query : {
88
+ id : userId
89
+ },
90
+ data : {
91
+ $inc : {
92
+ 'balance.ai' : amount
93
+ }
94
+ },
95
+ ...( session && { options : { session } })
96
+ });
97
+
98
+ const after = result?.value?.balance?.ai ?? result?.balance?.ai;
99
+
100
+ if( typeof after !== 'number' ){
101
+
102
+ throw new Error( `credit() could not read updated balance for user ${ userId }` );
103
+
104
+ }
105
+
106
+ const before = after - amount;
107
+
108
+ await recordTransaction({
109
+ db,
110
+ authenticated,
111
+ userId,
112
+ category,
113
+ tags,
114
+ amount,
115
+ balance : {
116
+ before,
117
+ after
118
+ },
119
+ stripeInvoiceId,
120
+ stripeEventId,
121
+ session
122
+ });
123
+
124
+ return {
125
+ balance : {
126
+ before,
127
+ after
128
+ }
129
+ };
130
+
131
+ };
132
+
133
+ // Subtractive: atomic gate-and-decrement against the user's balance. The
134
+ // $gte filter ensures concurrent calls can't drive balance negative — exactly
135
+ // one of N parallel requests with overlapping cost will succeed; the rest
136
+ // throw InsufficientCreditsError. Callers (e.g., ai.js) pre-compute `amount`
137
+ // from their domain-specific pricing.
138
+ class InsufficientCreditsError extends Error {
139
+
140
+ constructor( message = 'Insufficient AI credits' ){
141
+
142
+ super( message );
143
+ this.name = 'InsufficientCreditsError';
144
+ this.status = 402;
145
+
146
+ }
147
+
148
+ }
149
+
150
+ const debit = async ({
151
+ db,
152
+ authenticated,
153
+ userId,
154
+ amount,
155
+ category,
156
+ tags = [],
157
+ provider,
158
+ stripeInvoiceId,
159
+ stripeEventId,
160
+ session
161
+ }) => {
162
+
163
+ if( !Number.isInteger( amount ) || amount <= 0 ){
164
+
165
+ throw new Error( `debit() requires a positive integer amount, got ${ amount }` );
166
+
167
+ }
168
+
169
+ const result = await db.update({
170
+ authenticated,
171
+ collection : 'user',
172
+ query : {
173
+ id : userId,
174
+ 'balance.ai' : {
175
+ $gte : amount
176
+ }
177
+ },
178
+ data : {
179
+ $inc : {
180
+ 'balance.ai' : -amount
181
+ }
182
+ },
183
+ ...( session && { options : { session } })
184
+ });
185
+
186
+ const after = result?.value?.balance?.ai ?? result?.balance?.ai;
187
+
188
+ if( typeof after !== 'number' ){
189
+
190
+ throw new InsufficientCreditsError();
191
+
192
+ }
193
+
194
+ const before = after + amount;
195
+
196
+ await recordTransaction({
197
+ db,
198
+ authenticated,
199
+ userId,
200
+ category,
201
+ tags,
202
+ amount : -amount,
203
+ balance : {
204
+ before,
205
+ after
206
+ },
207
+ provider,
208
+ stripeInvoiceId,
209
+ stripeEventId,
210
+ session
211
+ });
212
+
213
+ return {
214
+ amount : -amount,
215
+ balance : {
216
+ before,
217
+ after
218
+ }
219
+ };
220
+
221
+ };
222
+
223
+ export { InsufficientCreditsError, credit, debit };
@@ -0,0 +1,223 @@
1
+ // Generic ledger primitives — authoritative balance for user.balance.ai lives
2
+ // in Mongo, and every change to that balance is recorded in the `transaction`
3
+ // collection. Domain-specific helpers (e.g., LLM pricing in ./ai.js) call into
4
+ // credit/debit with pre-computed amounts.
5
+ //
6
+ // import { credit, debit } from '@drawbridge/drawbridge-utils/transactions';
7
+ //
8
+ // await credit({ db, authenticated, userId, amount: 500,
9
+ // category: 'promotional', tags: [ 'welcome' ] });
10
+ //
11
+ // await debit({ db, authenticated, userId, amount: 9,
12
+ // category: 'usage', tags: [ 'google', 'gemini-2.5-flash' ],
13
+ // provider: { name, model, tokens, snapshot } });
14
+ //
15
+ // `db` is the wrapper returned by @drawbridge/mongodb — credit/debit duck-type
16
+ // against its `.create`, `.update` interface so this module has no hard dep.
17
+ //
18
+ // Both helpers accept an optional `session` so the caller can enroll them in
19
+ // a larger Mongo transaction. When session is omitted, the balance update +
20
+ // transaction insert run as two separate atomic ops (acceptable: balance is
21
+ // always correct, worst case is a missing ledger row on a Mongo blip between
22
+ // the two writes).
23
+
24
+ // Internal: write a transaction row. The user.balance.ai update happens in
25
+ // the callers (credit/debit) so we can keep the atomic gate-and-decrement
26
+ // contract.
27
+ const recordTransaction = async ({
28
+ db,
29
+ authenticated,
30
+ userId,
31
+ category,
32
+ tags,
33
+ amount,
34
+ balance,
35
+ provider,
36
+ stripeInvoiceId,
37
+ stripeEventId,
38
+ session
39
+ }) => {
40
+
41
+ await db.create({
42
+ authenticated,
43
+ collection : 'transaction',
44
+ data : {
45
+ userId,
46
+ billable : {
47
+ item : 'ai'
48
+ },
49
+ category,
50
+ tags,
51
+ amount,
52
+ balance,
53
+ ...( provider && { provider }),
54
+ ...( stripeInvoiceId && { stripeInvoiceId }),
55
+ ...( stripeEventId && { stripeEventId })
56
+ },
57
+ ...( session && { options : { session } })
58
+ });
59
+
60
+ };
61
+
62
+ // Additive: grant credits to a user. Used by welcome flow, top-up webhook,
63
+ // admin promo, refunds. If stripeEventId is provided, the unique sparse index
64
+ // on the transaction collection makes the insert idempotent against webhook
65
+ // retries — second call no-ops at the Mongo layer.
66
+ const credit = async ({
67
+ db,
68
+ authenticated,
69
+ userId,
70
+ amount,
71
+ category,
72
+ tags = [],
73
+ stripeInvoiceId,
74
+ stripeEventId,
75
+ session
76
+ }) => {
77
+
78
+ if( !Number.isInteger( amount ) || amount <= 0 ){
79
+
80
+ throw new Error( `credit() requires a positive integer amount, got ${ amount }` );
81
+
82
+ }
83
+
84
+ const result = await db.update({
85
+ authenticated,
86
+ collection : 'user',
87
+ query : {
88
+ id : userId
89
+ },
90
+ data : {
91
+ $inc : {
92
+ 'balance.ai' : amount
93
+ }
94
+ },
95
+ ...( session && { options : { session } })
96
+ });
97
+
98
+ const after = result?.value?.balance?.ai ?? result?.balance?.ai;
99
+
100
+ if( typeof after !== 'number' ){
101
+
102
+ throw new Error( `credit() could not read updated balance for user ${ userId }` );
103
+
104
+ }
105
+
106
+ const before = after - amount;
107
+
108
+ await recordTransaction({
109
+ db,
110
+ authenticated,
111
+ userId,
112
+ category,
113
+ tags,
114
+ amount,
115
+ balance : {
116
+ before,
117
+ after
118
+ },
119
+ stripeInvoiceId,
120
+ stripeEventId,
121
+ session
122
+ });
123
+
124
+ return {
125
+ balance : {
126
+ before,
127
+ after
128
+ }
129
+ };
130
+
131
+ };
132
+
133
+ // Subtractive: atomic gate-and-decrement against the user's balance. The
134
+ // $gte filter ensures concurrent calls can't drive balance negative — exactly
135
+ // one of N parallel requests with overlapping cost will succeed; the rest
136
+ // throw InsufficientCreditsError. Callers (e.g., ai.js) pre-compute `amount`
137
+ // from their domain-specific pricing.
138
+ class InsufficientCreditsError extends Error {
139
+
140
+ constructor( message = 'Insufficient AI credits' ){
141
+
142
+ super( message );
143
+ this.name = 'InsufficientCreditsError';
144
+ this.status = 402;
145
+
146
+ }
147
+
148
+ }
149
+
150
+ const debit = async ({
151
+ db,
152
+ authenticated,
153
+ userId,
154
+ amount,
155
+ category,
156
+ tags = [],
157
+ provider,
158
+ stripeInvoiceId,
159
+ stripeEventId,
160
+ session
161
+ }) => {
162
+
163
+ if( !Number.isInteger( amount ) || amount <= 0 ){
164
+
165
+ throw new Error( `debit() requires a positive integer amount, got ${ amount }` );
166
+
167
+ }
168
+
169
+ const result = await db.update({
170
+ authenticated,
171
+ collection : 'user',
172
+ query : {
173
+ id : userId,
174
+ 'balance.ai' : {
175
+ $gte : amount
176
+ }
177
+ },
178
+ data : {
179
+ $inc : {
180
+ 'balance.ai' : -amount
181
+ }
182
+ },
183
+ ...( session && { options : { session } })
184
+ });
185
+
186
+ const after = result?.value?.balance?.ai ?? result?.balance?.ai;
187
+
188
+ if( typeof after !== 'number' ){
189
+
190
+ throw new InsufficientCreditsError();
191
+
192
+ }
193
+
194
+ const before = after + amount;
195
+
196
+ await recordTransaction({
197
+ db,
198
+ authenticated,
199
+ userId,
200
+ category,
201
+ tags,
202
+ amount : -amount,
203
+ balance : {
204
+ before,
205
+ after
206
+ },
207
+ provider,
208
+ stripeInvoiceId,
209
+ stripeEventId,
210
+ session
211
+ });
212
+
213
+ return {
214
+ amount : -amount,
215
+ balance : {
216
+ before,
217
+ after
218
+ }
219
+ };
220
+
221
+ };
222
+
223
+ export { InsufficientCreditsError, credit, debit };
@@ -0,0 +1,10 @@
1
+ import {
2
+ InsufficientCreditsError,
3
+ credit,
4
+ debit
5
+ } from "./chunk-H735KDYS.js";
6
+ export {
7
+ InsufficientCreditsError,
8
+ credit,
9
+ debit
10
+ };
package/package.json CHANGED
@@ -2,6 +2,7 @@
2
2
  "type": "module",
3
3
  "dependencies": {
4
4
  "@drawbridge/drawbridge-agents": "0.0.10",
5
+ "@google/genai": "1.30.0",
5
6
  "axios": "1.16.0",
6
7
  "currency-codes": "2.2.0",
7
8
  "nanoid": "3.3.8",
@@ -16,6 +17,11 @@
16
17
  "import": "./dist/index.js",
17
18
  "require": "./dist/index.cjs"
18
19
  },
20
+ "./ai": {
21
+ "types": "./dist/ai.d.ts",
22
+ "import": "./dist/ai.js",
23
+ "require": "./dist/ai.cjs"
24
+ },
19
25
  "./encrypt": {
20
26
  "types": "./dist/encrypt.d.ts",
21
27
  "import": "./dist/encrypt.js",
@@ -75,6 +81,11 @@
75
81
  "types": "./dist/token.d.ts",
76
82
  "import": "./dist/token.js",
77
83
  "require": "./dist/token.cjs"
84
+ },
85
+ "./transactions": {
86
+ "types": "./dist/transactions.d.ts",
87
+ "import": "./dist/transactions.js",
88
+ "require": "./dist/transactions.cjs"
78
89
  }
79
90
  },
80
91
  "files": [
@@ -92,5 +103,5 @@
92
103
  "build": "tsup && npm publish"
93
104
  },
94
105
  "types": "dist/index.d.ts",
95
- "version": "0.0.27"
106
+ "version": "0.0.29"
96
107
  }