@vocoweb/meter 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 VocoWeb
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # @vocoweb/meter
2
+
3
+ [![npm version](https://badge.fury.io/js/%40vocoweb%2Fmeter.svg)](https://www.npmjs.com/package/@vocoweb/meter)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ Production-ready usage tracking and limits for B2B SaaS.
7
+
8
+ ## Overview
9
+
10
+ `@vocoweb/meter` is the "Meter" module that makes usage-based pricing simple. Track usage, enforce limits, and display beautiful progress barsโ€”all with a single function call.
11
+
12
+ **Key Features:**
13
+ - ๐Ÿ“Š **Usage Tracking** - Track any metric (API calls, PDFs, storage, etc.)
14
+ - ๐ŸŽฏ **Limit Enforcement** - Automatic 402 errors when limits exceeded
15
+ - ๐Ÿ”„ **Auto Reset** - Monthly/daily/weekly reset periods
16
+ - ๐Ÿ“ˆ **Usage UI** - Pre-built progress bar component
17
+ - โšก **High Performance** - Built on Supabase with row-level security
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install @vocoweb/meter
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ### Server-Side
28
+
29
+ ```typescript
30
+ import { meter } from '@vocoweb/meter';
31
+
32
+ // Increment usage
33
+ export async function POST(request: Request) {
34
+ const user = await auth.requireUser(request);
35
+
36
+ // Check limit before generating
37
+ await meter.checkLimit(user.id, 'pdf_generated');
38
+
39
+ // Generate PDF
40
+ const pdf = await generatePDF(data);
41
+
42
+ // Increment counter
43
+ await meter.increment(user.id, 'pdf_generated', 1);
44
+
45
+ return Response.json({ pdf });
46
+ }
47
+
48
+ // Get current usage
49
+ export async function GET(request: Request) {
50
+ const user = await auth.requireUser(request);
51
+ const usage = await meter.getUsage(user.id, 'pdf_generated');
52
+
53
+ return Response.json({
54
+ used: usage.current,
55
+ limit: usage.limit,
56
+ percentage: (usage.current / usage.limit) * 100,
57
+ });
58
+ }
59
+ ```
60
+
61
+ ### Client-Side (React)
62
+
63
+ ```tsx
64
+ import { VocoUsageBar } from '@vocoweb/meter/react';
65
+
66
+ export default function Dashboard() {
67
+ return (
68
+ <div>
69
+ <h1>Dashboard</h1>
70
+
71
+ <VocoUsageBar
72
+ metric="pdf_generated"
73
+ label="PDFs Generated This Month"
74
+ />
75
+
76
+ <VocoUsageBar
77
+ metric="api_calls"
78
+ label="API Calls"
79
+ color="blue"
80
+ />
81
+ </div>
82
+ );
83
+ }
84
+ ```
85
+
86
+ ## API Reference
87
+
88
+ ### Server Methods
89
+
90
+ - `increment(userId, metric, amount)` - Increment usage counter
91
+ - `checkLimit(userId, metric)` - Check if within limit (throws 402 if exceeded)
92
+ - `getUsage(userId, metric)` - Get current usage statistics
93
+ - `resetUsage(userId, metric)` - Reset usage counter
94
+ - `setLimit(userId, metric, limit)` - Update user's limit
95
+
96
+ ### React Components
97
+
98
+ - `<VocoUsageBar metric="name" label="Label" />` - Usage progress bar
99
+
100
+ ## Configuration Example
101
+
102
+ ```typescript
103
+ // vocoweb.config.ts
104
+ export const config = {
105
+ limits: {
106
+ pdf_generated: {
107
+ free: 10,
108
+ pro: 100,
109
+ enterprise: 1000
110
+ },
111
+ api_calls: {
112
+ free: 1000,
113
+ pro: 10000,
114
+ enterprise: 100000
115
+ },
116
+ },
117
+ resetPeriod: 'monthly', // 'daily', 'weekly', 'monthly'
118
+ };
119
+ ```
120
+
121
+ ## Environment Variables
122
+
123
+ ```bash
124
+ # Supabase (Required)
125
+ NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
126
+ NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
127
+ SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
128
+ ```
129
+
130
+ ## Database Schema
131
+
132
+ ```sql
133
+ CREATE TABLE usage_metrics (
134
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
135
+ user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
136
+ metric TEXT NOT NULL,
137
+ current_usage INTEGER DEFAULT 0,
138
+ limit_value INTEGER NOT NULL,
139
+ reset_at TIMESTAMPTZ NOT NULL,
140
+ created_at TIMESTAMPTZ DEFAULT NOW(),
141
+ UNIQUE(user_id, metric)
142
+ );
143
+ ```
144
+
145
+ ## License
146
+
147
+ MIT ยฉ VocoWeb
148
+
149
+ ## Support
150
+
151
+ - Email: legal@vocoweb.in
152
+ - Documentation: [GitHub Wiki](https://github.com/vocoweb/vocoweb-meter/wiki)
153
+
154
+ ---
155
+
156
+ **Built with care by [VocoWeb](https://vocoweb.in)**
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Type definitions for @vocoweb/meter
3
+ */
4
+ type MetricName = string;
5
+ type ResetPeriod = 'daily' | 'weekly' | 'monthly' | 'never';
6
+ interface UsageMetric {
7
+ id: string;
8
+ userId: string;
9
+ metric: MetricName;
10
+ currentUsage: number;
11
+ limitValue: number;
12
+ resetAt: Date;
13
+ createdAt: Date;
14
+ updatedAt?: Date;
15
+ }
16
+ interface UsageStats {
17
+ current: number;
18
+ limit: number;
19
+ percentage: number;
20
+ remaining: number;
21
+ }
22
+ interface IncrementOptions {
23
+ userId: string;
24
+ metric: MetricName;
25
+ amount?: number;
26
+ }
27
+ interface SetLimitOptions {
28
+ userId: string;
29
+ metric: MetricName;
30
+ limit: number;
31
+ }
32
+
33
+ /**
34
+ * Server-side functions for @vocoweb/meter
35
+ */
36
+
37
+ /**
38
+ * Increment usage counter
39
+ */
40
+ declare function increment(userId: string, metric: MetricName, amount?: number): Promise<void>;
41
+ /**
42
+ * Check if user is within limit (throws 402 if exceeded)
43
+ */
44
+ declare function checkLimit(userId: string, metric: MetricName): Promise<void>;
45
+ /**
46
+ * Get current usage statistics
47
+ */
48
+ declare function getUsage(userId: string, metric: MetricName): Promise<UsageStats>;
49
+ /**
50
+ * Reset usage counter
51
+ */
52
+ declare function resetUsage(userId: string, metric: MetricName): Promise<void>;
53
+ /**
54
+ * Set user's limit
55
+ */
56
+ declare function setLimit(userId: string, metric: MetricName, limit: number): Promise<void>;
57
+
58
+ /**
59
+ * @vocoweb/meter
60
+ * Production-ready usage tracking and limits
61
+ */
62
+
63
+ /**
64
+ * Main meter API
65
+ */
66
+ declare const meter: {
67
+ increment: typeof increment;
68
+ checkLimit: typeof checkLimit;
69
+ getUsage: typeof getUsage;
70
+ resetUsage: typeof resetUsage;
71
+ setLimit: typeof setLimit;
72
+ };
73
+ declare const VERSION = "1.0.0";
74
+
75
+ export { type IncrementOptions, type MetricName, type ResetPeriod, type SetLimitOptions, type UsageMetric, type UsageStats, VERSION, checkLimit, getUsage, increment, meter, resetUsage, setLimit };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Type definitions for @vocoweb/meter
3
+ */
4
+ type MetricName = string;
5
+ type ResetPeriod = 'daily' | 'weekly' | 'monthly' | 'never';
6
+ interface UsageMetric {
7
+ id: string;
8
+ userId: string;
9
+ metric: MetricName;
10
+ currentUsage: number;
11
+ limitValue: number;
12
+ resetAt: Date;
13
+ createdAt: Date;
14
+ updatedAt?: Date;
15
+ }
16
+ interface UsageStats {
17
+ current: number;
18
+ limit: number;
19
+ percentage: number;
20
+ remaining: number;
21
+ }
22
+ interface IncrementOptions {
23
+ userId: string;
24
+ metric: MetricName;
25
+ amount?: number;
26
+ }
27
+ interface SetLimitOptions {
28
+ userId: string;
29
+ metric: MetricName;
30
+ limit: number;
31
+ }
32
+
33
+ /**
34
+ * Server-side functions for @vocoweb/meter
35
+ */
36
+
37
+ /**
38
+ * Increment usage counter
39
+ */
40
+ declare function increment(userId: string, metric: MetricName, amount?: number): Promise<void>;
41
+ /**
42
+ * Check if user is within limit (throws 402 if exceeded)
43
+ */
44
+ declare function checkLimit(userId: string, metric: MetricName): Promise<void>;
45
+ /**
46
+ * Get current usage statistics
47
+ */
48
+ declare function getUsage(userId: string, metric: MetricName): Promise<UsageStats>;
49
+ /**
50
+ * Reset usage counter
51
+ */
52
+ declare function resetUsage(userId: string, metric: MetricName): Promise<void>;
53
+ /**
54
+ * Set user's limit
55
+ */
56
+ declare function setLimit(userId: string, metric: MetricName, limit: number): Promise<void>;
57
+
58
+ /**
59
+ * @vocoweb/meter
60
+ * Production-ready usage tracking and limits
61
+ */
62
+
63
+ /**
64
+ * Main meter API
65
+ */
66
+ declare const meter: {
67
+ increment: typeof increment;
68
+ checkLimit: typeof checkLimit;
69
+ getUsage: typeof getUsage;
70
+ resetUsage: typeof resetUsage;
71
+ setLimit: typeof setLimit;
72
+ };
73
+ declare const VERSION = "1.0.0";
74
+
75
+ export { type IncrementOptions, type MetricName, type ResetPeriod, type SetLimitOptions, type UsageMetric, type UsageStats, VERSION, checkLimit, getUsage, increment, meter, resetUsage, setLimit };
package/dist/index.js ADDED
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ var supabaseJs = require('@supabase/supabase-js');
4
+
5
+ // src/server.ts
6
+ var getSupabaseClient = () => {
7
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
8
+ const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
9
+ if (!supabaseUrl || !supabaseKey) {
10
+ throw new Error("Supabase credentials not configured");
11
+ }
12
+ return supabaseJs.createClient(supabaseUrl, supabaseKey);
13
+ };
14
+ async function increment(userId, metric, amount = 1) {
15
+ const supabase = getSupabaseClient();
16
+ const { data: existing } = await supabase.from("usage_metrics").select("*").eq("user_id", userId).eq("metric", metric).single();
17
+ if (existing) {
18
+ const { error } = await supabase.from("usage_metrics").update({ current_usage: existing.current_usage + amount }).eq("id", existing.id);
19
+ if (error) throw new Error(`Failed to increment usage: ${error.message}`);
20
+ } else {
21
+ const { error } = await supabase.from("usage_metrics").insert({
22
+ user_id: userId,
23
+ metric,
24
+ current_usage: amount,
25
+ limit_value: 100,
26
+ // Default limit
27
+ reset_at: getNextResetDate("monthly")
28
+ });
29
+ if (error) throw new Error(`Failed to create metric: ${error.message}`);
30
+ }
31
+ }
32
+ async function checkLimit(userId, metric) {
33
+ const stats = await getUsage(userId, metric);
34
+ if (stats.current >= stats.limit) {
35
+ throw new Error(`Usage limit exceeded for ${metric}`, {
36
+ cause: { code: 402, metric, stats }
37
+ });
38
+ }
39
+ }
40
+ async function getUsage(userId, metric) {
41
+ const supabase = getSupabaseClient();
42
+ const { data, error } = await supabase.from("usage_metrics").select("*").eq("user_id", userId).eq("metric", metric).single();
43
+ if (error || !data) {
44
+ return { current: 0, limit: 100, percentage: 0, remaining: 100 };
45
+ }
46
+ const current = data.current_usage;
47
+ const limit = data.limit_value;
48
+ const percentage = current / limit * 100;
49
+ const remaining = Math.max(0, limit - current);
50
+ return { current, limit, percentage, remaining };
51
+ }
52
+ async function resetUsage(userId, metric) {
53
+ const supabase = getSupabaseClient();
54
+ const { error } = await supabase.from("usage_metrics").update({ current_usage: 0, reset_at: getNextResetDate("monthly") }).eq("user_id", userId).eq("metric", metric);
55
+ if (error) throw new Error(`Failed to reset usage: ${error.message}`);
56
+ }
57
+ async function setLimit(userId, metric, limit) {
58
+ const supabase = getSupabaseClient();
59
+ const { error } = await supabase.from("usage_metrics").update({ limit_value: limit }).eq("user_id", userId).eq("metric", metric);
60
+ if (error) throw new Error(`Failed to set limit: ${error.message}`);
61
+ }
62
+ function getNextResetDate(period) {
63
+ const date = /* @__PURE__ */ new Date();
64
+ switch (period) {
65
+ case "daily":
66
+ date.setDate(date.getDate() + 1);
67
+ break;
68
+ case "weekly":
69
+ date.setDate(date.getDate() + 7);
70
+ break;
71
+ case "monthly":
72
+ date.setMonth(date.getMonth() + 1);
73
+ break;
74
+ }
75
+ return date.toISOString();
76
+ }
77
+
78
+ // src/index.ts
79
+ var meter = {
80
+ increment,
81
+ checkLimit,
82
+ getUsage,
83
+ resetUsage,
84
+ setLimit
85
+ };
86
+ var VERSION = "1.0.0";
87
+
88
+ exports.VERSION = VERSION;
89
+ exports.checkLimit = checkLimit;
90
+ exports.getUsage = getUsage;
91
+ exports.increment = increment;
92
+ exports.meter = meter;
93
+ exports.resetUsage = resetUsage;
94
+ exports.setLimit = setLimit;
95
+ //# sourceMappingURL=index.js.map
96
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts","../src/index.ts"],"names":["createClient"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Server-side functions for @vocoweb/meter\n */\n\nimport { createClient } from '@supabase/supabase-js';\nimport type { MetricName, UsageStats } from './types';\n\nconst getSupabaseClient = () => {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;\n\n if (!supabaseUrl || !supabaseKey) {\n throw new Error('Supabase credentials not configured');\n }\n\n return createClient(supabaseUrl, supabaseKey);\n};\n\n/**\n * Increment usage counter\n */\nexport async function increment(userId: string, metric: MetricName, amount: number = 1): Promise<void> {\n const supabase = getSupabaseClient();\n\n // Get or create metric\n const { data: existing } = await supabase\n .from('usage_metrics')\n .select('*')\n .eq('user_id', userId)\n .eq('metric', metric)\n .single();\n\n if (existing) {\n const { error } = await supabase\n .from('usage_metrics')\n .update({ current_usage: existing.current_usage + amount })\n .eq('id', existing.id);\n\n if (error) throw new Error(`Failed to increment usage: ${error.message}`);\n } else {\n // Create new metric with default limit\n const { error } = await supabase\n .from('usage_metrics')\n .insert({\n user_id: userId,\n metric,\n current_usage: amount,\n limit_value: 100, // Default limit\n reset_at: getNextResetDate('monthly'),\n });\n\n if (error) throw new Error(`Failed to create metric: ${error.message}`);\n }\n}\n\n/**\n * Check if user is within limit (throws 402 if exceeded)\n */\nexport async function checkLimit(userId: string, metric: MetricName): Promise<void> {\n const stats = await getUsage(userId, metric);\n\n if (stats.current >= stats.limit) {\n throw new Error(`Usage limit exceeded for ${metric}`, {\n cause: { code: 402, metric, stats },\n });\n }\n}\n\n/**\n * Get current usage statistics\n */\nexport async function getUsage(userId: string, metric: MetricName): Promise<UsageStats> {\n const supabase = getSupabaseClient();\n\n const { data, error } = await supabase\n .from('usage_metrics')\n .select('*')\n .eq('user_id', userId)\n .eq('metric', metric)\n .single();\n\n if (error || !data) {\n return { current: 0, limit: 100, percentage: 0, remaining: 100 };\n }\n\n const current = data.current_usage;\n const limit = data.limit_value;\n const percentage = (current / limit) * 100;\n const remaining = Math.max(0, limit - current);\n\n return { current, limit, percentage, remaining };\n}\n\n/**\n * Reset usage counter\n */\nexport async function resetUsage(userId: string, metric: MetricName): Promise<void> {\n const supabase = getSupabaseClient();\n\n const { error } = await supabase\n .from('usage_metrics')\n .update({ current_usage: 0, reset_at: getNextResetDate('monthly') })\n .eq('user_id', userId)\n .eq('metric', metric);\n\n if (error) throw new Error(`Failed to reset usage: ${error.message}`);\n}\n\n/**\n * Set user's limit\n */\nexport async function setLimit(userId: string, metric: MetricName, limit: number): Promise<void> {\n const supabase = getSupabaseClient();\n\n const { error } = await supabase\n .from('usage_metrics')\n .update({ limit_value: limit })\n .eq('user_id', userId)\n .eq('metric', metric);\n\n if (error) throw new Error(`Failed to set limit: ${error.message}`);\n}\n\n/**\n * Get next reset date based on period\n */\nfunction getNextResetDate(period: 'daily' | 'weekly' | 'monthly'): string {\n const date = new Date();\n\n switch (period) {\n case 'daily':\n date.setDate(date.getDate() + 1);\n break;\n case 'weekly':\n date.setDate(date.getDate() + 7);\n break;\n case 'monthly':\n date.setMonth(date.getMonth() + 1);\n break;\n }\n\n return date.toISOString();\n}\n","/**\n * @vocoweb/meter\n * Production-ready usage tracking and limits\n */\n\n// Types\nexport * from './types';\n\n// Server-side\nexport * from './server';\n\n// Import for unified API\nimport * as serverMeter from './server';\n\n/**\n * Main meter API\n */\nexport const meter = {\n increment: serverMeter.increment,\n checkLimit: serverMeter.checkLimit,\n getUsage: serverMeter.getUsage,\n resetUsage: serverMeter.resetUsage,\n setLimit: serverMeter.setLimit,\n};\n\n// Version\nexport const VERSION = '1.0.0';\n"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,88 @@
1
+ import { createClient } from '@supabase/supabase-js';
2
+
3
+ // src/server.ts
4
+ var getSupabaseClient = () => {
5
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
6
+ const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
7
+ if (!supabaseUrl || !supabaseKey) {
8
+ throw new Error("Supabase credentials not configured");
9
+ }
10
+ return createClient(supabaseUrl, supabaseKey);
11
+ };
12
+ async function increment(userId, metric, amount = 1) {
13
+ const supabase = getSupabaseClient();
14
+ const { data: existing } = await supabase.from("usage_metrics").select("*").eq("user_id", userId).eq("metric", metric).single();
15
+ if (existing) {
16
+ const { error } = await supabase.from("usage_metrics").update({ current_usage: existing.current_usage + amount }).eq("id", existing.id);
17
+ if (error) throw new Error(`Failed to increment usage: ${error.message}`);
18
+ } else {
19
+ const { error } = await supabase.from("usage_metrics").insert({
20
+ user_id: userId,
21
+ metric,
22
+ current_usage: amount,
23
+ limit_value: 100,
24
+ // Default limit
25
+ reset_at: getNextResetDate("monthly")
26
+ });
27
+ if (error) throw new Error(`Failed to create metric: ${error.message}`);
28
+ }
29
+ }
30
+ async function checkLimit(userId, metric) {
31
+ const stats = await getUsage(userId, metric);
32
+ if (stats.current >= stats.limit) {
33
+ throw new Error(`Usage limit exceeded for ${metric}`, {
34
+ cause: { code: 402, metric, stats }
35
+ });
36
+ }
37
+ }
38
+ async function getUsage(userId, metric) {
39
+ const supabase = getSupabaseClient();
40
+ const { data, error } = await supabase.from("usage_metrics").select("*").eq("user_id", userId).eq("metric", metric).single();
41
+ if (error || !data) {
42
+ return { current: 0, limit: 100, percentage: 0, remaining: 100 };
43
+ }
44
+ const current = data.current_usage;
45
+ const limit = data.limit_value;
46
+ const percentage = current / limit * 100;
47
+ const remaining = Math.max(0, limit - current);
48
+ return { current, limit, percentage, remaining };
49
+ }
50
+ async function resetUsage(userId, metric) {
51
+ const supabase = getSupabaseClient();
52
+ const { error } = await supabase.from("usage_metrics").update({ current_usage: 0, reset_at: getNextResetDate("monthly") }).eq("user_id", userId).eq("metric", metric);
53
+ if (error) throw new Error(`Failed to reset usage: ${error.message}`);
54
+ }
55
+ async function setLimit(userId, metric, limit) {
56
+ const supabase = getSupabaseClient();
57
+ const { error } = await supabase.from("usage_metrics").update({ limit_value: limit }).eq("user_id", userId).eq("metric", metric);
58
+ if (error) throw new Error(`Failed to set limit: ${error.message}`);
59
+ }
60
+ function getNextResetDate(period) {
61
+ const date = /* @__PURE__ */ new Date();
62
+ switch (period) {
63
+ case "daily":
64
+ date.setDate(date.getDate() + 1);
65
+ break;
66
+ case "weekly":
67
+ date.setDate(date.getDate() + 7);
68
+ break;
69
+ case "monthly":
70
+ date.setMonth(date.getMonth() + 1);
71
+ break;
72
+ }
73
+ return date.toISOString();
74
+ }
75
+
76
+ // src/index.ts
77
+ var meter = {
78
+ increment,
79
+ checkLimit,
80
+ getUsage,
81
+ resetUsage,
82
+ setLimit
83
+ };
84
+ var VERSION = "1.0.0";
85
+
86
+ export { VERSION, checkLimit, getUsage, increment, meter, resetUsage, setLimit };
87
+ //# sourceMappingURL=index.mjs.map
88
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * Server-side functions for @vocoweb/meter\n */\n\nimport { createClient } from '@supabase/supabase-js';\nimport type { MetricName, UsageStats } from './types';\n\nconst getSupabaseClient = () => {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;\n\n if (!supabaseUrl || !supabaseKey) {\n throw new Error('Supabase credentials not configured');\n }\n\n return createClient(supabaseUrl, supabaseKey);\n};\n\n/**\n * Increment usage counter\n */\nexport async function increment(userId: string, metric: MetricName, amount: number = 1): Promise<void> {\n const supabase = getSupabaseClient();\n\n // Get or create metric\n const { data: existing } = await supabase\n .from('usage_metrics')\n .select('*')\n .eq('user_id', userId)\n .eq('metric', metric)\n .single();\n\n if (existing) {\n const { error } = await supabase\n .from('usage_metrics')\n .update({ current_usage: existing.current_usage + amount })\n .eq('id', existing.id);\n\n if (error) throw new Error(`Failed to increment usage: ${error.message}`);\n } else {\n // Create new metric with default limit\n const { error } = await supabase\n .from('usage_metrics')\n .insert({\n user_id: userId,\n metric,\n current_usage: amount,\n limit_value: 100, // Default limit\n reset_at: getNextResetDate('monthly'),\n });\n\n if (error) throw new Error(`Failed to create metric: ${error.message}`);\n }\n}\n\n/**\n * Check if user is within limit (throws 402 if exceeded)\n */\nexport async function checkLimit(userId: string, metric: MetricName): Promise<void> {\n const stats = await getUsage(userId, metric);\n\n if (stats.current >= stats.limit) {\n throw new Error(`Usage limit exceeded for ${metric}`, {\n cause: { code: 402, metric, stats },\n });\n }\n}\n\n/**\n * Get current usage statistics\n */\nexport async function getUsage(userId: string, metric: MetricName): Promise<UsageStats> {\n const supabase = getSupabaseClient();\n\n const { data, error } = await supabase\n .from('usage_metrics')\n .select('*')\n .eq('user_id', userId)\n .eq('metric', metric)\n .single();\n\n if (error || !data) {\n return { current: 0, limit: 100, percentage: 0, remaining: 100 };\n }\n\n const current = data.current_usage;\n const limit = data.limit_value;\n const percentage = (current / limit) * 100;\n const remaining = Math.max(0, limit - current);\n\n return { current, limit, percentage, remaining };\n}\n\n/**\n * Reset usage counter\n */\nexport async function resetUsage(userId: string, metric: MetricName): Promise<void> {\n const supabase = getSupabaseClient();\n\n const { error } = await supabase\n .from('usage_metrics')\n .update({ current_usage: 0, reset_at: getNextResetDate('monthly') })\n .eq('user_id', userId)\n .eq('metric', metric);\n\n if (error) throw new Error(`Failed to reset usage: ${error.message}`);\n}\n\n/**\n * Set user's limit\n */\nexport async function setLimit(userId: string, metric: MetricName, limit: number): Promise<void> {\n const supabase = getSupabaseClient();\n\n const { error } = await supabase\n .from('usage_metrics')\n .update({ limit_value: limit })\n .eq('user_id', userId)\n .eq('metric', metric);\n\n if (error) throw new Error(`Failed to set limit: ${error.message}`);\n}\n\n/**\n * Get next reset date based on period\n */\nfunction getNextResetDate(period: 'daily' | 'weekly' | 'monthly'): string {\n const date = new Date();\n\n switch (period) {\n case 'daily':\n date.setDate(date.getDate() + 1);\n break;\n case 'weekly':\n date.setDate(date.getDate() + 7);\n break;\n case 'monthly':\n date.setMonth(date.getMonth() + 1);\n break;\n }\n\n return date.toISOString();\n}\n","/**\n * @vocoweb/meter\n * Production-ready usage tracking and limits\n */\n\n// Types\nexport * from './types';\n\n// Server-side\nexport * from './server';\n\n// Import for unified API\nimport * as serverMeter from './server';\n\n/**\n * Main meter API\n */\nexport const meter = {\n increment: serverMeter.increment,\n checkLimit: serverMeter.checkLimit,\n getUsage: serverMeter.getUsage,\n resetUsage: serverMeter.resetUsage,\n setLimit: serverMeter.setLimit,\n};\n\n// Version\nexport const VERSION = '1.0.0';\n"]}
@@ -0,0 +1,15 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ /**
4
+ * VocoUsageBar Component
5
+ * Usage progress bar
6
+ */
7
+ interface VocoUsageBarProps {
8
+ metric: string;
9
+ label?: string;
10
+ color?: 'blue' | 'green' | 'red' | 'purple';
11
+ className?: string;
12
+ }
13
+ declare function VocoUsageBar({ metric, label, color, className, }: VocoUsageBarProps): react_jsx_runtime.JSX.Element;
14
+
15
+ export { VocoUsageBar };
@@ -0,0 +1,15 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ /**
4
+ * VocoUsageBar Component
5
+ * Usage progress bar
6
+ */
7
+ interface VocoUsageBarProps {
8
+ metric: string;
9
+ label?: string;
10
+ color?: 'blue' | 'green' | 'red' | 'purple';
11
+ className?: string;
12
+ }
13
+ declare function VocoUsageBar({ metric, label, color, className, }: VocoUsageBarProps): react_jsx_runtime.JSX.Element;
14
+
15
+ export { VocoUsageBar };
package/dist/react.js ADDED
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ var React = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ function _interopNamespace(e) {
7
+ if (e && e.__esModule) return e;
8
+ var n = Object.create(null);
9
+ if (e) {
10
+ Object.keys(e).forEach(function (k) {
11
+ if (k !== 'default') {
12
+ var d = Object.getOwnPropertyDescriptor(e, k);
13
+ Object.defineProperty(n, k, d.get ? d : {
14
+ enumerable: true,
15
+ get: function () { return e[k]; }
16
+ });
17
+ }
18
+ });
19
+ }
20
+ n.default = e;
21
+ return Object.freeze(n);
22
+ }
23
+
24
+ var React__namespace = /*#__PURE__*/_interopNamespace(React);
25
+
26
+ // src/components/VocoUsageBar.tsx
27
+ function VocoUsageBar({
28
+ metric,
29
+ label,
30
+ color = "blue",
31
+ className = ""
32
+ }) {
33
+ const [usage, setUsage] = React__namespace.useState({ current: 0, limit: 0, percentage: 0 });
34
+ const [loading, setLoading] = React__namespace.useState(true);
35
+ React__namespace.useEffect(() => {
36
+ async function fetchUsage() {
37
+ try {
38
+ const res = await fetch(`/api/usage?metric=${metric}`);
39
+ const data = await res.json();
40
+ setUsage(data);
41
+ } catch (error) {
42
+ console.error("Failed to fetch usage:", error);
43
+ } finally {
44
+ setLoading(false);
45
+ }
46
+ }
47
+ fetchUsage();
48
+ }, [metric]);
49
+ if (loading) {
50
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 w-full animate-pulse rounded bg-gray-200" });
51
+ }
52
+ const colors = {
53
+ blue: "bg-blue-600",
54
+ green: "bg-green-600",
55
+ red: "bg-red-600",
56
+ purple: "bg-purple-600"
57
+ };
58
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `space-y-2 ${className}`, children: [
59
+ label && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-sm", children: [
60
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: label }),
61
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-gray-600", children: [
62
+ usage.current,
63
+ " / ",
64
+ usage.limit
65
+ ] })
66
+ ] }),
67
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-2 w-full overflow-hidden rounded-full bg-gray-200", children: /* @__PURE__ */ jsxRuntime.jsx(
68
+ "div",
69
+ {
70
+ className: `h-full transition-all ${colors[color]}`,
71
+ style: { width: `${Math.min(usage.percentage, 100)}%` }
72
+ }
73
+ ) })
74
+ ] });
75
+ }
76
+
77
+ exports.VocoUsageBar = VocoUsageBar;
78
+ //# sourceMappingURL=react.js.map
79
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/VocoUsageBar.tsx"],"names":["React","jsx","jsxs"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAgBO,SAAS,YAAA,CAAa;AAAA,EACzB,MAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA,GAAQ,MAAA;AAAA,EACR,SAAA,GAAY;AAChB,CAAA,EAAsB;AAClB,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAUA,gBAAA,CAAA,QAAA,CAAS,EAAE,OAAA,EAAS,CAAA,EAAG,KAAA,EAAO,CAAA,EAAG,UAAA,EAAY,CAAA,EAAG,CAAA;AAChF,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAUA,0BAAS,IAAI,CAAA;AAEjD,EAAMA,2BAAU,MAAM;AAClB,IAAA,eAAe,UAAA,GAAa;AACxB,MAAA,IAAI;AACA,QAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,CAAA,kBAAA,EAAqB,MAAM,CAAA,CAAE,CAAA;AACrD,QAAA,MAAM,IAAA,GAAO,MAAM,GAAA,CAAI,IAAA,EAAK;AAC5B,QAAA,QAAA,CAAS,IAAI,CAAA;AAAA,MACjB,SAAS,KAAA,EAAO;AACZ,QAAA,OAAA,CAAQ,KAAA,CAAM,0BAA0B,KAAK,CAAA;AAAA,MACjD,CAAA,SAAE;AACE,QAAA,UAAA,CAAW,KAAK,CAAA;AAAA,MACpB;AAAA,IACJ;AAEA,IAAA,UAAA,EAAW;AAAA,EACf,CAAA,EAAG,CAAC,MAAM,CAAC,CAAA;AAEX,EAAA,IAAI,OAAA,EAAS;AACT,IAAA,uBAAOC,cAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,8CAAA,EAA+C,CAAA;AAAA,EACzE;AAEA,EAAA,MAAM,MAAA,GAAS;AAAA,IACX,IAAA,EAAM,aAAA;AAAA,IACN,KAAA,EAAO,cAAA;AAAA,IACP,GAAA,EAAK,YAAA;AAAA,IACL,MAAA,EAAQ;AAAA,GACZ;AAEA,EAAA,uBACIC,eAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,CAAA,UAAA,EAAa,SAAS,CAAA,CAAA,EACjC,QAAA,EAAA;AAAA,IAAA,KAAA,oBACGA,eAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,2CAAA,EACX,QAAA,EAAA;AAAA,sBAAAD,cAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,aAAA,EAAe,QAAA,EAAA,KAAA,EAAM,CAAA;AAAA,sBACrCC,eAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,eAAA,EACX,QAAA,EAAA;AAAA,QAAA,KAAA,CAAM,OAAA;AAAA,QAAQ,KAAA;AAAA,QAAI,KAAA,CAAM;AAAA,OAAA,EAC7B;AAAA,KAAA,EACJ,CAAA;AAAA,oBAEJD,cAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,qDAAA,EACX,QAAA,kBAAAA,cAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACG,SAAA,EAAW,CAAA,sBAAA,EAAyB,MAAA,CAAO,KAAK,CAAC,CAAA,CAAA;AAAA,QACjD,KAAA,EAAO,EAAE,KAAA,EAAO,CAAA,EAAG,IAAA,CAAK,IAAI,KAAA,CAAM,UAAA,EAAY,GAAG,CAAC,CAAA,CAAA,CAAA;AAAI;AAAA,KAC1D,EACJ;AAAA,GAAA,EACJ,CAAA;AAER","file":"react.js","sourcesContent":["/**\n * VocoUsageBar Component\n * Usage progress bar\n */\n\n'use client';\n\nimport * as React from 'react';\n\nexport interface VocoUsageBarProps {\n metric: string;\n label?: string;\n color?: 'blue' | 'green' | 'red' | 'purple';\n className?: string;\n}\n\nexport function VocoUsageBar({\n metric,\n label,\n color = 'blue',\n className = '',\n}: VocoUsageBarProps) {\n const [usage, setUsage] = React.useState({ current: 0, limit: 0, percentage: 0 });\n const [loading, setLoading] = React.useState(true);\n\n React.useEffect(() => {\n async function fetchUsage() {\n try {\n const res = await fetch(`/api/usage?metric=${metric}`);\n const data = await res.json();\n setUsage(data);\n } catch (error) {\n console.error('Failed to fetch usage:', error);\n } finally {\n setLoading(false);\n }\n }\n\n fetchUsage();\n }, [metric]);\n\n if (loading) {\n return <div className=\"h-4 w-full animate-pulse rounded bg-gray-200\" />;\n }\n\n const colors = {\n blue: 'bg-blue-600',\n green: 'bg-green-600',\n red: 'bg-red-600',\n purple: 'bg-purple-600',\n };\n\n return (\n <div className={`space-y-2 ${className}`}>\n {label && (\n <div className=\"flex items-center justify-between text-sm\">\n <span className=\"font-medium\">{label}</span>\n <span className=\"text-gray-600\">\n {usage.current} / {usage.limit}\n </span>\n </div>\n )}\n <div className=\"h-2 w-full overflow-hidden rounded-full bg-gray-200\">\n <div\n className={`h-full transition-all ${colors[color]}`}\n style={{ width: `${Math.min(usage.percentage, 100)}%` }}\n />\n </div>\n </div>\n );\n}\n"]}
package/dist/react.mjs ADDED
@@ -0,0 +1,57 @@
1
+ import * as React from 'react';
2
+ import { jsx, jsxs } from 'react/jsx-runtime';
3
+
4
+ // src/components/VocoUsageBar.tsx
5
+ function VocoUsageBar({
6
+ metric,
7
+ label,
8
+ color = "blue",
9
+ className = ""
10
+ }) {
11
+ const [usage, setUsage] = React.useState({ current: 0, limit: 0, percentage: 0 });
12
+ const [loading, setLoading] = React.useState(true);
13
+ React.useEffect(() => {
14
+ async function fetchUsage() {
15
+ try {
16
+ const res = await fetch(`/api/usage?metric=${metric}`);
17
+ const data = await res.json();
18
+ setUsage(data);
19
+ } catch (error) {
20
+ console.error("Failed to fetch usage:", error);
21
+ } finally {
22
+ setLoading(false);
23
+ }
24
+ }
25
+ fetchUsage();
26
+ }, [metric]);
27
+ if (loading) {
28
+ return /* @__PURE__ */ jsx("div", { className: "h-4 w-full animate-pulse rounded bg-gray-200" });
29
+ }
30
+ const colors = {
31
+ blue: "bg-blue-600",
32
+ green: "bg-green-600",
33
+ red: "bg-red-600",
34
+ purple: "bg-purple-600"
35
+ };
36
+ return /* @__PURE__ */ jsxs("div", { className: `space-y-2 ${className}`, children: [
37
+ label && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-sm", children: [
38
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: label }),
39
+ /* @__PURE__ */ jsxs("span", { className: "text-gray-600", children: [
40
+ usage.current,
41
+ " / ",
42
+ usage.limit
43
+ ] })
44
+ ] }),
45
+ /* @__PURE__ */ jsx("div", { className: "h-2 w-full overflow-hidden rounded-full bg-gray-200", children: /* @__PURE__ */ jsx(
46
+ "div",
47
+ {
48
+ className: `h-full transition-all ${colors[color]}`,
49
+ style: { width: `${Math.min(usage.percentage, 100)}%` }
50
+ }
51
+ ) })
52
+ ] });
53
+ }
54
+
55
+ export { VocoUsageBar };
56
+ //# sourceMappingURL=react.mjs.map
57
+ //# sourceMappingURL=react.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/VocoUsageBar.tsx"],"names":[],"mappings":";;;;AAgBO,SAAS,YAAA,CAAa;AAAA,EACzB,MAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA,GAAQ,MAAA;AAAA,EACR,SAAA,GAAY;AAChB,CAAA,EAAsB;AAClB,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAU,KAAA,CAAA,QAAA,CAAS,EAAE,OAAA,EAAS,CAAA,EAAG,KAAA,EAAO,CAAA,EAAG,UAAA,EAAY,CAAA,EAAG,CAAA;AAChF,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAU,eAAS,IAAI,CAAA;AAEjD,EAAM,gBAAU,MAAM;AAClB,IAAA,eAAe,UAAA,GAAa;AACxB,MAAA,IAAI;AACA,QAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,CAAA,kBAAA,EAAqB,MAAM,CAAA,CAAE,CAAA;AACrD,QAAA,MAAM,IAAA,GAAO,MAAM,GAAA,CAAI,IAAA,EAAK;AAC5B,QAAA,QAAA,CAAS,IAAI,CAAA;AAAA,MACjB,SAAS,KAAA,EAAO;AACZ,QAAA,OAAA,CAAQ,KAAA,CAAM,0BAA0B,KAAK,CAAA;AAAA,MACjD,CAAA,SAAE;AACE,QAAA,UAAA,CAAW,KAAK,CAAA;AAAA,MACpB;AAAA,IACJ;AAEA,IAAA,UAAA,EAAW;AAAA,EACf,CAAA,EAAG,CAAC,MAAM,CAAC,CAAA;AAEX,EAAA,IAAI,OAAA,EAAS;AACT,IAAA,uBAAO,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,8CAAA,EAA+C,CAAA;AAAA,EACzE;AAEA,EAAA,MAAM,MAAA,GAAS;AAAA,IACX,IAAA,EAAM,aAAA;AAAA,IACN,KAAA,EAAO,cAAA;AAAA,IACP,GAAA,EAAK,YAAA;AAAA,IACL,MAAA,EAAQ;AAAA,GACZ;AAEA,EAAA,uBACI,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,CAAA,UAAA,EAAa,SAAS,CAAA,CAAA,EACjC,QAAA,EAAA;AAAA,IAAA,KAAA,oBACG,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,2CAAA,EACX,QAAA,EAAA;AAAA,sBAAA,GAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,aAAA,EAAe,QAAA,EAAA,KAAA,EAAM,CAAA;AAAA,sBACrC,IAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,eAAA,EACX,QAAA,EAAA;AAAA,QAAA,KAAA,CAAM,OAAA;AAAA,QAAQ,KAAA;AAAA,QAAI,KAAA,CAAM;AAAA,OAAA,EAC7B;AAAA,KAAA,EACJ,CAAA;AAAA,oBAEJ,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,qDAAA,EACX,QAAA,kBAAA,GAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACG,SAAA,EAAW,CAAA,sBAAA,EAAyB,MAAA,CAAO,KAAK,CAAC,CAAA,CAAA;AAAA,QACjD,KAAA,EAAO,EAAE,KAAA,EAAO,CAAA,EAAG,IAAA,CAAK,IAAI,KAAA,CAAM,UAAA,EAAY,GAAG,CAAC,CAAA,CAAA,CAAA;AAAI;AAAA,KAC1D,EACJ;AAAA,GAAA,EACJ,CAAA;AAER","file":"react.mjs","sourcesContent":["/**\n * VocoUsageBar Component\n * Usage progress bar\n */\n\n'use client';\n\nimport * as React from 'react';\n\nexport interface VocoUsageBarProps {\n metric: string;\n label?: string;\n color?: 'blue' | 'green' | 'red' | 'purple';\n className?: string;\n}\n\nexport function VocoUsageBar({\n metric,\n label,\n color = 'blue',\n className = '',\n}: VocoUsageBarProps) {\n const [usage, setUsage] = React.useState({ current: 0, limit: 0, percentage: 0 });\n const [loading, setLoading] = React.useState(true);\n\n React.useEffect(() => {\n async function fetchUsage() {\n try {\n const res = await fetch(`/api/usage?metric=${metric}`);\n const data = await res.json();\n setUsage(data);\n } catch (error) {\n console.error('Failed to fetch usage:', error);\n } finally {\n setLoading(false);\n }\n }\n\n fetchUsage();\n }, [metric]);\n\n if (loading) {\n return <div className=\"h-4 w-full animate-pulse rounded bg-gray-200\" />;\n }\n\n const colors = {\n blue: 'bg-blue-600',\n green: 'bg-green-600',\n red: 'bg-red-600',\n purple: 'bg-purple-600',\n };\n\n return (\n <div className={`space-y-2 ${className}`}>\n {label && (\n <div className=\"flex items-center justify-between text-sm\">\n <span className=\"font-medium\">{label}</span>\n <span className=\"text-gray-600\">\n {usage.current} / {usage.limit}\n </span>\n </div>\n )}\n <div className=\"h-2 w-full overflow-hidden rounded-full bg-gray-200\">\n <div\n className={`h-full transition-all ${colors[color]}`}\n style={{ width: `${Math.min(usage.percentage, 100)}%` }}\n />\n </div>\n </div>\n );\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@vocoweb/meter",
3
+ "version": "1.0.0",
4
+ "description": "Production-ready usage tracking and limits for B2B SaaS",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ },
14
+ "./react": {
15
+ "import": "./dist/react.mjs",
16
+ "types": "./dist/react.d.ts"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "dev": "tsup --watch",
27
+ "test": "vitest",
28
+ "lint": "eslint src",
29
+ "type-check": "tsc --noEmit",
30
+ "prepublishOnly": "npm run build"
31
+ },
32
+ "keywords": [
33
+ "usage-tracking",
34
+ "metering",
35
+ "limits",
36
+ "quotas",
37
+ "usage-based-pricing",
38
+ "saas"
39
+ ],
40
+ "author": "VocoWeb <legal@vocoweb.in>",
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/vocoweb/vocoweb-meter.git"
45
+ },
46
+ "homepage": "https://github.com/vocoweb/vocoweb-meter#readme",
47
+ "peerDependencies": {
48
+ "next": ">=14.0.0",
49
+ "react": ">=18.0.0",
50
+ "react-dom": ">=18.0.0"
51
+ },
52
+ "peerDependenciesMeta": {
53
+ "next": {
54
+ "optional": true
55
+ }
56
+ },
57
+ "dependencies": {
58
+ "@supabase/supabase-js": "^2.39.0",
59
+ "class-variance-authority": "^0.7.0",
60
+ "clsx": "^2.1.0",
61
+ "lucide-react": "^0.468.0",
62
+ "tailwind-merge": "^2.2.0",
63
+ "zod": "^3.22.0"
64
+ },
65
+ "devDependencies": {
66
+ "@types/node": "^20.11.0",
67
+ "@types/react": "^18.2.0",
68
+ "@types/react-dom": "^18.2.0",
69
+ "eslint": "^8.56.0",
70
+ "tsup": "^8.0.0",
71
+ "typescript": "^5.3.0",
72
+ "vitest": "^1.2.0"
73
+ },
74
+ "engines": {
75
+ "node": ">=18.0.0"
76
+ },
77
+ "publishConfig": {
78
+ "access": "public"
79
+ }
80
+ }