everything-dev 0.2.1 → 0.3.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/src/lib/env.ts CHANGED
@@ -2,90 +2,108 @@ import { existsSync } from "node:fs";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { Effect } from "every-plugin/effect";
5
- import { getConfigDir } from "../config";
5
+ import { getProjectRoot } from "../config";
6
6
 
7
7
  export interface BosEnv {
8
- ZE_SERVER_TOKEN?: string;
9
- ZE_USER_EMAIL?: string;
10
- NEAR_PRIVATE_KEY?: string;
11
- GATEWAY_PRIVATE_KEY?: string;
12
- NOVA_SECRETS_CID?: string;
8
+ ZE_SERVER_TOKEN?: string;
9
+ ZE_USER_EMAIL?: string;
10
+ NEAR_PRIVATE_KEY?: string;
11
+ GATEWAY_PRIVATE_KEY?: string;
12
+ NOVA_SECRETS_CID?: string;
13
13
  }
14
14
 
15
15
  function parseEnvFile(content: string): Record<string, string> {
16
- const result: Record<string, string> = {};
17
- const lines = content.split("\n");
16
+ const result: Record<string, string> = {};
17
+ const lines = content.split("\n");
18
18
 
19
- for (const line of lines) {
20
- const trimmed = line.trim();
21
- if (!trimmed || trimmed.startsWith("#")) continue;
19
+ for (const line of lines) {
20
+ const trimmed = line.trim();
21
+ if (!trimmed || trimmed.startsWith("#")) continue;
22
22
 
23
- const eqIndex = trimmed.indexOf("=");
24
- if (eqIndex === -1) continue;
23
+ const eqIndex = trimmed.indexOf("=");
24
+ if (eqIndex === -1) continue;
25
25
 
26
- const key = trimmed.slice(0, eqIndex).trim();
27
- let value = trimmed.slice(eqIndex + 1).trim();
26
+ const key = trimmed.slice(0, eqIndex).trim();
27
+ let value = trimmed.slice(eqIndex + 1).trim();
28
28
 
29
- if ((value.startsWith('"') && value.endsWith('"')) ||
30
- (value.startsWith("'") && value.endsWith("'"))) {
31
- value = value.slice(1, -1);
32
- }
29
+ if (
30
+ (value.startsWith('"') && value.endsWith('"')) ||
31
+ (value.startsWith("'") && value.endsWith("'"))
32
+ ) {
33
+ value = value.slice(1, -1);
34
+ }
33
35
 
34
- result[key] = value;
35
- }
36
+ result[key] = value;
37
+ }
36
38
 
37
- return result;
39
+ return result;
38
40
  }
39
41
 
40
42
  export const loadBosEnv = Effect.gen(function* () {
41
- const configDir = getConfigDir();
42
- const envBosPath = path.join(configDir, ".env.bos");
43
- const envPath = path.join(configDir, ".env");
44
-
45
- let envVars: BosEnv = {};
46
-
47
- const envFilePath = existsSync(envBosPath) ? envBosPath : existsSync(envPath) ? envPath : null;
48
-
49
- if (envFilePath) {
50
- const content = yield* Effect.tryPromise({
51
- try: () => readFile(envFilePath, "utf-8"),
52
- catch: () => new Error(`Failed to read ${envFilePath}`),
53
- });
54
-
55
- const parsed = parseEnvFile(content);
56
- envVars = {
57
- ZE_SERVER_TOKEN: parsed.ZE_SERVER_TOKEN,
58
- ZE_USER_EMAIL: parsed.ZE_USER_EMAIL,
59
- NEAR_PRIVATE_KEY: parsed.NEAR_PRIVATE_KEY,
60
- GATEWAY_PRIVATE_KEY: parsed.GATEWAY_PRIVATE_KEY,
61
- NOVA_SECRETS_CID: parsed.NOVA_SECRETS_CID,
62
- };
63
- }
64
-
65
- envVars.ZE_SERVER_TOKEN = envVars.ZE_SERVER_TOKEN || process.env.ZE_SERVER_TOKEN;
66
- envVars.ZE_USER_EMAIL = envVars.ZE_USER_EMAIL || process.env.ZE_USER_EMAIL;
67
- envVars.NEAR_PRIVATE_KEY = envVars.NEAR_PRIVATE_KEY || process.env.NEAR_PRIVATE_KEY;
68
- envVars.GATEWAY_PRIVATE_KEY = envVars.GATEWAY_PRIVATE_KEY || process.env.GATEWAY_PRIVATE_KEY;
69
- envVars.NOVA_SECRETS_CID = envVars.NOVA_SECRETS_CID || process.env.NOVA_SECRETS_CID;
70
-
71
- return envVars;
43
+ let configDir: string;
44
+ try {
45
+ configDir = getProjectRoot();
46
+ } catch {
47
+ configDir = process.cwd();
48
+ }
49
+ const envBosPath = path.join(configDir, ".env.bos");
50
+ const envPath = path.join(configDir, ".env");
51
+
52
+ let envVars: BosEnv = {};
53
+
54
+ const envFilePath = existsSync(envBosPath)
55
+ ? envBosPath
56
+ : existsSync(envPath)
57
+ ? envPath
58
+ : null;
59
+
60
+ if (envFilePath) {
61
+ const content = yield* Effect.tryPromise({
62
+ try: () => readFile(envFilePath, "utf-8"),
63
+ catch: () => new Error(`Failed to read ${envFilePath}`),
64
+ });
65
+
66
+ const parsed = parseEnvFile(content);
67
+ envVars = {
68
+ ZE_SERVER_TOKEN: parsed.ZE_SERVER_TOKEN,
69
+ ZE_USER_EMAIL: parsed.ZE_USER_EMAIL,
70
+ NEAR_PRIVATE_KEY: parsed.NEAR_PRIVATE_KEY,
71
+ GATEWAY_PRIVATE_KEY: parsed.GATEWAY_PRIVATE_KEY,
72
+ NOVA_SECRETS_CID: parsed.NOVA_SECRETS_CID,
73
+ };
74
+ }
75
+
76
+ envVars.ZE_SERVER_TOKEN =
77
+ envVars.ZE_SERVER_TOKEN || process.env.ZE_SERVER_TOKEN;
78
+ envVars.ZE_USER_EMAIL = envVars.ZE_USER_EMAIL || process.env.ZE_USER_EMAIL;
79
+ envVars.NEAR_PRIVATE_KEY =
80
+ envVars.NEAR_PRIVATE_KEY || process.env.NEAR_PRIVATE_KEY;
81
+ envVars.GATEWAY_PRIVATE_KEY =
82
+ envVars.GATEWAY_PRIVATE_KEY || process.env.GATEWAY_PRIVATE_KEY;
83
+ envVars.NOVA_SECRETS_CID =
84
+ envVars.NOVA_SECRETS_CID || process.env.NOVA_SECRETS_CID;
85
+
86
+ return envVars;
72
87
  });
73
88
 
74
- export const ZEPHYR_DOCS_URL = "https://docs.zephyr-cloud.io/features/ci-cd-server-token";
89
+ export const ZEPHYR_DOCS_URL =
90
+ "https://docs.zephyr-cloud.io/features/ci-cd-server-token";
75
91
 
76
92
  export const getBuildEnv = (bosEnv: BosEnv): Record<string, string> => {
77
- const env: Record<string, string> = { ...process.env as Record<string, string> };
78
-
79
- if (bosEnv.ZE_SERVER_TOKEN) {
80
- env.ZE_SERVER_TOKEN = bosEnv.ZE_SERVER_TOKEN;
81
- }
82
- if (bosEnv.ZE_USER_EMAIL) {
83
- env.ZE_USER_EMAIL = bosEnv.ZE_USER_EMAIL;
84
- }
85
-
86
- return env;
93
+ const env: Record<string, string> = {
94
+ ...(process.env as Record<string, string>),
95
+ };
96
+
97
+ if (bosEnv.ZE_SERVER_TOKEN) {
98
+ env.ZE_SERVER_TOKEN = bosEnv.ZE_SERVER_TOKEN;
99
+ }
100
+ if (bosEnv.ZE_USER_EMAIL) {
101
+ env.ZE_USER_EMAIL = bosEnv.ZE_USER_EMAIL;
102
+ }
103
+
104
+ return env;
87
105
  };
88
106
 
89
107
  export const hasZephyrConfig = (bosEnv: BosEnv): boolean => {
90
- return !!(bosEnv.ZE_SERVER_TOKEN && bosEnv.ZE_USER_EMAIL);
108
+ return !!(bosEnv.ZE_SERVER_TOKEN && bosEnv.ZE_USER_EMAIL);
91
109
  };
package/src/lib/nova.ts CHANGED
@@ -1,254 +1,266 @@
1
- import { NovaSdk } from "nova-sdk-js";
2
1
  import { Effect } from "every-plugin/effect";
3
- import { getConfigDir } from "../config";
2
+ import { NovaSdk } from "nova-sdk-js";
3
+ import { getProjectRoot } from "../config";
4
4
 
5
5
  export interface NovaConfig {
6
- accountId: string;
7
- sessionToken: string;
6
+ accountId: string;
7
+ sessionToken: string;
8
8
  }
9
9
 
10
10
  export interface SecretsData {
11
- secrets: Record<string, string>;
12
- updatedAt: string;
11
+ secrets: Record<string, string>;
12
+ updatedAt: string;
13
13
  }
14
14
 
15
15
  export interface UploadResult {
16
- cid: string;
17
- groupId: string;
18
- txHash?: string;
16
+ cid: string;
17
+ groupId: string;
18
+ txHash?: string;
19
19
  }
20
20
 
21
21
  export const getNovaConfig = Effect.gen(function* () {
22
- const accountId = process.env.NOVA_ACCOUNT_ID;
23
- const sessionToken = process.env.NOVA_API_KEY;
24
-
25
- if (!accountId || !sessionToken) {
26
- return yield* Effect.fail(
27
- new Error(
28
- "NOVA credentials not configured. Run 'bos login' to authenticate with NOVA."
29
- )
30
- );
31
- }
32
-
33
- return { accountId, sessionToken } satisfies NovaConfig;
22
+ const accountId = process.env.NOVA_ACCOUNT_ID;
23
+ const sessionToken = process.env.NOVA_API_KEY;
24
+
25
+ if (!accountId || !sessionToken) {
26
+ return yield* Effect.fail(
27
+ new Error(
28
+ "NOVA credentials not configured. Run 'bos login' to authenticate with NOVA.",
29
+ ),
30
+ );
31
+ }
32
+
33
+ return { accountId, sessionToken } satisfies NovaConfig;
34
34
  });
35
35
 
36
36
  export function createNovaClient(config: NovaConfig): NovaSdk {
37
- return new NovaSdk(config.accountId, {
38
- apiKey: config.sessionToken,
39
- });
37
+ return new NovaSdk(config.accountId, {
38
+ apiKey: config.sessionToken,
39
+ });
40
40
  }
41
41
 
42
42
  export function getSecretsGroupId(nearAccount: string): string {
43
- return `${nearAccount}-secrets`;
43
+ return `${nearAccount}-secrets`;
44
44
  }
45
45
 
46
46
  export const registerSecretsGroup = (
47
- nova: NovaSdk,
48
- nearAccount: string,
49
- novaAccount: string
47
+ nova: NovaSdk,
48
+ nearAccount: string,
49
+ novaAccount: string,
50
50
  ) =>
51
- Effect.gen(function* () {
52
- const groupId = getSecretsGroupId(nearAccount);
51
+ Effect.gen(function* () {
52
+ const groupId = getSecretsGroupId(nearAccount);
53
53
 
54
- yield* Effect.tryPromise({
55
- try: () => nova.registerGroup(groupId),
56
- catch: (e) => new Error(`Failed to register NOVA group: ${e}`),
57
- });
54
+ yield* Effect.tryPromise({
55
+ try: () => nova.registerGroup(groupId),
56
+ catch: (e) => new Error(`Failed to register NOVA group: ${e}`),
57
+ });
58
58
 
59
- yield* Effect.tryPromise({
60
- try: () => nova.addGroupMember(groupId, novaAccount),
61
- catch: (e) => new Error(`Failed to add gateway Nova account to group: ${e}`),
62
- });
59
+ yield* Effect.tryPromise({
60
+ try: () => nova.addGroupMember(groupId, novaAccount),
61
+ catch: (e) =>
62
+ new Error(`Failed to add gateway Nova account to group: ${e}`),
63
+ });
63
64
 
64
- return groupId;
65
- });
65
+ return groupId;
66
+ });
66
67
 
67
68
  export const uploadSecrets = (
68
- nova: NovaSdk,
69
- groupId: string,
70
- secrets: Record<string, string>
69
+ nova: NovaSdk,
70
+ groupId: string,
71
+ secrets: Record<string, string>,
71
72
  ) =>
72
- Effect.gen(function* () {
73
- const secretsData: SecretsData = {
74
- secrets,
75
- updatedAt: new Date().toISOString(),
76
- };
77
-
78
- const buffer = Buffer.from(JSON.stringify(secretsData, null, 2));
79
-
80
- const result = yield* Effect.tryPromise({
81
- try: () => nova.upload(groupId, buffer, "secrets.json"),
82
- catch: (e) => new Error(`Failed to upload secrets to NOVA: ${e}`),
83
- });
84
-
85
- return {
86
- cid: result.cid,
87
- groupId,
88
- txHash: result.trans_id,
89
- } satisfies UploadResult;
90
- });
73
+ Effect.gen(function* () {
74
+ const secretsData: SecretsData = {
75
+ secrets,
76
+ updatedAt: new Date().toISOString(),
77
+ };
78
+
79
+ const buffer = Buffer.from(JSON.stringify(secretsData, null, 2));
80
+
81
+ const result = yield* Effect.tryPromise({
82
+ try: () => nova.upload(groupId, buffer, "secrets.json"),
83
+ catch: (e) => new Error(`Failed to upload secrets to NOVA: ${e}`),
84
+ });
85
+
86
+ return {
87
+ cid: result.cid,
88
+ groupId,
89
+ txHash: result.trans_id,
90
+ } satisfies UploadResult;
91
+ });
91
92
 
92
93
  export const retrieveSecrets = (nova: NovaSdk, groupId: string, cid: string) =>
93
- Effect.gen(function* () {
94
- const result = yield* Effect.tryPromise({
95
- try: () => nova.retrieve(groupId, cid),
96
- catch: (e) => new Error(`Failed to retrieve secrets from NOVA: ${e}`),
97
- });
94
+ Effect.gen(function* () {
95
+ const result = yield* Effect.tryPromise({
96
+ try: () => nova.retrieve(groupId, cid),
97
+ catch: (e) => new Error(`Failed to retrieve secrets from NOVA: ${e}`),
98
+ });
98
99
 
99
- const secretsData = JSON.parse(result.data.toString()) as SecretsData;
100
- return secretsData;
101
- });
100
+ const secretsData = JSON.parse(result.data.toString()) as SecretsData;
101
+ return secretsData;
102
+ });
102
103
 
103
104
  export function parseEnvFile(content: string): Record<string, string> {
104
- const secrets: Record<string, string> = {};
105
- const lines = content.split("\n");
105
+ const secrets: Record<string, string> = {};
106
+ const lines = content.split("\n");
106
107
 
107
- for (const line of lines) {
108
- const trimmed = line.trim();
108
+ for (const line of lines) {
109
+ const trimmed = line.trim();
109
110
 
110
- if (!trimmed || trimmed.startsWith("#")) {
111
- continue;
112
- }
111
+ if (!trimmed || trimmed.startsWith("#")) {
112
+ continue;
113
+ }
113
114
 
114
- const eqIndex = trimmed.indexOf("=");
115
- if (eqIndex === -1) {
116
- continue;
117
- }
115
+ const eqIndex = trimmed.indexOf("=");
116
+ if (eqIndex === -1) {
117
+ continue;
118
+ }
118
119
 
119
- const key = trimmed.slice(0, eqIndex).trim();
120
- let value = trimmed.slice(eqIndex + 1).trim();
120
+ const key = trimmed.slice(0, eqIndex).trim();
121
+ let value = trimmed.slice(eqIndex + 1).trim();
121
122
 
122
- if (
123
- (value.startsWith('"') && value.endsWith('"')) ||
124
- (value.startsWith("'") && value.endsWith("'"))
125
- ) {
126
- value = value.slice(1, -1);
127
- }
123
+ if (
124
+ (value.startsWith('"') && value.endsWith('"')) ||
125
+ (value.startsWith("'") && value.endsWith("'"))
126
+ ) {
127
+ value = value.slice(1, -1);
128
+ }
128
129
 
129
- if (key) {
130
- secrets[key] = value;
131
- }
132
- }
130
+ if (key) {
131
+ secrets[key] = value;
132
+ }
133
+ }
133
134
 
134
- return secrets;
135
+ return secrets;
135
136
  }
136
137
 
137
138
  export function filterSecretsToRequired(
138
- allSecrets: Record<string, string>,
139
- requiredKeys: string[]
139
+ allSecrets: Record<string, string>,
140
+ requiredKeys: string[],
140
141
  ): Record<string, string> {
141
- const filtered: Record<string, string> = {};
142
+ const filtered: Record<string, string> = {};
142
143
 
143
- for (const key of requiredKeys) {
144
- if (key in allSecrets) {
145
- filtered[key] = allSecrets[key];
146
- }
147
- }
144
+ for (const key of requiredKeys) {
145
+ if (key in allSecrets) {
146
+ filtered[key] = allSecrets[key];
147
+ }
148
+ }
148
149
 
149
- return filtered;
150
+ return filtered;
150
151
  }
151
152
 
152
153
  export function hasNovaCredentials(): boolean {
153
- return !!(process.env.NOVA_ACCOUNT_ID && process.env.NOVA_API_KEY);
154
+ return !!(process.env.NOVA_ACCOUNT_ID && process.env.NOVA_API_KEY);
154
155
  }
155
156
 
156
157
  function getBosEnvPath(): string {
157
- return `${getConfigDir()}/.env.bos`;
158
+ try {
159
+ return `${getProjectRoot()}/.env.bos`;
160
+ } catch {
161
+ // Fallback to cwd if config not loaded
162
+ return `${process.cwd()}/.env.bos`;
163
+ }
158
164
  }
159
165
 
160
166
  export const saveNovaCredentials = (accountId: string, sessionToken: string) =>
161
- Effect.gen(function* () {
162
- const envPath = getBosEnvPath();
163
- let content = "";
164
-
165
- const file = Bun.file(envPath);
166
- const exists = yield* Effect.promise(() => file.exists());
167
-
168
- if (exists) {
169
- content = yield* Effect.tryPromise({
170
- try: () => file.text(),
171
- catch: () => new Error("File read failed"),
172
- }).pipe(Effect.orElseSucceed(() => ""));
173
- }
174
-
175
- const lines = content.split("\n");
176
- const newLines: string[] = [];
177
- let foundAccountId = false;
178
- let foundSessionToken = false;
179
-
180
- for (const line of lines) {
181
- const trimmed = line.trim();
182
- if (trimmed.startsWith("NOVA_ACCOUNT_ID=")) {
183
- newLines.push(`NOVA_ACCOUNT_ID=${accountId}`);
184
- foundAccountId = true;
185
- } else if (trimmed.startsWith("NOVA_API_KEY=")) {
186
- newLines.push(`NOVA_API_KEY=${sessionToken}`);
187
- foundSessionToken = true;
188
- } else {
189
- newLines.push(line);
190
- }
191
- }
192
-
193
- if (!foundAccountId) {
194
- if (newLines.length > 0 && newLines[newLines.length - 1] !== "") {
195
- newLines.push("");
196
- }
197
- newLines.push(`NOVA_ACCOUNT_ID=${accountId}`);
198
- }
199
-
200
- if (!foundSessionToken) {
201
- newLines.push(`NOVA_API_KEY=${sessionToken}`);
202
- }
203
-
204
- yield* Effect.tryPromise({
205
- try: () => Bun.write(envPath, newLines.join("\n")),
206
- catch: (e) => new Error(`Failed to save credentials: ${e}`),
207
- });
208
-
209
- process.env.NOVA_ACCOUNT_ID = accountId;
210
- process.env.NOVA_API_KEY = sessionToken;
211
- });
167
+ Effect.gen(function* () {
168
+ const envPath = getBosEnvPath();
169
+ let content = "";
170
+
171
+ const file = Bun.file(envPath);
172
+ const exists = yield* Effect.promise(() => file.exists());
173
+
174
+ if (exists) {
175
+ content = yield* Effect.tryPromise({
176
+ try: () => file.text(),
177
+ catch: () => new Error("File read failed"),
178
+ }).pipe(Effect.orElseSucceed(() => ""));
179
+ }
180
+
181
+ const lines = content.split("\n");
182
+ const newLines: string[] = [];
183
+ let foundAccountId = false;
184
+ let foundSessionToken = false;
185
+
186
+ for (const line of lines) {
187
+ const trimmed = line.trim();
188
+ if (trimmed.startsWith("NOVA_ACCOUNT_ID=")) {
189
+ newLines.push(`NOVA_ACCOUNT_ID=${accountId}`);
190
+ foundAccountId = true;
191
+ } else if (trimmed.startsWith("NOVA_API_KEY=")) {
192
+ newLines.push(`NOVA_API_KEY=${sessionToken}`);
193
+ foundSessionToken = true;
194
+ } else {
195
+ newLines.push(line);
196
+ }
197
+ }
198
+
199
+ if (!foundAccountId) {
200
+ if (newLines.length > 0 && newLines[newLines.length - 1] !== "") {
201
+ newLines.push("");
202
+ }
203
+ newLines.push(`NOVA_ACCOUNT_ID=${accountId}`);
204
+ }
205
+
206
+ if (!foundSessionToken) {
207
+ newLines.push(`NOVA_API_KEY=${sessionToken}`);
208
+ }
209
+
210
+ yield* Effect.tryPromise({
211
+ try: () => Bun.write(envPath, newLines.join("\n")),
212
+ catch: (e) => new Error(`Failed to save credentials: ${e}`),
213
+ });
214
+
215
+ process.env.NOVA_ACCOUNT_ID = accountId;
216
+ process.env.NOVA_API_KEY = sessionToken;
217
+ });
212
218
 
213
219
  export const removeNovaCredentials = Effect.gen(function* () {
214
- const envPath = getBosEnvPath();
215
- let content = "";
216
-
217
- try {
218
- content = yield* Effect.tryPromise({
219
- try: () => Bun.file(envPath).text(),
220
- catch: () => "",
221
- });
222
- } catch {
223
- return;
224
- }
225
-
226
- const lines = content.split("\n");
227
- const newLines = lines.filter((line) => {
228
- const trimmed = line.trim();
229
- return !trimmed.startsWith("NOVA_ACCOUNT_ID=") && !trimmed.startsWith("NOVA_API_KEY=");
230
- });
231
-
232
- yield* Effect.tryPromise({
233
- try: () => Bun.write(envPath, newLines.join("\n")),
234
- catch: (e) => new Error(`Failed to remove credentials: ${e}`),
235
- });
236
-
237
- delete process.env.NOVA_ACCOUNT_ID;
238
- delete process.env.NOVA_API_KEY;
220
+ const envPath = getBosEnvPath();
221
+ let content = "";
222
+
223
+ try {
224
+ content = yield* Effect.tryPromise({
225
+ try: () => Bun.file(envPath).text(),
226
+ catch: () => "",
227
+ });
228
+ } catch {
229
+ return;
230
+ }
231
+
232
+ const lines = content.split("\n");
233
+ const newLines = lines.filter((line) => {
234
+ const trimmed = line.trim();
235
+ return (
236
+ !trimmed.startsWith("NOVA_ACCOUNT_ID=") &&
237
+ !trimmed.startsWith("NOVA_API_KEY=")
238
+ );
239
+ });
240
+
241
+ yield* Effect.tryPromise({
242
+ try: () => Bun.write(envPath, newLines.join("\n")),
243
+ catch: (e) => new Error(`Failed to remove credentials: ${e}`),
244
+ });
245
+
246
+ delete process.env.NOVA_ACCOUNT_ID;
247
+ delete process.env.NOVA_API_KEY;
239
248
  });
240
249
 
241
- export const verifyNovaCredentials = (accountId: string, sessionToken: string) =>
242
- Effect.gen(function* () {
243
- const nova = new NovaSdk(accountId, { apiKey: sessionToken });
244
-
245
- yield* Effect.tryPromise({
246
- try: () => nova.getBalance(),
247
- catch: (e) => {
248
- const message = e instanceof Error ? e.message : String(e);
249
- return new Error(`NOVA verification failed: ${message}`);
250
- },
251
- });
252
-
253
- return true;
254
- });
250
+ export const verifyNovaCredentials = (
251
+ accountId: string,
252
+ sessionToken: string,
253
+ ) =>
254
+ Effect.gen(function* () {
255
+ const nova = new NovaSdk(accountId, { apiKey: sessionToken });
256
+
257
+ yield* Effect.tryPromise({
258
+ try: () => nova.getBalance(),
259
+ catch: (e) => {
260
+ const message = e instanceof Error ? e.message : String(e);
261
+ return new Error(`NOVA verification failed: ${message}`);
262
+ },
263
+ });
264
+
265
+ return true;
266
+ });