@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.
- package/.turbo/turbo-build.log +34 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/dist/errors/index.d.mts +288 -0
- package/dist/errors/index.d.ts +288 -0
- package/dist/errors/index.js +341 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/index.mjs +310 -0
- package/dist/errors/index.mjs.map +1 -0
- package/dist/index.d.mts +57 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +458 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +421 -0
- package/dist/index.mjs.map +1 -0
- package/dist/types/index.d.mts +122 -0
- package/dist/types/index.d.ts +122 -0
- package/dist/types/index.js +45 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/index.mjs +19 -0
- package/dist/types/index.mjs.map +1 -0
- package/package.json +48 -0
- package/src/__tests__/api-key.test.ts +129 -0
- package/src/__tests__/encryption.test.ts +129 -0
- package/src/__tests__/errors.test.ts +185 -0
- package/src/errors/codes.ts +213 -0
- package/src/errors/factory.ts +164 -0
- package/src/errors/index.ts +10 -0
- package/src/index.ts +8 -0
- package/src/types/index.ts +164 -0
- package/src/utils/api-key.ts +72 -0
- package/src/utils/encryption.ts +79 -0
- package/src/utils/index.ts +15 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +10 -0
|
@@ -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
package/tsup.config.ts
ADDED