@getjack/jack 0.1.12 → 0.1.14
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/package.json +2 -5
- package/src/commands/down.ts +1 -4
- package/src/commands/login.ts +6 -199
- package/src/lib/auth/ensure-auth.test.ts +285 -0
- package/src/lib/auth/ensure-auth.ts +165 -0
- package/src/lib/auth/index.ts +10 -0
- package/src/lib/auth/login-flow.ts +287 -0
- package/src/lib/control-plane.ts +42 -0
- package/src/lib/deploy-mode.ts +23 -2
- package/src/lib/deploy-upload.ts +9 -0
- package/src/lib/hooks.ts +25 -9
- package/src/lib/json-edit.ts +1 -5
- package/src/lib/project-operations.ts +391 -310
- package/src/lib/project-resolver.ts +12 -0
- package/src/lib/telemetry.ts +1 -0
- package/templates/miniapp/.jack.json +5 -5
- package/templates/miniapp/public/.well-known/farcaster.json +15 -15
- package/templates/miniapp/src/lib/api.ts +71 -3
- package/templates/miniapp/src/worker.ts +45 -5
package/src/lib/auth/index.ts
CHANGED
|
@@ -6,7 +6,17 @@ export {
|
|
|
6
6
|
refreshToken,
|
|
7
7
|
startDeviceAuth,
|
|
8
8
|
} from "./client.ts";
|
|
9
|
+
export {
|
|
10
|
+
ensureAuthForCreate,
|
|
11
|
+
type EnsureAuthOptions,
|
|
12
|
+
type EnsureAuthResult,
|
|
13
|
+
} from "./ensure-auth.ts";
|
|
9
14
|
export { requireAuth, requireAuthOrLogin, getCurrentUser } from "./guard.ts";
|
|
15
|
+
export {
|
|
16
|
+
runLoginFlow,
|
|
17
|
+
type LoginFlowOptions,
|
|
18
|
+
type LoginFlowResult,
|
|
19
|
+
} from "./login-flow.ts";
|
|
10
20
|
export {
|
|
11
21
|
deleteCredentials,
|
|
12
22
|
getAuthState,
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared login flow for CLI and programmatic use
|
|
3
|
+
*/
|
|
4
|
+
import { input } from "@inquirer/prompts";
|
|
5
|
+
import {
|
|
6
|
+
checkUsernameAvailable,
|
|
7
|
+
getCurrentUserProfile,
|
|
8
|
+
registerUser,
|
|
9
|
+
setUsername,
|
|
10
|
+
} from "../control-plane.ts";
|
|
11
|
+
import { promptSelect } from "../hooks.ts";
|
|
12
|
+
import { celebrate, error, info, spinner, success, warn } from "../output.ts";
|
|
13
|
+
import { identifyUser } from "../telemetry.ts";
|
|
14
|
+
import { type DeviceAuthResponse, pollDeviceToken, startDeviceAuth } from "./client.ts";
|
|
15
|
+
import { type AuthCredentials, saveCredentials } from "./store.ts";
|
|
16
|
+
|
|
17
|
+
export interface LoginFlowOptions {
|
|
18
|
+
/** Skip the initial "Logging in..." message (used when called from auto-login) */
|
|
19
|
+
silent?: boolean;
|
|
20
|
+
/** Skip the username prompt after login */
|
|
21
|
+
skipUsernamePrompt?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface LoginFlowResult {
|
|
25
|
+
success: boolean;
|
|
26
|
+
user?: {
|
|
27
|
+
id: string;
|
|
28
|
+
email: string;
|
|
29
|
+
first_name: string | null;
|
|
30
|
+
last_name: string | null;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Run the complete login flow including device auth, token polling, and user registration.
|
|
36
|
+
* Returns a result object instead of calling process.exit().
|
|
37
|
+
*/
|
|
38
|
+
export async function runLoginFlow(options?: LoginFlowOptions): Promise<LoginFlowResult> {
|
|
39
|
+
if (!options?.silent) {
|
|
40
|
+
info("Logging in to jack cloud...");
|
|
41
|
+
console.error("");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const spin = spinner("Starting login...");
|
|
45
|
+
let deviceAuth: DeviceAuthResponse;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
deviceAuth = await startDeviceAuth();
|
|
49
|
+
spin.stop();
|
|
50
|
+
} catch (err) {
|
|
51
|
+
spin.stop();
|
|
52
|
+
error(err instanceof Error ? err.message : "Failed to start login");
|
|
53
|
+
return { success: false };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
celebrate("Your code:", [deviceAuth.user_code]);
|
|
57
|
+
info(`Opening ${deviceAuth.verification_uri} in your browser...`);
|
|
58
|
+
console.error("");
|
|
59
|
+
|
|
60
|
+
// Open browser - use Bun.spawn for cross-platform
|
|
61
|
+
try {
|
|
62
|
+
const platform = process.platform;
|
|
63
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
64
|
+
Bun.spawn([cmd, deviceAuth.verification_uri_complete]);
|
|
65
|
+
} catch {
|
|
66
|
+
info(`If the browser didn't open, go to: ${deviceAuth.verification_uri_complete}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const pollSpin = spinner("Waiting for you to complete login in browser...");
|
|
70
|
+
const interval = (deviceAuth.interval || 5) * 1000;
|
|
71
|
+
const expiresAt = Date.now() + deviceAuth.expires_in * 1000;
|
|
72
|
+
|
|
73
|
+
while (Date.now() < expiresAt) {
|
|
74
|
+
await sleep(interval);
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const tokens = await pollDeviceToken(deviceAuth.device_code);
|
|
78
|
+
|
|
79
|
+
if (tokens) {
|
|
80
|
+
pollSpin.stop();
|
|
81
|
+
|
|
82
|
+
// Default to 5 minutes if expires_in not provided
|
|
83
|
+
const expiresIn = tokens.expires_in ?? 300;
|
|
84
|
+
const creds: AuthCredentials = {
|
|
85
|
+
access_token: tokens.access_token,
|
|
86
|
+
refresh_token: tokens.refresh_token,
|
|
87
|
+
expires_at: Math.floor(Date.now() / 1000) + expiresIn,
|
|
88
|
+
user: tokens.user,
|
|
89
|
+
};
|
|
90
|
+
await saveCredentials(creds);
|
|
91
|
+
|
|
92
|
+
// Register user in control plane database (required for subsequent API calls)
|
|
93
|
+
try {
|
|
94
|
+
await registerUser({
|
|
95
|
+
email: tokens.user.email,
|
|
96
|
+
first_name: tokens.user.first_name,
|
|
97
|
+
last_name: tokens.user.last_name,
|
|
98
|
+
});
|
|
99
|
+
} catch (_regError) {
|
|
100
|
+
// Registration is required - without it, all API calls will fail
|
|
101
|
+
error("Failed to complete login - could not reach jack cloud.");
|
|
102
|
+
error("Please check your internet connection and try again.");
|
|
103
|
+
return { success: false };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Link user identity for cross-platform analytics
|
|
107
|
+
await identifyUser(tokens.user.id, { email: tokens.user.email });
|
|
108
|
+
|
|
109
|
+
// Prompt for username if not set (unless explicitly skipped)
|
|
110
|
+
// Do this before welcome message so we know if user is new or returning
|
|
111
|
+
let isNewUser = false;
|
|
112
|
+
if (!options?.skipUsernamePrompt) {
|
|
113
|
+
isNewUser = await promptForUsername(tokens.user.email, tokens.user.first_name);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.error("");
|
|
117
|
+
const displayName = tokens.user.first_name || "you";
|
|
118
|
+
if (isNewUser) {
|
|
119
|
+
success(`Welcome, ${displayName}!`);
|
|
120
|
+
} else {
|
|
121
|
+
success(`Welcome back, ${displayName}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
success: true,
|
|
126
|
+
user: tokens.user,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
pollSpin.stop();
|
|
131
|
+
error(err instanceof Error ? err.message : "Login failed");
|
|
132
|
+
return { success: false };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
pollSpin.stop();
|
|
137
|
+
error("Login timed out. Please try again.");
|
|
138
|
+
return { success: false };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function sleep(ms: number): Promise<void> {
|
|
142
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Prompt user to set their username if not already set.
|
|
147
|
+
* Returns true if this was a new user (username was set), false if returning user.
|
|
148
|
+
*/
|
|
149
|
+
async function promptForUsername(email: string, firstName: string | null): Promise<boolean> {
|
|
150
|
+
// Skip in non-TTY environments
|
|
151
|
+
if (!process.stdout.isTTY) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const spin = spinner("Checking account...");
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const profile = await getCurrentUserProfile();
|
|
159
|
+
spin.stop();
|
|
160
|
+
|
|
161
|
+
// If user already has a username, they're a returning user
|
|
162
|
+
if (profile?.username) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.error("");
|
|
167
|
+
info("Choose a username for your jack cloud account.");
|
|
168
|
+
info("URLs will look like: alice-vibes.runjack.xyz");
|
|
169
|
+
console.error("");
|
|
170
|
+
|
|
171
|
+
// Generate suggestions from first name, $USER env var, and email
|
|
172
|
+
const suggestions = generateUsernameSuggestions(email, firstName);
|
|
173
|
+
|
|
174
|
+
let username: string | null = null;
|
|
175
|
+
|
|
176
|
+
while (!username) {
|
|
177
|
+
// Build options for promptSelect
|
|
178
|
+
const options = [...suggestions, "Type custom username"];
|
|
179
|
+
info("Pick a username:");
|
|
180
|
+
const choice = await promptSelect(options);
|
|
181
|
+
|
|
182
|
+
let inputUsername: string;
|
|
183
|
+
|
|
184
|
+
if (choice === -1) {
|
|
185
|
+
// User pressed Esc - skip username setup
|
|
186
|
+
warn("Skipped username setup. You can set it later.");
|
|
187
|
+
return true; // Still a new user, just skipped
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (choice === options.length - 1) {
|
|
191
|
+
// User chose to type custom username
|
|
192
|
+
inputUsername = await input({
|
|
193
|
+
message: "Username:",
|
|
194
|
+
validate: validateUsername,
|
|
195
|
+
});
|
|
196
|
+
} else {
|
|
197
|
+
// User picked a suggestion (choice is guaranteed to be valid index)
|
|
198
|
+
inputUsername = suggestions[choice] as string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check availability
|
|
202
|
+
const checkSpin = spinner("Checking availability...");
|
|
203
|
+
const availability = await checkUsernameAvailable(inputUsername);
|
|
204
|
+
checkSpin.stop();
|
|
205
|
+
|
|
206
|
+
if (!availability.available) {
|
|
207
|
+
warn(availability.error || `Username "${inputUsername}" is already taken. Try another.`);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Try to set the username
|
|
212
|
+
const setSpin = spinner("Setting username...");
|
|
213
|
+
try {
|
|
214
|
+
await setUsername(inputUsername);
|
|
215
|
+
setSpin.stop();
|
|
216
|
+
username = inputUsername;
|
|
217
|
+
success(`Username set to "${username}"`);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
setSpin.stop();
|
|
220
|
+
warn(err instanceof Error ? err.message : "Failed to set username");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return true; // New user - username was set
|
|
225
|
+
} catch (_err) {
|
|
226
|
+
spin.stop();
|
|
227
|
+
// Non-fatal - user can set username later
|
|
228
|
+
warn("Could not set username. You can set it later.");
|
|
229
|
+
return true; // Assume new user if we couldn't check
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function validateUsername(value: string): string | true {
|
|
234
|
+
if (!value || value.length < 3) {
|
|
235
|
+
return "Username must be at least 3 characters";
|
|
236
|
+
}
|
|
237
|
+
if (value.length > 39) {
|
|
238
|
+
return "Username must be 39 characters or less";
|
|
239
|
+
}
|
|
240
|
+
if (value !== value.toLowerCase()) {
|
|
241
|
+
return "Username must be lowercase";
|
|
242
|
+
}
|
|
243
|
+
if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]{1,2}$/.test(value)) {
|
|
244
|
+
return "Use only lowercase letters, numbers, and hyphens";
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function generateUsernameSuggestions(email: string, firstName: string | null): string[] {
|
|
250
|
+
const suggestions: string[] = [];
|
|
251
|
+
|
|
252
|
+
// Try first name first (most personal)
|
|
253
|
+
if (firstName) {
|
|
254
|
+
const normalized = normalizeToUsername(firstName);
|
|
255
|
+
if (normalized && normalized.length >= 3) {
|
|
256
|
+
suggestions.push(normalized);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Try $USER environment variable
|
|
261
|
+
const envUser = process.env.USER || process.env.USERNAME;
|
|
262
|
+
if (envUser) {
|
|
263
|
+
const normalized = normalizeToUsername(envUser);
|
|
264
|
+
if (normalized && normalized.length >= 3 && !suggestions.includes(normalized)) {
|
|
265
|
+
suggestions.push(normalized);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Try email local part
|
|
270
|
+
const emailLocal = email.split("@")[0];
|
|
271
|
+
if (emailLocal) {
|
|
272
|
+
const normalized = normalizeToUsername(emailLocal);
|
|
273
|
+
if (normalized && normalized.length >= 3 && !suggestions.includes(normalized)) {
|
|
274
|
+
suggestions.push(normalized);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return suggestions.slice(0, 3); // Max 3 suggestions
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function normalizeToUsername(input: string): string {
|
|
282
|
+
return input
|
|
283
|
+
.toLowerCase()
|
|
284
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
285
|
+
.replace(/^-+|-+$/g, "")
|
|
286
|
+
.slice(0, 39);
|
|
287
|
+
}
|
package/src/lib/control-plane.ts
CHANGED
|
@@ -370,6 +370,48 @@ export async function fetchProjectTags(projectId: string): Promise<string[]> {
|
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
+
export interface RegisterUserRequest {
|
|
374
|
+
email: string;
|
|
375
|
+
first_name?: string | null;
|
|
376
|
+
last_name?: string | null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export interface RegisterUserResponse {
|
|
380
|
+
user: {
|
|
381
|
+
id: string;
|
|
382
|
+
email: string;
|
|
383
|
+
first_name?: string;
|
|
384
|
+
last_name?: string;
|
|
385
|
+
};
|
|
386
|
+
org: {
|
|
387
|
+
id: string;
|
|
388
|
+
workos_org_id: string;
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Register or update user in the control plane after login.
|
|
394
|
+
* This must be called after device auth to create/sync the user in the database.
|
|
395
|
+
*/
|
|
396
|
+
export async function registerUser(userInfo: RegisterUserRequest): Promise<RegisterUserResponse> {
|
|
397
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
398
|
+
|
|
399
|
+
const response = await authFetch(`${getControlApiUrl()}/v1/register`, {
|
|
400
|
+
method: "POST",
|
|
401
|
+
headers: { "Content-Type": "application/json" },
|
|
402
|
+
body: JSON.stringify(userInfo),
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
if (!response.ok) {
|
|
406
|
+
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
|
407
|
+
message?: string;
|
|
408
|
+
};
|
|
409
|
+
throw new Error(err.message || `Failed to register user: ${response.status}`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return response.json() as Promise<RegisterUserResponse>;
|
|
413
|
+
}
|
|
414
|
+
|
|
373
415
|
/**
|
|
374
416
|
* Check if a username is available on jack cloud.
|
|
375
417
|
* Does not require authentication.
|
package/src/lib/deploy-mode.ts
CHANGED
|
@@ -24,6 +24,27 @@ export async function isWranglerAvailable(): Promise<boolean> {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Ensure wrangler is installed, auto-installing if needed.
|
|
29
|
+
*
|
|
30
|
+
* @param onInstalling - Optional callback when installation starts (for UI feedback)
|
|
31
|
+
* @returns true if wrangler is available, false if installation failed
|
|
32
|
+
*/
|
|
33
|
+
export async function ensureWranglerInstalled(onInstalling?: () => void): Promise<boolean> {
|
|
34
|
+
if (await isWranglerAvailable()) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Auto-install wrangler
|
|
39
|
+
onInstalling?.();
|
|
40
|
+
try {
|
|
41
|
+
await $`bun add -g wrangler`.quiet();
|
|
42
|
+
return await isWranglerAvailable();
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
27
48
|
/**
|
|
28
49
|
* Determine deploy mode based on login status and flags.
|
|
29
50
|
*
|
|
@@ -71,14 +92,14 @@ export async function validateModeAvailability(mode: DeployMode): Promise<string
|
|
|
71
92
|
}
|
|
72
93
|
const hasWrangler = await isWranglerAvailable();
|
|
73
94
|
if (!hasWrangler) {
|
|
74
|
-
return "wrangler
|
|
95
|
+
return "wrangler installation failed. Please install manually: bun add -g wrangler";
|
|
75
96
|
}
|
|
76
97
|
}
|
|
77
98
|
|
|
78
99
|
if (mode === "byo") {
|
|
79
100
|
const hasWrangler = await isWranglerAvailable();
|
|
80
101
|
if (!hasWrangler) {
|
|
81
|
-
return "wrangler
|
|
102
|
+
return "wrangler installation failed. Please install manually: bun add -g wrangler";
|
|
82
103
|
}
|
|
83
104
|
}
|
|
84
105
|
|
package/src/lib/deploy-upload.ts
CHANGED
|
@@ -84,7 +84,16 @@ export async function uploadDeployment(options: DeployUploadOptions): Promise<De
|
|
|
84
84
|
if (!response.ok) {
|
|
85
85
|
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
|
86
86
|
message?: string;
|
|
87
|
+
error?: string;
|
|
87
88
|
};
|
|
89
|
+
|
|
90
|
+
// Provide actionable error for orphaned local links
|
|
91
|
+
if (response.status === 404 && err.error === "not_found") {
|
|
92
|
+
throw new Error(
|
|
93
|
+
"Project not found in jack cloud. The local link may be orphaned.\nFix: jack unlink && jack ship",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
88
97
|
throw new Error(err.message || `Upload failed: ${response.status}`);
|
|
89
98
|
}
|
|
90
99
|
|
package/src/lib/hooks.ts
CHANGED
|
@@ -185,13 +185,21 @@ function isAccountAssociation(value: unknown): boolean {
|
|
|
185
185
|
if (!value || typeof value !== "object") return false;
|
|
186
186
|
// Check direct format: { header, payload, signature }
|
|
187
187
|
const obj = value as Record<string, unknown>;
|
|
188
|
-
if (
|
|
188
|
+
if (
|
|
189
|
+
typeof obj.header === "string" &&
|
|
190
|
+
typeof obj.payload === "string" &&
|
|
191
|
+
typeof obj.signature === "string"
|
|
192
|
+
) {
|
|
189
193
|
return true;
|
|
190
194
|
}
|
|
191
195
|
// Check nested format from Farcaster: { accountAssociation: { header, payload, signature } }
|
|
192
196
|
if (obj.accountAssociation && typeof obj.accountAssociation === "object") {
|
|
193
197
|
const inner = obj.accountAssociation as Record<string, unknown>;
|
|
194
|
-
return
|
|
198
|
+
return (
|
|
199
|
+
typeof inner.header === "string" &&
|
|
200
|
+
typeof inner.payload === "string" &&
|
|
201
|
+
typeof inner.signature === "string"
|
|
202
|
+
);
|
|
195
203
|
}
|
|
196
204
|
return false;
|
|
197
205
|
}
|
|
@@ -199,17 +207,27 @@ function isAccountAssociation(value: unknown): boolean {
|
|
|
199
207
|
/**
|
|
200
208
|
* Extract the accountAssociation object (handles both nested and flat formats)
|
|
201
209
|
*/
|
|
202
|
-
function extractAccountAssociation(
|
|
210
|
+
function extractAccountAssociation(
|
|
211
|
+
value: unknown,
|
|
212
|
+
): { header: string; payload: string; signature: string } | null {
|
|
203
213
|
if (!value || typeof value !== "object") return null;
|
|
204
214
|
const obj = value as Record<string, unknown>;
|
|
205
215
|
// Direct format
|
|
206
|
-
if (
|
|
216
|
+
if (
|
|
217
|
+
typeof obj.header === "string" &&
|
|
218
|
+
typeof obj.payload === "string" &&
|
|
219
|
+
typeof obj.signature === "string"
|
|
220
|
+
) {
|
|
207
221
|
return { header: obj.header, payload: obj.payload, signature: obj.signature };
|
|
208
222
|
}
|
|
209
223
|
// Nested format from Farcaster
|
|
210
224
|
if (obj.accountAssociation && typeof obj.accountAssociation === "object") {
|
|
211
225
|
const inner = obj.accountAssociation as Record<string, unknown>;
|
|
212
|
-
if (
|
|
226
|
+
if (
|
|
227
|
+
typeof inner.header === "string" &&
|
|
228
|
+
typeof inner.payload === "string" &&
|
|
229
|
+
typeof inner.signature === "string"
|
|
230
|
+
) {
|
|
213
231
|
return { header: inner.header, payload: inner.payload, signature: inner.signature };
|
|
214
232
|
}
|
|
215
233
|
}
|
|
@@ -490,10 +508,8 @@ const actionHandlers: {
|
|
|
490
508
|
writeJson: async (action, context, options) => {
|
|
491
509
|
const ui = options.output ?? noopOutput;
|
|
492
510
|
const targetPath = resolveHookPath(action.path, context);
|
|
493
|
-
const ok = await applyJsonWrite(
|
|
494
|
-
|
|
495
|
-
action.set,
|
|
496
|
-
(value) => substituteVars(value, context),
|
|
511
|
+
const ok = await applyJsonWrite(targetPath, action.set, (value) =>
|
|
512
|
+
substituteVars(value, context),
|
|
497
513
|
);
|
|
498
514
|
if (!ok) {
|
|
499
515
|
ui.error(`Invalid JSON file: ${targetPath}`);
|
package/src/lib/json-edit.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
|
|
3
|
-
export function setJsonPath(
|
|
4
|
-
target: Record<string, unknown>,
|
|
5
|
-
path: string,
|
|
6
|
-
value: unknown,
|
|
7
|
-
): void {
|
|
3
|
+
export function setJsonPath(target: Record<string, unknown>, path: string, value: unknown): void {
|
|
8
4
|
const keys = path.split(".").filter(Boolean);
|
|
9
5
|
let current: Record<string, unknown> = target;
|
|
10
6
|
|