@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,122 @@
|
|
|
1
|
+
type SubscriptionTier = 'free' | 'starter' | 'pro' | 'enterprise';
|
|
2
|
+
type SubscriptionStatus = 'active' | 'canceled' | 'past_due' | 'trialing' | 'incomplete';
|
|
3
|
+
interface UserProfile {
|
|
4
|
+
id: string;
|
|
5
|
+
email: string;
|
|
6
|
+
fullName: string | null;
|
|
7
|
+
avatarUrl: string | null;
|
|
8
|
+
subscriptionTier: SubscriptionTier;
|
|
9
|
+
subscriptionStatus: SubscriptionStatus;
|
|
10
|
+
stripeCustomerId: string | null;
|
|
11
|
+
stripeSubscriptionId: string | null;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
updatedAt: string;
|
|
14
|
+
}
|
|
15
|
+
interface ApiKey {
|
|
16
|
+
id: string;
|
|
17
|
+
userId: string;
|
|
18
|
+
name: string;
|
|
19
|
+
keyPrefix: string;
|
|
20
|
+
lastUsedAt: string | null;
|
|
21
|
+
expiresAt: string | null;
|
|
22
|
+
revokedAt: string | null;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
}
|
|
25
|
+
interface ApiKeyWithSecret extends Omit<ApiKey, 'revokedAt'> {
|
|
26
|
+
key: string;
|
|
27
|
+
}
|
|
28
|
+
interface UsageEvent {
|
|
29
|
+
id: string;
|
|
30
|
+
userId: string;
|
|
31
|
+
apiKeyId: string | null;
|
|
32
|
+
toolName: string;
|
|
33
|
+
tokensUsed: number;
|
|
34
|
+
latencyMs: number | null;
|
|
35
|
+
success: boolean;
|
|
36
|
+
errorCode: string | null;
|
|
37
|
+
requestId: string | null;
|
|
38
|
+
metadata: Record<string, unknown>;
|
|
39
|
+
createdAt: string;
|
|
40
|
+
}
|
|
41
|
+
interface UsageSummary {
|
|
42
|
+
period: {
|
|
43
|
+
start: string;
|
|
44
|
+
end: string;
|
|
45
|
+
};
|
|
46
|
+
summary: {
|
|
47
|
+
totalToolCalls: number;
|
|
48
|
+
totalTokensUsed: number;
|
|
49
|
+
quotaUsed: number;
|
|
50
|
+
quotaLimit: number;
|
|
51
|
+
quotaPercentage: number;
|
|
52
|
+
};
|
|
53
|
+
byTool: Array<{
|
|
54
|
+
tool: string;
|
|
55
|
+
calls: number;
|
|
56
|
+
tokensUsed: number;
|
|
57
|
+
avgLatencyMs: number;
|
|
58
|
+
}>;
|
|
59
|
+
}
|
|
60
|
+
declare const QUOTA_LIMITS: Record<SubscriptionTier, number>;
|
|
61
|
+
declare const RATE_LIMITS: Record<SubscriptionTier, {
|
|
62
|
+
perMinute: number;
|
|
63
|
+
perDay: number;
|
|
64
|
+
}>;
|
|
65
|
+
type IntegrationProvider = 'jira' | 'linear' | 'github' | 'gitlab' | 'asana';
|
|
66
|
+
interface IntegrationToken {
|
|
67
|
+
id: string;
|
|
68
|
+
userId: string;
|
|
69
|
+
provider: IntegrationProvider;
|
|
70
|
+
tokenType: string;
|
|
71
|
+
expiresAt: string | null;
|
|
72
|
+
scopes: string[] | null;
|
|
73
|
+
providerUserId: string | null;
|
|
74
|
+
providerEmail: string | null;
|
|
75
|
+
metadata: Record<string, unknown>;
|
|
76
|
+
createdAt: string;
|
|
77
|
+
updatedAt: string;
|
|
78
|
+
}
|
|
79
|
+
interface IntegrationStatus {
|
|
80
|
+
provider: IntegrationProvider;
|
|
81
|
+
connected: boolean;
|
|
82
|
+
connectedAt?: string;
|
|
83
|
+
providerEmail?: string;
|
|
84
|
+
scopes?: string[];
|
|
85
|
+
metadata?: Record<string, unknown>;
|
|
86
|
+
}
|
|
87
|
+
type McpToolName = 'synthesize_requirements' | 'review_prd' | 'explode_backlog' | 'generate_architecture' | 'sync_to_tracker' | 'infer_prd_from_code' | 'reverse_engineer_user_flows' | 'generate_test_specs' | 'explain_codebase' | 'validate_implementation' | 'suggest_refactors' | 'generate_api_docs' | 'dependency_audit' | 'estimate_migration';
|
|
88
|
+
interface McpToolResult<T = unknown> {
|
|
89
|
+
content: Array<{
|
|
90
|
+
type: 'text' | 'image' | 'resource';
|
|
91
|
+
text?: string;
|
|
92
|
+
data?: string;
|
|
93
|
+
mimeType?: string;
|
|
94
|
+
}>;
|
|
95
|
+
data?: T;
|
|
96
|
+
tokensUsed?: number;
|
|
97
|
+
isError?: boolean;
|
|
98
|
+
}
|
|
99
|
+
interface ApiSuccessResponse<T> {
|
|
100
|
+
success: true;
|
|
101
|
+
data: T;
|
|
102
|
+
meta?: {
|
|
103
|
+
requestId: string;
|
|
104
|
+
timestamp: string;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
interface ApiErrorResponse {
|
|
108
|
+
success: false;
|
|
109
|
+
error: {
|
|
110
|
+
code: string;
|
|
111
|
+
message: string;
|
|
112
|
+
details?: Record<string, unknown>;
|
|
113
|
+
docUrl?: string;
|
|
114
|
+
};
|
|
115
|
+
meta?: {
|
|
116
|
+
requestId: string;
|
|
117
|
+
timestamp: string;
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
type ApiResponse<T> = ApiSuccessResponse<T> | ApiErrorResponse;
|
|
121
|
+
|
|
122
|
+
export { type ApiErrorResponse, type ApiKey, type ApiKeyWithSecret, type ApiResponse, type ApiSuccessResponse, type IntegrationProvider, type IntegrationStatus, type IntegrationToken, type McpToolName, type McpToolResult, QUOTA_LIMITS, RATE_LIMITS, type SubscriptionStatus, type SubscriptionTier, type UsageEvent, type UsageSummary, type UserProfile };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/types/index.ts
|
|
21
|
+
var types_exports = {};
|
|
22
|
+
__export(types_exports, {
|
|
23
|
+
QUOTA_LIMITS: () => QUOTA_LIMITS,
|
|
24
|
+
RATE_LIMITS: () => RATE_LIMITS
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(types_exports);
|
|
27
|
+
var QUOTA_LIMITS = {
|
|
28
|
+
free: 5,
|
|
29
|
+
starter: 100,
|
|
30
|
+
pro: 500,
|
|
31
|
+
enterprise: 999999
|
|
32
|
+
// Effectively unlimited
|
|
33
|
+
};
|
|
34
|
+
var RATE_LIMITS = {
|
|
35
|
+
free: { perMinute: 10, perDay: 100 },
|
|
36
|
+
starter: { perMinute: 30, perDay: 1e3 },
|
|
37
|
+
pro: { perMinute: 60, perDay: 5e3 },
|
|
38
|
+
enterprise: { perMinute: 120, perDay: 999999 }
|
|
39
|
+
};
|
|
40
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
41
|
+
0 && (module.exports = {
|
|
42
|
+
QUOTA_LIMITS,
|
|
43
|
+
RATE_LIMITS
|
|
44
|
+
});
|
|
45
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/types/index.ts"],"sourcesContent":["// Subscription Types\nexport type SubscriptionTier = 'free' | 'starter' | 'pro' | 'enterprise';\nexport type SubscriptionStatus = 'active' | 'canceled' | 'past_due' | 'trialing' | 'incomplete';\n\n// User Profile\nexport interface UserProfile {\n id: string;\n email: string;\n fullName: string | null;\n avatarUrl: string | null;\n subscriptionTier: SubscriptionTier;\n subscriptionStatus: SubscriptionStatus;\n stripeCustomerId: string | null;\n stripeSubscriptionId: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\n// API Key\nexport interface ApiKey {\n id: string;\n userId: string;\n name: string;\n keyPrefix: string;\n lastUsedAt: string | null;\n expiresAt: string | null;\n revokedAt: string | null;\n createdAt: string;\n}\n\nexport interface ApiKeyWithSecret extends Omit<ApiKey, 'revokedAt'> {\n key: string; // Full key (only shown once)\n}\n\n// Usage\nexport interface UsageEvent {\n id: string;\n userId: string;\n apiKeyId: string | null;\n toolName: string;\n tokensUsed: number;\n latencyMs: number | null;\n success: boolean;\n errorCode: string | null;\n requestId: string | null;\n metadata: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface UsageSummary {\n period: {\n start: string;\n end: string;\n };\n summary: {\n totalToolCalls: number;\n totalTokensUsed: number;\n quotaUsed: number;\n quotaLimit: number;\n quotaPercentage: number;\n };\n byTool: Array<{\n tool: string;\n calls: number;\n tokensUsed: number;\n avgLatencyMs: number;\n }>;\n}\n\n// Quota Limits by Tier\nexport const QUOTA_LIMITS: Record<SubscriptionTier, number> = {\n free: 5,\n starter: 100,\n pro: 500,\n enterprise: 999999, // Effectively unlimited\n};\n\nexport const RATE_LIMITS: Record<SubscriptionTier, { perMinute: number; perDay: number }> = {\n free: { perMinute: 10, perDay: 100 },\n starter: { perMinute: 30, perDay: 1000 },\n pro: { perMinute: 60, perDay: 5000 },\n enterprise: { perMinute: 120, perDay: 999999 },\n};\n\n// Integration Types\nexport type IntegrationProvider = 'jira' | 'linear' | 'github' | 'gitlab' | 'asana';\n\nexport interface IntegrationToken {\n id: string;\n userId: string;\n provider: IntegrationProvider;\n tokenType: string;\n expiresAt: string | null;\n scopes: string[] | null;\n providerUserId: string | null;\n providerEmail: string | null;\n metadata: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface IntegrationStatus {\n provider: IntegrationProvider;\n connected: boolean;\n connectedAt?: string;\n providerEmail?: string;\n scopes?: string[];\n metadata?: Record<string, unknown>;\n}\n\n// MCP Tool Types\nexport type McpToolName =\n | 'synthesize_requirements'\n | 'review_prd'\n | 'explode_backlog'\n | 'generate_architecture'\n | 'sync_to_tracker'\n | 'infer_prd_from_code'\n | 'reverse_engineer_user_flows'\n | 'generate_test_specs'\n | 'explain_codebase'\n | 'validate_implementation'\n | 'suggest_refactors'\n | 'generate_api_docs'\n | 'dependency_audit'\n | 'estimate_migration';\n\nexport interface McpToolResult<T = unknown> {\n content: Array<{\n type: 'text' | 'image' | 'resource';\n text?: string;\n data?: string;\n mimeType?: string;\n }>;\n data?: T;\n tokensUsed?: number;\n isError?: boolean;\n}\n\n// API Response Types\nexport interface ApiSuccessResponse<T> {\n success: true;\n data: T;\n meta?: {\n requestId: string;\n timestamp: string;\n };\n}\n\nexport interface ApiErrorResponse {\n success: false;\n error: {\n code: string;\n message: string;\n details?: Record<string, unknown>;\n docUrl?: string;\n };\n meta?: {\n requestId: string;\n timestamp: string;\n };\n}\n\nexport type ApiResponse<T> = ApiSuccessResponse<T> | ApiErrorResponse;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsEO,IAAM,eAAiD;AAAA,EAC5D,MAAM;AAAA,EACN,SAAS;AAAA,EACT,KAAK;AAAA,EACL,YAAY;AAAA;AACd;AAEO,IAAM,cAA+E;AAAA,EAC1F,MAAM,EAAE,WAAW,IAAI,QAAQ,IAAI;AAAA,EACnC,SAAS,EAAE,WAAW,IAAI,QAAQ,IAAK;AAAA,EACvC,KAAK,EAAE,WAAW,IAAI,QAAQ,IAAK;AAAA,EACnC,YAAY,EAAE,WAAW,KAAK,QAAQ,OAAO;AAC/C;","names":[]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/types/index.ts
|
|
2
|
+
var QUOTA_LIMITS = {
|
|
3
|
+
free: 5,
|
|
4
|
+
starter: 100,
|
|
5
|
+
pro: 500,
|
|
6
|
+
enterprise: 999999
|
|
7
|
+
// Effectively unlimited
|
|
8
|
+
};
|
|
9
|
+
var RATE_LIMITS = {
|
|
10
|
+
free: { perMinute: 10, perDay: 100 },
|
|
11
|
+
starter: { perMinute: 30, perDay: 1e3 },
|
|
12
|
+
pro: { perMinute: 60, perDay: 5e3 },
|
|
13
|
+
enterprise: { perMinute: 120, perDay: 999999 }
|
|
14
|
+
};
|
|
15
|
+
export {
|
|
16
|
+
QUOTA_LIMITS,
|
|
17
|
+
RATE_LIMITS
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/types/index.ts"],"sourcesContent":["// Subscription Types\nexport type SubscriptionTier = 'free' | 'starter' | 'pro' | 'enterprise';\nexport type SubscriptionStatus = 'active' | 'canceled' | 'past_due' | 'trialing' | 'incomplete';\n\n// User Profile\nexport interface UserProfile {\n id: string;\n email: string;\n fullName: string | null;\n avatarUrl: string | null;\n subscriptionTier: SubscriptionTier;\n subscriptionStatus: SubscriptionStatus;\n stripeCustomerId: string | null;\n stripeSubscriptionId: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\n// API Key\nexport interface ApiKey {\n id: string;\n userId: string;\n name: string;\n keyPrefix: string;\n lastUsedAt: string | null;\n expiresAt: string | null;\n revokedAt: string | null;\n createdAt: string;\n}\n\nexport interface ApiKeyWithSecret extends Omit<ApiKey, 'revokedAt'> {\n key: string; // Full key (only shown once)\n}\n\n// Usage\nexport interface UsageEvent {\n id: string;\n userId: string;\n apiKeyId: string | null;\n toolName: string;\n tokensUsed: number;\n latencyMs: number | null;\n success: boolean;\n errorCode: string | null;\n requestId: string | null;\n metadata: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface UsageSummary {\n period: {\n start: string;\n end: string;\n };\n summary: {\n totalToolCalls: number;\n totalTokensUsed: number;\n quotaUsed: number;\n quotaLimit: number;\n quotaPercentage: number;\n };\n byTool: Array<{\n tool: string;\n calls: number;\n tokensUsed: number;\n avgLatencyMs: number;\n }>;\n}\n\n// Quota Limits by Tier\nexport const QUOTA_LIMITS: Record<SubscriptionTier, number> = {\n free: 5,\n starter: 100,\n pro: 500,\n enterprise: 999999, // Effectively unlimited\n};\n\nexport const RATE_LIMITS: Record<SubscriptionTier, { perMinute: number; perDay: number }> = {\n free: { perMinute: 10, perDay: 100 },\n starter: { perMinute: 30, perDay: 1000 },\n pro: { perMinute: 60, perDay: 5000 },\n enterprise: { perMinute: 120, perDay: 999999 },\n};\n\n// Integration Types\nexport type IntegrationProvider = 'jira' | 'linear' | 'github' | 'gitlab' | 'asana';\n\nexport interface IntegrationToken {\n id: string;\n userId: string;\n provider: IntegrationProvider;\n tokenType: string;\n expiresAt: string | null;\n scopes: string[] | null;\n providerUserId: string | null;\n providerEmail: string | null;\n metadata: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface IntegrationStatus {\n provider: IntegrationProvider;\n connected: boolean;\n connectedAt?: string;\n providerEmail?: string;\n scopes?: string[];\n metadata?: Record<string, unknown>;\n}\n\n// MCP Tool Types\nexport type McpToolName =\n | 'synthesize_requirements'\n | 'review_prd'\n | 'explode_backlog'\n | 'generate_architecture'\n | 'sync_to_tracker'\n | 'infer_prd_from_code'\n | 'reverse_engineer_user_flows'\n | 'generate_test_specs'\n | 'explain_codebase'\n | 'validate_implementation'\n | 'suggest_refactors'\n | 'generate_api_docs'\n | 'dependency_audit'\n | 'estimate_migration';\n\nexport interface McpToolResult<T = unknown> {\n content: Array<{\n type: 'text' | 'image' | 'resource';\n text?: string;\n data?: string;\n mimeType?: string;\n }>;\n data?: T;\n tokensUsed?: number;\n isError?: boolean;\n}\n\n// API Response Types\nexport interface ApiSuccessResponse<T> {\n success: true;\n data: T;\n meta?: {\n requestId: string;\n timestamp: string;\n };\n}\n\nexport interface ApiErrorResponse {\n success: false;\n error: {\n code: string;\n message: string;\n details?: Record<string, unknown>;\n docUrl?: string;\n };\n meta?: {\n requestId: string;\n timestamp: string;\n };\n}\n\nexport type ApiResponse<T> = ApiSuccessResponse<T> | ApiErrorResponse;\n"],"mappings":";AAsEO,IAAM,eAAiD;AAAA,EAC5D,MAAM;AAAA,EACN,SAAS;AAAA,EACT,KAAK;AAAA,EACL,YAAY;AAAA;AACd;AAEO,IAAM,cAA+E;AAAA,EAC1F,MAAM,EAAE,WAAW,IAAI,QAAQ,IAAI;AAAA,EACnC,SAAS,EAAE,WAAW,IAAI,QAAQ,IAAK;AAAA,EACvC,KAAK,EAAE,WAAW,IAAI,QAAQ,IAAK;AAAA,EACnC,YAAY,EAAE,WAAW,KAAK,QAAQ,OAAO;AAC/C;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vasperacapital/vaspera-shared",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared utilities for VasperaPM - API key generation, encryption, error handling",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/rcolkitt/VasperaPM"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"module": "./dist/index.mjs",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.mjs",
|
|
20
|
+
"require": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./errors": {
|
|
23
|
+
"types": "./dist/errors/index.d.ts",
|
|
24
|
+
"import": "./dist/errors/index.mjs",
|
|
25
|
+
"require": "./dist/errors/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./types": {
|
|
28
|
+
"types": "./dist/types/index.d.ts",
|
|
29
|
+
"import": "./dist/types/index.mjs",
|
|
30
|
+
"require": "./dist/types/index.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsup",
|
|
35
|
+
"dev": "tsup --watch",
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"clean": "rm -rf dist"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"zod": "^3.25.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^20",
|
|
44
|
+
"@vaspera/tsconfig": "workspace:*",
|
|
45
|
+
"tsup": "^8.3.0",
|
|
46
|
+
"typescript": "^5"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
generateApiKey,
|
|
4
|
+
hashApiKey,
|
|
5
|
+
isValidApiKeyFormat,
|
|
6
|
+
extractKeyPrefix,
|
|
7
|
+
isTestKey,
|
|
8
|
+
maskApiKey,
|
|
9
|
+
} from '../utils/api-key.js';
|
|
10
|
+
|
|
11
|
+
describe('API Key Utilities', () => {
|
|
12
|
+
describe('generateApiKey', () => {
|
|
13
|
+
it('should generate a live key by default', () => {
|
|
14
|
+
const result = generateApiKey();
|
|
15
|
+
|
|
16
|
+
expect(result.key).toMatch(/^vpm_live_[a-zA-Z0-9_-]{32}$/);
|
|
17
|
+
expect(result.keyPrefix).toBe(result.key.slice(0, 16));
|
|
18
|
+
expect(result.keyHash).toBeDefined();
|
|
19
|
+
expect(result.keyHash).toHaveLength(64); // SHA-256 hex
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should generate a live key when specified', () => {
|
|
23
|
+
const result = generateApiKey('live');
|
|
24
|
+
|
|
25
|
+
expect(result.key).toMatch(/^vpm_live_[a-zA-Z0-9_-]{32}$/);
|
|
26
|
+
expect(result.key.startsWith('vpm_live_')).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should generate a test key when specified', () => {
|
|
30
|
+
const result = generateApiKey('test');
|
|
31
|
+
|
|
32
|
+
expect(result.key).toMatch(/^vpm_test_[a-zA-Z0-9_-]{32}$/);
|
|
33
|
+
expect(result.key.startsWith('vpm_test_')).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should generate unique keys each time', () => {
|
|
37
|
+
const key1 = generateApiKey();
|
|
38
|
+
const key2 = generateApiKey();
|
|
39
|
+
|
|
40
|
+
expect(key1.key).not.toBe(key2.key);
|
|
41
|
+
expect(key1.keyHash).not.toBe(key2.keyHash);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should generate consistent hash for the same key', () => {
|
|
45
|
+
const { key } = generateApiKey();
|
|
46
|
+
const hash1 = hashApiKey(key);
|
|
47
|
+
const hash2 = hashApiKey(key);
|
|
48
|
+
|
|
49
|
+
expect(hash1).toBe(hash2);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('hashApiKey', () => {
|
|
54
|
+
it('should return a SHA-256 hex hash', () => {
|
|
55
|
+
const hash = hashApiKey('vpm_live_test1234567890abcdefghij');
|
|
56
|
+
|
|
57
|
+
expect(hash).toHaveLength(64);
|
|
58
|
+
expect(hash).toMatch(/^[a-f0-9]{64}$/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should return different hashes for different keys', () => {
|
|
62
|
+
const hash1 = hashApiKey('vpm_live_key1aaaaaaaaaaaaaaaaaaaaa');
|
|
63
|
+
const hash2 = hashApiKey('vpm_live_key2bbbbbbbbbbbbbbbbbbbbb');
|
|
64
|
+
|
|
65
|
+
expect(hash1).not.toBe(hash2);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('isValidApiKeyFormat', () => {
|
|
70
|
+
it('should validate correct live key format', () => {
|
|
71
|
+
expect(isValidApiKeyFormat('vpm_live_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')).toBe(true);
|
|
72
|
+
expect(isValidApiKeyFormat('vpm_live_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef')).toBe(true);
|
|
73
|
+
expect(isValidApiKeyFormat('vpm_live_0123456789-_0123456789-_01234567')).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should validate correct test key format', () => {
|
|
77
|
+
expect(isValidApiKeyFormat('vpm_test_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')).toBe(true);
|
|
78
|
+
expect(isValidApiKeyFormat('vpm_test_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef')).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should reject invalid prefixes', () => {
|
|
82
|
+
expect(isValidApiKeyFormat('vpm_prod_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')).toBe(false);
|
|
83
|
+
expect(isValidApiKeyFormat('xxx_live_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')).toBe(false);
|
|
84
|
+
expect(isValidApiKeyFormat('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should reject keys with wrong length', () => {
|
|
88
|
+
expect(isValidApiKeyFormat('vpm_live_short')).toBe(false);
|
|
89
|
+
expect(isValidApiKeyFormat('vpm_live_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaextralong')).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should reject keys with invalid characters', () => {
|
|
93
|
+
expect(isValidApiKeyFormat('vpm_live_aaaaaaaaaaaaaaaa!@#$aaaaaaaaaaaaa')).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('extractKeyPrefix', () => {
|
|
98
|
+
it('should extract first 16 characters', () => {
|
|
99
|
+
const key = 'vpm_live_abcdefghijklmnopqrstuvwxyz012345';
|
|
100
|
+
expect(extractKeyPrefix(key)).toBe('vpm_live_abcdefg');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('isTestKey', () => {
|
|
105
|
+
it('should return true for test keys', () => {
|
|
106
|
+
expect(isTestKey('vpm_test_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should return false for live keys', () => {
|
|
110
|
+
expect(isTestKey('vpm_live_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('maskApiKey', () => {
|
|
115
|
+
it('should mask middle portion of key', () => {
|
|
116
|
+
const key = 'vpm_live_abcdefghijklmnopqrstuvwxyz012345';
|
|
117
|
+
const masked = maskApiKey(key);
|
|
118
|
+
|
|
119
|
+
expect(masked).toBe('vpm_live_abcdefg...2345');
|
|
120
|
+
expect(masked).toContain('...');
|
|
121
|
+
expect(masked.startsWith('vpm_live_')).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should return short keys unchanged', () => {
|
|
125
|
+
const shortKey = 'short_key';
|
|
126
|
+
expect(maskApiKey(shortKey)).toBe(shortKey);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
encrypt,
|
|
4
|
+
decrypt,
|
|
5
|
+
generateEncryptionSecret,
|
|
6
|
+
} from '../utils/encryption.js';
|
|
7
|
+
|
|
8
|
+
describe('Encryption Utilities', () => {
|
|
9
|
+
describe('generateEncryptionSecret', () => {
|
|
10
|
+
it('should generate a 64-character hex string', () => {
|
|
11
|
+
const secret = generateEncryptionSecret();
|
|
12
|
+
|
|
13
|
+
expect(secret).toHaveLength(64);
|
|
14
|
+
expect(secret).toMatch(/^[a-f0-9]{64}$/);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should generate unique secrets each time', () => {
|
|
18
|
+
const secret1 = generateEncryptionSecret();
|
|
19
|
+
const secret2 = generateEncryptionSecret();
|
|
20
|
+
|
|
21
|
+
expect(secret1).not.toBe(secret2);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('encrypt and decrypt', () => {
|
|
26
|
+
it('should encrypt and decrypt text correctly', async () => {
|
|
27
|
+
const secret = generateEncryptionSecret();
|
|
28
|
+
const plaintext = 'Hello, World!';
|
|
29
|
+
|
|
30
|
+
const encrypted = await encrypt(plaintext, secret);
|
|
31
|
+
const decrypted = await decrypt(encrypted, secret);
|
|
32
|
+
|
|
33
|
+
expect(decrypted).toBe(plaintext);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should produce different ciphertext for same plaintext', async () => {
|
|
37
|
+
const secret = generateEncryptionSecret();
|
|
38
|
+
const plaintext = 'Same text twice';
|
|
39
|
+
|
|
40
|
+
const encrypted1 = await encrypt(plaintext, secret);
|
|
41
|
+
const encrypted2 = await encrypt(plaintext, secret);
|
|
42
|
+
|
|
43
|
+
// Due to random salt/IV, encryptions should be different
|
|
44
|
+
expect(encrypted1).not.toBe(encrypted2);
|
|
45
|
+
|
|
46
|
+
// But both should decrypt to same plaintext
|
|
47
|
+
expect(await decrypt(encrypted1, secret)).toBe(plaintext);
|
|
48
|
+
expect(await decrypt(encrypted2, secret)).toBe(plaintext);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should encrypt/decrypt unicode text', async () => {
|
|
52
|
+
const secret = generateEncryptionSecret();
|
|
53
|
+
const plaintext = '你好世界! 🚀 Привет мир!';
|
|
54
|
+
|
|
55
|
+
const encrypted = await encrypt(plaintext, secret);
|
|
56
|
+
const decrypted = await decrypt(encrypted, secret);
|
|
57
|
+
|
|
58
|
+
expect(decrypted).toBe(plaintext);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should encrypt/decrypt long text', async () => {
|
|
62
|
+
const secret = generateEncryptionSecret();
|
|
63
|
+
const plaintext = 'A'.repeat(10000);
|
|
64
|
+
|
|
65
|
+
const encrypted = await encrypt(plaintext, secret);
|
|
66
|
+
const decrypted = await decrypt(encrypted, secret);
|
|
67
|
+
|
|
68
|
+
expect(decrypted).toBe(plaintext);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should encrypt/decrypt empty string', async () => {
|
|
72
|
+
const secret = generateEncryptionSecret();
|
|
73
|
+
const plaintext = '';
|
|
74
|
+
|
|
75
|
+
const encrypted = await encrypt(plaintext, secret);
|
|
76
|
+
const decrypted = await decrypt(encrypted, secret);
|
|
77
|
+
|
|
78
|
+
expect(decrypted).toBe(plaintext);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should produce encrypted text in correct format', async () => {
|
|
82
|
+
const secret = generateEncryptionSecret();
|
|
83
|
+
const encrypted = await encrypt('test', secret);
|
|
84
|
+
|
|
85
|
+
// Format: salt:iv:tag:ciphertext (all base64)
|
|
86
|
+
const parts = encrypted.split(':');
|
|
87
|
+
expect(parts).toHaveLength(4);
|
|
88
|
+
|
|
89
|
+
// Each part should be valid base64
|
|
90
|
+
parts.forEach(part => {
|
|
91
|
+
expect(() => Buffer.from(part, 'base64')).not.toThrow();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should fail decryption with wrong secret', async () => {
|
|
96
|
+
const secret1 = generateEncryptionSecret();
|
|
97
|
+
const secret2 = generateEncryptionSecret();
|
|
98
|
+
const plaintext = 'Secret message';
|
|
99
|
+
|
|
100
|
+
const encrypted = await encrypt(plaintext, secret1);
|
|
101
|
+
|
|
102
|
+
await expect(decrypt(encrypted, secret2)).rejects.toThrow();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should fail decryption with invalid format', async () => {
|
|
106
|
+
const secret = generateEncryptionSecret();
|
|
107
|
+
|
|
108
|
+
await expect(decrypt('not:enough:parts', secret)).rejects.toThrow(
|
|
109
|
+
'Invalid encrypted text format'
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
await expect(decrypt('single', secret)).rejects.toThrow(
|
|
113
|
+
'Invalid encrypted text format'
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should fail decryption with tampered ciphertext', async () => {
|
|
118
|
+
const secret = generateEncryptionSecret();
|
|
119
|
+
const encrypted = await encrypt('test', secret);
|
|
120
|
+
|
|
121
|
+
// Tamper with the ciphertext part
|
|
122
|
+
const parts = encrypted.split(':');
|
|
123
|
+
parts[3] = 'AAAA' + parts[3]!.slice(4); // Modify ciphertext
|
|
124
|
+
const tampered = parts.join(':');
|
|
125
|
+
|
|
126
|
+
await expect(decrypt(tampered, secret)).rejects.toThrow();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|