relic 0.4.0 → 0.4.1

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/lib/api.ts DELETED
@@ -1,411 +0,0 @@
1
- import { CONVEX_SITE_URL, CONVEX_URL, ensureValidJwt } from "@repo/auth";
2
- import { api, type Id, type TableNames } from "@repo/backend";
3
- import { trackError } from "@repo/logger";
4
- import { ConvexHttpClient } from "convex/browser";
5
-
6
- export interface User {
7
- id: string;
8
- name: string;
9
- email: string;
10
- image?: string;
11
- hasPro: boolean;
12
- }
13
-
14
- export interface ProjectListItem {
15
- id: string;
16
- name: string;
17
- slug: string;
18
- status: "owned" | "shared" | "archived" | "restricted";
19
- isRestricted: boolean;
20
- isArchived: boolean;
21
- ownerId?: string;
22
- createdAt: number;
23
- updatedAt: number;
24
- }
25
-
26
- export interface Environment {
27
- id: string;
28
- name: string;
29
- projectId: string;
30
- color?: string;
31
- }
32
-
33
- export interface Folder {
34
- id: string;
35
- name: string;
36
- environmentId: string;
37
- }
38
-
39
- export interface Secret {
40
- id: string;
41
- key: string;
42
- encryptedValue: string;
43
- environmentId: string;
44
- folderId?: string;
45
- valueType: "string" | "number" | "boolean";
46
- scope: "client" | "server" | "shared";
47
- }
48
-
49
- export interface SecretData {
50
- id: string;
51
- key: string;
52
- encryptedValue: string;
53
- scope: "client" | "server" | "shared";
54
- valueType: "string" | "number" | "boolean";
55
- }
56
-
57
- export interface EnvironmentData {
58
- secrets: Secret[];
59
- folders: Folder[];
60
- }
61
-
62
- export interface Project {
63
- id: string;
64
- name: string;
65
- slug: string;
66
- encryptedProjectKey: string;
67
- keyVersion: number;
68
- isArchived: boolean;
69
- }
70
-
71
- export interface FullUser extends User {
72
- publicKey?: string;
73
- encryptedPrivateKey?: string;
74
- salt?: string;
75
- keysUpdatedAt?: number;
76
- }
77
-
78
- function createClient(): ConvexHttpClient {
79
- return new ConvexHttpClient(CONVEX_URL);
80
- }
81
-
82
- function toId<T extends TableNames>(id: string): Id<T> {
83
- return id as Id<T>;
84
- }
85
-
86
- export class ProtectedApi {
87
- private client: ConvexHttpClient;
88
- private authPromise: Promise<void> | null = null;
89
-
90
- constructor() {
91
- this.client = createClient();
92
- }
93
-
94
- private async ensureAuth(): Promise<void> {
95
- if (this.authPromise) {
96
- await this.authPromise;
97
- return;
98
- }
99
-
100
- this.authPromise = (async () => {
101
- try {
102
- const token = await ensureValidJwt();
103
- this.client.setAuth(token);
104
- } catch (error) {
105
- trackError("cli", error, { action: "cli_auth" });
106
- this.client.clearAuth();
107
- throw error;
108
- } finally {
109
- this.authPromise = null;
110
- }
111
- })();
112
-
113
- await this.authPromise;
114
- }
115
-
116
- private async withAuth<T>(fn: () => Promise<T>): Promise<T> {
117
- await this.ensureAuth();
118
- return fn();
119
- }
120
-
121
- async getCurrentUser(): Promise<User> {
122
- const result = await this.withAuth(() => this.client.query(api.user.getCurrentUser, {}));
123
- return {
124
- id: String(result.id),
125
- name: result.name,
126
- email: result.email,
127
- image: result.image ?? undefined,
128
- hasPro: result.hasPro ?? false,
129
- };
130
- }
131
-
132
- async listProjects(): Promise<ProjectListItem[]> {
133
- const result = await this.withAuth(() => this.client.query(api.project.listUserProjects, {}));
134
- return result.projects.map((p) => ({
135
- ...p,
136
- id: String(p.id),
137
- })) as ProjectListItem[];
138
- }
139
-
140
- async listSharedProjects(): Promise<ProjectListItem[]> {
141
- const result = await this.withAuth(() =>
142
- this.client.query(api.projectShare.listActiveSharedProjectsForCurrentUser, {}),
143
- );
144
- return result.shares.map(
145
- (share: {
146
- projectId: string;
147
- projectName: string;
148
- projectSlug: string;
149
- status: string;
150
- isRestricted: boolean;
151
- isArchived: boolean;
152
- ownerId?: string;
153
- }) => ({
154
- id: String(share.projectId),
155
- name: share.projectName,
156
- slug: share.projectSlug,
157
- status: share.status as ProjectListItem["status"],
158
- isRestricted: share.isRestricted,
159
- isArchived: share.isArchived,
160
- ownerId: share.ownerId,
161
- createdAt: 0,
162
- updatedAt: 0,
163
- }),
164
- );
165
- }
166
-
167
- async getProjectEnvironments(projectId: string): Promise<Environment[]> {
168
- const result = await this.withAuth(() =>
169
- this.client.query(api.environment.getProjectEnvironments, {
170
- projectId: toId<"project">(projectId),
171
- }),
172
- );
173
- return result.map((e) => ({
174
- id: String(e.id),
175
- name: e.name,
176
- projectId: String(e.projectId),
177
- color: e.color,
178
- }));
179
- }
180
-
181
- async getEnvironmentData(environmentId: string): Promise<EnvironmentData> {
182
- const result = await this.withAuth(() =>
183
- this.client.query(api.environment.getEnvironmentData, {
184
- environmentId: toId<"environment">(environmentId),
185
- }),
186
- );
187
- return {
188
- secrets: result.secrets
189
- .filter((s) => !s.isDeleted)
190
- .map((s) => ({
191
- id: String(s.id),
192
- key: s.key,
193
- encryptedValue: s.encryptedValue,
194
- environmentId: String(s.environmentId),
195
- folderId: s.folderId ? String(s.folderId) : undefined,
196
- valueType: s.valueType,
197
- scope: s.scope,
198
- })),
199
- folders: result.folders.map((f) => ({
200
- id: String(f.id),
201
- name: f.name,
202
- environmentId: String(f.environmentId),
203
- })),
204
- };
205
- }
206
-
207
- async getFullUser(): Promise<FullUser> {
208
- const result = await this.withAuth(() => this.client.query(api.user.getCurrentUser, {}));
209
- return {
210
- id: String(result.id),
211
- name: result.name,
212
- email: result.email,
213
- image: result.image ?? undefined,
214
- hasPro: result.hasPro ?? false,
215
- publicKey: result.publicKey ?? undefined,
216
- encryptedPrivateKey: result.encryptedPrivateKey ?? undefined,
217
- salt: result.salt ?? undefined,
218
- keysUpdatedAt: result.keysUpdatedAt ?? undefined,
219
- };
220
- }
221
-
222
- async getProject(projectId: string): Promise<Project> {
223
- const result = await this.withAuth(() =>
224
- this.client.query(api.project.getProject, {
225
- projectId: toId<"project">(projectId),
226
- }),
227
- );
228
- return {
229
- id: String(result.id),
230
- name: result.name,
231
- slug: result.slug,
232
- encryptedProjectKey: result.encryptedProjectKey,
233
- keyVersion: result.keyVersion,
234
- isArchived: result.isArchived,
235
- };
236
- }
237
-
238
- async getProjectShare(projectId: string): Promise<{ encryptedProjectKey: string } | null> {
239
- const result = await this.withAuth(() =>
240
- this.client.query(api.projectShare.getProjectShareByProjectForCurrentUser, {
241
- projectId: toId<"project">(projectId),
242
- }),
243
- );
244
- if (!result) return null;
245
- return { encryptedProjectKey: result.encryptedProjectKey };
246
- }
247
-
248
- async getSecretsForFolder(environmentId: string, folderId?: string): Promise<Secret[]> {
249
- const envData = await this.getEnvironmentData(environmentId);
250
- if (folderId) {
251
- return envData.secrets.filter((s) => s.folderId === folderId);
252
- }
253
- return envData.secrets.filter((s) => !s.folderId);
254
- }
255
-
256
- async getSecretsCacheValidation(
257
- projectId: string,
258
- environmentId?: string,
259
- folderId?: string,
260
- ): Promise<{ updatedAt: number } | null> {
261
- return await this.withAuth(() =>
262
- this.client.query(api.environment.getSecretsCacheValidation, {
263
- projectId: toId<"project">(projectId),
264
- environmentId: environmentId ? toId<"environment">(environmentId) : undefined,
265
- folderId: folderId ? toId<"folder">(folderId) : undefined,
266
- }),
267
- );
268
- }
269
-
270
- async exportSecrets(args: {
271
- projectId: string;
272
- environmentName?: string;
273
- environmentId?: string;
274
- folderName?: string;
275
- folderId?: string;
276
- scope?: "client" | "server" | "shared";
277
- }): Promise<{
278
- secrets: SecretData[];
279
- count: number;
280
- encryptedProjectKey: string;
281
- environmentId: string;
282
- folderId: string | null;
283
- }> {
284
- const result: {
285
- secrets: SecretData[];
286
- count: number;
287
- encryptedProjectKey: string;
288
- environmentId: string;
289
- folderId: string | null;
290
- } = await this.withAuth(() =>
291
- this.client.mutation(api.secret.exportSecrets, {
292
- projectId: toId<"project">(args.projectId),
293
- environmentName: args.environmentName,
294
- environmentId: args.environmentId ? toId<"environment">(args.environmentId) : undefined,
295
- folderName: args.folderName,
296
- folderId: args.folderId ? toId<"folder">(args.folderId) : undefined,
297
- scope: args.scope,
298
- }),
299
- );
300
-
301
- return {
302
- secrets: result.secrets,
303
- count: result.count,
304
- encryptedProjectKey: result.encryptedProjectKey,
305
- environmentId: String(result.environmentId),
306
- folderId: result.folderId ? String(result.folderId) : null,
307
- };
308
- }
309
- }
310
-
311
- let instance: ProtectedApi | null = null;
312
-
313
- export function getApi(): ProtectedApi {
314
- if (!instance) {
315
- instance = new ProtectedApi();
316
- }
317
- return instance;
318
- }
319
-
320
- export class ProPlanRequiredError extends Error {
321
- upgradeUrl: string;
322
- constructor(message: string, upgradeUrl: string) {
323
- super(message);
324
- this.name = "ProPlanRequiredError";
325
- this.upgradeUrl = upgradeUrl;
326
- }
327
- }
328
-
329
- export interface ExportSecretsHttpResponse {
330
- secrets: {
331
- id: string;
332
- key: string;
333
- encryptedValue: string;
334
- scope: "client" | "server" | "shared";
335
- valueType: "string" | "number" | "boolean";
336
- }[];
337
- count: number;
338
- encryptedProjectKey: string;
339
- environmentId: string;
340
- folderId: string | null;
341
- }
342
-
343
- export interface UserCryptoKeysResponse {
344
- encryptedPrivateKey: string;
345
- salt: string;
346
- publicKey: string;
347
- }
348
-
349
- export async function exportSecretsViaApiKey(
350
- apiKey: string,
351
- body: {
352
- projectId: string;
353
- environmentName: string;
354
- folderName?: string;
355
- scope?: string;
356
- },
357
- ): Promise<ExportSecretsHttpResponse> {
358
- const url = `${CONVEX_SITE_URL}/api/secrets/export`;
359
-
360
- const response = await fetch(url, {
361
- method: "POST",
362
- headers: {
363
- Authorization: `Bearer ${apiKey}`,
364
- "Content-Type": "application/json",
365
- },
366
- body: JSON.stringify(body),
367
- });
368
-
369
- if (!response.ok) {
370
- const errorBody = await response.json().catch(() => null);
371
- const parsed = errorBody as { error?: string; code?: string; upgradeUrl?: string } | null;
372
-
373
- if (response.status === 402 || parsed?.code === "PRO_PLAN_REQUIRED") {
374
- throw new ProPlanRequiredError(
375
- parsed?.error || "API keys require a Pro plan.",
376
- parsed?.upgradeUrl || "https://relic.so/dashboard?action=upgrade",
377
- );
378
- }
379
-
380
- throw new Error(parsed?.error ?? `HTTP ${response.status}`);
381
- }
382
-
383
- return (await response.json()) as ExportSecretsHttpResponse;
384
- }
385
-
386
- export async function fetchUserKeysViaApiKey(apiKey: string): Promise<UserCryptoKeysResponse> {
387
- const url = `${CONVEX_SITE_URL}/api/user/keys`;
388
-
389
- const response = await fetch(url, {
390
- method: "GET",
391
- headers: {
392
- Authorization: `Bearer ${apiKey}`,
393
- },
394
- });
395
-
396
- if (!response.ok) {
397
- const errorBody = await response.json().catch(() => null);
398
- const parsed = errorBody as { error?: string; code?: string; upgradeUrl?: string } | null;
399
-
400
- if (response.status === 402 || parsed?.code === "PRO_PLAN_REQUIRED") {
401
- throw new ProPlanRequiredError(
402
- parsed?.error || "API keys require a Pro plan.",
403
- parsed?.upgradeUrl || "https://relic.so/dashboard?action=upgrade",
404
- );
405
- }
406
-
407
- throw new Error(parsed?.error ?? `HTTP ${response.status}`);
408
- }
409
-
410
- return (await response.json()) as UserCryptoKeysResponse;
411
- }
package/lib/config.ts DELETED
@@ -1,118 +0,0 @@
1
- import { mkdir } from "node:fs/promises";
2
- import { dirname, join, resolve } from "node:path";
3
- import { parse, stringify } from "smol-toml";
4
-
5
- const CONFIG_FILE = "relic.toml";
6
- const RELIC_DIR = ".relic";
7
- const CACHE_DB = "cache.db";
8
-
9
- export interface RelicConfig {
10
- project_id: string;
11
- }
12
-
13
- export interface ConfigResult {
14
- config: RelicConfig;
15
- configPath: string;
16
- rootDir: string;
17
- }
18
-
19
- export async function loadConfig(dir?: string): Promise<ConfigResult | null> {
20
- const targetDir = dir ?? process.cwd();
21
- const configPath = join(targetDir, CONFIG_FILE);
22
-
23
- try {
24
- const file = Bun.file(configPath);
25
- if (!(await file.exists())) {
26
- return null;
27
- }
28
- const content = await file.text();
29
- const config = parse(content) as unknown as RelicConfig;
30
-
31
- return {
32
- config,
33
- configPath,
34
- rootDir: targetDir,
35
- };
36
- } catch {
37
- return null;
38
- }
39
- }
40
-
41
- export async function saveConfig(config: RelicConfig, dir?: string): Promise<string> {
42
- const targetDir = dir ?? process.cwd();
43
- const configPath = join(targetDir, CONFIG_FILE);
44
-
45
- const content = stringify(config);
46
- await Bun.write(configPath, content);
47
-
48
- return configPath;
49
- }
50
-
51
- export async function findConfig(startDir?: string): Promise<ConfigResult | null> {
52
- let currentDir = resolve(startDir ?? process.cwd());
53
- const root = resolve("/");
54
- const home = process.env.HOME ? resolve(process.env.HOME) : null;
55
-
56
- while (currentDir !== root) {
57
- const configPath = join(currentDir, CONFIG_FILE);
58
-
59
- const file = Bun.file(configPath);
60
- if (await file.exists()) {
61
- try {
62
- const content = await file.text();
63
- const config = parse(content) as unknown as RelicConfig;
64
- return { config, configPath, rootDir: currentDir };
65
- } catch {
66
- return null;
67
- }
68
- }
69
-
70
- if (home && currentDir === home) {
71
- return null;
72
- }
73
-
74
- const parentDir = dirname(currentDir);
75
- if (parentDir === currentDir) break;
76
- currentDir = parentDir;
77
- }
78
-
79
- return null;
80
- }
81
-
82
- export async function configExists(dir?: string): Promise<boolean> {
83
- const targetDir = dir ?? process.cwd();
84
- const configPath = join(targetDir, CONFIG_FILE);
85
- const file = Bun.file(configPath);
86
- return file.exists();
87
- }
88
-
89
- export function createConfig(projectId: string): RelicConfig {
90
- return {
91
- project_id: projectId,
92
- };
93
- }
94
-
95
- export function getConfigFilePath(): string {
96
- return CONFIG_FILE;
97
- }
98
-
99
- export function getRelicDir(rootDir: string): string {
100
- return join(rootDir, RELIC_DIR);
101
- }
102
-
103
- export function getCacheDbPath(rootDir: string): string {
104
- return join(rootDir, RELIC_DIR, CACHE_DB);
105
- }
106
-
107
- export async function createRelicDir(dir?: string): Promise<string> {
108
- const targetDir = dir ?? process.cwd();
109
- const relicDir = join(targetDir, RELIC_DIR);
110
- await mkdir(relicDir, { recursive: true });
111
- return relicDir;
112
- }
113
-
114
- export async function findRelicDir(startDir?: string): Promise<string | null> {
115
- const configResult = await findConfig(startDir);
116
- if (!configResult) return null;
117
- return getRelicDir(configResult.rootDir);
118
- }
package/lib/crypto.ts DELETED
@@ -1,81 +0,0 @@
1
- import { getPasswordFromStorage } from "@repo/auth";
2
- import { decryptSecret, unwrapProjectKey } from "@repo/crypto";
3
- import { trackError } from "@repo/logger";
4
-
5
- export class ProjectKeyError extends Error {
6
- constructor(
7
- message: string,
8
- public readonly code: "NO_PASSWORD" | "DECRYPTION_FAILED" | "UNKNOWN",
9
- public readonly originalError?: Error,
10
- ) {
11
- super(message);
12
- this.name = "ProjectKeyError";
13
- }
14
- }
15
-
16
- export async function getProjectKey(
17
- encryptedProjectKey: string,
18
- userEncryptedPrivateKey: string,
19
- userSalt: string,
20
- ): Promise<CryptoKey> {
21
- const password = await getPasswordFromStorage();
22
- if (!password) {
23
- throw new ProjectKeyError(
24
- "No password available. Run 'relic login' and set up your password first.",
25
- "NO_PASSWORD",
26
- );
27
- }
28
-
29
- try {
30
- return await unwrapProjectKey(encryptedProjectKey, userEncryptedPrivateKey, password, userSalt);
31
- } catch (error) {
32
- const errorMessage = error instanceof Error ? error.message : String(error);
33
-
34
- trackError("cli", error, { action: "decrypt_project_key" });
35
-
36
- if (errorMessage.includes("DECRYPTION_FAILED") || errorMessage.includes("incorrect password")) {
37
- throw new ProjectKeyError(
38
- "Failed to decrypt project key. Your stored password may be incorrect.",
39
- "DECRYPTION_FAILED",
40
- error instanceof Error ? error : new Error(String(error)),
41
- );
42
- }
43
-
44
- throw new ProjectKeyError(
45
- `Failed to unwrap project key: ${errorMessage}`,
46
- "UNKNOWN",
47
- error instanceof Error ? error : new Error(String(error)),
48
- );
49
- }
50
- }
51
-
52
- export async function decryptSecretValue(
53
- projectKey: CryptoKey,
54
- encryptedValue: string,
55
- ): Promise<string> {
56
- return await decryptSecret(projectKey, encryptedValue);
57
- }
58
-
59
- export interface DecryptedSecret {
60
- key: string;
61
- value: string;
62
- }
63
-
64
- export async function decryptSecrets(
65
- projectKey: CryptoKey,
66
- secrets: Array<{ key: string; encryptedValue: string }>,
67
- ): Promise<DecryptedSecret[]> {
68
- const decrypted: DecryptedSecret[] = [];
69
-
70
- for (const secret of secrets) {
71
- try {
72
- const value = await decryptSecretValue(projectKey, secret.encryptedValue);
73
- decrypted.push({ key: secret.key, value });
74
- } catch (error) {
75
- trackError("cli", error, { action: "decrypt_secret" });
76
- throw new Error(`Failed to decrypt secret "${secret.key}": ${error}`);
77
- }
78
- }
79
-
80
- return decrypted;
81
- }
package/lib/types.ts DELETED
@@ -1 +0,0 @@
1
- export type SecretScope = "client" | "server" | "shared";