stagent 0.8.0 → 0.9.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.
Files changed (75) hide show
  1. package/README.md +41 -0
  2. package/dist/cli.js +19 -2
  3. package/package.json +2 -1
  4. package/src/app/analytics/page.tsx +60 -0
  5. package/src/app/api/license/checkout/route.ts +28 -0
  6. package/src/app/api/license/portal/route.ts +26 -0
  7. package/src/app/api/license/route.ts +88 -0
  8. package/src/app/api/license/usage/route.ts +63 -0
  9. package/src/app/api/marketplace/browse/route.ts +15 -0
  10. package/src/app/api/marketplace/import/route.ts +28 -0
  11. package/src/app/api/marketplace/publish/route.ts +40 -0
  12. package/src/app/api/memory/route.ts +11 -0
  13. package/src/app/api/onboarding/email/route.ts +53 -0
  14. package/src/app/api/onboarding/progress/route.ts +60 -0
  15. package/src/app/api/schedules/route.ts +11 -0
  16. package/src/app/api/settings/telemetry/route.ts +14 -0
  17. package/src/app/api/sync/export/route.ts +54 -0
  18. package/src/app/api/sync/restore/route.ts +37 -0
  19. package/src/app/api/sync/sessions/route.ts +24 -0
  20. package/src/app/api/tasks/[id]/execute/route.ts +21 -0
  21. package/src/app/auth/callback/route.ts +79 -0
  22. package/src/app/marketplace/page.tsx +19 -0
  23. package/src/app/page.tsx +6 -2
  24. package/src/app/settings/page.tsx +8 -0
  25. package/src/components/analytics/analytics-dashboard.tsx +200 -0
  26. package/src/components/analytics/analytics-gate-card.tsx +101 -0
  27. package/src/components/marketplace/blueprint-card.tsx +61 -0
  28. package/src/components/marketplace/marketplace-browser.tsx +131 -0
  29. package/src/components/onboarding/activation-checklist.tsx +64 -0
  30. package/src/components/onboarding/donut-ring.tsx +52 -0
  31. package/src/components/onboarding/email-capture-card.tsx +104 -0
  32. package/src/components/settings/activation-form.tsx +95 -0
  33. package/src/components/settings/cloud-account-section.tsx +145 -0
  34. package/src/components/settings/cloud-sync-section.tsx +155 -0
  35. package/src/components/settings/subscription-section.tsx +410 -0
  36. package/src/components/settings/telemetry-section.tsx +80 -0
  37. package/src/components/shared/app-sidebar.tsx +136 -29
  38. package/src/components/shared/premium-gate-overlay.tsx +50 -0
  39. package/src/components/shared/schedule-gate-dialog.tsx +64 -0
  40. package/src/components/shared/upgrade-banner.tsx +112 -0
  41. package/src/hooks/use-snoozed-banners.ts +73 -0
  42. package/src/hooks/use-supabase-auth.ts +79 -0
  43. package/src/instrumentation.ts +34 -0
  44. package/src/lib/agents/__tests__/execution-manager.test.ts +28 -1
  45. package/src/lib/agents/__tests__/learned-context.test.ts +13 -0
  46. package/src/lib/agents/execution-manager.ts +35 -0
  47. package/src/lib/agents/learned-context.ts +13 -0
  48. package/src/lib/analytics/queries.ts +207 -0
  49. package/src/lib/billing/email.ts +54 -0
  50. package/src/lib/billing/products.ts +80 -0
  51. package/src/lib/billing/stripe.ts +101 -0
  52. package/src/lib/cloud/supabase-browser.ts +38 -0
  53. package/src/lib/cloud/supabase-client.ts +49 -0
  54. package/src/lib/constants/settings.ts +18 -0
  55. package/src/lib/data/clear.ts +5 -0
  56. package/src/lib/db/bootstrap.ts +16 -0
  57. package/src/lib/db/schema.ts +24 -0
  58. package/src/lib/license/__tests__/features.test.ts +56 -0
  59. package/src/lib/license/__tests__/key-format.test.ts +88 -0
  60. package/src/lib/license/__tests__/tier-limits.test.ts +79 -0
  61. package/src/lib/license/cloud-validation.ts +64 -0
  62. package/src/lib/license/features.ts +44 -0
  63. package/src/lib/license/key-format.ts +101 -0
  64. package/src/lib/license/limit-check.ts +111 -0
  65. package/src/lib/license/limit-queries.ts +51 -0
  66. package/src/lib/license/manager.ts +290 -0
  67. package/src/lib/license/notifications.ts +59 -0
  68. package/src/lib/license/tier-limits.ts +71 -0
  69. package/src/lib/marketplace/marketplace-client.ts +107 -0
  70. package/src/lib/sync/cloud-sync.ts +237 -0
  71. package/src/lib/telemetry/conversion-events.ts +73 -0
  72. package/src/lib/telemetry/queue.ts +122 -0
  73. package/src/lib/usage/ledger.ts +18 -0
  74. package/src/lib/validators/license.ts +33 -0
  75. package/tsconfig.json +2 -1
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Cloud Sync — encrypted SQLite backup and restore via Supabase Storage.
3
+ *
4
+ * Encryption: AES-256-GCM with HKDF-derived keys.
5
+ * Envelope: [4B version][32B salt][12B IV][N bytes ciphertext][16B auth tag]
6
+ *
7
+ * V1: Full-database export (no incremental). Manual backup/restore only.
8
+ */
9
+
10
+ import { createHash, createCipheriv, createDecipheriv, randomBytes } from "crypto";
11
+ import { readFileSync, writeFileSync, existsSync, copyFileSync } from "fs";
12
+ import { join } from "path";
13
+ import { tmpdir } from "os";
14
+ import { getStagentDataDir } from "@/lib/utils/stagent-paths";
15
+ import { getSupabaseClient } from "@/lib/cloud/supabase-client";
16
+ import { sqlite } from "@/lib/db";
17
+
18
+ const SYNC_VERSION = Buffer.from([0, 0, 0, 1]); // Version 1
19
+ const BUCKET_NAME = "stagent-sync";
20
+
21
+ export interface SyncResult {
22
+ success: boolean;
23
+ blobPath?: string;
24
+ sizeBytes?: number;
25
+ error?: string;
26
+ }
27
+
28
+ /**
29
+ * Derive an AES-256 key from userId using HKDF-like construction.
30
+ * Uses SHA-256 HMAC with a fixed info string and random salt.
31
+ */
32
+ function deriveKey(userId: string, salt: Buffer): Buffer {
33
+ const hmac = createHash("sha256");
34
+ hmac.update(salt);
35
+ hmac.update(userId);
36
+ hmac.update("stagent-sync-v1");
37
+ return Buffer.from(hmac.digest());
38
+ }
39
+
40
+ /**
41
+ * Encrypt data using AES-256-GCM.
42
+ * Returns envelope: [4B version][32B salt][12B IV][ciphertext + 16B auth tag]
43
+ */
44
+ function encrypt(data: Buffer, userId: string): Buffer {
45
+ const salt = randomBytes(32);
46
+ const iv = randomBytes(12);
47
+ const key = deriveKey(userId, salt);
48
+
49
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
50
+ const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
51
+ const authTag = cipher.getAuthTag();
52
+
53
+ return Buffer.concat([SYNC_VERSION, salt, iv, encrypted, authTag]);
54
+ }
55
+
56
+ /**
57
+ * Decrypt an envelope back to raw data.
58
+ */
59
+ function decrypt(envelope: Buffer, userId: string): Buffer {
60
+ // Parse envelope
61
+ const version = envelope.subarray(0, 4);
62
+ if (!version.equals(SYNC_VERSION)) {
63
+ throw new Error(`Unsupported sync version: ${version.toString("hex")}`);
64
+ }
65
+
66
+ const salt = envelope.subarray(4, 36);
67
+ const iv = envelope.subarray(36, 48);
68
+ const authTag = envelope.subarray(envelope.length - 16);
69
+ const ciphertext = envelope.subarray(48, envelope.length - 16);
70
+
71
+ const key = deriveKey(userId, salt);
72
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
73
+ decipher.setAuthTag(authTag);
74
+
75
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
76
+ }
77
+
78
+ /**
79
+ * Export the SQLite database, encrypt it, and upload to Supabase Storage.
80
+ */
81
+ export async function exportAndUpload(
82
+ userId: string,
83
+ deviceId: string,
84
+ accessToken?: string
85
+ ): Promise<SyncResult> {
86
+ let supabase = getSupabaseClient();
87
+ if (!supabase) return { success: false, error: "Cloud not configured" };
88
+
89
+ // If an access token is provided, create an authenticated client for Storage RLS
90
+ if (accessToken) {
91
+ const { createClient } = await import("@supabase/supabase-js");
92
+ const url = process.env.NEXT_PUBLIC_SUPABASE_URL || "https://yznantjbmacbllhcyzwc.supabase.co";
93
+ const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl6bmFudGpibWFjYmxsaGN5endjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI1MDg1ODMsImV4cCI6MjA4ODA4NDU4M30.i-P7MXpR1_emBjhUkzbFeSX7fgjgPDv90_wkqF7sW3Y";
94
+ supabase = createClient(url, anonKey, {
95
+ global: { headers: { Authorization: `Bearer ${accessToken}` } },
96
+ auth: { persistSession: false },
97
+ });
98
+ }
99
+
100
+ try {
101
+ // 1. Create a consistent backup using better-sqlite3 .backup()
102
+ const tempPath = join(tmpdir(), `stagent-export-${Date.now()}.db`);
103
+ await sqlite.backup(tempPath);
104
+
105
+ // 2. Read and encrypt
106
+ const dbBuffer = readFileSync(tempPath);
107
+ const encrypted = encrypt(dbBuffer, userId);
108
+
109
+ // 3. Upload to Supabase Storage
110
+ const blobPath = `${userId}/${deviceId}/${Date.now()}.enc`;
111
+ const { error: uploadError } = await supabase.storage
112
+ .from(BUCKET_NAME)
113
+ .upload(blobPath, encrypted, {
114
+ contentType: "application/octet-stream",
115
+ upsert: false,
116
+ });
117
+
118
+ if (uploadError) {
119
+ return { success: false, error: uploadError.message };
120
+ }
121
+
122
+ // 4. Record sync session
123
+ await supabase.from("sync_sessions").insert({
124
+ user_id: userId,
125
+ device_name: deviceId,
126
+ device_id: deviceId,
127
+ blob_path: blobPath,
128
+ blob_size_bytes: encrypted.length,
129
+ sync_type: "backup",
130
+ status: "completed",
131
+ });
132
+
133
+ // Clean up temp file
134
+ try { require("fs").unlinkSync(tempPath); } catch { /* ignore */ }
135
+
136
+ return { success: true, blobPath, sizeBytes: encrypted.length };
137
+ } catch (err) {
138
+ return { success: false, error: err instanceof Error ? err.message : "Export failed" };
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Download the latest snapshot and restore it.
144
+ * Always creates a safety backup before restoring.
145
+ */
146
+ export async function downloadAndRestore(userId: string): Promise<SyncResult> {
147
+ const supabase = getSupabaseClient();
148
+ if (!supabase) return { success: false, error: "Cloud not configured" };
149
+
150
+ try {
151
+ // 1. Find the latest snapshot
152
+ const { data: sessions, error: listError } = await supabase
153
+ .from("sync_sessions")
154
+ .select("blob_path, blob_size_bytes")
155
+ .eq("user_id", userId)
156
+ .eq("sync_type", "backup")
157
+ .eq("status", "completed")
158
+ .order("created_at", { ascending: false })
159
+ .limit(1);
160
+
161
+ if (listError || !sessions?.length) {
162
+ return { success: false, error: "No backup found" };
163
+ }
164
+
165
+ const { blob_path } = sessions[0];
166
+
167
+ // 2. Download encrypted snapshot
168
+ const { data: blob, error: downloadError } = await supabase.storage
169
+ .from(BUCKET_NAME)
170
+ .download(blob_path);
171
+
172
+ if (downloadError || !blob) {
173
+ return { success: false, error: downloadError?.message ?? "Download failed" };
174
+ }
175
+
176
+ const encrypted = Buffer.from(await blob.arrayBuffer());
177
+
178
+ // 3. Decrypt
179
+ const decrypted = decrypt(encrypted, userId);
180
+
181
+ // 4. Create safety backup of current DB
182
+ const dataDir = getStagentDataDir();
183
+ const safetyPath = join(dataDir, `stagent-safety-${Date.now()}.db`);
184
+ const dbPath = join(dataDir, "stagent.db");
185
+ if (existsSync(dbPath)) {
186
+ copyFileSync(dbPath, safetyPath);
187
+ }
188
+
189
+ // 5. Write restored DB to temp location and validate
190
+ const tempRestore = join(tmpdir(), `stagent-restore-${Date.now()}.db`);
191
+ writeFileSync(tempRestore, decrypted);
192
+
193
+ // Basic validation: check it's a valid SQLite file
194
+ const header = decrypted.subarray(0, 16).toString("ascii");
195
+ if (!header.startsWith("SQLite format 3")) {
196
+ return { success: false, error: "Decrypted data is not a valid SQLite database" };
197
+ }
198
+
199
+ // 6. Replace current DB (requires app restart)
200
+ copyFileSync(tempRestore, dbPath);
201
+
202
+ // Clean up
203
+ try { require("fs").unlinkSync(tempRestore); } catch { /* ignore */ }
204
+
205
+ // Record restore session
206
+ await supabase.from("sync_sessions").insert({
207
+ user_id: userId,
208
+ device_name: "restore",
209
+ device_id: "restore",
210
+ blob_path,
211
+ blob_size_bytes: decrypted.length,
212
+ sync_type: "restore",
213
+ status: "completed",
214
+ });
215
+
216
+ return { success: true, sizeBytes: decrypted.length };
217
+ } catch (err) {
218
+ return { success: false, error: err instanceof Error ? err.message : "Restore failed" };
219
+ }
220
+ }
221
+
222
+ /**
223
+ * List recent sync sessions for the user.
224
+ */
225
+ export async function listSyncSessions(userId: string) {
226
+ const supabase = getSupabaseClient();
227
+ if (!supabase) return [];
228
+
229
+ const { data } = await supabase
230
+ .from("sync_sessions")
231
+ .select("*")
232
+ .eq("user_id", userId)
233
+ .order("created_at", { ascending: false })
234
+ .limit(20);
235
+
236
+ return data ?? [];
237
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Conversion funnel tracking — lightweight, anonymous event tracking
3
+ * for Community→Premium conversion optimization.
4
+ *
5
+ * Events: banner_impression, banner_click, checkout_started,
6
+ * checkout_completed, limit_hit
7
+ *
8
+ * Fire-and-forget — never blocks user actions. No PII.
9
+ * No-op if Supabase is not configured.
10
+ */
11
+
12
+ import { isCloudConfigured } from "@/lib/cloud/supabase-client";
13
+ import { getSettingSync, setSetting } from "@/lib/settings/helpers";
14
+
15
+ const SESSION_KEY = "conversion.sessionId";
16
+
17
+ export type ConversionEventType =
18
+ | "banner_impression"
19
+ | "banner_click"
20
+ | "checkout_started"
21
+ | "checkout_completed"
22
+ | "limit_hit";
23
+
24
+ /**
25
+ * Get or create an anonymous session ID for conversion tracking.
26
+ * Stored in settings, not tied to any user identity.
27
+ */
28
+ function getSessionId(): string {
29
+ let id = getSettingSync(SESSION_KEY);
30
+ if (!id) {
31
+ id = crypto.randomUUID();
32
+ setSetting(SESSION_KEY, id).catch(() => {});
33
+ }
34
+ return id;
35
+ }
36
+
37
+ /**
38
+ * Track a conversion funnel event. Fire-and-forget.
39
+ *
40
+ * @param eventType - The event type
41
+ * @param source - Where the event originated (e.g., "memory_banner", "schedule_gate")
42
+ * @param metadata - Optional additional context
43
+ */
44
+ export function trackConversionEvent(
45
+ eventType: ConversionEventType,
46
+ source?: string,
47
+ metadata?: Record<string, unknown>
48
+ ): void {
49
+ if (!isCloudConfigured()) return;
50
+
51
+ const sessionId = getSessionId();
52
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
53
+ const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
54
+
55
+ if (!supabaseUrl || !anonKey) return;
56
+
57
+ // Fire-and-forget — don't await, don't block
58
+ fetch(`${supabaseUrl}/functions/v1/conversion-ingest`, {
59
+ method: "POST",
60
+ headers: {
61
+ "Content-Type": "application/json",
62
+ Authorization: `Bearer ${anonKey}`,
63
+ },
64
+ body: JSON.stringify({
65
+ eventType,
66
+ sessionId,
67
+ source: source ?? null,
68
+ metadata: metadata ?? null,
69
+ }),
70
+ }).catch(() => {
71
+ // Silently ignore — conversion tracking is non-critical
72
+ });
73
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Telemetry batch queue — opt-in anonymized usage data.
3
+ *
4
+ * Events are queued in the settings table as a JSON array,
5
+ * flushed every 5 minutes to the Supabase telemetry-ingest Edge Function.
6
+ * Capped at 200 events to prevent unbounded growth.
7
+ *
8
+ * EXPLICITLY ABSENT from events: taskId, projectId, taskTitle,
9
+ * description, result, userId, email (no PII).
10
+ */
11
+
12
+ import { getSettingSync, setSetting } from "@/lib/settings/helpers";
13
+ import { SETTINGS_KEYS } from "@/lib/constants/settings";
14
+ import { isCloudConfigured } from "@/lib/cloud/supabase-client";
15
+
16
+ const MAX_BATCH_SIZE = 200;
17
+ const FLUSH_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
18
+
19
+ export interface TelemetryEvent {
20
+ runtimeId: string;
21
+ providerId: string;
22
+ modelId: string;
23
+ profileDomain?: string;
24
+ workflowPattern?: string;
25
+ activityType: string;
26
+ outcomeStatus?: string;
27
+ tokenCount?: number;
28
+ costMicros?: number;
29
+ durationMs?: number;
30
+ stepCount?: number;
31
+ }
32
+
33
+ /**
34
+ * Check if telemetry is opt-in enabled.
35
+ */
36
+ export function isTelemetryEnabled(): boolean {
37
+ return getSettingSync(SETTINGS_KEYS.TELEMETRY_ENABLED) === "true";
38
+ }
39
+
40
+ /**
41
+ * Get or create the anonymous runtime ID.
42
+ */
43
+ export function getRuntimeId(): string {
44
+ let id = getSettingSync(SETTINGS_KEYS.TELEMETRY_RUNTIME_ID);
45
+ if (!id) {
46
+ id = crypto.randomUUID();
47
+ setSetting(SETTINGS_KEYS.TELEMETRY_RUNTIME_ID, id).catch(() => {});
48
+ }
49
+ return id;
50
+ }
51
+
52
+ /**
53
+ * Queue a telemetry event for batch flush.
54
+ * No-op if telemetry is disabled.
55
+ */
56
+ export function queueTelemetryEvent(event: TelemetryEvent): void {
57
+ if (!isTelemetryEnabled()) return;
58
+
59
+ const batch = loadBatch();
60
+ if (batch.length >= MAX_BATCH_SIZE) {
61
+ // Drop oldest events when at capacity
62
+ batch.shift();
63
+ }
64
+ batch.push(event);
65
+ saveBatch(batch);
66
+ }
67
+
68
+ /**
69
+ * Flush the batch to the cloud telemetry endpoint.
70
+ * Called on an interval from instrumentation.ts.
71
+ */
72
+ export async function flushTelemetryBatch(): Promise<void> {
73
+ if (!isTelemetryEnabled() || !isCloudConfigured()) return;
74
+
75
+ const batch = loadBatch();
76
+ if (batch.length === 0) return;
77
+
78
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
79
+ const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
80
+
81
+ try {
82
+ const res = await fetch(`${supabaseUrl}/functions/v1/telemetry-ingest`, {
83
+ method: "POST",
84
+ headers: {
85
+ "Content-Type": "application/json",
86
+ Authorization: `Bearer ${anonKey}`,
87
+ },
88
+ body: JSON.stringify({ events: batch }),
89
+ });
90
+
91
+ if (res.ok) {
92
+ // Clear the batch on successful flush
93
+ saveBatch([]);
94
+ }
95
+ } catch {
96
+ // Network failure — retain batch for next flush
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Start the periodic flush timer. Call from instrumentation.ts.
102
+ */
103
+ export function startTelemetryFlush(): void {
104
+ // Flush once on startup
105
+ flushTelemetryBatch().catch(() => {});
106
+ // Then every 5 minutes
107
+ setInterval(() => flushTelemetryBatch().catch(() => {}), FLUSH_INTERVAL_MS);
108
+ }
109
+
110
+ function loadBatch(): TelemetryEvent[] {
111
+ try {
112
+ const raw = getSettingSync(SETTINGS_KEYS.TELEMETRY_BATCH);
113
+ if (!raw) return [];
114
+ return JSON.parse(raw) as TelemetryEvent[];
115
+ } catch {
116
+ return [];
117
+ }
118
+ }
119
+
120
+ function saveBatch(batch: TelemetryEvent[]): void {
121
+ setSetting(SETTINGS_KEYS.TELEMETRY_BATCH, JSON.stringify(batch)).catch(() => {});
122
+ }
@@ -248,6 +248,24 @@ export async function recordUsageLedgerEntry(input: UsageLedgerWriteInput) {
248
248
  } as const;
249
249
 
250
250
  await db.insert(usageLedger).values(row);
251
+
252
+ // Queue telemetry event (opt-in, fire-and-forget)
253
+ try {
254
+ const { queueTelemetryEvent } = await import("@/lib/telemetry/queue");
255
+ queueTelemetryEvent({
256
+ runtimeId: input.runtimeId,
257
+ providerId: input.providerId,
258
+ modelId: input.modelId ?? "unknown",
259
+ activityType: input.activityType,
260
+ outcomeStatus: status,
261
+ tokenCount: normalizedTotalTokens ?? undefined,
262
+ costMicros: resolvedCostMicros ?? undefined,
263
+ durationMs: input.finishedAt.getTime() - input.startedAt.getTime(),
264
+ });
265
+ } catch {
266
+ // Telemetry is non-critical
267
+ }
268
+
251
269
  return row;
252
270
  }
253
271
 
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+ import { TIERS } from "@/lib/license/tier-limits";
3
+
4
+ export const activateLicenseSchema = z.object({
5
+ key: z
6
+ .string()
7
+ .min(1, "License key is required")
8
+ .regex(
9
+ /^STAG-[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}$/,
10
+ "Invalid license key format (expected STAG-XXXX-XXXX-XXXX-XXXX)"
11
+ )
12
+ .optional(),
13
+ email: z.string().email("Invalid email address").optional(),
14
+ tier: z.enum(TIERS).optional(),
15
+ token: z.string().optional(),
16
+ });
17
+
18
+ export type ActivateLicenseInput = z.infer<typeof activateLicenseSchema>;
19
+
20
+ export const licenseStatusSchema = z.object({
21
+ tier: z.enum(TIERS),
22
+ status: z.enum(["active", "inactive", "grace"]),
23
+ email: z.string().nullable(),
24
+ activatedAt: z.string().nullable(),
25
+ expiresAt: z.string().nullable(),
26
+ lastValidatedAt: z.string().nullable(),
27
+ gracePeriodExpiresAt: z.string().nullable(),
28
+ isPremium: z.boolean(),
29
+ features: z.record(z.string(), z.boolean()),
30
+ limits: z.record(z.string(), z.number()),
31
+ });
32
+
33
+ export type LicenseStatusResponse = z.infer<typeof licenseStatusSchema>;
package/tsconfig.json CHANGED
@@ -39,6 +39,7 @@
39
39
  "node_modules",
40
40
  "vitest.config*.ts",
41
41
  "src/**/__tests__/**",
42
- "src/__tests__/**"
42
+ "src/__tests__/**",
43
+ "supabase/**"
43
44
  ]
44
45
  }