@sudobility/consumables_service 0.0.2

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/CLAUDE.md ADDED
@@ -0,0 +1,92 @@
1
+ # Consumables Service
2
+
3
+ Shared backend library for consumable credits management with Drizzle ORM.
4
+
5
+ **npm**: `@sudobility/consumables_service` (public)
6
+
7
+ ## Tech Stack
8
+
9
+ - **Language**: TypeScript (strict mode)
10
+ - **Runtime**: Bun
11
+ - **Package Manager**: Bun (do not use npm/yarn/pnpm for installing dependencies)
12
+ - **Build**: TypeScript compiler (ESM)
13
+ - **Test**: vitest
14
+
15
+ ## Project Structure
16
+
17
+ ```
18
+ src/
19
+ ├── index.ts # Main exports
20
+ ├── types/
21
+ │ └── index.ts # All type definitions
22
+ ├── schema/
23
+ │ └── index.ts # Drizzle schema creator
24
+ └── helpers/
25
+ ├── index.ts # Helper re-exports
26
+ ├── ConsumablesHelper.ts # Core business logic
27
+ └── WebhookHelper.ts # RevenueCat webhook validation
28
+ tests/
29
+ ├── ConsumablesHelper.test.ts
30
+ └── WebhookHelper.test.ts
31
+ ```
32
+
33
+ ## Commands
34
+
35
+ ```bash
36
+ bun run build # Build ESM
37
+ bun run clean # Remove dist/
38
+ bun run dev # Watch mode
39
+ bun test # Run tests
40
+ bun run lint # Run ESLint
41
+ bun run typecheck # TypeScript check
42
+ bun run verify # All checks + build (use before commit)
43
+ ```
44
+
45
+ ## Key Concepts
46
+
47
+ ### Schema Creator
48
+
49
+ `createConsumablesSchema(pgSchema)` creates three tables within a given Drizzle PgSchema:
50
+ - `consumable_balances` — user balance with stored credit count
51
+ - `consumable_purchases` — purchase audit trail (source, transaction ref, price)
52
+ - `consumable_usages` — usage audit trail (filename, timestamp)
53
+
54
+ The consuming API passes its own schema so migrations stay in one place.
55
+
56
+ ### ConsumablesHelper
57
+
58
+ Core business logic class. Constructed with `(db, tables, config)`:
59
+ - `getBalance(userId)` — get-or-create; auto-grants free credits on first access
60
+ - `recordPurchase(userId, request)` — insert purchase + atomic balance increment
61
+ - `recordUsage(userId, filename?)` — atomic decrement with balance > 0 guard
62
+ - `getPurchaseHistory/getUsageHistory` — paginated audit trail queries
63
+ - `recordPurchaseFromWebhook(...)` — idempotent webhook processing
64
+
65
+ ### WebhookHelper
66
+
67
+ - `validateWebhookSignature(rawBody, signature, secret)` — HMAC-SHA256 validation
68
+ - `parseConsumablePurchaseEvent(event)` — extract purchase data from RevenueCat webhook
69
+
70
+ ## Usage
71
+
72
+ ```typescript
73
+ import {
74
+ createConsumablesSchema,
75
+ ConsumablesHelper,
76
+ } from "@sudobility/consumables_service";
77
+
78
+ // In your API's schema file:
79
+ const tables = createConsumablesSchema(mySchema);
80
+
81
+ // In your API's service file:
82
+ const helper = new ConsumablesHelper(db, tables, { initialFreeCredits: 3 });
83
+
84
+ const balance = await helper.getBalance(userId);
85
+ const updated = await helper.recordPurchase(userId, { credits: 25, source: "web", ... });
86
+ const result = await helper.recordUsage(userId, "logo.svg");
87
+ ```
88
+
89
+ ## Consuming APIs
90
+
91
+ APIs using this library:
92
+ - svgr_api
@@ -0,0 +1,27 @@
1
+ import type { ConsumablesSchemaResult } from "../schema";
2
+ import type { BalanceResponse, PurchaseRequest, UseResponse, ConsumablePurchase, ConsumableUsage, ConsumablesConfig, ConsumableSource } from "../types";
3
+ export declare class ConsumablesHelper {
4
+ private db;
5
+ private tables;
6
+ private config;
7
+ constructor(db: any, tables: ConsumablesSchemaResult, config: ConsumablesConfig);
8
+ /** Get or create balance record. Auto-grants free credits on first access. */
9
+ getBalance(userId: string): Promise<BalanceResponse>;
10
+ /** Record a purchase. Atomically adds credits to balance. */
11
+ recordPurchase(userId: string, request: PurchaseRequest): Promise<BalanceResponse>;
12
+ /** Record a usage. Atomically deducts 1 credit. Returns error if insufficient. */
13
+ recordUsage(userId: string, filename?: string): Promise<UseResponse>;
14
+ /** Get purchase history (most recent first). */
15
+ getPurchaseHistory(userId: string, limit?: number, offset?: number): Promise<ConsumablePurchase[]>;
16
+ /** Get usage history (most recent first). */
17
+ getUsageHistory(userId: string, limit?: number, offset?: number): Promise<ConsumableUsage[]>;
18
+ /**
19
+ * Idempotent purchase recording from webhook.
20
+ * Checks if transaction_ref_id already exists to prevent duplicates.
21
+ */
22
+ recordPurchaseFromWebhook(userId: string, transactionId: string, credits: number, source: ConsumableSource, productId: string, priceCents: number, currency: string): Promise<{
23
+ alreadyProcessed: boolean;
24
+ balance: number;
25
+ }>;
26
+ }
27
+ //# sourceMappingURL=ConsumablesHelper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConsumablesHelper.d.ts","sourceRoot":"","sources":["../../src/helpers/ConsumablesHelper.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AACzD,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,WAAW,EACX,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EACjB,MAAM,UAAU,CAAC;AAElB,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,EAAE,CAAM;IAChB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,MAAM,CAAoB;gBAGhC,EAAE,EAAE,GAAG,EACP,MAAM,EAAE,uBAAuB,EAC/B,MAAM,EAAE,iBAAiB;IAO3B,8EAA8E;IACxE,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAkC1D,6DAA6D;IACvD,cAAc,CAClB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,eAAe,CAAC;IAsC3B,kFAAkF;IAC5E,WAAW,CACf,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,WAAW,CAAC;IA8BvB,gDAAgD;IAC1C,kBAAkB,CACtB,MAAM,EAAE,MAAM,EACd,KAAK,SAAK,EACV,MAAM,SAAI,GACT,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAWhC,6CAA6C;IACvC,eAAe,CACnB,MAAM,EAAE,MAAM,EACd,KAAK,SAAK,EACV,MAAM,SAAI,GACT,OAAO,CAAC,eAAe,EAAE,CAAC;IAW7B;;;OAGG;IACG,yBAAyB,CAC7B,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,gBAAgB,EACxB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,gBAAgB,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAyB3D"}
@@ -0,0 +1,142 @@
1
+ import { eq, desc, sql } from "drizzle-orm";
2
+ export class ConsumablesHelper {
3
+ constructor(db, tables, config) {
4
+ this.db = db;
5
+ this.tables = tables;
6
+ this.config = config;
7
+ }
8
+ /** Get or create balance record. Auto-grants free credits on first access. */
9
+ async getBalance(userId) {
10
+ const { consumableBalances } = this.tables;
11
+ const existing = await this.db
12
+ .select()
13
+ .from(consumableBalances)
14
+ .where(eq(consumableBalances.user_id, userId));
15
+ if (existing.length > 0) {
16
+ return {
17
+ balance: existing[0].balance,
18
+ initial_credits: existing[0].initial_credits,
19
+ };
20
+ }
21
+ // First access — create with free credits
22
+ const freeCredits = this.config.initialFreeCredits;
23
+ await this.db.insert(consumableBalances).values({
24
+ user_id: userId,
25
+ balance: freeCredits,
26
+ initial_credits: freeCredits,
27
+ });
28
+ // Record the free credits as a purchase audit entry
29
+ if (freeCredits > 0) {
30
+ await this.db.insert(this.tables.consumablePurchases).values({
31
+ user_id: userId,
32
+ credits: freeCredits,
33
+ source: "free",
34
+ });
35
+ }
36
+ return { balance: freeCredits, initial_credits: freeCredits };
37
+ }
38
+ /** Record a purchase. Atomically adds credits to balance. */
39
+ async recordPurchase(userId, request) {
40
+ const { consumableBalances, consumablePurchases } = this.tables;
41
+ // Ensure balance record exists (idempotent)
42
+ await this.getBalance(userId);
43
+ // Insert purchase record
44
+ await this.db.insert(consumablePurchases).values({
45
+ user_id: userId,
46
+ credits: request.credits,
47
+ source: request.source,
48
+ transaction_ref_id: request.transaction_ref_id ?? null,
49
+ product_id: request.product_id ?? null,
50
+ price_cents: request.price_cents ?? null,
51
+ currency: request.currency ?? null,
52
+ });
53
+ // Atomically increment balance
54
+ await this.db
55
+ .update(consumableBalances)
56
+ .set({
57
+ balance: sql `${consumableBalances.balance} + ${request.credits}`,
58
+ updated_at: new Date(),
59
+ })
60
+ .where(eq(consumableBalances.user_id, userId));
61
+ // Return updated balance
62
+ const updated = await this.db
63
+ .select()
64
+ .from(consumableBalances)
65
+ .where(eq(consumableBalances.user_id, userId));
66
+ return {
67
+ balance: updated[0].balance,
68
+ initial_credits: updated[0].initial_credits,
69
+ };
70
+ }
71
+ /** Record a usage. Atomically deducts 1 credit. Returns error if insufficient. */
72
+ async recordUsage(userId, filename) {
73
+ const { consumableBalances, consumableUsages } = this.tables;
74
+ // Atomically decrement (with guard against going negative)
75
+ const result = await this.db
76
+ .update(consumableBalances)
77
+ .set({
78
+ balance: sql `${consumableBalances.balance} - 1`,
79
+ updated_at: new Date(),
80
+ })
81
+ .where(sql `${consumableBalances.user_id} = ${userId} AND ${consumableBalances.balance} > 0`)
82
+ .returning({ balance: consumableBalances.balance });
83
+ if (result.length === 0) {
84
+ // Either user doesn't exist or balance is 0
85
+ const current = await this.getBalance(userId);
86
+ return { balance: current.balance, success: false };
87
+ }
88
+ // Record usage
89
+ await this.db.insert(consumableUsages).values({
90
+ user_id: userId,
91
+ filename: filename ?? null,
92
+ });
93
+ return { balance: result[0].balance, success: true };
94
+ }
95
+ /** Get purchase history (most recent first). */
96
+ async getPurchaseHistory(userId, limit = 50, offset = 0) {
97
+ const { consumablePurchases } = this.tables;
98
+ return this.db
99
+ .select()
100
+ .from(consumablePurchases)
101
+ .where(eq(consumablePurchases.user_id, userId))
102
+ .orderBy(desc(consumablePurchases.created_at))
103
+ .limit(limit)
104
+ .offset(offset);
105
+ }
106
+ /** Get usage history (most recent first). */
107
+ async getUsageHistory(userId, limit = 50, offset = 0) {
108
+ const { consumableUsages } = this.tables;
109
+ return this.db
110
+ .select()
111
+ .from(consumableUsages)
112
+ .where(eq(consumableUsages.user_id, userId))
113
+ .orderBy(desc(consumableUsages.created_at))
114
+ .limit(limit)
115
+ .offset(offset);
116
+ }
117
+ /**
118
+ * Idempotent purchase recording from webhook.
119
+ * Checks if transaction_ref_id already exists to prevent duplicates.
120
+ */
121
+ async recordPurchaseFromWebhook(userId, transactionId, credits, source, productId, priceCents, currency) {
122
+ // Check for duplicate
123
+ const existing = await this.db
124
+ .select()
125
+ .from(this.tables.consumablePurchases)
126
+ .where(eq(this.tables.consumablePurchases.transaction_ref_id, transactionId));
127
+ if (existing.length > 0) {
128
+ const bal = await this.getBalance(userId);
129
+ return { alreadyProcessed: true, balance: bal.balance };
130
+ }
131
+ const result = await this.recordPurchase(userId, {
132
+ credits,
133
+ source,
134
+ transaction_ref_id: transactionId,
135
+ product_id: productId,
136
+ price_cents: priceCents,
137
+ currency,
138
+ });
139
+ return { alreadyProcessed: false, balance: result.balance };
140
+ }
141
+ }
142
+ //# sourceMappingURL=ConsumablesHelper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConsumablesHelper.js","sourceRoot":"","sources":["../../src/helpers/ConsumablesHelper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAY5C,MAAM,OAAO,iBAAiB;IAK5B,YACE,EAAO,EACP,MAA+B,EAC/B,MAAyB;QAEzB,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,8EAA8E;IAC9E,KAAK,CAAC,UAAU,CAAC,MAAc;QAC7B,MAAM,EAAE,kBAAkB,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC;QAC3C,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,EAAE;aAC3B,MAAM,EAAE;aACR,IAAI,CAAC,kBAAkB,CAAC;aACxB,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;QAEjD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,OAAO;gBACL,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO;gBAC5B,eAAe,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,eAAe;aAC7C,CAAC;QACJ,CAAC;QAED,0CAA0C;QAC1C,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,kBAAkB,CAAC;QACnD,MAAM,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,MAAM,CAAC;YAC9C,OAAO,EAAE,MAAM;YACf,OAAO,EAAE,WAAW;YACpB,eAAe,EAAE,WAAW;SAC7B,CAAC,CAAC;QAEH,oDAAoD;QACpD,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;YACpB,MAAM,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC,MAAM,CAAC;gBAC3D,OAAO,EAAE,MAAM;gBACf,OAAO,EAAE,WAAW;gBACpB,MAAM,EAAE,MAA0B;aACnC,CAAC,CAAC;QACL,CAAC;QAED,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,WAAW,EAAE,CAAC;IAChE,CAAC;IAED,6DAA6D;IAC7D,KAAK,CAAC,cAAc,CAClB,MAAc,EACd,OAAwB;QAExB,MAAM,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC;QAEhE,4CAA4C;QAC5C,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QAE9B,yBAAyB;QACzB,MAAM,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC,MAAM,CAAC;YAC/C,OAAO,EAAE,MAAM;YACf,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,kBAAkB,EAAE,OAAO,CAAC,kBAAkB,IAAI,IAAI;YACtD,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,IAAI;YACtC,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,IAAI;YACxC,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,IAAI;SACnC,CAAC,CAAC;QAEH,+BAA+B;QAC/B,MAAM,IAAI,CAAC,EAAE;aACV,MAAM,CAAC,kBAAkB,CAAC;aAC1B,GAAG,CAAC;YACH,OAAO,EAAE,GAAG,CAAA,GAAG,kBAAkB,CAAC,OAAO,MAAM,OAAO,CAAC,OAAO,EAAE;YAChE,UAAU,EAAE,IAAI,IAAI,EAAE;SACvB,CAAC;aACD,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;QAEjD,yBAAyB;QACzB,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,EAAE;aAC1B,MAAM,EAAE;aACR,IAAI,CAAC,kBAAkB,CAAC;aACxB,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;QAEjD,OAAO;YACL,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO;YAC3B,eAAe,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,eAAe;SAC5C,CAAC;IACJ,CAAC;IAED,kFAAkF;IAClF,KAAK,CAAC,WAAW,CACf,MAAc,EACd,QAAiB;QAEjB,MAAM,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC;QAE7D,2DAA2D;QAC3D,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,EAAE;aACzB,MAAM,CAAC,kBAAkB,CAAC;aAC1B,GAAG,CAAC;YACH,OAAO,EAAE,GAAG,CAAA,GAAG,kBAAkB,CAAC,OAAO,MAAM;YAC/C,UAAU,EAAE,IAAI,IAAI,EAAE;SACvB,CAAC;aACD,KAAK,CACJ,GAAG,CAAA,GAAG,kBAAkB,CAAC,OAAO,MAAM,MAAM,QAAQ,kBAAkB,CAAC,OAAO,MAAM,CACrF;aACA,SAAS,CAAC,EAAE,OAAO,EAAE,kBAAkB,CAAC,OAAO,EAAE,CAAC,CAAC;QAEtD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,4CAA4C;YAC5C,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;YAC9C,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QACtD,CAAC;QAED,eAAe;QACf,MAAM,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,MAAM,CAAC;YAC5C,OAAO,EAAE,MAAM;YACf,QAAQ,EAAE,QAAQ,IAAI,IAAI;SAC3B,CAAC,CAAC;QAEH,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IACvD,CAAC;IAED,gDAAgD;IAChD,KAAK,CAAC,kBAAkB,CACtB,MAAc,EACd,KAAK,GAAG,EAAE,EACV,MAAM,GAAG,CAAC;QAEV,MAAM,EAAE,mBAAmB,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC;QAC5C,OAAO,IAAI,CAAC,EAAE;aACX,MAAM,EAAE;aACR,IAAI,CAAC,mBAAmB,CAAC;aACzB,KAAK,CAAC,EAAE,CAAC,mBAAmB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;aAC9C,OAAO,CAAC,IAAI,CAAC,mBAAmB,CAAC,UAAU,CAAC,CAAC;aAC7C,KAAK,CAAC,KAAK,CAAC;aACZ,MAAM,CAAC,MAAM,CAAC,CAAC;IACpB,CAAC;IAED,6CAA6C;IAC7C,KAAK,CAAC,eAAe,CACnB,MAAc,EACd,KAAK,GAAG,EAAE,EACV,MAAM,GAAG,CAAC;QAEV,MAAM,EAAE,gBAAgB,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC;QACzC,OAAO,IAAI,CAAC,EAAE;aACX,MAAM,EAAE;aACR,IAAI,CAAC,gBAAgB,CAAC;aACtB,KAAK,CAAC,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;aAC3C,OAAO,CAAC,IAAI,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC;aAC1C,KAAK,CAAC,KAAK,CAAC;aACZ,MAAM,CAAC,MAAM,CAAC,CAAC;IACpB,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,yBAAyB,CAC7B,MAAc,EACd,aAAqB,EACrB,OAAe,EACf,MAAwB,EACxB,SAAiB,EACjB,UAAkB,EAClB,QAAgB;QAEhB,sBAAsB;QACtB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,EAAE;aAC3B,MAAM,EAAE;aACR,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,mBAAmB,CAAC;aACrC,KAAK,CACJ,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,mBAAmB,CAAC,kBAAkB,EAAE,aAAa,CAAC,CACtE,CAAC;QAEJ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;YAC1C,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC;QAC1D,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE;YAC/C,OAAO;YACP,MAAM;YACN,kBAAkB,EAAE,aAAa;YACjC,UAAU,EAAE,SAAS;YACrB,WAAW,EAAE,UAAU;YACvB,QAAQ;SACT,CAAC,CAAC;QAEH,OAAO,EAAE,gBAAgB,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC;IAC9D,CAAC;CACF"}
@@ -0,0 +1,18 @@
1
+ import type { RevenueCatWebhookEvent } from "../types";
2
+ /**
3
+ * Validates RevenueCat webhook HMAC signature.
4
+ */
5
+ export declare function validateWebhookSignature(rawBody: string, signature: string, secret: string): boolean;
6
+ /**
7
+ * Parse the webhook event and extract purchase data.
8
+ * Returns null if the event type is not a consumable purchase.
9
+ */
10
+ export declare function parseConsumablePurchaseEvent(event: RevenueCatWebhookEvent): {
11
+ userId: string;
12
+ transactionId: string;
13
+ productId: string;
14
+ priceCents: number;
15
+ currency: string;
16
+ store: string;
17
+ } | null;
18
+ //# sourceMappingURL=WebhookHelper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WebhookHelper.d.ts","sourceRoot":"","sources":["../../src/helpers/WebhookHelper.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAEvD;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAKT;AAQD;;;GAGG;AACH,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,sBAAsB,GAC5B;IACD,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,IAAI,CAeP"}
@@ -0,0 +1,34 @@
1
+ import { createHmac } from "crypto";
2
+ /**
3
+ * Validates RevenueCat webhook HMAC signature.
4
+ */
5
+ export function validateWebhookSignature(rawBody, signature, secret) {
6
+ const hmac = createHmac("sha256", secret);
7
+ hmac.update(rawBody);
8
+ const expected = hmac.digest("hex");
9
+ return signature === expected;
10
+ }
11
+ const STORE_TO_SOURCE = {
12
+ STRIPE: "web",
13
+ APP_STORE: "apple",
14
+ PLAY_STORE: "google",
15
+ };
16
+ /**
17
+ * Parse the webhook event and extract purchase data.
18
+ * Returns null if the event type is not a consumable purchase.
19
+ */
20
+ export function parseConsumablePurchaseEvent(event) {
21
+ const validTypes = ["NON_RENEWING_PURCHASE", "INITIAL_PURCHASE"];
22
+ if (!validTypes.includes(event.event.type)) {
23
+ return null;
24
+ }
25
+ return {
26
+ userId: event.event.app_user_id,
27
+ transactionId: event.event.transaction_id,
28
+ productId: event.event.product_id,
29
+ priceCents: Math.round(event.event.price_in_purchased_currency * 100),
30
+ currency: event.event.currency,
31
+ store: STORE_TO_SOURCE[event.event.store] ?? event.event.store,
32
+ };
33
+ }
34
+ //# sourceMappingURL=WebhookHelper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WebhookHelper.js","sourceRoot":"","sources":["../../src/helpers/WebhookHelper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAGpC;;GAEG;AACH,MAAM,UAAU,wBAAwB,CACtC,OAAe,EACf,SAAiB,EACjB,MAAc;IAEd,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC1C,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACrB,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACpC,OAAO,SAAS,KAAK,QAAQ,CAAC;AAChC,CAAC;AAED,MAAM,eAAe,GAA2B;IAC9C,MAAM,EAAE,KAAK;IACb,SAAS,EAAE,OAAO;IAClB,UAAU,EAAE,QAAQ;CACrB,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,4BAA4B,CAC1C,KAA6B;IAS7B,MAAM,UAAU,GAAG,CAAC,uBAAuB,EAAE,kBAAkB,CAAC,CAAC;IAEjE,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,WAAW;QAC/B,aAAa,EAAE,KAAK,CAAC,KAAK,CAAC,cAAc;QACzC,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,UAAU;QACjC,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,2BAA2B,GAAG,GAAG,CAAC;QACrE,QAAQ,EAAE,KAAK,CAAC,KAAK,CAAC,QAAQ;QAC9B,KAAK,EAAE,eAAe,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,KAAK;KAC/D,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { ConsumablesHelper } from "./ConsumablesHelper";
2
+ export { validateWebhookSignature, parseConsumablePurchaseEvent, } from "./WebhookHelper";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/helpers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EACL,wBAAwB,EACxB,4BAA4B,GAC7B,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { ConsumablesHelper } from "./ConsumablesHelper";
2
+ export { validateWebhookSignature, parseConsumablePurchaseEvent, } from "./WebhookHelper";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/helpers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EACL,wBAAwB,EACxB,4BAA4B,GAC7B,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,5 @@
1
+ export type { ConsumableSource, ConsumableBalanceResponse, ConsumablePurchaseRequest, ConsumableUseRequest, ConsumableUseResponse, ConsumablePurchaseRecord, ConsumableUsageRecord, BalanceResponse, PurchaseRequest, UseRequest, UseResponse, } from "./types";
2
+ export type { ConsumableBalance, ConsumablePurchase, ConsumableUsage, RevenueCatWebhookEvent, ConsumablesConfig, } from "./types";
3
+ export { createConsumablesSchema, type ConsumablesSchemaResult, } from "./schema";
4
+ export { ConsumablesHelper, validateWebhookSignature, parseConsumablePurchaseEvent, } from "./helpers";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,gBAAgB,EAChB,yBAAyB,EACzB,yBAAyB,EACzB,oBAAoB,EACpB,qBAAqB,EACrB,wBAAwB,EACxB,qBAAqB,EAErB,eAAe,EACf,eAAe,EACf,UAAU,EACV,WAAW,GACZ,MAAM,SAAS,CAAC;AAGjB,YAAY,EACV,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,sBAAsB,EACtB,iBAAiB,GAClB,MAAM,SAAS,CAAC;AAGjB,OAAO,EACL,uBAAuB,EACvB,KAAK,uBAAuB,GAC7B,MAAM,UAAU,CAAC;AAGlB,OAAO,EACL,iBAAiB,EACjB,wBAAwB,EACxB,4BAA4B,GAC7B,MAAM,WAAW,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ // Schema
2
+ export { createConsumablesSchema, } from "./schema";
3
+ // Helpers
4
+ export { ConsumablesHelper, validateWebhookSignature, parseConsumablePurchaseEvent, } from "./helpers";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAyBA,SAAS;AACT,OAAO,EACL,uBAAuB,GAExB,MAAM,UAAU,CAAC;AAElB,UAAU;AACV,OAAO,EACL,iBAAiB,EACjB,wBAAwB,EACxB,4BAA4B,GAC7B,MAAM,WAAW,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Create consumable tables within a given Drizzle PgSchema.
3
+ * The consuming API passes its own schema so migrations stay in one place.
4
+ *
5
+ * Uses `any` for schema param to avoid drizzle-orm version coupling
6
+ * between this library and the consuming API.
7
+ */
8
+ export declare function createConsumablesSchema(schema: any): {
9
+ consumableBalances: any;
10
+ consumablePurchases: any;
11
+ consumableUsages: any;
12
+ };
13
+ export type ConsumablesSchemaResult = ReturnType<typeof createConsumablesSchema>;
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schema/index.ts"],"names":[],"mappings":"AAOA;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,GAAG;;;;EA6BlD;AAED,MAAM,MAAM,uBAAuB,GAAG,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAC"}
@@ -0,0 +1,36 @@
1
+ import { varchar, timestamp, serial, integer, } from "drizzle-orm/pg-core";
2
+ /**
3
+ * Create consumable tables within a given Drizzle PgSchema.
4
+ * The consuming API passes its own schema so migrations stay in one place.
5
+ *
6
+ * Uses `any` for schema param to avoid drizzle-orm version coupling
7
+ * between this library and the consuming API.
8
+ */
9
+ export function createConsumablesSchema(schema) {
10
+ const consumableBalances = schema.table("consumable_balances", {
11
+ user_id: varchar("user_id", { length: 128 }).primaryKey(),
12
+ balance: integer("balance").notNull().default(0),
13
+ initial_credits: integer("initial_credits").notNull().default(0),
14
+ created_at: timestamp("created_at").defaultNow().notNull(),
15
+ updated_at: timestamp("updated_at").defaultNow().notNull(),
16
+ });
17
+ const consumablePurchases = schema.table("consumable_purchases", {
18
+ id: serial("id").primaryKey(),
19
+ user_id: varchar("user_id", { length: 128 }).notNull(),
20
+ credits: integer("credits").notNull(),
21
+ source: varchar("source", { length: 20 }).notNull(),
22
+ transaction_ref_id: varchar("transaction_ref_id", { length: 255 }),
23
+ product_id: varchar("product_id", { length: 255 }),
24
+ price_cents: integer("price_cents"),
25
+ currency: varchar("currency", { length: 10 }),
26
+ created_at: timestamp("created_at").defaultNow().notNull(),
27
+ });
28
+ const consumableUsages = schema.table("consumable_usages", {
29
+ id: serial("id").primaryKey(),
30
+ user_id: varchar("user_id", { length: 128 }).notNull(),
31
+ filename: varchar("filename", { length: 500 }),
32
+ created_at: timestamp("created_at").defaultNow().notNull(),
33
+ });
34
+ return { consumableBalances, consumablePurchases, consumableUsages };
35
+ }
36
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/schema/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,SAAS,EACT,MAAM,EACN,OAAO,GACR,MAAM,qBAAqB,CAAC;AAE7B;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAW;IACjD,MAAM,kBAAkB,GAAG,MAAM,CAAC,KAAK,CAAC,qBAAqB,EAAE;QAC7D,OAAO,EAAE,OAAO,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,UAAU,EAAE;QACzD,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;QAChD,eAAe,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;QAChE,UAAU,EAAE,SAAS,CAAC,YAAY,CAAC,CAAC,UAAU,EAAE,CAAC,OAAO,EAAE;QAC1D,UAAU,EAAE,SAAS,CAAC,YAAY,CAAC,CAAC,UAAU,EAAE,CAAC,OAAO,EAAE;KAC3D,CAAC,CAAC;IAEH,MAAM,mBAAmB,GAAG,MAAM,CAAC,KAAK,CAAC,sBAAsB,EAAE;QAC/D,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE;QAC7B,OAAO,EAAE,OAAO,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,OAAO,EAAE;QACtD,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE;QACrC,MAAM,EAAE,OAAO,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE;QACnD,kBAAkB,EAAE,OAAO,CAAC,oBAAoB,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;QAClE,UAAU,EAAE,OAAO,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;QAClD,WAAW,EAAE,OAAO,CAAC,aAAa,CAAC;QACnC,QAAQ,EAAE,OAAO,CAAC,UAAU,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;QAC7C,UAAU,EAAE,SAAS,CAAC,YAAY,CAAC,CAAC,UAAU,EAAE,CAAC,OAAO,EAAE;KAC3D,CAAC,CAAC;IAEH,MAAM,gBAAgB,GAAG,MAAM,CAAC,KAAK,CAAC,mBAAmB,EAAE;QACzD,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE;QAC7B,OAAO,EAAE,OAAO,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,OAAO,EAAE;QACtD,QAAQ,EAAE,OAAO,CAAC,UAAU,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;QAC9C,UAAU,EAAE,SAAS,CAAC,YAAY,CAAC,CAAC,UAAU,EAAE,CAAC,OAAO,EAAE;KAC3D,CAAC,CAAC;IAEH,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,CAAC;AACvE,CAAC"}
@@ -0,0 +1,44 @@
1
+ export type { ConsumableSource, ConsumableBalanceResponse, ConsumablePurchaseRequest, ConsumableUseRequest, ConsumableUseResponse, ConsumablePurchaseRecord, ConsumableUsageRecord, } from "@sudobility/types";
2
+ export type { ConsumableBalanceResponse as BalanceResponse, ConsumablePurchaseRequest as PurchaseRequest, ConsumableUseRequest as UseRequest, ConsumableUseResponse as UseResponse, } from "@sudobility/types";
3
+ export interface ConsumableBalance {
4
+ user_id: string;
5
+ balance: number;
6
+ initial_credits: number;
7
+ created_at: Date;
8
+ updated_at: Date;
9
+ }
10
+ export interface ConsumablePurchase {
11
+ id: number;
12
+ user_id: string;
13
+ credits: number;
14
+ source: string;
15
+ transaction_ref_id: string | null;
16
+ product_id: string | null;
17
+ price_cents: number | null;
18
+ currency: string | null;
19
+ created_at: Date;
20
+ }
21
+ export interface ConsumableUsage {
22
+ id: number;
23
+ user_id: string;
24
+ filename: string | null;
25
+ created_at: Date;
26
+ }
27
+ export interface RevenueCatWebhookEvent {
28
+ api_version: string;
29
+ event: {
30
+ type: string;
31
+ app_user_id: string;
32
+ product_id: string;
33
+ price_in_purchased_currency: number;
34
+ currency: string;
35
+ store: string;
36
+ transaction_id: string;
37
+ purchased_at_ms: number;
38
+ };
39
+ }
40
+ export interface ConsumablesConfig {
41
+ initialFreeCredits: number;
42
+ revenueCatWebhookSecret?: string;
43
+ }
44
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,gBAAgB,EAChB,yBAAyB,EACzB,yBAAyB,EACzB,oBAAoB,EACpB,qBAAqB,EACrB,wBAAwB,EACxB,qBAAqB,GACtB,MAAM,mBAAmB,CAAC;AAG3B,YAAY,EACV,yBAAyB,IAAI,eAAe,EAC5C,yBAAyB,IAAI,eAAe,EAC5C,oBAAoB,IAAI,UAAU,EAClC,qBAAqB,IAAI,WAAW,GACrC,MAAM,mBAAmB,CAAC;AAI3B,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,IAAI,CAAC;IACjB,UAAU,EAAE,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,IAAI,CAAC;CAClB;AAID,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,MAAM,CAAC;QACnB,2BAA2B,EAAE,MAAM,CAAC;QACpC,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,cAAc,EAAE,MAAM,CAAC;QACvB,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAID,MAAM,WAAW,iBAAiB;IAChC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@sudobility/consumables_service",
3
+ "version": "0.0.2",
4
+ "description": "Shared backend library for consumable credits management with Drizzle ORM",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.esm.json",
16
+ "clean": "rm -rf dist",
17
+ "dev": "tsc --watch",
18
+ "test": "vitest run",
19
+ "test:watch": "vitest",
20
+ "lint": "eslint src/",
21
+ "lint:fix": "eslint src/ --fix",
22
+ "typecheck": "tsc --noEmit",
23
+ "format": "prettier --write \"src/**/*.ts\"",
24
+ "format:check": "prettier --check \"src/**/*.ts\"",
25
+ "verify": "bun run typecheck && bun run lint && bun run test && bun run build",
26
+ "prepublishOnly": "bun run clean && bun run verify"
27
+ },
28
+ "files": [
29
+ "dist/**/*",
30
+ "CLAUDE.md"
31
+ ],
32
+ "keywords": [
33
+ "consumables",
34
+ "credits",
35
+ "revenuecat",
36
+ "in-app-purchase",
37
+ "drizzle"
38
+ ],
39
+ "author": "Sudobility",
40
+ "license": "BUSL-1.1",
41
+ "peerDependencies": {
42
+ "@sudobility/types": "^1.9.51",
43
+ "drizzle-orm": ">=0.44.0"
44
+ },
45
+ "devDependencies": {
46
+ "@sudobility/types": "^1.9.51",
47
+ "drizzle-orm": "^0.45.1",
48
+ "vitest": "^4.0.4",
49
+ "@types/bun": "latest",
50
+ "@types/node": "^24.0.0",
51
+ "@typescript-eslint/eslint-plugin": "^8.50.0",
52
+ "@typescript-eslint/parser": "^8.50.0",
53
+ "eslint": "^9.39.0",
54
+ "eslint-plugin-import": "^2.32.0",
55
+ "prettier": "^3.7.0",
56
+ "typescript": "^5.9.0"
57
+ },
58
+ "publishConfig": {
59
+ "access": "public"
60
+ }
61
+ }