@drawbridge/drawbridge-utils 0.0.60 → 0.0.62

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 CHANGED
@@ -157,7 +157,7 @@ var debit = async ({
157
157
 
158
158
  // lib/ai.js
159
159
  var google_client = new import_genai.GoogleGenAI({
160
- apiKey: process.env.GOOGLE_API_KEY
160
+ apiKey: process.env.GOOGLE_GEMINI_API_KEY
161
161
  });
162
162
  var googleCircuit = circuit({
163
163
  name: "google-ai",
@@ -303,13 +303,14 @@ var google = {
303
303
  text: async ({
304
304
  config = {},
305
305
  images = [],
306
+ parts = null,
306
307
  prompt = []
307
308
  }, {
308
309
  db,
309
310
  user
310
311
  }) => {
311
312
  const model = models.text;
312
- const contents = images.length ? { parts: [...images.map((image) => ({ inlineData: { data: image.data, mimeType: image.mimeType } })), { text: prompt.join("\n") }] } : prompt.join("\n");
313
+ const contents = parts ? { parts } : images.length ? { parts: [...images.map((image) => ({ inlineData: { data: image.data, mimeType: image.mimeType } })), { text: prompt.join("\n") }] } : prompt.join("\n");
313
314
  const response = await googleCircuit(() => google_client.models.generateContent({
314
315
  config: {
315
316
  ...config,
package/dist/ai.d.cts CHANGED
@@ -23,7 +23,7 @@ import '@drawbridge/drawbridge-telemetry';
23
23
 
24
24
 
25
25
  const google_client = new GoogleGenAI({
26
- apiKey : process.env.GOOGLE_API_KEY
26
+ apiKey : process.env.GOOGLE_GEMINI_API_KEY
27
27
  });
28
28
 
29
29
  const googleCircuit = circuit({
@@ -266,6 +266,7 @@ const google = {
266
266
  {
267
267
  config = {},
268
268
  images = [],
269
+ parts = null,
269
270
  prompt = []
270
271
  },
271
272
  {
@@ -276,12 +277,18 @@ const google = {
276
277
 
277
278
  const model = models.text;
278
279
 
279
- // When images are supplied (e.g. a page screenshot), send a multimodal
280
- // parts payload so the model can SEE them alongside the text prompt;
281
- // otherwise keep the plain-string contents for text-only calls.
282
- const contents = images.length
283
- ? { parts : [ ...images.map( ( image ) => ({ inlineData : { data : image.data, mimeType : image.mimeType } }) ), { text : prompt.join( '\n' ) } ] }
284
- : prompt.join( '\n' );
280
+ // Three contents shapes, in priority order:
281
+ // 1. `parts` a caller-built array of interleaved text/inlineData parts,
282
+ // used verbatim. For calls where ordering matters (e.g. the hero-pick
283
+ // interleaves "Image N:" labels with each thumbnail so the model can
284
+ // map an index to an image); images/prompt can't express that.
285
+ // 2. `images` — a screenshot etc. sent multimodally before the prompt.
286
+ // 3. plain joined string for text-only calls.
287
+ const contents = parts
288
+ ? { parts }
289
+ : images.length
290
+ ? { parts : [ ...images.map( ( image ) => ({ inlineData : { data : image.data, mimeType : image.mimeType } }) ), { text : prompt.join( '\n' ) } ] }
291
+ : prompt.join( '\n' );
285
292
 
286
293
  const response = await googleCircuit( () => google_client.models.generateContent({
287
294
  config : {
package/dist/ai.d.ts CHANGED
@@ -23,7 +23,7 @@ import '@drawbridge/drawbridge-telemetry';
23
23
 
24
24
 
25
25
  const google_client = new GoogleGenAI({
26
- apiKey : process.env.GOOGLE_API_KEY
26
+ apiKey : process.env.GOOGLE_GEMINI_API_KEY
27
27
  });
28
28
 
29
29
  const googleCircuit = circuit({
@@ -266,6 +266,7 @@ const google = {
266
266
  {
267
267
  config = {},
268
268
  images = [],
269
+ parts = null,
269
270
  prompt = []
270
271
  },
271
272
  {
@@ -276,12 +277,18 @@ const google = {
276
277
 
277
278
  const model = models.text;
278
279
 
279
- // When images are supplied (e.g. a page screenshot), send a multimodal
280
- // parts payload so the model can SEE them alongside the text prompt;
281
- // otherwise keep the plain-string contents for text-only calls.
282
- const contents = images.length
283
- ? { parts : [ ...images.map( ( image ) => ({ inlineData : { data : image.data, mimeType : image.mimeType } }) ), { text : prompt.join( '\n' ) } ] }
284
- : prompt.join( '\n' );
280
+ // Three contents shapes, in priority order:
281
+ // 1. `parts` a caller-built array of interleaved text/inlineData parts,
282
+ // used verbatim. For calls where ordering matters (e.g. the hero-pick
283
+ // interleaves "Image N:" labels with each thumbnail so the model can
284
+ // map an index to an image); images/prompt can't express that.
285
+ // 2. `images` — a screenshot etc. sent multimodally before the prompt.
286
+ // 3. plain joined string for text-only calls.
287
+ const contents = parts
288
+ ? { parts }
289
+ : images.length
290
+ ? { parts : [ ...images.map( ( image ) => ({ inlineData : { data : image.data, mimeType : image.mimeType } }) ), { text : prompt.join( '\n' ) } ] }
291
+ : prompt.join( '\n' );
285
292
 
286
293
  const response = await googleCircuit( () => google_client.models.generateContent({
287
294
  config : {
package/dist/ai.js CHANGED
@@ -127,7 +127,7 @@ var debit = async ({
127
127
 
128
128
  // lib/ai.js
129
129
  var google_client = new GoogleGenAI({
130
- apiKey: process.env.GOOGLE_API_KEY
130
+ apiKey: process.env.GOOGLE_GEMINI_API_KEY
131
131
  });
132
132
  var googleCircuit = circuit({
133
133
  name: "google-ai",
@@ -273,13 +273,14 @@ var google = {
273
273
  text: async ({
274
274
  config = {},
275
275
  images = [],
276
+ parts = null,
276
277
  prompt = []
277
278
  }, {
278
279
  db,
279
280
  user
280
281
  }) => {
281
282
  const model = models.text;
282
- const contents = images.length ? { parts: [...images.map((image) => ({ inlineData: { data: image.data, mimeType: image.mimeType } })), { text: prompt.join("\n") }] } : prompt.join("\n");
283
+ const contents = parts ? { parts } : images.length ? { parts: [...images.map((image) => ({ inlineData: { data: image.data, mimeType: image.mimeType } })), { text: prompt.join("\n") }] } : prompt.join("\n");
283
284
  const response = await googleCircuit(() => google_client.models.generateContent({
284
285
  config: {
285
286
  ...config,
package/dist/index.cjs CHANGED
@@ -43,10 +43,12 @@ __export(index_exports, {
43
43
  gigabyte: () => gigabyte,
44
44
  incrementUsageTotals: () => incrementUsageTotals,
45
45
  infinite: () => infinite,
46
+ isDuplicateKey: () => isDuplicateKey,
46
47
  isInfinite: () => isInfinite,
47
48
  megabyte: () => megabyte,
48
49
  nanoid: () => nanoid,
49
50
  percentage: () => percentage,
51
+ recordAction: () => recordAction,
50
52
  reducers: () => reducers,
51
53
  regex: () => regex,
52
54
  shareUrls: () => shareUrls,
@@ -390,6 +392,30 @@ var incrementUsageTotals = async ({ controller, usageId, $inc, session }) => {
390
392
  query: { id: usageId }
391
393
  });
392
394
  };
395
+ var isDuplicateKey = (error) => (error == null ? void 0 : error.code) === 11e3 || (error == null ? void 0 : error.code) === "11000" || /E11000|duplicate key/i.test((error == null ? void 0 : error.message) || "");
396
+ var recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId }) => {
397
+ if (!controller || !organization || !usageId || !type || !key) return;
398
+ const amount = Number(units);
399
+ if (!Number.isFinite(amount) || amount === 0) return;
400
+ try {
401
+ await controller.create({
402
+ collection: "action",
403
+ data: {
404
+ organization,
405
+ usage: usageId,
406
+ type,
407
+ units: amount,
408
+ key,
409
+ meta,
410
+ ...traceId && { traceId }
411
+ }
412
+ });
413
+ } catch (error) {
414
+ if (isDuplicateKey(error)) return;
415
+ throw error;
416
+ }
417
+ await incrementUsageTotals({ controller, usageId, $inc: { "totals.actions": amount } });
418
+ };
393
419
  var percentage = (value1, value2, decimals = 1) => ((value1 || 0) / (value2 || 0) * 100 || 0).toFixed(decimals);
394
420
  var reducers = {
395
421
  fields: (data2 = [], callback = () => ({})) => data2.reduce(
@@ -483,10 +509,12 @@ var currency = (val) => (0, import_currency_codes.code)(val);
483
509
  gigabyte,
484
510
  incrementUsageTotals,
485
511
  infinite,
512
+ isDuplicateKey,
486
513
  isInfinite,
487
514
  megabyte,
488
515
  nanoid,
489
516
  percentage,
517
+ recordAction,
490
518
  reducers,
491
519
  regex,
492
520
  shareUrls,
package/dist/index.d.cts CHANGED
@@ -379,6 +379,63 @@ const incrementUsageTotals = async ({ controller, usageId, $inc, session }) => {
379
379
 
380
380
  };
381
381
 
382
+ // True for a MongoDB duplicate-key error (unique-index violation).
383
+ const isDuplicateKey = ( error ) =>
384
+ error?.code === 11000
385
+ || error?.code === '11000'
386
+ || /E11000|duplicate key/i.test( error?.message || '' );
387
+
388
+ // Record a billable action as a row in the append-only `action` ledger — the
389
+ // source of truth for usage billing (summed per period at changeover). The unique
390
+ // `key` makes recording idempotent: a duplicate (the segment realtime path AND the
391
+ // sweep firing for the same membership, or a job retry) hits the unique index and
392
+ // is skipped, so it can never double-count. On a genuinely new row we bump the
393
+ // display cache `usage.totals.actions` by the same units; the billed number always
394
+ // comes from summing the ledger, so a missed cache $inc is only cosmetic (it
395
+ // self-heals at changeover). No transaction needed — the unique index is the
396
+ // atomic dedup.
397
+ // key deterministic per source event, e.g. 'segment.join:<contact>:<segment>'
398
+ // units billable units (default 1; N for batch, e.g. accepted entries)
399
+ // meta attribution ids { campaign, page, submission, segment, workflow, step, ... }
400
+ const recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId }) => {
401
+
402
+ if( ! controller || ! organization || ! usageId || ! type || ! key ) return;
403
+
404
+ const amount = Number( units );
405
+
406
+ if( ! Number.isFinite( amount ) || amount === 0 ) return;
407
+
408
+ try {
409
+
410
+ await controller.create({
411
+ collection : 'action',
412
+ data : {
413
+ organization,
414
+ usage : usageId,
415
+ type,
416
+ units : amount,
417
+ key,
418
+ meta,
419
+ ...( traceId && { traceId })
420
+ }
421
+ });
422
+
423
+ } catch ( error ) {
424
+
425
+ // Duplicate key = this exact action was already recorded — skip silently.
426
+ // This is the dedup that makes billing drift-proof. Re-throw anything else.
427
+ if( isDuplicateKey( error ) ) return;
428
+
429
+ throw error;
430
+
431
+ }
432
+
433
+ // New row only — keep the display cache in step. Billing sums the ledger, so
434
+ // this is cosmetic and self-heals at changeover if it ever fails.
435
+ await incrementUsageTotals({ controller, usageId, $inc : { 'totals.actions' : amount } });
436
+
437
+ };
438
+
382
439
  const percentage = ( value1, value2, decimals = 1 ) => ( ( ( ( value1 || 0 ) / ( value2 || 0 ) ) * 100 ) || 0 ).toFixed( decimals );
383
440
 
384
441
  const reducers = {
@@ -477,4 +534,4 @@ const currencies = data.map( ( item ) => ({
477
534
 
478
535
  const currency = ( val ) => code( val );
479
536
 
480
- export { bytesToGB, bytesToMB, capitalize, constants, currencies, currency, expiredPaymentMethod, formatCurrency, formatDateString, formatNumber, getPlanFeature, gigabyte, incrementUsageTotals, infinite, isInfinite, megabyte, nanoid, percentage, reducers, regex, shareUrls, urlRoot };
537
+ export { bytesToGB, bytesToMB, capitalize, constants, currencies, currency, expiredPaymentMethod, formatCurrency, formatDateString, formatNumber, getPlanFeature, gigabyte, incrementUsageTotals, infinite, isDuplicateKey, isInfinite, megabyte, nanoid, percentage, recordAction, reducers, regex, shareUrls, urlRoot };
package/dist/index.d.ts CHANGED
@@ -379,6 +379,63 @@ const incrementUsageTotals = async ({ controller, usageId, $inc, session }) => {
379
379
 
380
380
  };
381
381
 
382
+ // True for a MongoDB duplicate-key error (unique-index violation).
383
+ const isDuplicateKey = ( error ) =>
384
+ error?.code === 11000
385
+ || error?.code === '11000'
386
+ || /E11000|duplicate key/i.test( error?.message || '' );
387
+
388
+ // Record a billable action as a row in the append-only `action` ledger — the
389
+ // source of truth for usage billing (summed per period at changeover). The unique
390
+ // `key` makes recording idempotent: a duplicate (the segment realtime path AND the
391
+ // sweep firing for the same membership, or a job retry) hits the unique index and
392
+ // is skipped, so it can never double-count. On a genuinely new row we bump the
393
+ // display cache `usage.totals.actions` by the same units; the billed number always
394
+ // comes from summing the ledger, so a missed cache $inc is only cosmetic (it
395
+ // self-heals at changeover). No transaction needed — the unique index is the
396
+ // atomic dedup.
397
+ // key deterministic per source event, e.g. 'segment.join:<contact>:<segment>'
398
+ // units billable units (default 1; N for batch, e.g. accepted entries)
399
+ // meta attribution ids { campaign, page, submission, segment, workflow, step, ... }
400
+ const recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId }) => {
401
+
402
+ if( ! controller || ! organization || ! usageId || ! type || ! key ) return;
403
+
404
+ const amount = Number( units );
405
+
406
+ if( ! Number.isFinite( amount ) || amount === 0 ) return;
407
+
408
+ try {
409
+
410
+ await controller.create({
411
+ collection : 'action',
412
+ data : {
413
+ organization,
414
+ usage : usageId,
415
+ type,
416
+ units : amount,
417
+ key,
418
+ meta,
419
+ ...( traceId && { traceId })
420
+ }
421
+ });
422
+
423
+ } catch ( error ) {
424
+
425
+ // Duplicate key = this exact action was already recorded — skip silently.
426
+ // This is the dedup that makes billing drift-proof. Re-throw anything else.
427
+ if( isDuplicateKey( error ) ) return;
428
+
429
+ throw error;
430
+
431
+ }
432
+
433
+ // New row only — keep the display cache in step. Billing sums the ledger, so
434
+ // this is cosmetic and self-heals at changeover if it ever fails.
435
+ await incrementUsageTotals({ controller, usageId, $inc : { 'totals.actions' : amount } });
436
+
437
+ };
438
+
382
439
  const percentage = ( value1, value2, decimals = 1 ) => ( ( ( ( value1 || 0 ) / ( value2 || 0 ) ) * 100 ) || 0 ).toFixed( decimals );
383
440
 
384
441
  const reducers = {
@@ -477,4 +534,4 @@ const currencies = data.map( ( item ) => ({
477
534
 
478
535
  const currency = ( val ) => code( val );
479
536
 
480
- export { bytesToGB, bytesToMB, capitalize, constants, currencies, currency, expiredPaymentMethod, formatCurrency, formatDateString, formatNumber, getPlanFeature, gigabyte, incrementUsageTotals, infinite, isInfinite, megabyte, nanoid, percentage, reducers, regex, shareUrls, urlRoot };
537
+ export { bytesToGB, bytesToMB, capitalize, constants, currencies, currency, expiredPaymentMethod, formatCurrency, formatDateString, formatNumber, getPlanFeature, gigabyte, incrementUsageTotals, infinite, isDuplicateKey, isInfinite, megabyte, nanoid, percentage, recordAction, reducers, regex, shareUrls, urlRoot };
package/dist/index.js CHANGED
@@ -336,6 +336,30 @@ var incrementUsageTotals = async ({ controller, usageId, $inc, session }) => {
336
336
  query: { id: usageId }
337
337
  });
338
338
  };
339
+ var isDuplicateKey = (error) => (error == null ? void 0 : error.code) === 11e3 || (error == null ? void 0 : error.code) === "11000" || /E11000|duplicate key/i.test((error == null ? void 0 : error.message) || "");
340
+ var recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId }) => {
341
+ if (!controller || !organization || !usageId || !type || !key) return;
342
+ const amount = Number(units);
343
+ if (!Number.isFinite(amount) || amount === 0) return;
344
+ try {
345
+ await controller.create({
346
+ collection: "action",
347
+ data: {
348
+ organization,
349
+ usage: usageId,
350
+ type,
351
+ units: amount,
352
+ key,
353
+ meta,
354
+ ...traceId && { traceId }
355
+ }
356
+ });
357
+ } catch (error) {
358
+ if (isDuplicateKey(error)) return;
359
+ throw error;
360
+ }
361
+ await incrementUsageTotals({ controller, usageId, $inc: { "totals.actions": amount } });
362
+ };
339
363
  var percentage = (value1, value2, decimals = 1) => ((value1 || 0) / (value2 || 0) * 100 || 0).toFixed(decimals);
340
364
  var reducers = {
341
365
  fields: (data2 = [], callback = () => ({})) => data2.reduce(
@@ -428,10 +452,12 @@ export {
428
452
  gigabyte,
429
453
  incrementUsageTotals,
430
454
  infinite,
455
+ isDuplicateKey,
431
456
  isInfinite,
432
457
  megabyte,
433
458
  nanoid,
434
459
  percentage,
460
+ recordAction,
435
461
  reducers,
436
462
  regex,
437
463
  shareUrls,
package/package.json CHANGED
@@ -17,9 +17,6 @@
17
17
  "tsup": "8.5.1",
18
18
  "typescript": "5.9.3"
19
19
  },
20
- "overrides": {
21
- "esbuild": "0.28.1"
22
- },
23
20
  "peerDependencies": {
24
21
  "@node-oauth/oauth2-server": "^5.3.0"
25
22
  },
@@ -160,5 +157,5 @@
160
157
  "build": "tsup && npm publish"
161
158
  },
162
159
  "types": "dist/index.d.ts",
163
- "version": "0.0.60"
160
+ "version": "0.0.62"
164
161
  }