@vasperacapital/vaspera-shared 0.1.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.
@@ -0,0 +1,164 @@
1
+ // Subscription Types
2
+ export type SubscriptionTier = 'free' | 'starter' | 'pro' | 'enterprise';
3
+ export type SubscriptionStatus = 'active' | 'canceled' | 'past_due' | 'trialing' | 'incomplete';
4
+
5
+ // User Profile
6
+ export interface UserProfile {
7
+ id: string;
8
+ email: string;
9
+ fullName: string | null;
10
+ avatarUrl: string | null;
11
+ subscriptionTier: SubscriptionTier;
12
+ subscriptionStatus: SubscriptionStatus;
13
+ stripeCustomerId: string | null;
14
+ stripeSubscriptionId: string | null;
15
+ createdAt: string;
16
+ updatedAt: string;
17
+ }
18
+
19
+ // API Key
20
+ export interface ApiKey {
21
+ id: string;
22
+ userId: string;
23
+ name: string;
24
+ keyPrefix: string;
25
+ lastUsedAt: string | null;
26
+ expiresAt: string | null;
27
+ revokedAt: string | null;
28
+ createdAt: string;
29
+ }
30
+
31
+ export interface ApiKeyWithSecret extends Omit<ApiKey, 'revokedAt'> {
32
+ key: string; // Full key (only shown once)
33
+ }
34
+
35
+ // Usage
36
+ export interface UsageEvent {
37
+ id: string;
38
+ userId: string;
39
+ apiKeyId: string | null;
40
+ toolName: string;
41
+ tokensUsed: number;
42
+ latencyMs: number | null;
43
+ success: boolean;
44
+ errorCode: string | null;
45
+ requestId: string | null;
46
+ metadata: Record<string, unknown>;
47
+ createdAt: string;
48
+ }
49
+
50
+ export interface UsageSummary {
51
+ period: {
52
+ start: string;
53
+ end: string;
54
+ };
55
+ summary: {
56
+ totalToolCalls: number;
57
+ totalTokensUsed: number;
58
+ quotaUsed: number;
59
+ quotaLimit: number;
60
+ quotaPercentage: number;
61
+ };
62
+ byTool: Array<{
63
+ tool: string;
64
+ calls: number;
65
+ tokensUsed: number;
66
+ avgLatencyMs: number;
67
+ }>;
68
+ }
69
+
70
+ // Quota Limits by Tier
71
+ export const QUOTA_LIMITS: Record<SubscriptionTier, number> = {
72
+ free: 5,
73
+ starter: 100,
74
+ pro: 500,
75
+ enterprise: 999999, // Effectively unlimited
76
+ };
77
+
78
+ export const RATE_LIMITS: Record<SubscriptionTier, { perMinute: number; perDay: number }> = {
79
+ free: { perMinute: 10, perDay: 100 },
80
+ starter: { perMinute: 30, perDay: 1000 },
81
+ pro: { perMinute: 60, perDay: 5000 },
82
+ enterprise: { perMinute: 120, perDay: 999999 },
83
+ };
84
+
85
+ // Integration Types
86
+ export type IntegrationProvider = 'jira' | 'linear' | 'github' | 'gitlab' | 'asana';
87
+
88
+ export interface IntegrationToken {
89
+ id: string;
90
+ userId: string;
91
+ provider: IntegrationProvider;
92
+ tokenType: string;
93
+ expiresAt: string | null;
94
+ scopes: string[] | null;
95
+ providerUserId: string | null;
96
+ providerEmail: string | null;
97
+ metadata: Record<string, unknown>;
98
+ createdAt: string;
99
+ updatedAt: string;
100
+ }
101
+
102
+ export interface IntegrationStatus {
103
+ provider: IntegrationProvider;
104
+ connected: boolean;
105
+ connectedAt?: string;
106
+ providerEmail?: string;
107
+ scopes?: string[];
108
+ metadata?: Record<string, unknown>;
109
+ }
110
+
111
+ // MCP Tool Types
112
+ export type McpToolName =
113
+ | 'synthesize_requirements'
114
+ | 'review_prd'
115
+ | 'explode_backlog'
116
+ | 'generate_architecture'
117
+ | 'sync_to_tracker'
118
+ | 'infer_prd_from_code'
119
+ | 'reverse_engineer_user_flows'
120
+ | 'generate_test_specs'
121
+ | 'explain_codebase'
122
+ | 'validate_implementation'
123
+ | 'suggest_refactors'
124
+ | 'generate_api_docs'
125
+ | 'dependency_audit'
126
+ | 'estimate_migration';
127
+
128
+ export interface McpToolResult<T = unknown> {
129
+ content: Array<{
130
+ type: 'text' | 'image' | 'resource';
131
+ text?: string;
132
+ data?: string;
133
+ mimeType?: string;
134
+ }>;
135
+ data?: T;
136
+ tokensUsed?: number;
137
+ isError?: boolean;
138
+ }
139
+
140
+ // API Response Types
141
+ export interface ApiSuccessResponse<T> {
142
+ success: true;
143
+ data: T;
144
+ meta?: {
145
+ requestId: string;
146
+ timestamp: string;
147
+ };
148
+ }
149
+
150
+ export interface ApiErrorResponse {
151
+ success: false;
152
+ error: {
153
+ code: string;
154
+ message: string;
155
+ details?: Record<string, unknown>;
156
+ docUrl?: string;
157
+ };
158
+ meta?: {
159
+ requestId: string;
160
+ timestamp: string;
161
+ };
162
+ }
163
+
164
+ export type ApiResponse<T> = ApiSuccessResponse<T> | ApiErrorResponse;
@@ -0,0 +1,72 @@
1
+ import { randomBytes, createHash } from 'crypto';
2
+
3
+ const API_KEY_PREFIX_LIVE = 'vpm_live_';
4
+ const API_KEY_PREFIX_TEST = 'vpm_test_';
5
+ const API_KEY_RANDOM_BYTES = 24; // 32 chars in base64url
6
+
7
+ export interface GeneratedApiKey {
8
+ key: string; // Full key to show user once
9
+ keyPrefix: string; // First 16 chars for identification
10
+ keyHash: string; // Hash for storage and validation
11
+ }
12
+
13
+ /**
14
+ * Generate a new API key
15
+ * @param environment - 'live' or 'test'
16
+ * @returns Generated key with prefix and hash
17
+ */
18
+ export function generateApiKey(environment: 'live' | 'test' = 'live'): GeneratedApiKey {
19
+ const prefix = environment === 'live' ? API_KEY_PREFIX_LIVE : API_KEY_PREFIX_TEST;
20
+ const randomPart = randomBytes(API_KEY_RANDOM_BYTES)
21
+ .toString('base64url')
22
+ .slice(0, 32);
23
+
24
+ const key = `${prefix}${randomPart}`;
25
+ const keyPrefix = key.slice(0, 16);
26
+ const keyHash = hashApiKey(key);
27
+
28
+ return {
29
+ key,
30
+ keyPrefix,
31
+ keyHash,
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Hash an API key for storage
37
+ * Using SHA-256 since we need to look up by hash
38
+ */
39
+ export function hashApiKey(key: string): string {
40
+ return createHash('sha256').update(key).digest('hex');
41
+ }
42
+
43
+ /**
44
+ * Validate API key format
45
+ */
46
+ export function isValidApiKeyFormat(key: string): boolean {
47
+ const livePattern = /^vpm_live_[a-zA-Z0-9_-]{32}$/;
48
+ const testPattern = /^vpm_test_[a-zA-Z0-9_-]{32}$/;
49
+ return livePattern.test(key) || testPattern.test(key);
50
+ }
51
+
52
+ /**
53
+ * Extract prefix from API key
54
+ */
55
+ export function extractKeyPrefix(key: string): string {
56
+ return key.slice(0, 16);
57
+ }
58
+
59
+ /**
60
+ * Check if key is a test key
61
+ */
62
+ export function isTestKey(key: string): boolean {
63
+ return key.startsWith(API_KEY_PREFIX_TEST);
64
+ }
65
+
66
+ /**
67
+ * Mask an API key for display (show first 16 and last 4 chars)
68
+ */
69
+ export function maskApiKey(key: string): string {
70
+ if (key.length < 24) return key;
71
+ return `${key.slice(0, 16)}...${key.slice(-4)}`;
72
+ }
@@ -0,0 +1,79 @@
1
+ import {
2
+ createCipheriv,
3
+ createDecipheriv,
4
+ randomBytes,
5
+ scrypt,
6
+ } from 'crypto';
7
+ import { promisify } from 'util';
8
+
9
+ const scryptAsync = promisify(scrypt);
10
+ const ALGORITHM = 'aes-256-gcm';
11
+ const IV_LENGTH = 16;
12
+ const SALT_LENGTH = 32;
13
+ const TAG_LENGTH = 16;
14
+ const KEY_LENGTH = 32;
15
+
16
+ /**
17
+ * Encrypt text using AES-256-GCM
18
+ * @param text - Plain text to encrypt
19
+ * @param secret - Encryption secret
20
+ * @returns Encrypted string in format: salt:iv:tag:ciphertext (all base64)
21
+ */
22
+ export async function encrypt(text: string, secret: string): Promise<string> {
23
+ const salt = randomBytes(SALT_LENGTH);
24
+ const iv = randomBytes(IV_LENGTH);
25
+ const key = (await scryptAsync(secret, salt, KEY_LENGTH)) as Buffer;
26
+
27
+ const cipher = createCipheriv(ALGORITHM, key, iv);
28
+ const encrypted = Buffer.concat([
29
+ cipher.update(text, 'utf8'),
30
+ cipher.final(),
31
+ ]);
32
+ const tag = cipher.getAuthTag();
33
+
34
+ // Format: salt:iv:tag:encrypted (all base64)
35
+ return [
36
+ salt.toString('base64'),
37
+ iv.toString('base64'),
38
+ tag.toString('base64'),
39
+ encrypted.toString('base64'),
40
+ ].join(':');
41
+ }
42
+
43
+ /**
44
+ * Decrypt text that was encrypted with encrypt()
45
+ * @param encryptedText - Encrypted string in format: salt:iv:tag:ciphertext
46
+ * @param secret - Encryption secret (must match encryption)
47
+ * @returns Decrypted plain text
48
+ */
49
+ export async function decrypt(
50
+ encryptedText: string,
51
+ secret: string
52
+ ): Promise<string> {
53
+ const parts = encryptedText.split(':');
54
+ if (parts.length !== 4) {
55
+ throw new Error('Invalid encrypted text format');
56
+ }
57
+
58
+ const [saltB64, ivB64, tagB64, encryptedB64] = parts;
59
+
60
+ const salt = Buffer.from(saltB64!, 'base64');
61
+ const iv = Buffer.from(ivB64!, 'base64');
62
+ const tag = Buffer.from(tagB64!, 'base64');
63
+ const encrypted = Buffer.from(encryptedB64!, 'base64');
64
+
65
+ const key = (await scryptAsync(secret, salt, KEY_LENGTH)) as Buffer;
66
+
67
+ const decipher = createDecipheriv(ALGORITHM, key, iv);
68
+ decipher.setAuthTag(tag);
69
+
70
+ return decipher.update(encrypted) + decipher.final('utf8');
71
+ }
72
+
73
+ /**
74
+ * Generate a random encryption secret
75
+ * @returns 32-byte hex string suitable for use as encryption secret
76
+ */
77
+ export function generateEncryptionSecret(): string {
78
+ return randomBytes(32).toString('hex');
79
+ }
@@ -0,0 +1,15 @@
1
+ export {
2
+ generateApiKey,
3
+ hashApiKey,
4
+ isValidApiKeyFormat,
5
+ extractKeyPrefix,
6
+ isTestKey,
7
+ maskApiKey,
8
+ } from './api-key.js';
9
+ export type { GeneratedApiKey } from './api-key.js';
10
+
11
+ export {
12
+ encrypt,
13
+ decrypt,
14
+ generateEncryptionSecret,
15
+ } from './encryption.js';
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "@vaspera/tsconfig/node.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"],
8
+ "exclude": ["node_modules", "dist"]
9
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts', 'src/errors/index.ts', 'src/types/index.ts'],
5
+ format: ['cjs', 'esm'],
6
+ dts: true,
7
+ splitting: false,
8
+ sourcemap: true,
9
+ clean: true,
10
+ });