@appfleet-cli/cli 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/README.md +14 -0
- package/dist/appfleet.d.ts +4 -0
- package/dist/appfleet.js +12253 -0
- package/dist/audit.d.ts +10 -0
- package/dist/audit.js +85 -0
- package/dist/billing-cost.d.ts +8 -0
- package/dist/billing-cost.js +186 -0
- package/dist/cloud-session.d.ts +124 -0
- package/dist/cloud-session.js +1819 -0
- package/dist/command-registry.d.ts +18 -0
- package/dist/command-registry.js +1067 -0
- package/dist/demo-fixture.d.ts +11 -0
- package/dist/demo-fixture.js +39 -0
- package/dist/generate-cli-docs.d.ts +1 -0
- package/dist/generate-cli-docs.js +94 -0
- package/dist/health.d.ts +8 -0
- package/dist/health.js +60 -0
- package/dist/local-vault.d.ts +75 -0
- package/dist/local-vault.js +1169 -0
- package/dist/operations.d.ts +8 -0
- package/dist/operations.js +220 -0
- package/dist/project-memory.d.ts +138 -0
- package/dist/project-memory.js +1529 -0
- package/dist/prototype-inject.d.ts +21 -0
- package/dist/prototype-inject.js +170 -0
- package/dist/provider-integrations.d.ts +8 -0
- package/dist/provider-integrations.js +197 -0
- package/package.json +45 -0
|
@@ -0,0 +1,1819 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { createAppProjectMemory, createCloudSyncMetadata, createCloudSyncRuntimeConflictState, createLocalMetadataSyncQueueEntry, createProjectMemorySyncRequest, createProjectMemorySyncResult, } from "@appfleet/domain";
|
|
7
|
+
import { readProjectMemories, writeProjectMemories } from "./project-memory.js";
|
|
8
|
+
export async function runCloudSessionCommand(argv, options = {}) {
|
|
9
|
+
const now = options.now ?? (() => new Date());
|
|
10
|
+
let env = options.env ?? process.env;
|
|
11
|
+
let parsed;
|
|
12
|
+
try {
|
|
13
|
+
parsed = parseCloudSessionCommand(argv, env);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
return {
|
|
17
|
+
exitCode: 1,
|
|
18
|
+
stdout: "",
|
|
19
|
+
stderr: `AppFleet cloud scaffold failed: ${errorMessage(error)}\n`,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
23
|
+
const sessionPath = options.sessionPath ?? defaultSessionPath(projectRoot);
|
|
24
|
+
const authTokenPath = defaultAuthTokenPath(sessionPath);
|
|
25
|
+
const storedAuthToken = await readAuthToken(authTokenPath);
|
|
26
|
+
if (!env.APPFLEET_CLOUD_AUTH_TOKEN && storedAuthToken) {
|
|
27
|
+
env = {
|
|
28
|
+
...env,
|
|
29
|
+
APPFLEET_CLOUD_AUTH_TOKEN: storedAuthToken,
|
|
30
|
+
APPFLEET_CLOUD_AUTH_TOKEN_SOURCE: "device_store",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const projectMemoryStorePath = options.projectMemoryStorePath ?? defaultProjectMemoryStorePath(projectRoot);
|
|
34
|
+
const metadataSyncStorePath = options.metadataSyncStorePath ?? defaultMetadataSyncStorePath(projectRoot);
|
|
35
|
+
try {
|
|
36
|
+
if (parsed.namespace === "auth" && parsed.action === "login") {
|
|
37
|
+
const productionConfig = resolveProductionCloudConfig(parsed, env);
|
|
38
|
+
if (productionConfig.enabled) {
|
|
39
|
+
const configReport = createRedactedCloudConfigReport("production", productionConfig);
|
|
40
|
+
const missing = missingProductionAuthConfig(productionConfig);
|
|
41
|
+
if (missing.length > 0) {
|
|
42
|
+
const report = createAuthMisconfiguredReport({
|
|
43
|
+
action: "login",
|
|
44
|
+
workspaceId: parsed.workspaceId,
|
|
45
|
+
configReport,
|
|
46
|
+
missing,
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
exitCode: 1,
|
|
50
|
+
stdout: parsed.outputFormat === "json"
|
|
51
|
+
? formatJson(report)
|
|
52
|
+
: formatAuthMisconfigured(report),
|
|
53
|
+
stderr: "",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const createdAt = now();
|
|
57
|
+
const request = buildHostedAuthRequest({
|
|
58
|
+
apiBaseUrl: productionConfig.apiBaseUrl,
|
|
59
|
+
action: "login",
|
|
60
|
+
workspaceId: parsed.workspaceId,
|
|
61
|
+
email: parsed.email,
|
|
62
|
+
});
|
|
63
|
+
const transport = options.hostedAuthTransport ?? defaultHostedAuthTransport;
|
|
64
|
+
const authResult = await transport(request, {
|
|
65
|
+
configReport,
|
|
66
|
+
action: "login",
|
|
67
|
+
timeoutMs: resolveHostedAuthTimeoutMs(env, options.hostedAuthTimeoutMs),
|
|
68
|
+
});
|
|
69
|
+
const session = createHostedSessionMetadata({
|
|
70
|
+
requestedWorkspaceId: parsed.workspaceId,
|
|
71
|
+
requestedEmail: parsed.email,
|
|
72
|
+
createdAt,
|
|
73
|
+
result: authResult,
|
|
74
|
+
});
|
|
75
|
+
if (authResult.authToken) {
|
|
76
|
+
await writeAuthToken(authTokenPath, authResult.authToken);
|
|
77
|
+
}
|
|
78
|
+
await writeJson(sessionPath, session);
|
|
79
|
+
const document = {
|
|
80
|
+
type: "hosted_auth_login_result",
|
|
81
|
+
version: 1,
|
|
82
|
+
cloudMode: "production",
|
|
83
|
+
status: "logged_in",
|
|
84
|
+
config: configReport,
|
|
85
|
+
request,
|
|
86
|
+
session,
|
|
87
|
+
trustBoundary: session.trustBoundary,
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
exitCode: 0,
|
|
91
|
+
stdout: parsed.outputFormat === "json"
|
|
92
|
+
? formatJson(document)
|
|
93
|
+
: formatHostedLogin(document, sessionPath),
|
|
94
|
+
stderr: "",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const createdAt = now();
|
|
98
|
+
const session = {
|
|
99
|
+
type: "local_demo_cloud_session",
|
|
100
|
+
version: 1,
|
|
101
|
+
sessionId: `local_${randomUUID()}`,
|
|
102
|
+
status: "active",
|
|
103
|
+
workspaceId: parsed.workspaceId,
|
|
104
|
+
workspace: {
|
|
105
|
+
id: parsed.workspaceId,
|
|
106
|
+
source: "local_test_override",
|
|
107
|
+
},
|
|
108
|
+
user: {
|
|
109
|
+
id: localUserId(parsed.workspaceId, parsed.email),
|
|
110
|
+
email: parsed.email,
|
|
111
|
+
source: "local_test_override",
|
|
112
|
+
},
|
|
113
|
+
email: parsed.email,
|
|
114
|
+
createdAt: createdAt.toISOString(),
|
|
115
|
+
expiresAt: sessionExpiry(createdAt).toISOString(),
|
|
116
|
+
hostedAuthImplemented: false,
|
|
117
|
+
productionCloudPersistenceImplemented: false,
|
|
118
|
+
localTestPersistenceImplemented: true,
|
|
119
|
+
secretMaterialStored: false,
|
|
120
|
+
zeroSecretBoundary: createZeroSecretBoundary(),
|
|
121
|
+
trustBoundary: [
|
|
122
|
+
"This is local-test hosted auth session metadata only.",
|
|
123
|
+
"No hosted AppFleet auth session is created.",
|
|
124
|
+
"No bearer auth material, OAuth refresh material, session cookie, key material, provider credential, encrypted credential blob, command output, or secret fragment is stored.",
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
await writeJson(sessionPath, session);
|
|
128
|
+
return {
|
|
129
|
+
exitCode: 0,
|
|
130
|
+
stdout: parsed.outputFormat === "json"
|
|
131
|
+
? formatJson({ session })
|
|
132
|
+
: formatLogin(session, sessionPath),
|
|
133
|
+
stderr: "",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (parsed.namespace === "auth" && parsed.action === "logout") {
|
|
137
|
+
const productionConfig = resolveProductionCloudConfig(parsed, env);
|
|
138
|
+
if (productionConfig.enabled) {
|
|
139
|
+
const configReport = createRedactedCloudConfigReport("production", productionConfig);
|
|
140
|
+
const missing = missingProductionAuthConfig(productionConfig);
|
|
141
|
+
if (missing.length > 0) {
|
|
142
|
+
const report = createAuthMisconfiguredReport({
|
|
143
|
+
action: "logout",
|
|
144
|
+
workspaceId: "workspace_local",
|
|
145
|
+
configReport,
|
|
146
|
+
missing,
|
|
147
|
+
});
|
|
148
|
+
return {
|
|
149
|
+
exitCode: 1,
|
|
150
|
+
stdout: parsed.outputFormat === "json"
|
|
151
|
+
? formatJson(report)
|
|
152
|
+
: formatAuthMisconfigured(report),
|
|
153
|
+
stderr: "",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const existing = await readSession(sessionPath);
|
|
157
|
+
if (existing?.type !== "hosted_cloud_session_metadata") {
|
|
158
|
+
const report = createHostedLogoutMissingSessionReport({
|
|
159
|
+
configReport,
|
|
160
|
+
loggedOutAt: now().toISOString(),
|
|
161
|
+
});
|
|
162
|
+
return {
|
|
163
|
+
exitCode: 1,
|
|
164
|
+
stdout: parsed.outputFormat === "json"
|
|
165
|
+
? formatJson(report)
|
|
166
|
+
: formatHostedLogoutMissingSession(report),
|
|
167
|
+
stderr: "",
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const request = buildHostedAuthRequest({
|
|
171
|
+
apiBaseUrl: productionConfig.apiBaseUrl,
|
|
172
|
+
action: "logout",
|
|
173
|
+
workspaceId: existing.workspaceId,
|
|
174
|
+
sessionId: existing.sessionId,
|
|
175
|
+
});
|
|
176
|
+
const transport = options.hostedAuthTransport ?? defaultHostedAuthTransport;
|
|
177
|
+
const authResult = await transport(request, {
|
|
178
|
+
configReport,
|
|
179
|
+
action: "logout",
|
|
180
|
+
timeoutMs: resolveHostedAuthTimeoutMs(env, options.hostedAuthTimeoutMs),
|
|
181
|
+
});
|
|
182
|
+
await rm(sessionPath, { force: true });
|
|
183
|
+
await rm(authTokenPath, { force: true });
|
|
184
|
+
const document = {
|
|
185
|
+
type: "hosted_auth_logout_result",
|
|
186
|
+
version: 1,
|
|
187
|
+
cloudMode: "production",
|
|
188
|
+
status: authResult.revoked === false ? "logout_unconfirmed" : "logged_out",
|
|
189
|
+
removedLocalSessionMetadata: true,
|
|
190
|
+
hostedAuthSessionRevoked: authResult.revoked !== false,
|
|
191
|
+
loggedOutAt: now().toISOString(),
|
|
192
|
+
config: configReport,
|
|
193
|
+
request,
|
|
194
|
+
session: {
|
|
195
|
+
sessionId: existing.sessionId,
|
|
196
|
+
workspaceId: existing.workspaceId,
|
|
197
|
+
status: "logged_out",
|
|
198
|
+
expiresAt: existing.expiresAt,
|
|
199
|
+
},
|
|
200
|
+
zeroSecretBoundary: createZeroSecretBoundary(),
|
|
201
|
+
trustBoundary: [
|
|
202
|
+
"Hosted logout used explicit production mode and redacted auth material.",
|
|
203
|
+
"Removed local hosted session metadata after the revocation request completed.",
|
|
204
|
+
"No bearer auth material, OAuth refresh material, session cookie, key material, provider credential, encrypted credential blob, command output, or secret fragment was printed or stored.",
|
|
205
|
+
],
|
|
206
|
+
};
|
|
207
|
+
return {
|
|
208
|
+
exitCode: 0,
|
|
209
|
+
stdout: parsed.outputFormat === "json"
|
|
210
|
+
? formatJson(document)
|
|
211
|
+
: formatHostedLogout(document),
|
|
212
|
+
stderr: "",
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const existing = await readSession(sessionPath);
|
|
216
|
+
await rm(sessionPath, { force: true });
|
|
217
|
+
await rm(authTokenPath, { force: true });
|
|
218
|
+
const loggedOutAt = now().toISOString();
|
|
219
|
+
const result = {
|
|
220
|
+
type: "local_demo_logout_result",
|
|
221
|
+
version: 1,
|
|
222
|
+
removedLocalSessionMetadata: existing !== undefined,
|
|
223
|
+
sessionStatus: existing === undefined ? "not_found" : "logged_out",
|
|
224
|
+
loggedOutAt,
|
|
225
|
+
session: existing
|
|
226
|
+
? {
|
|
227
|
+
sessionId: existing.sessionId,
|
|
228
|
+
workspaceId: existing.workspaceId,
|
|
229
|
+
userId: existing.user?.id,
|
|
230
|
+
status: "logged_out",
|
|
231
|
+
expiresAt: existing.expiresAt,
|
|
232
|
+
}
|
|
233
|
+
: undefined,
|
|
234
|
+
hostedAuthSessionRevoked: false,
|
|
235
|
+
productionCloudPersistenceImplemented: false,
|
|
236
|
+
localTestPersistenceImplemented: true,
|
|
237
|
+
zeroSecretBoundary: createZeroSecretBoundary(),
|
|
238
|
+
trustBoundary: [
|
|
239
|
+
"Only local-test session metadata was removed.",
|
|
240
|
+
"No hosted AppFleet auth session exists to revoke in V1.",
|
|
241
|
+
"No bearer auth material, OAuth refresh material, session cookie, key material, provider credential, encrypted credential blob, command output, or secret fragment was read or removed.",
|
|
242
|
+
],
|
|
243
|
+
};
|
|
244
|
+
return {
|
|
245
|
+
exitCode: 0,
|
|
246
|
+
stdout: parsed.outputFormat === "json"
|
|
247
|
+
? formatJson(result)
|
|
248
|
+
: formatLogout(result.removedLocalSessionMetadata),
|
|
249
|
+
stderr: "",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (parsed.namespace === "cloud") {
|
|
253
|
+
if (parsed.action === "resolve-conflict") {
|
|
254
|
+
const session = await readSession(sessionPath);
|
|
255
|
+
const workspaceId = parsed.workspaceId ?? safeSessionWorkspaceId(session) ?? "workspace_local";
|
|
256
|
+
const resolvedAt = now().toISOString();
|
|
257
|
+
const syncStore = await readMetadataSyncStore(metadataSyncStorePath);
|
|
258
|
+
const workspaceState = getWorkspaceSyncState(syncStore, workspaceId);
|
|
259
|
+
const pendingConflicts = workspaceState.conflicts.filter((conflict) => conflict.projectId === parsed.projectId &&
|
|
260
|
+
conflict.status === "pending");
|
|
261
|
+
if (pendingConflicts.length === 0) {
|
|
262
|
+
const document = {
|
|
263
|
+
type: "cloud_metadata_sync_conflict_resolution_result",
|
|
264
|
+
version: 1,
|
|
265
|
+
cloudMode: "local-test",
|
|
266
|
+
status: "not_found",
|
|
267
|
+
workspaceId,
|
|
268
|
+
projectId: parsed.projectId,
|
|
269
|
+
resolution: parsed.resolution,
|
|
270
|
+
metadataSyncStorePath,
|
|
271
|
+
syncAttempted: false,
|
|
272
|
+
trustBoundary: [
|
|
273
|
+
"No pending runtime conflict was found for this project.",
|
|
274
|
+
"No cloud transport was called and no project memory payload was stored.",
|
|
275
|
+
],
|
|
276
|
+
};
|
|
277
|
+
return {
|
|
278
|
+
exitCode: 1,
|
|
279
|
+
stdout: parsed.outputFormat === "json"
|
|
280
|
+
? formatJson(document)
|
|
281
|
+
: formatConflictResolution(document),
|
|
282
|
+
stderr: "",
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const status = parsed.resolution === "local"
|
|
286
|
+
? "resolved_accept_local_metadata"
|
|
287
|
+
: "resolved_accept_remote_metadata";
|
|
288
|
+
workspaceState.conflicts = workspaceState.conflicts.map((conflict) => conflict.projectId === parsed.projectId && conflict.status === "pending"
|
|
289
|
+
? createCloudSyncRuntimeConflictState({
|
|
290
|
+
...conflict,
|
|
291
|
+
status,
|
|
292
|
+
resolvedAt,
|
|
293
|
+
resolution: parsed.resolution === "local"
|
|
294
|
+
? "accept_local_metadata"
|
|
295
|
+
: "accept_remote_metadata",
|
|
296
|
+
})
|
|
297
|
+
: conflict);
|
|
298
|
+
workspaceState.queue = workspaceState.queue.map((entry) => entry.rejectedProjectIds.includes(parsed.projectId) &&
|
|
299
|
+
entry.status === "pending_conflict"
|
|
300
|
+
? createLocalMetadataSyncQueueEntry({
|
|
301
|
+
...entry,
|
|
302
|
+
status,
|
|
303
|
+
updatedAt: resolvedAt,
|
|
304
|
+
retryable: parsed.resolution === "local",
|
|
305
|
+
})
|
|
306
|
+
: entry);
|
|
307
|
+
if (parsed.resolution === "local") {
|
|
308
|
+
const localMemories = await readProjectMemories(projectMemoryStorePath);
|
|
309
|
+
const memory = localMemories.find((candidate) => candidate.id === parsed.projectId);
|
|
310
|
+
if (memory) {
|
|
311
|
+
workspaceState.projectFingerprints[parsed.projectId] = {
|
|
312
|
+
projectId: parsed.projectId,
|
|
313
|
+
fingerprint: projectMemoryFingerprint(memory),
|
|
314
|
+
lastSyncedAt: memory.cloudSync.lastSyncedAt ?? resolvedAt,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
syncStore.workspaces[workspaceId] = workspaceState;
|
|
319
|
+
await writeMetadataSyncStore(metadataSyncStorePath, syncStore);
|
|
320
|
+
const pendingConflictCount = workspaceState.conflicts.filter((conflict) => conflict.status === "pending").length;
|
|
321
|
+
const document = {
|
|
322
|
+
type: "cloud_metadata_sync_conflict_resolution_result",
|
|
323
|
+
version: 1,
|
|
324
|
+
cloudMode: "local-test",
|
|
325
|
+
status: "resolved",
|
|
326
|
+
workspaceId,
|
|
327
|
+
projectId: parsed.projectId,
|
|
328
|
+
resolution: parsed.resolution,
|
|
329
|
+
resolvedAt,
|
|
330
|
+
pendingConflictCount,
|
|
331
|
+
metadataSyncStorePath,
|
|
332
|
+
syncAttempted: false,
|
|
333
|
+
localProjectMemoryOverwritten: false,
|
|
334
|
+
remoteProjectMemoryPayloadStored: false,
|
|
335
|
+
trustBoundary: [
|
|
336
|
+
"Resolved local/cloud metadata conflict state only.",
|
|
337
|
+
"No cloud transport was called during conflict resolution.",
|
|
338
|
+
"Did not store remote project payloads, local project payloads, plaintext credential values, encrypted credential blobs, key wrappers, key material, command output, or secret fragments.",
|
|
339
|
+
],
|
|
340
|
+
};
|
|
341
|
+
return {
|
|
342
|
+
exitCode: 0,
|
|
343
|
+
stdout: parsed.outputFormat === "json"
|
|
344
|
+
? formatJson(document)
|
|
345
|
+
: formatConflictResolution(document),
|
|
346
|
+
stderr: "",
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
const productionConfig = resolveProductionCloudConfig(parsed, env);
|
|
350
|
+
const mode = productionConfig.enabled ? "production" : "local-test";
|
|
351
|
+
if (mode === "production") {
|
|
352
|
+
const configReport = createRedactedCloudConfigReport(mode, productionConfig);
|
|
353
|
+
const missing = missingProductionConfig(productionConfig);
|
|
354
|
+
if (missing.length > 0) {
|
|
355
|
+
const report = createProductionMisconfiguredReport({
|
|
356
|
+
action: parsed.action,
|
|
357
|
+
workspaceId: parsed.workspaceId ?? "workspace_local",
|
|
358
|
+
configReport,
|
|
359
|
+
missing,
|
|
360
|
+
});
|
|
361
|
+
return {
|
|
362
|
+
exitCode: 1,
|
|
363
|
+
stdout: parsed.outputFormat === "json"
|
|
364
|
+
? formatJson(report)
|
|
365
|
+
: formatProductionMisconfigured(report),
|
|
366
|
+
stderr: "",
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const localMemories = await readProjectMemories(projectMemoryStorePath);
|
|
370
|
+
const memories = localMemories.map(cloudSafeProjectMemory);
|
|
371
|
+
const session = await readSession(sessionPath);
|
|
372
|
+
const workspaceId = parsed.workspaceId ?? safeSessionWorkspaceId(session) ?? "workspace_local";
|
|
373
|
+
const createdAt = now().toISOString();
|
|
374
|
+
const request = buildProductionCloudMetadataSyncRequest({
|
|
375
|
+
apiBaseUrl: productionConfig.apiBaseUrl,
|
|
376
|
+
workspaceId,
|
|
377
|
+
createdAt,
|
|
378
|
+
memories,
|
|
379
|
+
idempotencyKey: parsed.idempotencyKey ?? env.APPFLEET_METADATA_SYNC_IDEMPOTENCY_KEY,
|
|
380
|
+
});
|
|
381
|
+
const syncStore = await readMetadataSyncStore(metadataSyncStorePath);
|
|
382
|
+
const workspaceState = getWorkspaceSyncState(syncStore, workspaceId);
|
|
383
|
+
const pendingEntry = createSyncQueueEntry({
|
|
384
|
+
id: `queue_${randomUUID()}`,
|
|
385
|
+
workspaceId,
|
|
386
|
+
cloudMode: "production",
|
|
387
|
+
requestedProjectIds: request.body.envelopes.map((envelope) => envelope.projectId),
|
|
388
|
+
acceptedProjectIds: [],
|
|
389
|
+
rejectedProjectIds: [],
|
|
390
|
+
status: "pending_upload",
|
|
391
|
+
idempotencyKey: request.headers.idempotencyKey,
|
|
392
|
+
createdAt,
|
|
393
|
+
updatedAt: createdAt,
|
|
394
|
+
productionCloudRequestCompleted: false,
|
|
395
|
+
retryable: true,
|
|
396
|
+
});
|
|
397
|
+
workspaceState.queue = [...workspaceState.queue, pendingEntry];
|
|
398
|
+
syncStore.workspaces[workspaceId] = workspaceState;
|
|
399
|
+
await writeMetadataSyncStore(metadataSyncStorePath, syncStore);
|
|
400
|
+
const transport = options.transport ?? defaultProductionCloudTransport;
|
|
401
|
+
const maxRetries = options.productionSyncMaxRetries ?? 2;
|
|
402
|
+
const transportResult = await runProductionMetadataSyncWithRetry({
|
|
403
|
+
request,
|
|
404
|
+
transport,
|
|
405
|
+
authToken: productionConfig.authToken,
|
|
406
|
+
configReport,
|
|
407
|
+
maxRetries,
|
|
408
|
+
});
|
|
409
|
+
if (transportResult.status === "failed") {
|
|
410
|
+
workspaceState.queue = workspaceState.queue.map((entry) => entry.id === pendingEntry.id
|
|
411
|
+
? createSyncQueueEntry({
|
|
412
|
+
...entry,
|
|
413
|
+
status: "failed_retryable",
|
|
414
|
+
updatedAt: now().toISOString(),
|
|
415
|
+
productionCloudRequestCompleted: false,
|
|
416
|
+
retryable: true,
|
|
417
|
+
})
|
|
418
|
+
: entry);
|
|
419
|
+
syncStore.workspaces[workspaceId] = workspaceState;
|
|
420
|
+
await writeMetadataSyncStore(metadataSyncStorePath, syncStore);
|
|
421
|
+
const document = createProductionTransportFailedReport({
|
|
422
|
+
action: parsed.action,
|
|
423
|
+
mode,
|
|
424
|
+
localDemoSessionPresent: session !== undefined,
|
|
425
|
+
hostedSessionMetadataPresent: session?.type === "hosted_cloud_session_metadata",
|
|
426
|
+
configReport,
|
|
427
|
+
request,
|
|
428
|
+
retry: transportResult.retry,
|
|
429
|
+
});
|
|
430
|
+
return {
|
|
431
|
+
exitCode: 1,
|
|
432
|
+
stdout: parsed.outputFormat === "json"
|
|
433
|
+
? formatJson(document)
|
|
434
|
+
: formatProductionTransportFailed(document),
|
|
435
|
+
stderr: "",
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
const result = createProjectMemorySyncResult({
|
|
439
|
+
workspaceId,
|
|
440
|
+
createdAt,
|
|
441
|
+
acceptedProjectIds: transportResult.result.acceptedProjectIds,
|
|
442
|
+
rejectedProjectIds: transportResult.result.rejectedProjectIds,
|
|
443
|
+
});
|
|
444
|
+
const rejectedProjectIdSet = new Set(result.rejectedProjectIds);
|
|
445
|
+
for (const envelope of request.body.envelopes) {
|
|
446
|
+
if (rejectedProjectIdSet.has(envelope.projectId)) {
|
|
447
|
+
workspaceState.conflicts = upsertRuntimeConflict({
|
|
448
|
+
conflicts: workspaceState.conflicts,
|
|
449
|
+
workspaceId,
|
|
450
|
+
projectId: envelope.projectId,
|
|
451
|
+
localFingerprint: projectMemoryFingerprint(envelope.memory),
|
|
452
|
+
remoteFingerprint: "production_cloud_rejected_metadata",
|
|
453
|
+
localLastSyncedAt: envelope.memory.cloudSync.lastSyncedAt,
|
|
454
|
+
remoteLastSyncedAt: "unknown",
|
|
455
|
+
detectedAt: createdAt,
|
|
456
|
+
});
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
workspaceState.projectFingerprints[envelope.projectId] = {
|
|
460
|
+
projectId: envelope.projectId,
|
|
461
|
+
fingerprint: projectMemoryFingerprint(envelope.memory),
|
|
462
|
+
lastSyncedAt: createdAt,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
workspaceState.queue = workspaceState.queue.map((entry) => entry.id === pendingEntry.id
|
|
466
|
+
? createSyncQueueEntry({
|
|
467
|
+
...entry,
|
|
468
|
+
acceptedProjectIds: result.acceptedProjectIds,
|
|
469
|
+
rejectedProjectIds: result.rejectedProjectIds,
|
|
470
|
+
status: result.rejectedProjectIds.length > 0
|
|
471
|
+
? "pending_conflict"
|
|
472
|
+
: "applied",
|
|
473
|
+
updatedAt: createdAt,
|
|
474
|
+
productionCloudRequestCompleted: true,
|
|
475
|
+
retryable: result.rejectedProjectIds.length > 0,
|
|
476
|
+
})
|
|
477
|
+
: entry);
|
|
478
|
+
const syncMetadata = createCloudSyncMetadata({
|
|
479
|
+
id: `sync_${randomUUID()}`,
|
|
480
|
+
request: createProjectMemorySyncRequest({
|
|
481
|
+
workspaceId,
|
|
482
|
+
createdAt,
|
|
483
|
+
memories,
|
|
484
|
+
}),
|
|
485
|
+
result,
|
|
486
|
+
completedAt: createdAt,
|
|
487
|
+
});
|
|
488
|
+
workspaceState.syncMetadata = [...workspaceState.syncMetadata, syncMetadata];
|
|
489
|
+
syncStore.workspaces[workspaceId] = workspaceState;
|
|
490
|
+
await writeMetadataSyncStore(metadataSyncStorePath, syncStore);
|
|
491
|
+
if (result.acceptedProjectIds.length > 0) {
|
|
492
|
+
const acceptedProjectIdSet = new Set(result.acceptedProjectIds);
|
|
493
|
+
await writeProjectMemories(projectMemoryStorePath, localMemories.map((memory) => acceptedProjectIdSet.has(memory.id)
|
|
494
|
+
? createAppProjectMemory({
|
|
495
|
+
...memory,
|
|
496
|
+
cloudSync: {
|
|
497
|
+
...memory.cloudSync,
|
|
498
|
+
lastSyncedAt: createdAt,
|
|
499
|
+
},
|
|
500
|
+
})
|
|
501
|
+
: memory));
|
|
502
|
+
}
|
|
503
|
+
const document = {
|
|
504
|
+
type: "cloud_metadata_sync_result",
|
|
505
|
+
version: 1,
|
|
506
|
+
mode: parsed.action,
|
|
507
|
+
cloudMode: mode,
|
|
508
|
+
localDemoSessionPresent: session !== undefined,
|
|
509
|
+
hostedSessionMetadataPresent: session?.type === "hosted_cloud_session_metadata",
|
|
510
|
+
hostedAuthImplemented: false,
|
|
511
|
+
productionCloudPersistenceImplemented: false,
|
|
512
|
+
productionCloudRequestCompleted: true,
|
|
513
|
+
localTestPersistenceImplemented: false,
|
|
514
|
+
config: configReport,
|
|
515
|
+
request,
|
|
516
|
+
retry: transportResult.retry,
|
|
517
|
+
result,
|
|
518
|
+
syncMetadata,
|
|
519
|
+
queue: {
|
|
520
|
+
addedEntryId: pendingEntry.id,
|
|
521
|
+
latestStatus: result.rejectedProjectIds.length > 0
|
|
522
|
+
? "pending_conflict"
|
|
523
|
+
: "applied",
|
|
524
|
+
pendingUploadCount: workspaceState.queue.filter((entry) => entry.status === "pending_upload").length,
|
|
525
|
+
pendingConflictCount: workspaceState.queue.filter((entry) => entry.status === "pending_conflict").length,
|
|
526
|
+
},
|
|
527
|
+
trustBoundary: [
|
|
528
|
+
"Production cloud mode was explicitly enabled.",
|
|
529
|
+
"The request descriptor contains sanitized project metadata only.",
|
|
530
|
+
"Safe transient transport failures are retried with the same idempotency key.",
|
|
531
|
+
"Persisted durable local sync queue metadata for production attempts without storing project payloads.",
|
|
532
|
+
"Bearer auth material, cookies, DSNs, provider credentials, encrypted credential blob ids, key wrappers, command output, and secret fragments are redacted or omitted from output.",
|
|
533
|
+
],
|
|
534
|
+
};
|
|
535
|
+
return {
|
|
536
|
+
exitCode: 0,
|
|
537
|
+
stdout: parsed.outputFormat === "json"
|
|
538
|
+
? formatJson(document)
|
|
539
|
+
: formatProductionCloudSync(document),
|
|
540
|
+
stderr: "",
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const localMemories = await readProjectMemories(projectMemoryStorePath);
|
|
545
|
+
const memories = localMemories.map(cloudSafeProjectMemory);
|
|
546
|
+
const session = await readSession(sessionPath);
|
|
547
|
+
const workspaceId = parsed.workspaceId ?? safeSessionWorkspaceId(session) ?? "workspace_local";
|
|
548
|
+
const createdAt = now().toISOString();
|
|
549
|
+
const request = createProjectMemorySyncRequest({
|
|
550
|
+
workspaceId,
|
|
551
|
+
createdAt,
|
|
552
|
+
memories,
|
|
553
|
+
});
|
|
554
|
+
const syncStore = await readMetadataSyncStore(metadataSyncStorePath);
|
|
555
|
+
const workspaceState = getWorkspaceSyncState(syncStore, workspaceId);
|
|
556
|
+
const conflicts = request.envelopes
|
|
557
|
+
.map((envelope) => {
|
|
558
|
+
const incomingFingerprint = projectMemoryFingerprint(envelope.memory);
|
|
559
|
+
const existing = workspaceState.projectFingerprints[envelope.projectId];
|
|
560
|
+
const localLastSyncedAt = envelope.memory.cloudSync.lastSyncedAt;
|
|
561
|
+
const isConflict = existing !== undefined &&
|
|
562
|
+
existing.fingerprint !== incomingFingerprint &&
|
|
563
|
+
existing.lastSyncedAt !== localLastSyncedAt;
|
|
564
|
+
if (!isConflict) {
|
|
565
|
+
return undefined;
|
|
566
|
+
}
|
|
567
|
+
workspaceState.conflicts = upsertRuntimeConflict({
|
|
568
|
+
conflicts: workspaceState.conflicts,
|
|
569
|
+
workspaceId,
|
|
570
|
+
projectId: envelope.projectId,
|
|
571
|
+
localFingerprint: incomingFingerprint,
|
|
572
|
+
remoteFingerprint: existing.fingerprint,
|
|
573
|
+
localLastSyncedAt,
|
|
574
|
+
remoteLastSyncedAt: existing.lastSyncedAt,
|
|
575
|
+
detectedAt: createdAt,
|
|
576
|
+
});
|
|
577
|
+
return {
|
|
578
|
+
projectId: envelope.projectId,
|
|
579
|
+
localLastSyncedAt: localLastSyncedAt ?? "never",
|
|
580
|
+
storedLastSyncedAt: existing.lastSyncedAt,
|
|
581
|
+
resolution: "Run cloud resolve-conflict after choosing local or remote metadata; no overwrite was performed.",
|
|
582
|
+
};
|
|
583
|
+
})
|
|
584
|
+
.filter((conflict) => conflict !== undefined);
|
|
585
|
+
const rejectedProjectIds = conflicts.map((conflict) => conflict.projectId);
|
|
586
|
+
const rejectedProjectIdSet = new Set(rejectedProjectIds);
|
|
587
|
+
const acceptedProjectIds = request.envelopes
|
|
588
|
+
.map((envelope) => envelope.projectId)
|
|
589
|
+
.filter((projectId) => !rejectedProjectIdSet.has(projectId));
|
|
590
|
+
const result = createProjectMemorySyncResult({
|
|
591
|
+
workspaceId,
|
|
592
|
+
createdAt,
|
|
593
|
+
acceptedProjectIds,
|
|
594
|
+
rejectedProjectIds,
|
|
595
|
+
});
|
|
596
|
+
const syncMetadata = createCloudSyncMetadata({
|
|
597
|
+
id: `sync_${randomUUID()}`,
|
|
598
|
+
request,
|
|
599
|
+
result,
|
|
600
|
+
completedAt: createdAt,
|
|
601
|
+
});
|
|
602
|
+
for (const envelope of request.envelopes) {
|
|
603
|
+
if (rejectedProjectIdSet.has(envelope.projectId)) {
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
workspaceState.projectFingerprints[envelope.projectId] = {
|
|
607
|
+
projectId: envelope.projectId,
|
|
608
|
+
fingerprint: projectMemoryFingerprint(envelope.memory),
|
|
609
|
+
lastSyncedAt: createdAt,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
const queueEntry = createSyncQueueEntry({
|
|
613
|
+
id: `queue_${randomUUID()}`,
|
|
614
|
+
workspaceId,
|
|
615
|
+
cloudMode: "local-test",
|
|
616
|
+
requestedProjectIds: request.envelopes.map((envelope) => envelope.projectId),
|
|
617
|
+
acceptedProjectIds,
|
|
618
|
+
rejectedProjectIds,
|
|
619
|
+
status: rejectedProjectIds.length > 0 ? "pending_conflict" : "applied",
|
|
620
|
+
createdAt,
|
|
621
|
+
updatedAt: createdAt,
|
|
622
|
+
productionCloudRequestCompleted: false,
|
|
623
|
+
retryable: rejectedProjectIds.length > 0,
|
|
624
|
+
});
|
|
625
|
+
workspaceState.syncMetadata = [...workspaceState.syncMetadata, syncMetadata];
|
|
626
|
+
workspaceState.queue = [...workspaceState.queue, queueEntry];
|
|
627
|
+
syncStore.workspaces[workspaceId] = workspaceState;
|
|
628
|
+
await writeMetadataSyncStore(metadataSyncStorePath, syncStore);
|
|
629
|
+
if (acceptedProjectIds.length > 0) {
|
|
630
|
+
const acceptedProjectIdSet = new Set(acceptedProjectIds);
|
|
631
|
+
await writeProjectMemories(projectMemoryStorePath, localMemories.map((memory) => acceptedProjectIdSet.has(memory.id)
|
|
632
|
+
? createAppProjectMemory({
|
|
633
|
+
...memory,
|
|
634
|
+
cloudSync: {
|
|
635
|
+
...memory.cloudSync,
|
|
636
|
+
lastSyncedAt: createdAt,
|
|
637
|
+
},
|
|
638
|
+
})
|
|
639
|
+
: memory));
|
|
640
|
+
}
|
|
641
|
+
const pendingQueueDepth = workspaceState.queue.filter((entry) => entry.status === "pending_conflict").length;
|
|
642
|
+
const document = {
|
|
643
|
+
type: "cloud_metadata_sync_result",
|
|
644
|
+
version: 1,
|
|
645
|
+
mode: parsed.action,
|
|
646
|
+
cloudMode: "local-test",
|
|
647
|
+
localDemoSessionPresent: session !== undefined,
|
|
648
|
+
hostedAuthImplemented: false,
|
|
649
|
+
productionCloudPersistenceImplemented: false,
|
|
650
|
+
localTestPersistenceImplemented: true,
|
|
651
|
+
config: createRedactedCloudConfigReport("local-test", {
|
|
652
|
+
enabled: false,
|
|
653
|
+
apiBaseUrl: undefined,
|
|
654
|
+
authToken: undefined,
|
|
655
|
+
databaseDsn: env.APPFLEET_DATABASE_URL,
|
|
656
|
+
sources: {
|
|
657
|
+
enabled: "default",
|
|
658
|
+
apiBaseUrl: "missing",
|
|
659
|
+
authToken: "missing",
|
|
660
|
+
databaseDsn: env.APPFLEET_DATABASE_URL ? "env" : "missing",
|
|
661
|
+
},
|
|
662
|
+
}),
|
|
663
|
+
metadataSyncStorePath,
|
|
664
|
+
request,
|
|
665
|
+
result,
|
|
666
|
+
syncMetadata,
|
|
667
|
+
queue: {
|
|
668
|
+
addedEntryId: queueEntry.id,
|
|
669
|
+
pendingConflictCount: pendingQueueDepth,
|
|
670
|
+
latestStatus: queueEntry.status,
|
|
671
|
+
},
|
|
672
|
+
conflicts,
|
|
673
|
+
trustBoundary: [
|
|
674
|
+
"Persisted local-test sync metadata only.",
|
|
675
|
+
"Queued local sync attempts by project id and status only.",
|
|
676
|
+
"Did not upload to hosted AppFleet Cloud.",
|
|
677
|
+
"Did not persist project memory payloads, plaintext credential values, encrypted credential blobs, key wrappers, key material, command output, or secret fragments.",
|
|
678
|
+
],
|
|
679
|
+
};
|
|
680
|
+
return {
|
|
681
|
+
exitCode: 0,
|
|
682
|
+
stdout: parsed.outputFormat === "json"
|
|
683
|
+
? formatJson(document)
|
|
684
|
+
: formatCloudSync(document),
|
|
685
|
+
stderr: "",
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
return {
|
|
690
|
+
exitCode: 1,
|
|
691
|
+
stdout: "",
|
|
692
|
+
stderr: `AppFleet cloud scaffold failed: ${errorMessage(error)}\n`,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
function parseCloudSessionCommand(argv, env) {
|
|
697
|
+
const normalizedArgv = argv[0] === "--" ? argv.slice(1) : argv;
|
|
698
|
+
const namespace = normalizedArgv[0];
|
|
699
|
+
const action = normalizedArgv[1];
|
|
700
|
+
const outputFormat = hasFlag(normalizedArgv, "--json") ? "json" : "human";
|
|
701
|
+
if (namespace === "auth") {
|
|
702
|
+
if (action === "login") {
|
|
703
|
+
return {
|
|
704
|
+
namespace,
|
|
705
|
+
action,
|
|
706
|
+
workspaceId: readFlag(normalizedArgv, "--workspace") ?? "workspace_local",
|
|
707
|
+
email: readFlag(normalizedArgv, "--email"),
|
|
708
|
+
productionCloud: cloudProductionFlag(normalizedArgv, env),
|
|
709
|
+
productionCloudSource: cloudProductionSource(normalizedArgv, env),
|
|
710
|
+
apiBaseUrl: readFlag(normalizedArgv, "--api-base-url"),
|
|
711
|
+
outputFormat,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
if (action === "logout") {
|
|
715
|
+
return {
|
|
716
|
+
namespace,
|
|
717
|
+
action,
|
|
718
|
+
productionCloud: cloudProductionFlag(normalizedArgv, env),
|
|
719
|
+
productionCloudSource: cloudProductionSource(normalizedArgv, env),
|
|
720
|
+
apiBaseUrl: readFlag(normalizedArgv, "--api-base-url"),
|
|
721
|
+
outputFormat,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
throw new Error("usage: auth <login|logout> [arguments]");
|
|
725
|
+
}
|
|
726
|
+
if (namespace === "cloud") {
|
|
727
|
+
if (action === "sync") {
|
|
728
|
+
return {
|
|
729
|
+
namespace,
|
|
730
|
+
action,
|
|
731
|
+
workspaceId: readFlag(normalizedArgv, "--workspace"),
|
|
732
|
+
idempotencyKey: readFlag(normalizedArgv, "--idempotency-key"),
|
|
733
|
+
productionCloud: cloudProductionFlag(normalizedArgv, env),
|
|
734
|
+
productionCloudSource: cloudProductionSource(normalizedArgv, env),
|
|
735
|
+
apiBaseUrl: readFlag(normalizedArgv, "--api-base-url"),
|
|
736
|
+
outputFormat,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
if (action === "metadata-sync") {
|
|
740
|
+
return {
|
|
741
|
+
namespace,
|
|
742
|
+
action,
|
|
743
|
+
workspaceId: readFlag(normalizedArgv, "--workspace"),
|
|
744
|
+
idempotencyKey: readFlag(normalizedArgv, "--idempotency-key"),
|
|
745
|
+
productionCloud: cloudProductionFlag(normalizedArgv, env),
|
|
746
|
+
productionCloudSource: cloudProductionSource(normalizedArgv, env),
|
|
747
|
+
apiBaseUrl: readFlag(normalizedArgv, "--api-base-url"),
|
|
748
|
+
outputFormat,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
if (action === "resolve-conflict") {
|
|
752
|
+
const projectId = readFlag(normalizedArgv, "--project");
|
|
753
|
+
const resolution = readFlag(normalizedArgv, "--accept");
|
|
754
|
+
if (!projectId || (resolution !== "local" && resolution !== "remote")) {
|
|
755
|
+
throw new Error("usage: cloud resolve-conflict --project <project-id> --accept <local|remote> [--workspace <workspace-id>] [--json]");
|
|
756
|
+
}
|
|
757
|
+
return {
|
|
758
|
+
namespace,
|
|
759
|
+
action,
|
|
760
|
+
workspaceId: readFlag(normalizedArgv, "--workspace"),
|
|
761
|
+
projectId,
|
|
762
|
+
resolution,
|
|
763
|
+
outputFormat,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
throw new Error("usage: cloud <sync|metadata-sync> [arguments]");
|
|
767
|
+
}
|
|
768
|
+
throw new Error("usage: auth <login|logout> or cloud <sync|metadata-sync>");
|
|
769
|
+
}
|
|
770
|
+
async function readSession(path) {
|
|
771
|
+
try {
|
|
772
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
773
|
+
}
|
|
774
|
+
catch (error) {
|
|
775
|
+
if (isNotFound(error)) {
|
|
776
|
+
return undefined;
|
|
777
|
+
}
|
|
778
|
+
throw error;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
function safeSessionWorkspaceId(session) {
|
|
782
|
+
if (!session) {
|
|
783
|
+
return undefined;
|
|
784
|
+
}
|
|
785
|
+
return session.workspaceId;
|
|
786
|
+
}
|
|
787
|
+
async function writeJson(path, value) {
|
|
788
|
+
await mkdir(dirname(path), { recursive: true });
|
|
789
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
790
|
+
}
|
|
791
|
+
function formatLogin(session, sessionPath) {
|
|
792
|
+
return `${[
|
|
793
|
+
"AppFleet auth scaffold: local-test hosted auth session metadata saved.",
|
|
794
|
+
`sessionPath=${sessionPath}`,
|
|
795
|
+
`sessionId=${session.sessionId}`,
|
|
796
|
+
`status=${session.status}`,
|
|
797
|
+
`workspaceId=${session.workspaceId}`,
|
|
798
|
+
`userId=${session.user.id}`,
|
|
799
|
+
`email=${session.email ?? "not-set"}`,
|
|
800
|
+
`expiresAt=${session.expiresAt}`,
|
|
801
|
+
"hostedAuthImplemented=false",
|
|
802
|
+
"productionCloudPersistence=false",
|
|
803
|
+
"localTestPersistence=true",
|
|
804
|
+
"secretMaterialStored=false",
|
|
805
|
+
"trustBoundary:",
|
|
806
|
+
...session.trustBoundary.map((item) => `- ${item}`),
|
|
807
|
+
].join("\n")}\n`;
|
|
808
|
+
}
|
|
809
|
+
function formatLogout(removedLocalSessionMetadata) {
|
|
810
|
+
return `${[
|
|
811
|
+
"AppFleet auth scaffold: logout complete.",
|
|
812
|
+
`removedLocalSessionMetadata=${removedLocalSessionMetadata}`,
|
|
813
|
+
`sessionStatus=${removedLocalSessionMetadata ? "logged_out" : "not_found"}`,
|
|
814
|
+
"hostedAuthSessionRevoked=false",
|
|
815
|
+
"productionCloudPersistence=false",
|
|
816
|
+
"localTestPersistence=true",
|
|
817
|
+
].join("\n")}\n`;
|
|
818
|
+
}
|
|
819
|
+
function formatHostedLogin(document, sessionPath) {
|
|
820
|
+
return `${[
|
|
821
|
+
"AppFleet auth: hosted login metadata saved.",
|
|
822
|
+
`cloudMode=${document.cloudMode}`,
|
|
823
|
+
`status=${document.status}`,
|
|
824
|
+
`sessionPath=${sessionPath}`,
|
|
825
|
+
`sessionId=${document.session.sessionId}`,
|
|
826
|
+
`workspaceId=${document.session.workspaceId}`,
|
|
827
|
+
`userId=${document.session.user.id ?? "not-returned"}`,
|
|
828
|
+
`email=${document.session.user.email ?? "not-set"}`,
|
|
829
|
+
`expiresAt=${document.session.expiresAt ?? "not-returned"}`,
|
|
830
|
+
`apiBaseUrl=${document.config.api.baseUrl ?? "missing"}`,
|
|
831
|
+
`apiAuth=${document.config.api.auth.bearerToken}`,
|
|
832
|
+
"secretMaterialStored=false",
|
|
833
|
+
"trustBoundary:",
|
|
834
|
+
...document.session.trustBoundary.map((item) => `- ${item}`),
|
|
835
|
+
].join("\n")}\n`;
|
|
836
|
+
}
|
|
837
|
+
function formatHostedLogout(document) {
|
|
838
|
+
return `${[
|
|
839
|
+
"AppFleet auth: hosted logout complete.",
|
|
840
|
+
`cloudMode=${document.cloudMode}`,
|
|
841
|
+
`status=${document.status}`,
|
|
842
|
+
`removedLocalSessionMetadata=${document.removedLocalSessionMetadata}`,
|
|
843
|
+
`hostedAuthSessionRevoked=${document.hostedAuthSessionRevoked}`,
|
|
844
|
+
`sessionId=${document.session.sessionId}`,
|
|
845
|
+
`workspaceId=${document.session.workspaceId}`,
|
|
846
|
+
"trustBoundary:",
|
|
847
|
+
...document.trustBoundary.map((item) => `- ${item}`),
|
|
848
|
+
].join("\n")}\n`;
|
|
849
|
+
}
|
|
850
|
+
function formatCloudSync(document) {
|
|
851
|
+
return `${[
|
|
852
|
+
"AppFleet cloud sync: metadata persisted to local test store.",
|
|
853
|
+
`cloudMode=${document.cloudMode ?? "local-test"}`,
|
|
854
|
+
`workspaceId=${document.request.workspaceId}`,
|
|
855
|
+
`localDemoSessionPresent=${document.localDemoSessionPresent}`,
|
|
856
|
+
"hostedAuthImplemented=false",
|
|
857
|
+
"productionCloudPersistence=false",
|
|
858
|
+
`localTestPersistence=${document.localTestPersistenceImplemented}`,
|
|
859
|
+
`metadataSyncStorePath=${document.metadataSyncStorePath}`,
|
|
860
|
+
`envelopeCount=${document.request.envelopes.length}`,
|
|
861
|
+
`projectIds=${document.request.envelopes.map((envelope) => envelope.projectId).join(", ")}`,
|
|
862
|
+
`acceptedProjectIds=${document.result.acceptedProjectIds.join(", ")}`,
|
|
863
|
+
`rejectedProjectIds=${document.result.rejectedProjectIds.join(", ")}`,
|
|
864
|
+
`queueStatus=${document.queue.latestStatus}`,
|
|
865
|
+
`pendingConflictCount=${document.queue.pendingConflictCount}`,
|
|
866
|
+
...document.conflicts.map((conflict) => `conflict projectId=${conflict.projectId} resolution=${conflict.resolution}`),
|
|
867
|
+
"trustBoundary:",
|
|
868
|
+
...document.trustBoundary.map((item) => `- ${item}`),
|
|
869
|
+
].join("\n")}\n`;
|
|
870
|
+
}
|
|
871
|
+
function formatConflictResolution(document) {
|
|
872
|
+
return `${[
|
|
873
|
+
"AppFleet cloud sync: conflict resolution state updated.",
|
|
874
|
+
`status=${document.status}`,
|
|
875
|
+
`workspaceId=${document.workspaceId}`,
|
|
876
|
+
`projectId=${document.projectId}`,
|
|
877
|
+
`resolution=${document.resolution}`,
|
|
878
|
+
`pendingConflictCount=${document.pendingConflictCount ?? "unknown"}`,
|
|
879
|
+
`metadataSyncStorePath=${document.metadataSyncStorePath}`,
|
|
880
|
+
"trustBoundary:",
|
|
881
|
+
...document.trustBoundary.map((item) => `- ${item}`),
|
|
882
|
+
].join("\n")}\n`;
|
|
883
|
+
}
|
|
884
|
+
function formatProductionMisconfigured(document) {
|
|
885
|
+
return `${[
|
|
886
|
+
"AppFleet cloud sync: production mode misconfigured.",
|
|
887
|
+
`cloudMode=${document.cloudMode}`,
|
|
888
|
+
`action=${document.mode}`,
|
|
889
|
+
`workspaceId=${document.workspaceId}`,
|
|
890
|
+
`status=${document.status}`,
|
|
891
|
+
`missing=${document.missing.join(", ")}`,
|
|
892
|
+
`apiBaseUrl=${document.config.api.baseUrl ?? "missing"}`,
|
|
893
|
+
`apiAuth=${document.config.api.auth.bearerToken}`,
|
|
894
|
+
`databaseDsn=${document.config.database.dsn}`,
|
|
895
|
+
"trustBoundary:",
|
|
896
|
+
...document.trustBoundary.map((item) => `- ${item}`),
|
|
897
|
+
].join("\n")}\n`;
|
|
898
|
+
}
|
|
899
|
+
function formatAuthMisconfigured(document) {
|
|
900
|
+
return `${[
|
|
901
|
+
"AppFleet auth: production mode misconfigured.",
|
|
902
|
+
`cloudMode=${document.cloudMode}`,
|
|
903
|
+
`action=${document.mode}`,
|
|
904
|
+
`workspaceId=${document.workspaceId}`,
|
|
905
|
+
`status=${document.status}`,
|
|
906
|
+
`missing=${document.missing.join(", ")}`,
|
|
907
|
+
`apiBaseUrl=${document.config.api.baseUrl ?? "missing"}`,
|
|
908
|
+
`apiAuth=${document.config.api.auth.bearerToken}`,
|
|
909
|
+
"trustBoundary:",
|
|
910
|
+
...document.trustBoundary.map((item) => `- ${item}`),
|
|
911
|
+
].join("\n")}\n`;
|
|
912
|
+
}
|
|
913
|
+
function formatHostedLogoutMissingSession(document) {
|
|
914
|
+
return `${[
|
|
915
|
+
"AppFleet auth: hosted logout failed closed.",
|
|
916
|
+
`cloudMode=${document.cloudMode}`,
|
|
917
|
+
`status=${document.status}`,
|
|
918
|
+
`apiBaseUrl=${document.config.api.baseUrl ?? "missing"}`,
|
|
919
|
+
`apiAuth=${document.config.api.auth.bearerToken}`,
|
|
920
|
+
"trustBoundary:",
|
|
921
|
+
...document.trustBoundary.map((item) => `- ${item}`),
|
|
922
|
+
].join("\n")}\n`;
|
|
923
|
+
}
|
|
924
|
+
function formatProductionCloudSync(document) {
|
|
925
|
+
return `${[
|
|
926
|
+
"AppFleet cloud sync: production metadata sync request completed.",
|
|
927
|
+
`cloudMode=${document.cloudMode}`,
|
|
928
|
+
`workspaceId=${document.request.body.workspaceId}`,
|
|
929
|
+
`localDemoSessionPresent=${document.localDemoSessionPresent}`,
|
|
930
|
+
`productionCloudPersistence=${document.productionCloudPersistenceImplemented}`,
|
|
931
|
+
`localTestPersistence=${document.localTestPersistenceImplemented}`,
|
|
932
|
+
`apiBaseUrl=${document.config.api.baseUrl ?? "missing"}`,
|
|
933
|
+
`apiAuth=${document.config.api.auth.bearerToken}`,
|
|
934
|
+
`databaseDsn=${document.config.database.dsn}`,
|
|
935
|
+
`idempotencyKey=${document.request.headers.idempotencyKey}`,
|
|
936
|
+
`attempts=${document.retry.attempts}`,
|
|
937
|
+
`transientFailureCount=${document.retry.transientFailureCount}`,
|
|
938
|
+
`envelopeCount=${document.request.body.envelopes.length}`,
|
|
939
|
+
`projectIds=${document.request.body.envelopes.map((envelope) => envelope.projectId).join(", ")}`,
|
|
940
|
+
`acceptedProjectIds=${document.result.acceptedProjectIds.join(", ")}`,
|
|
941
|
+
`rejectedProjectIds=${document.result.rejectedProjectIds.join(", ")}`,
|
|
942
|
+
`queueStatus=${document.queue?.latestStatus ?? "unknown"}`,
|
|
943
|
+
`pendingUploadCount=${document.queue?.pendingUploadCount ?? "unknown"}`,
|
|
944
|
+
`pendingConflictCount=${document.queue?.pendingConflictCount ?? "unknown"}`,
|
|
945
|
+
"trustBoundary:",
|
|
946
|
+
...document.trustBoundary.map((item) => `- ${item}`),
|
|
947
|
+
].join("\n")}\n`;
|
|
948
|
+
}
|
|
949
|
+
function formatProductionTransportFailed(document) {
|
|
950
|
+
return `${[
|
|
951
|
+
"AppFleet cloud sync: production metadata sync failed closed.",
|
|
952
|
+
`cloudMode=${document.cloudMode}`,
|
|
953
|
+
`status=${document.status}`,
|
|
954
|
+
`workspaceId=${document.request.body.workspaceId}`,
|
|
955
|
+
`localDemoSessionPresent=${document.localDemoSessionPresent}`,
|
|
956
|
+
`productionCloudPersistence=${document.productionCloudPersistenceImplemented}`,
|
|
957
|
+
`localTestPersistence=${document.localTestPersistenceImplemented}`,
|
|
958
|
+
`apiBaseUrl=${document.config.api.baseUrl ?? "missing"}`,
|
|
959
|
+
`apiAuth=${document.config.api.auth.bearerToken}`,
|
|
960
|
+
`databaseDsn=${document.config.database.dsn}`,
|
|
961
|
+
`idempotencyKey=${document.request.headers.idempotencyKey}`,
|
|
962
|
+
`attempts=${document.retry.attempts}`,
|
|
963
|
+
`maxRetries=${document.retry.maxRetries}`,
|
|
964
|
+
`transientFailureCount=${document.retry.transientFailureCount}`,
|
|
965
|
+
`retryExhausted=${document.retry.exhausted}`,
|
|
966
|
+
`envelopeCount=${document.request.body.envelopes.length}`,
|
|
967
|
+
`projectIds=${document.request.body.envelopes.map((envelope) => envelope.projectId).join(", ")}`,
|
|
968
|
+
"trustBoundary:",
|
|
969
|
+
...document.trustBoundary.map((item) => `- ${item}`),
|
|
970
|
+
].join("\n")}\n`;
|
|
971
|
+
}
|
|
972
|
+
function defaultSessionPath(projectRoot) {
|
|
973
|
+
return join(projectRoot, ".appfleet", "cloud-session.json");
|
|
974
|
+
}
|
|
975
|
+
function defaultAuthTokenPath(sessionPath) {
|
|
976
|
+
return join(dirname(sessionPath), "cloud-auth-token");
|
|
977
|
+
}
|
|
978
|
+
async function readAuthToken(path) {
|
|
979
|
+
try {
|
|
980
|
+
return stringOrUndefined(await readFile(path, "utf8"));
|
|
981
|
+
}
|
|
982
|
+
catch (error) {
|
|
983
|
+
if (isNotFound(error))
|
|
984
|
+
return undefined;
|
|
985
|
+
throw error;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
async function writeAuthToken(path, token) {
|
|
989
|
+
await mkdir(dirname(path), { recursive: true });
|
|
990
|
+
await writeFile(path, token, { mode: 0o600 });
|
|
991
|
+
await chmod(path, 0o600);
|
|
992
|
+
}
|
|
993
|
+
function sessionExpiry(createdAt) {
|
|
994
|
+
const expiresAt = new Date(createdAt);
|
|
995
|
+
expiresAt.setUTCHours(expiresAt.getUTCHours() + 8);
|
|
996
|
+
return expiresAt;
|
|
997
|
+
}
|
|
998
|
+
function localUserId(workspaceId, email) {
|
|
999
|
+
return `local_user_${createHash("sha256")
|
|
1000
|
+
.update(`${workspaceId}:${email ?? "anonymous"}`)
|
|
1001
|
+
.digest("hex")
|
|
1002
|
+
.slice(0, 16)}`;
|
|
1003
|
+
}
|
|
1004
|
+
function createZeroSecretBoundary() {
|
|
1005
|
+
return {
|
|
1006
|
+
storesBearerAuthMaterial: false,
|
|
1007
|
+
storesOAuthRefreshMaterial: false,
|
|
1008
|
+
storesSessionCookies: false,
|
|
1009
|
+
storesProviderCredentials: false,
|
|
1010
|
+
storesEncryptedCredentialBlobs: false,
|
|
1011
|
+
storesKeyManagementMaterial: false,
|
|
1012
|
+
storesCommandOutput: false,
|
|
1013
|
+
storesSecretFragments: false,
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
function cloudProductionFlag(argv, env) {
|
|
1017
|
+
if (hasFlag(argv, "--production-cloud")) {
|
|
1018
|
+
return true;
|
|
1019
|
+
}
|
|
1020
|
+
return (env.APPFLEET_PRODUCTION_CLOUD_ENABLED === "true" ||
|
|
1021
|
+
env.APPFLEET_CLOUD_MODE === "production");
|
|
1022
|
+
}
|
|
1023
|
+
function cloudProductionSource(argv, env) {
|
|
1024
|
+
if (hasFlag(argv, "--production-cloud")) {
|
|
1025
|
+
return "flag";
|
|
1026
|
+
}
|
|
1027
|
+
if (productionEnvEnabled(env)) {
|
|
1028
|
+
return "env";
|
|
1029
|
+
}
|
|
1030
|
+
return "default";
|
|
1031
|
+
}
|
|
1032
|
+
function resolveProductionCloudConfig(parsed, env) {
|
|
1033
|
+
const apiBaseUrl = parsed.apiBaseUrl ?? env.APPFLEET_API_BASE_URL;
|
|
1034
|
+
return {
|
|
1035
|
+
enabled: parsed.productionCloud,
|
|
1036
|
+
apiBaseUrl,
|
|
1037
|
+
authToken: env.APPFLEET_CLOUD_AUTH_TOKEN,
|
|
1038
|
+
databaseDsn: env.APPFLEET_DATABASE_URL,
|
|
1039
|
+
sources: {
|
|
1040
|
+
enabled: parsed.productionCloudSource,
|
|
1041
|
+
apiBaseUrl: parsed.apiBaseUrl
|
|
1042
|
+
? "flag"
|
|
1043
|
+
: env.APPFLEET_API_BASE_URL
|
|
1044
|
+
? "env"
|
|
1045
|
+
: "missing",
|
|
1046
|
+
authToken: env.APPFLEET_CLOUD_AUTH_TOKEN
|
|
1047
|
+
? env.APPFLEET_CLOUD_AUTH_TOKEN_SOURCE === "device_store"
|
|
1048
|
+
? "device_store"
|
|
1049
|
+
: "env"
|
|
1050
|
+
: "missing",
|
|
1051
|
+
databaseDsn: env.APPFLEET_DATABASE_URL ? "env" : "missing",
|
|
1052
|
+
},
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
function productionEnvEnabled(env) {
|
|
1056
|
+
return (env.APPFLEET_PRODUCTION_CLOUD_ENABLED === "true" ||
|
|
1057
|
+
env.APPFLEET_CLOUD_MODE === "production");
|
|
1058
|
+
}
|
|
1059
|
+
function missingProductionConfig(config) {
|
|
1060
|
+
return [
|
|
1061
|
+
config.apiBaseUrl ? undefined : "APPFLEET_API_BASE_URL or --api-base-url",
|
|
1062
|
+
config.authToken ? undefined : "APPFLEET_CLOUD_AUTH_TOKEN",
|
|
1063
|
+
].filter((value) => value !== undefined);
|
|
1064
|
+
}
|
|
1065
|
+
function missingProductionAuthConfig(config) {
|
|
1066
|
+
return [
|
|
1067
|
+
config.apiBaseUrl ? undefined : "APPFLEET_API_BASE_URL or --api-base-url",
|
|
1068
|
+
].filter((value) => value !== undefined);
|
|
1069
|
+
}
|
|
1070
|
+
function resolveHostedAuthTimeoutMs(env, override) {
|
|
1071
|
+
const raw = override ?? Number.parseInt(env.APPFLEET_HOSTED_AUTH_TIMEOUT_MS ?? "", 10);
|
|
1072
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 120_000;
|
|
1073
|
+
}
|
|
1074
|
+
function createRedactedCloudConfigReport(mode, config) {
|
|
1075
|
+
return {
|
|
1076
|
+
type: "cloud_config_report",
|
|
1077
|
+
mode,
|
|
1078
|
+
productionCloudEnabled: config.enabled,
|
|
1079
|
+
api: {
|
|
1080
|
+
baseUrl: redactUrl(config.apiBaseUrl),
|
|
1081
|
+
source: config.sources.apiBaseUrl,
|
|
1082
|
+
auth: {
|
|
1083
|
+
bearerToken: config.authToken ? "configured_redacted" : "missing",
|
|
1084
|
+
source: config.sources.authToken,
|
|
1085
|
+
},
|
|
1086
|
+
},
|
|
1087
|
+
database: {
|
|
1088
|
+
dsn: config.databaseDsn ? "configured_redacted" : "missing",
|
|
1089
|
+
source: config.sources.databaseDsn,
|
|
1090
|
+
},
|
|
1091
|
+
redaction: {
|
|
1092
|
+
tokens: "redacted",
|
|
1093
|
+
cookies: "redacted",
|
|
1094
|
+
dsns: "redacted",
|
|
1095
|
+
secrets: "redacted",
|
|
1096
|
+
commandOutput: "omitted",
|
|
1097
|
+
encryptedBlobIds: "omitted",
|
|
1098
|
+
keyManagementMaterial: "omitted",
|
|
1099
|
+
},
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
function createProductionMisconfiguredReport(input) {
|
|
1103
|
+
return {
|
|
1104
|
+
type: "cloud_metadata_sync_result",
|
|
1105
|
+
version: 1,
|
|
1106
|
+
mode: input.action,
|
|
1107
|
+
cloudMode: "production",
|
|
1108
|
+
status: "misconfigured",
|
|
1109
|
+
workspaceId: input.workspaceId,
|
|
1110
|
+
productionCloudPersistenceImplemented: false,
|
|
1111
|
+
localTestPersistenceImplemented: false,
|
|
1112
|
+
config: input.configReport,
|
|
1113
|
+
missing: input.missing,
|
|
1114
|
+
requestBuilt: false,
|
|
1115
|
+
syncAttempted: false,
|
|
1116
|
+
trustBoundary: [
|
|
1117
|
+
"Production cloud mode was explicitly enabled, but required API/auth config is missing.",
|
|
1118
|
+
"Failed closed before reading or writing local sync metadata.",
|
|
1119
|
+
"No tokens, cookies, DSNs, secrets, command output, encrypted blob ids, or key wrappers are printed.",
|
|
1120
|
+
],
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
function createProductionTransportFailedReport(input) {
|
|
1124
|
+
return {
|
|
1125
|
+
type: "cloud_metadata_sync_result",
|
|
1126
|
+
version: 1,
|
|
1127
|
+
mode: input.action,
|
|
1128
|
+
cloudMode: input.mode,
|
|
1129
|
+
status: "transport_failed",
|
|
1130
|
+
localDemoSessionPresent: input.localDemoSessionPresent,
|
|
1131
|
+
hostedSessionMetadataPresent: input.hostedSessionMetadataPresent,
|
|
1132
|
+
hostedAuthImplemented: false,
|
|
1133
|
+
productionCloudPersistenceImplemented: false,
|
|
1134
|
+
productionCloudRequestCompleted: false,
|
|
1135
|
+
localTestPersistenceImplemented: false,
|
|
1136
|
+
localQueuePreserved: true,
|
|
1137
|
+
conflictMetadataPreserved: true,
|
|
1138
|
+
config: input.configReport,
|
|
1139
|
+
request: input.request,
|
|
1140
|
+
retry: input.retry,
|
|
1141
|
+
requestBuilt: true,
|
|
1142
|
+
syncAttempted: true,
|
|
1143
|
+
failure: {
|
|
1144
|
+
category: input.retry.exhausted
|
|
1145
|
+
? "transient_transport_exhausted"
|
|
1146
|
+
: "transport_failed",
|
|
1147
|
+
message: "Production metadata sync failed closed without printing transport details.",
|
|
1148
|
+
},
|
|
1149
|
+
trustBoundary: [
|
|
1150
|
+
"Production cloud mode was explicitly enabled.",
|
|
1151
|
+
"The request descriptor contains sanitized project metadata only.",
|
|
1152
|
+
"Safe transient transport failures were retried with the same idempotency key.",
|
|
1153
|
+
"Failed closed without mutating the local queue, conflict metadata, project memory sync timestamps, or local-test sync store.",
|
|
1154
|
+
"Bearer auth material, cookies, DSNs, provider credentials, encrypted credential blob ids, key wrappers, command output, transport response bodies, and secret fragments are redacted or omitted from output.",
|
|
1155
|
+
],
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
function createAuthMisconfiguredReport(input) {
|
|
1159
|
+
return {
|
|
1160
|
+
type: "hosted_auth_result",
|
|
1161
|
+
version: 1,
|
|
1162
|
+
mode: input.action,
|
|
1163
|
+
cloudMode: "production",
|
|
1164
|
+
status: "misconfigured",
|
|
1165
|
+
workspaceId: input.workspaceId,
|
|
1166
|
+
productionCloudPersistenceImplemented: false,
|
|
1167
|
+
localTestPersistenceImplemented: false,
|
|
1168
|
+
config: input.configReport,
|
|
1169
|
+
missing: input.missing,
|
|
1170
|
+
requestBuilt: false,
|
|
1171
|
+
authAttempted: false,
|
|
1172
|
+
trustBoundary: [
|
|
1173
|
+
"Production auth mode was explicitly enabled, but required API/auth config is missing.",
|
|
1174
|
+
"Failed closed before creating, revoking, or storing hosted session metadata.",
|
|
1175
|
+
"No tokens, cookies, DSNs, secrets, command output, encrypted blob ids, or key wrappers are printed.",
|
|
1176
|
+
],
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
function createHostedLogoutMissingSessionReport(input) {
|
|
1180
|
+
return {
|
|
1181
|
+
type: "hosted_auth_logout_result",
|
|
1182
|
+
version: 1,
|
|
1183
|
+
cloudMode: "production",
|
|
1184
|
+
status: "missing_hosted_session",
|
|
1185
|
+
removedLocalSessionMetadata: false,
|
|
1186
|
+
hostedAuthSessionRevoked: false,
|
|
1187
|
+
loggedOutAt: input.loggedOutAt,
|
|
1188
|
+
config: input.configReport,
|
|
1189
|
+
requestBuilt: false,
|
|
1190
|
+
authAttempted: false,
|
|
1191
|
+
zeroSecretBoundary: createZeroSecretBoundary(),
|
|
1192
|
+
trustBoundary: [
|
|
1193
|
+
"Production hosted logout requires existing hosted session metadata.",
|
|
1194
|
+
"Failed closed before calling the hosted logout endpoint.",
|
|
1195
|
+
"No bearer auth material, OAuth refresh material, session cookie, key material, provider credential, encrypted credential blob, command output, or secret fragment was printed or stored.",
|
|
1196
|
+
],
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
export function buildProductionCloudMetadataSyncRequest(input) {
|
|
1200
|
+
const baseUrl = new URL(input.apiBaseUrl);
|
|
1201
|
+
const url = new URL(`/api/workspaces/${encodeURIComponent(input.workspaceId)}/metadata-sync`, baseUrl);
|
|
1202
|
+
const safeMemories = input.memories.map(cloudSafeProjectMemory);
|
|
1203
|
+
const idempotencyKey = input.idempotencyKey ??
|
|
1204
|
+
createMetadataSyncIdempotencyKey({
|
|
1205
|
+
workspaceId: input.workspaceId,
|
|
1206
|
+
createdAt: input.createdAt,
|
|
1207
|
+
memories: safeMemories,
|
|
1208
|
+
});
|
|
1209
|
+
return {
|
|
1210
|
+
method: "POST",
|
|
1211
|
+
url: redactUrl(url.toString()) ?? url.toString(),
|
|
1212
|
+
headers: {
|
|
1213
|
+
accept: "application/json",
|
|
1214
|
+
contentType: "application/json",
|
|
1215
|
+
authorization: "bearer_env_redacted",
|
|
1216
|
+
idempotencyKey,
|
|
1217
|
+
},
|
|
1218
|
+
body: {
|
|
1219
|
+
idempotencyKey,
|
|
1220
|
+
workspaceId: input.workspaceId,
|
|
1221
|
+
createdAt: input.createdAt,
|
|
1222
|
+
envelopes: safeMemories.map((memory) => ({
|
|
1223
|
+
type: "project_memory_sync",
|
|
1224
|
+
version: 1,
|
|
1225
|
+
workspaceId: input.workspaceId,
|
|
1226
|
+
projectId: memory.id,
|
|
1227
|
+
createdAt: input.createdAt,
|
|
1228
|
+
memory,
|
|
1229
|
+
})),
|
|
1230
|
+
},
|
|
1231
|
+
redaction: {
|
|
1232
|
+
tokens: "redacted",
|
|
1233
|
+
cookies: "redacted",
|
|
1234
|
+
dsns: "redacted",
|
|
1235
|
+
secrets: "redacted",
|
|
1236
|
+
commandOutput: "omitted",
|
|
1237
|
+
encryptedBlobIds: "omitted",
|
|
1238
|
+
keyManagementMaterial: "omitted",
|
|
1239
|
+
},
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
async function runProductionMetadataSyncWithRetry(input) {
|
|
1243
|
+
const maxRetries = Math.max(0, input.maxRetries);
|
|
1244
|
+
let attempts = 0;
|
|
1245
|
+
let transientFailureCount = 0;
|
|
1246
|
+
while (attempts <= maxRetries) {
|
|
1247
|
+
attempts += 1;
|
|
1248
|
+
try {
|
|
1249
|
+
const result = await input.transport(input.request, {
|
|
1250
|
+
authToken: input.authToken,
|
|
1251
|
+
configReport: input.configReport,
|
|
1252
|
+
});
|
|
1253
|
+
return {
|
|
1254
|
+
status: "completed",
|
|
1255
|
+
result,
|
|
1256
|
+
retry: {
|
|
1257
|
+
attempts,
|
|
1258
|
+
maxRetries,
|
|
1259
|
+
transientFailureCount,
|
|
1260
|
+
exhausted: false,
|
|
1261
|
+
idempotencyKey: input.request.headers.idempotencyKey,
|
|
1262
|
+
},
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
catch (error) {
|
|
1266
|
+
const isTransient = isTransientTransportFailure(error);
|
|
1267
|
+
if (isTransient) {
|
|
1268
|
+
transientFailureCount += 1;
|
|
1269
|
+
}
|
|
1270
|
+
if (!isTransient || attempts > maxRetries) {
|
|
1271
|
+
return {
|
|
1272
|
+
status: "failed",
|
|
1273
|
+
retry: {
|
|
1274
|
+
attempts,
|
|
1275
|
+
maxRetries,
|
|
1276
|
+
transientFailureCount,
|
|
1277
|
+
exhausted: isTransient,
|
|
1278
|
+
idempotencyKey: input.request.headers.idempotencyKey,
|
|
1279
|
+
},
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return {
|
|
1285
|
+
status: "failed",
|
|
1286
|
+
retry: {
|
|
1287
|
+
attempts,
|
|
1288
|
+
maxRetries,
|
|
1289
|
+
transientFailureCount,
|
|
1290
|
+
exhausted: true,
|
|
1291
|
+
idempotencyKey: input.request.headers.idempotencyKey,
|
|
1292
|
+
},
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
function isTransientTransportFailure(error) {
|
|
1296
|
+
if (typeof error !== "object" || error === null) {
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
const record = error;
|
|
1300
|
+
if (record.transient === true) {
|
|
1301
|
+
return true;
|
|
1302
|
+
}
|
|
1303
|
+
const status = typeof record.status === "number"
|
|
1304
|
+
? record.status
|
|
1305
|
+
: typeof record.statusCode === "number"
|
|
1306
|
+
? record.statusCode
|
|
1307
|
+
: httpStatusFromMessage(error);
|
|
1308
|
+
if (status !== undefined) {
|
|
1309
|
+
return status === 408 || status === 425 || status === 429 || status >= 500;
|
|
1310
|
+
}
|
|
1311
|
+
const code = typeof record.code === "string" ? record.code : undefined;
|
|
1312
|
+
return (code === "ECONNRESET" ||
|
|
1313
|
+
code === "ETIMEDOUT" ||
|
|
1314
|
+
code === "EAI_AGAIN" ||
|
|
1315
|
+
code === "ENETUNREACH" ||
|
|
1316
|
+
code === "ECONNREFUSED");
|
|
1317
|
+
}
|
|
1318
|
+
function httpStatusFromMessage(error) {
|
|
1319
|
+
if (!(error instanceof Error)) {
|
|
1320
|
+
return undefined;
|
|
1321
|
+
}
|
|
1322
|
+
const match = error.message.match(/\bHTTP\s+(\d{3})\b/i);
|
|
1323
|
+
if (!match) {
|
|
1324
|
+
return undefined;
|
|
1325
|
+
}
|
|
1326
|
+
return Number.parseInt(match[1], 10);
|
|
1327
|
+
}
|
|
1328
|
+
function buildHostedAuthRequest(input) {
|
|
1329
|
+
const baseUrl = new URL(input.apiBaseUrl);
|
|
1330
|
+
const url = new URL("/auth/hosted", baseUrl);
|
|
1331
|
+
if (input.workspaceId) {
|
|
1332
|
+
url.searchParams.set("workspaceId", input.workspaceId);
|
|
1333
|
+
}
|
|
1334
|
+
if (input.email) {
|
|
1335
|
+
url.searchParams.set("email", input.email);
|
|
1336
|
+
}
|
|
1337
|
+
if (input.sessionId) {
|
|
1338
|
+
url.searchParams.set("cliLogoutSessionId", input.sessionId);
|
|
1339
|
+
}
|
|
1340
|
+
url.searchParams.set("cliAction", input.action);
|
|
1341
|
+
return {
|
|
1342
|
+
method: "POST",
|
|
1343
|
+
url: redactUrl(url.toString()) ?? url.toString(),
|
|
1344
|
+
headers: {
|
|
1345
|
+
accept: "application/json",
|
|
1346
|
+
contentType: "application/json",
|
|
1347
|
+
},
|
|
1348
|
+
body: {
|
|
1349
|
+
workspaceId: input.workspaceId,
|
|
1350
|
+
email: input.email,
|
|
1351
|
+
sessionId: input.sessionId,
|
|
1352
|
+
},
|
|
1353
|
+
redaction: {
|
|
1354
|
+
tokens: "redacted",
|
|
1355
|
+
cookies: "redacted",
|
|
1356
|
+
dsns: "redacted",
|
|
1357
|
+
secrets: "redacted",
|
|
1358
|
+
commandOutput: "omitted",
|
|
1359
|
+
encryptedBlobIds: "omitted",
|
|
1360
|
+
keyManagementMaterial: "omitted",
|
|
1361
|
+
},
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
function createHostedSessionMetadata(input) {
|
|
1365
|
+
const workspaceId = input.result.workspaceId ?? input.requestedWorkspaceId;
|
|
1366
|
+
return {
|
|
1367
|
+
type: "hosted_cloud_session_metadata",
|
|
1368
|
+
version: 1,
|
|
1369
|
+
sessionId: input.result.sessionId ?? `hosted_${randomUUID()}`,
|
|
1370
|
+
status: "active",
|
|
1371
|
+
workspaceId,
|
|
1372
|
+
workspace: {
|
|
1373
|
+
id: workspaceId,
|
|
1374
|
+
source: input.result.workspaceId ? "hosted_appfleet_api" : "cli_override",
|
|
1375
|
+
},
|
|
1376
|
+
user: {
|
|
1377
|
+
id: input.result.userId,
|
|
1378
|
+
email: input.result.email ?? input.requestedEmail,
|
|
1379
|
+
source: input.result.userId
|
|
1380
|
+
? "hosted_appfleet_api"
|
|
1381
|
+
: input.requestedEmail
|
|
1382
|
+
? "cli_override"
|
|
1383
|
+
: "not_returned",
|
|
1384
|
+
},
|
|
1385
|
+
createdAt: input.createdAt.toISOString(),
|
|
1386
|
+
expiresAt: input.result.expiresAt,
|
|
1387
|
+
hostedAuthImplemented: true,
|
|
1388
|
+
productionCloudPersistenceImplemented: true,
|
|
1389
|
+
localTestPersistenceImplemented: false,
|
|
1390
|
+
secretMaterialStored: false,
|
|
1391
|
+
auth: {
|
|
1392
|
+
source: "hosted_browser_session",
|
|
1393
|
+
tokensStored: false,
|
|
1394
|
+
cookiesStored: false,
|
|
1395
|
+
localCallbackStored: false,
|
|
1396
|
+
},
|
|
1397
|
+
zeroSecretBoundary: createZeroSecretBoundary(),
|
|
1398
|
+
trustBoundary: [
|
|
1399
|
+
"Hosted auth was explicitly enabled and completed through an authenticated API contract.",
|
|
1400
|
+
"The session metadata file stores only workspace/user labels, status, and expiry metadata.",
|
|
1401
|
+
"The short-lived Clerk session token is isolated in a mode-0600 local credential file and is never printed or included in project memory.",
|
|
1402
|
+
"No OAuth refresh material, session cookie, key material, provider credential, encrypted credential blob, command output, or secret fragment is stored.",
|
|
1403
|
+
],
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
function createMetadataSyncIdempotencyKey(input) {
|
|
1407
|
+
const projectIds = input.memories.map((memory) => memory.id).sort();
|
|
1408
|
+
const digest = createHash("sha256")
|
|
1409
|
+
.update(JSON.stringify({ workspaceId: input.workspaceId, projectIds }))
|
|
1410
|
+
.digest("hex")
|
|
1411
|
+
.slice(0, 16);
|
|
1412
|
+
return `metadata-sync:${input.workspaceId}:${input.createdAt}:${digest}`;
|
|
1413
|
+
}
|
|
1414
|
+
async function defaultProductionCloudTransport(request, context) {
|
|
1415
|
+
const response = await fetch(request.url, {
|
|
1416
|
+
method: request.method,
|
|
1417
|
+
headers: {
|
|
1418
|
+
Accept: request.headers.accept,
|
|
1419
|
+
"Content-Type": request.headers.contentType,
|
|
1420
|
+
Authorization: `Bearer ${context.authToken}`,
|
|
1421
|
+
"Idempotency-Key": request.headers.idempotencyKey,
|
|
1422
|
+
},
|
|
1423
|
+
body: JSON.stringify(request.body),
|
|
1424
|
+
});
|
|
1425
|
+
if (!response.ok) {
|
|
1426
|
+
const error = new Error(`production cloud API request failed with HTTP ${response.status}`);
|
|
1427
|
+
error.status = response.status;
|
|
1428
|
+
throw error;
|
|
1429
|
+
}
|
|
1430
|
+
const parsed = (await response.json());
|
|
1431
|
+
return normalizeProductionMetadataSyncResponse(parsed);
|
|
1432
|
+
}
|
|
1433
|
+
function normalizeProductionMetadataSyncResponse(parsed) {
|
|
1434
|
+
const routeAcceptedProjectIds = parsed.data?.sync?.acceptedProjectIds ??
|
|
1435
|
+
parsed.data?.projects
|
|
1436
|
+
?.map((project) => project.projectId ?? project.id)
|
|
1437
|
+
.filter((projectId) => projectId !== undefined);
|
|
1438
|
+
return {
|
|
1439
|
+
acceptedProjectIds: parsed.acceptedProjectIds ?? routeAcceptedProjectIds ?? [],
|
|
1440
|
+
rejectedProjectIds: parsed.rejectedProjectIds ?? parsed.data?.sync?.rejectedProjectIds ?? [],
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
async function defaultHostedAuthTransport(request, context) {
|
|
1444
|
+
const state = `cli_${randomUUID()}`;
|
|
1445
|
+
const callback = await createHostedAuthCallbackServer({
|
|
1446
|
+
expectedState: state,
|
|
1447
|
+
timeoutMs: context.timeoutMs,
|
|
1448
|
+
});
|
|
1449
|
+
try {
|
|
1450
|
+
const browserUrl = new URL(request.url);
|
|
1451
|
+
browserUrl.searchParams.set("cliCallback", callback.url);
|
|
1452
|
+
browserUrl.searchParams.set("cliState", state);
|
|
1453
|
+
browserUrl.searchParams.set("cliAction", context.action);
|
|
1454
|
+
if (request.body.workspaceId) {
|
|
1455
|
+
browserUrl.searchParams.set("workspaceId", request.body.workspaceId);
|
|
1456
|
+
}
|
|
1457
|
+
if (request.body.email) {
|
|
1458
|
+
browserUrl.searchParams.set("email", request.body.email);
|
|
1459
|
+
}
|
|
1460
|
+
if (request.body.sessionId) {
|
|
1461
|
+
browserUrl.searchParams.set("cliLogoutSessionId", request.body.sessionId);
|
|
1462
|
+
}
|
|
1463
|
+
openHostedAuthBrowser(browserUrl.toString());
|
|
1464
|
+
const result = await callback.result;
|
|
1465
|
+
if (context.action === "logout") {
|
|
1466
|
+
return {
|
|
1467
|
+
sessionId: result.sessionId,
|
|
1468
|
+
workspaceId: result.workspaceId,
|
|
1469
|
+
revoked: result.revoked === "true",
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
return {
|
|
1473
|
+
sessionId: result.sessionId,
|
|
1474
|
+
workspaceId: result.workspaceId,
|
|
1475
|
+
userId: result.userId,
|
|
1476
|
+
email: result.email,
|
|
1477
|
+
expiresAt: result.expiresAt,
|
|
1478
|
+
authToken: result.authToken,
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
finally {
|
|
1482
|
+
await closeServer(callback.server);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
async function createHostedAuthCallbackServer(input) {
|
|
1486
|
+
let settle;
|
|
1487
|
+
let reject;
|
|
1488
|
+
const result = new Promise((resolve, rejectPromise) => {
|
|
1489
|
+
settle = resolve;
|
|
1490
|
+
reject = rejectPromise;
|
|
1491
|
+
});
|
|
1492
|
+
const timeout = setTimeout(() => {
|
|
1493
|
+
reject?.(new Error("hosted auth browser callback timed out before a Clerk-backed session was completed"));
|
|
1494
|
+
}, input.timeoutMs);
|
|
1495
|
+
const server = createServer((request, response) => {
|
|
1496
|
+
try {
|
|
1497
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
1498
|
+
const state = url.searchParams.get("cliState");
|
|
1499
|
+
if (state !== input.expectedState) {
|
|
1500
|
+
response.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1501
|
+
response.end("AppFleet hosted auth callback rejected.");
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
const fields = {
|
|
1505
|
+
sessionId: stringOrUndefined(url.searchParams.get("sessionId") ?? undefined),
|
|
1506
|
+
workspaceId: stringOrUndefined(url.searchParams.get("workspaceId") ?? undefined),
|
|
1507
|
+
userId: stringOrUndefined(url.searchParams.get("userId") ?? undefined),
|
|
1508
|
+
email: stringOrUndefined(url.searchParams.get("email") ?? undefined),
|
|
1509
|
+
expiresAt: stringOrUndefined(url.searchParams.get("expiresAt") ?? undefined),
|
|
1510
|
+
revoked: stringOrUndefined(url.searchParams.get("revoked") ?? undefined),
|
|
1511
|
+
authToken: stringOrUndefined(url.searchParams.get("sessionToken") ?? undefined),
|
|
1512
|
+
};
|
|
1513
|
+
clearTimeout(timeout);
|
|
1514
|
+
settle?.(fields);
|
|
1515
|
+
response.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1516
|
+
response.end("AppFleet hosted auth complete. You can close this tab.");
|
|
1517
|
+
}
|
|
1518
|
+
catch (error) {
|
|
1519
|
+
clearTimeout(timeout);
|
|
1520
|
+
reject?.(error);
|
|
1521
|
+
response.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1522
|
+
response.end("AppFleet hosted auth callback failed.");
|
|
1523
|
+
}
|
|
1524
|
+
});
|
|
1525
|
+
await new Promise((resolve, rejectListen) => {
|
|
1526
|
+
server.once("error", rejectListen);
|
|
1527
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1528
|
+
server.off("error", rejectListen);
|
|
1529
|
+
resolve();
|
|
1530
|
+
});
|
|
1531
|
+
});
|
|
1532
|
+
const address = server.address();
|
|
1533
|
+
if (!address || typeof address === "string") {
|
|
1534
|
+
await closeServer(server);
|
|
1535
|
+
throw new Error("hosted auth callback server did not bind to a local port");
|
|
1536
|
+
}
|
|
1537
|
+
return {
|
|
1538
|
+
url: `http://127.0.0.1:${address.port}/callback`,
|
|
1539
|
+
server,
|
|
1540
|
+
result,
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
function openHostedAuthBrowser(url) {
|
|
1544
|
+
const command = process.platform === "darwin"
|
|
1545
|
+
? "open"
|
|
1546
|
+
: process.platform === "win32"
|
|
1547
|
+
? "cmd"
|
|
1548
|
+
: "xdg-open";
|
|
1549
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
1550
|
+
const child = spawn(command, args, {
|
|
1551
|
+
detached: true,
|
|
1552
|
+
stdio: "ignore",
|
|
1553
|
+
});
|
|
1554
|
+
child.unref();
|
|
1555
|
+
}
|
|
1556
|
+
async function closeServer(server) {
|
|
1557
|
+
if (!server.listening) {
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
await new Promise((resolve) => {
|
|
1561
|
+
server.close(() => resolve());
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
function stringOrUndefined(value) {
|
|
1565
|
+
const trimmed = value?.trim();
|
|
1566
|
+
return trimmed ? trimmed : undefined;
|
|
1567
|
+
}
|
|
1568
|
+
function cloudSafeProjectMemory(memory) {
|
|
1569
|
+
return createAppProjectMemory({
|
|
1570
|
+
...memory,
|
|
1571
|
+
identity: {
|
|
1572
|
+
id: memory.identity.id,
|
|
1573
|
+
name: memory.identity.name,
|
|
1574
|
+
repoUrl: sanitizeUrl(memory.identity.repoUrl),
|
|
1575
|
+
gitRemoteFingerprint: memory.identity.gitRemoteFingerprint,
|
|
1576
|
+
rememberedLocalPaths: [],
|
|
1577
|
+
},
|
|
1578
|
+
canonicalUrl: sanitizeUrl(memory.canonicalUrl) ?? memory.canonicalUrl,
|
|
1579
|
+
rememberedUrls: memory.rememberedUrls
|
|
1580
|
+
.map((url) => sanitizeUrl(url))
|
|
1581
|
+
.filter((url) => url !== undefined),
|
|
1582
|
+
domain: {
|
|
1583
|
+
canonicalUrl: sanitizeUrl(memory.domain.canonicalUrl) ?? memory.domain.canonicalUrl,
|
|
1584
|
+
rememberedUrls: memory.domain.rememberedUrls
|
|
1585
|
+
.map((url) => sanitizeUrl(url))
|
|
1586
|
+
.filter((url) => url !== undefined),
|
|
1587
|
+
customDomainBought: memory.domain.customDomainBought,
|
|
1588
|
+
notes: "Omitted from metadata sync to preserve the zero-secret boundary.",
|
|
1589
|
+
},
|
|
1590
|
+
providers: memory.providers.map((provider) => ({
|
|
1591
|
+
id: provider.id,
|
|
1592
|
+
name: provider.name,
|
|
1593
|
+
kind: provider.kind,
|
|
1594
|
+
role: "metadata",
|
|
1595
|
+
status: provider.status,
|
|
1596
|
+
})),
|
|
1597
|
+
environmentAliases: memory.environmentAliases.map((alias) => ({
|
|
1598
|
+
name: alias.name,
|
|
1599
|
+
purpose: "Alias name only; purpose omitted from metadata sync.",
|
|
1600
|
+
required: alias.required,
|
|
1601
|
+
valueStored: false,
|
|
1602
|
+
})),
|
|
1603
|
+
credentialReferences: [],
|
|
1604
|
+
troubleshootingNotes: [],
|
|
1605
|
+
latestCheck: memory.latestCheck
|
|
1606
|
+
? {
|
|
1607
|
+
checkedAt: memory.latestCheck.checkedAt,
|
|
1608
|
+
url: sanitizeUrl(memory.latestCheck.url) ?? memory.latestCheck.url,
|
|
1609
|
+
status: memory.latestCheck.status,
|
|
1610
|
+
latencyMs: memory.latestCheck.latencyMs,
|
|
1611
|
+
responseTimeMs: memory.latestCheck.responseTimeMs,
|
|
1612
|
+
httpStatusCode: memory.latestCheck.httpStatusCode,
|
|
1613
|
+
failureCategory: memory.latestCheck.failureCategory,
|
|
1614
|
+
}
|
|
1615
|
+
: undefined,
|
|
1616
|
+
latestDoctorReport: memory.latestDoctorReport
|
|
1617
|
+
? {
|
|
1618
|
+
...memory.latestDoctorReport,
|
|
1619
|
+
gitRemoteFingerprint: memory.latestDoctorReport.gitRemoteFingerprint,
|
|
1620
|
+
latestCheck: memory.latestDoctorReport.latestCheck
|
|
1621
|
+
? {
|
|
1622
|
+
...memory.latestDoctorReport.latestCheck,
|
|
1623
|
+
url: sanitizeUrl(memory.latestDoctorReport.latestCheck.url) ??
|
|
1624
|
+
memory.latestDoctorReport.latestCheck.url,
|
|
1625
|
+
message: undefined,
|
|
1626
|
+
}
|
|
1627
|
+
: undefined,
|
|
1628
|
+
findings: memory.latestDoctorReport.findings.map((finding) => ({
|
|
1629
|
+
...finding,
|
|
1630
|
+
evidence: undefined,
|
|
1631
|
+
})),
|
|
1632
|
+
}
|
|
1633
|
+
: undefined,
|
|
1634
|
+
nextPlaceToLook: [],
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
function defaultProjectMemoryStorePath(projectRoot) {
|
|
1638
|
+
return join(projectRoot, ".appfleet", "project-memory.json");
|
|
1639
|
+
}
|
|
1640
|
+
function defaultMetadataSyncStorePath(projectRoot) {
|
|
1641
|
+
return join(projectRoot, ".appfleet", "cloud-metadata-sync.json");
|
|
1642
|
+
}
|
|
1643
|
+
async function readMetadataSyncStore(path) {
|
|
1644
|
+
try {
|
|
1645
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
1646
|
+
const workspaces = {};
|
|
1647
|
+
for (const [workspaceId, state] of Object.entries(parsed.workspaces ?? {})) {
|
|
1648
|
+
workspaces[workspaceId] = {
|
|
1649
|
+
projectFingerprints: state.projectFingerprints ?? {},
|
|
1650
|
+
syncMetadata: state.syncMetadata ?? [],
|
|
1651
|
+
queue: (state.queue ?? []).map((entry) => createLocalMetadataSyncQueueEntry({
|
|
1652
|
+
...entry,
|
|
1653
|
+
type: "local_metadata_sync_queue_entry",
|
|
1654
|
+
version: 1,
|
|
1655
|
+
cloudMode: entry.cloudMode ?? "local-test",
|
|
1656
|
+
updatedAt: entry.updatedAt ?? entry.createdAt,
|
|
1657
|
+
productionCloudRequestCompleted: entry.productionCloudRequestCompleted ?? false,
|
|
1658
|
+
retryable: entry.retryable ?? entry.status === "pending_conflict",
|
|
1659
|
+
containsProjectMemoryPayload: false,
|
|
1660
|
+
containsCredentialValues: false,
|
|
1661
|
+
containsCommandOutput: false,
|
|
1662
|
+
containsEncryptedBlobPayload: false,
|
|
1663
|
+
storesDecryptedMaterial: false,
|
|
1664
|
+
})),
|
|
1665
|
+
conflicts: (state.conflicts ?? []).map(createCloudSyncRuntimeConflictState),
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
return {
|
|
1669
|
+
...createEmptyMetadataSyncStore(),
|
|
1670
|
+
...parsed,
|
|
1671
|
+
workspaces,
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
catch (error) {
|
|
1675
|
+
if (isNotFound(error)) {
|
|
1676
|
+
return createEmptyMetadataSyncStore();
|
|
1677
|
+
}
|
|
1678
|
+
throw error;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
async function writeMetadataSyncStore(path, store) {
|
|
1682
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1683
|
+
await writeFile(path, `${JSON.stringify(store, null, 2)}\n`, { mode: 0o600 });
|
|
1684
|
+
}
|
|
1685
|
+
function createEmptyMetadataSyncStore() {
|
|
1686
|
+
return {
|
|
1687
|
+
type: "local_metadata_sync_store",
|
|
1688
|
+
version: 1,
|
|
1689
|
+
workspaces: {},
|
|
1690
|
+
zeroSecretBoundary: {
|
|
1691
|
+
storesProjectMemoryPayload: false,
|
|
1692
|
+
storesPlaintextCredentials: false,
|
|
1693
|
+
storesEncryptedCredentialBlobs: false,
|
|
1694
|
+
storesKeyMaterial: false,
|
|
1695
|
+
storesCommandOutput: false,
|
|
1696
|
+
storesSecretFragments: false,
|
|
1697
|
+
},
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
function getWorkspaceSyncState(store, workspaceId) {
|
|
1701
|
+
return store.workspaces[workspaceId] ?? {
|
|
1702
|
+
projectFingerprints: {},
|
|
1703
|
+
syncMetadata: [],
|
|
1704
|
+
queue: [],
|
|
1705
|
+
conflicts: [],
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
function createSyncQueueEntry(entry) {
|
|
1709
|
+
return createLocalMetadataSyncQueueEntry({
|
|
1710
|
+
type: "local_metadata_sync_queue_entry",
|
|
1711
|
+
version: 1,
|
|
1712
|
+
...entry,
|
|
1713
|
+
containsProjectMemoryPayload: false,
|
|
1714
|
+
containsCredentialValues: false,
|
|
1715
|
+
containsCommandOutput: false,
|
|
1716
|
+
containsEncryptedBlobPayload: false,
|
|
1717
|
+
storesDecryptedMaterial: false,
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
function upsertRuntimeConflict(input) {
|
|
1721
|
+
const state = createCloudSyncRuntimeConflictState({
|
|
1722
|
+
type: "cloud_sync_runtime_conflict_state",
|
|
1723
|
+
version: 1,
|
|
1724
|
+
id: `conflict_${input.workspaceId}_${input.projectId}`,
|
|
1725
|
+
workspaceId: input.workspaceId,
|
|
1726
|
+
projectId: input.projectId,
|
|
1727
|
+
status: "pending",
|
|
1728
|
+
detectedAt: input.detectedAt,
|
|
1729
|
+
localLastSyncedAt: input.localLastSyncedAt ?? "never",
|
|
1730
|
+
remoteLastSyncedAt: input.remoteLastSyncedAt,
|
|
1731
|
+
localMetadataFingerprint: input.localFingerprint,
|
|
1732
|
+
remoteMetadataFingerprint: input.remoteFingerprint,
|
|
1733
|
+
safeMetadataFields: [
|
|
1734
|
+
"projectId",
|
|
1735
|
+
"name",
|
|
1736
|
+
"repoUrl",
|
|
1737
|
+
"gitRemoteFingerprint",
|
|
1738
|
+
"canonicalUrl",
|
|
1739
|
+
"rememberedUrls",
|
|
1740
|
+
"providerNames",
|
|
1741
|
+
"environmentAliases",
|
|
1742
|
+
"credentialMetadataIds",
|
|
1743
|
+
"cloudSyncStatus",
|
|
1744
|
+
],
|
|
1745
|
+
resolution: "manual_review_required",
|
|
1746
|
+
productionReady: false,
|
|
1747
|
+
productionReadiness: "local_test_metadata_persistence",
|
|
1748
|
+
containsCredentialValues: false,
|
|
1749
|
+
containsCommandOutput: false,
|
|
1750
|
+
containsEncryptedBlobPayload: false,
|
|
1751
|
+
storesProjectMemoryPayload: false,
|
|
1752
|
+
storesDecryptedMaterial: false,
|
|
1753
|
+
});
|
|
1754
|
+
return [
|
|
1755
|
+
...input.conflicts.filter((conflict) => !(conflict.workspaceId === input.workspaceId &&
|
|
1756
|
+
conflict.projectId === input.projectId &&
|
|
1757
|
+
conflict.status === "pending")),
|
|
1758
|
+
state,
|
|
1759
|
+
];
|
|
1760
|
+
}
|
|
1761
|
+
function projectMemoryFingerprint(memory) {
|
|
1762
|
+
return createHash("sha256")
|
|
1763
|
+
.update(JSON.stringify(cloudSafeProjectMemory(memory)))
|
|
1764
|
+
.digest("hex");
|
|
1765
|
+
}
|
|
1766
|
+
function sanitizeUrl(value) {
|
|
1767
|
+
if (!value) {
|
|
1768
|
+
return undefined;
|
|
1769
|
+
}
|
|
1770
|
+
try {
|
|
1771
|
+
const parsed = new URL(value);
|
|
1772
|
+
parsed.username = "";
|
|
1773
|
+
parsed.password = "";
|
|
1774
|
+
parsed.search = "";
|
|
1775
|
+
parsed.hash = "";
|
|
1776
|
+
return parsed.toString();
|
|
1777
|
+
}
|
|
1778
|
+
catch {
|
|
1779
|
+
return undefined;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
function redactUrl(value) {
|
|
1783
|
+
if (!value) {
|
|
1784
|
+
return undefined;
|
|
1785
|
+
}
|
|
1786
|
+
try {
|
|
1787
|
+
const parsed = new URL(value);
|
|
1788
|
+
parsed.username = "";
|
|
1789
|
+
parsed.password = "";
|
|
1790
|
+
parsed.search = "";
|
|
1791
|
+
parsed.hash = "";
|
|
1792
|
+
return parsed.toString();
|
|
1793
|
+
}
|
|
1794
|
+
catch {
|
|
1795
|
+
return "invalid_redacted";
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
function readFlag(argv, flag) {
|
|
1799
|
+
const index = argv.indexOf(flag);
|
|
1800
|
+
if (index === -1) {
|
|
1801
|
+
return undefined;
|
|
1802
|
+
}
|
|
1803
|
+
return argv[index + 1];
|
|
1804
|
+
}
|
|
1805
|
+
function hasFlag(argv, flag) {
|
|
1806
|
+
return argv.includes(flag);
|
|
1807
|
+
}
|
|
1808
|
+
function formatJson(value) {
|
|
1809
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
1810
|
+
}
|
|
1811
|
+
function isNotFound(error) {
|
|
1812
|
+
return (typeof error === "object" &&
|
|
1813
|
+
error !== null &&
|
|
1814
|
+
"code" in error &&
|
|
1815
|
+
error.code === "ENOENT");
|
|
1816
|
+
}
|
|
1817
|
+
function errorMessage(error) {
|
|
1818
|
+
return error instanceof Error ? error.message : String(error);
|
|
1819
|
+
}
|