@doubling/compound-sync 1.12.4 → 1.12.6
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/auth-persistence.d.ts +22 -0
- package/auth-persistence.js +79 -83
- package/config.d.ts +13 -0
- package/config.js +55 -54
- package/files.d.ts +221 -0
- package/files.js +373 -438
- package/folder-chain.d.ts +44 -0
- package/folder-chain.js +52 -59
- package/manifest.d.ts +38 -0
- package/manifest.js +87 -89
- package/org-sync.d.ts +16 -0
- package/org-sync.js +1156 -1203
- package/package.json +24 -3
- package/paths.d.ts +1 -1
- package/pre-mint-app-check.d.ts +6 -0
- package/pre-mint-app-check.js +46 -39
- package/push-outcome.d.ts +9 -0
- package/push-outcome.js +6 -5
- package/setup-auth-with-pre-mint.d.ts +24 -0
- package/setup-auth-with-pre-mint.js +48 -55
- package/storage-helpers.d.ts +14 -0
- package/storage-helpers.js +43 -107
- package/sync-state.d.ts +4 -0
- package/sync-state.js +30 -16
- package/sync.d.ts +18 -0
- package/sync.js +464 -515
- package/team-registry.d.ts +41 -0
- package/team-registry.js +90 -81
- package/yjs-binding-state.d.ts +4 -0
- package/yjs-binding-state.js +25 -22
- package/yjs-file-binding.d.ts +32 -0
- package/yjs-file-binding.js +217 -204
- package/yjs-firestore-update-store.d.ts +13 -0
- package/yjs-firestore-update-store.js +99 -103
- package/yjs-provider.d.ts +43 -0
- package/yjs-provider.js +79 -75
package/sync.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
2
|
/**
|
|
4
3
|
* Bidirectional sync between Firestore files and local markdown files.
|
|
5
4
|
* Uses Firebase client SDK with browser-based auth — no gcloud needed.
|
|
@@ -15,7 +14,6 @@
|
|
|
15
14
|
* node sync.js --version
|
|
16
15
|
* node sync.js --help
|
|
17
16
|
*/
|
|
18
|
-
|
|
19
17
|
// DOU-208: Node 22+ ships its own CA bundle and does not consult the
|
|
20
18
|
// macOS system trust store. Without NODE_EXTRA_CA_CERTS pointing at
|
|
21
19
|
// /etc/ssl/cert.pem, fetch() to securetoken.googleapis.com fails
|
|
@@ -29,39 +27,30 @@
|
|
|
29
27
|
// or a wrapper explicitly chose a different bundle).
|
|
30
28
|
import { spawnSync as __dou208_spawnSync } from 'node:child_process';
|
|
31
29
|
import { existsSync as __dou208_existsSync } from 'node:fs';
|
|
32
|
-
if (!process.env
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
},
|
|
47
|
-
);
|
|
48
|
-
process.exit(__dou208_result.status ?? 1);
|
|
30
|
+
if (!process.env['NODE_EXTRA_CA_CERTS']) {
|
|
31
|
+
const __dou208_candidates = process.platform === 'darwin'
|
|
32
|
+
? ['/etc/ssl/cert.pem']
|
|
33
|
+
: process.platform === 'linux'
|
|
34
|
+
? ['/etc/ssl/certs/ca-certificates.crt', '/etc/pki/tls/certs/ca-bundle.crt']
|
|
35
|
+
: [];
|
|
36
|
+
for (const __dou208_candidate of __dou208_candidates) {
|
|
37
|
+
if (__dou208_existsSync(__dou208_candidate)) {
|
|
38
|
+
const __dou208_result = __dou208_spawnSync(process.execPath, process.argv.slice(1), {
|
|
39
|
+
stdio: 'inherit',
|
|
40
|
+
env: { ...process.env, NODE_EXTRA_CA_CERTS: __dou208_candidate },
|
|
41
|
+
});
|
|
42
|
+
process.exit(__dou208_result.status ?? 1);
|
|
43
|
+
}
|
|
49
44
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// set NODE_EXTRA_CA_CERTS manually for corporate cert chains.
|
|
45
|
+
// No candidate found (Windows, unusual Linux distro): fall through
|
|
46
|
+
// and let Node's bundled CA bundle handle TLS. The user can still
|
|
47
|
+
// set NODE_EXTRA_CA_CERTS manually for corporate cert chains.
|
|
54
48
|
}
|
|
55
|
-
|
|
56
49
|
import { initializeApp } from 'firebase/app';
|
|
57
50
|
import { getAuth, initializeAuth, signInWithEmailAndPassword, signInWithCredential, GoogleAuthProvider } from 'firebase/auth';
|
|
58
|
-
import {
|
|
59
|
-
getFirestore, collection, doc, getDocs, getDoc, setDoc, addDoc, updateDoc, deleteDoc,
|
|
60
|
-
query, where, onSnapshot
|
|
61
|
-
} from 'firebase/firestore';
|
|
51
|
+
import { getFirestore, collection, doc, getDocs, getDoc, query, where } from 'firebase/firestore';
|
|
62
52
|
import { getStorage } from 'firebase/storage';
|
|
63
53
|
import { initializeAppCheck, CustomProvider } from 'firebase/app-check';
|
|
64
|
-
import chokidar from 'chokidar';
|
|
65
54
|
import fs from 'fs';
|
|
66
55
|
import path from 'path';
|
|
67
56
|
import readline from 'readline';
|
|
@@ -69,99 +58,69 @@ import os from 'os';
|
|
|
69
58
|
import http from 'http';
|
|
70
59
|
import { exec } from 'child_process';
|
|
71
60
|
import { fileURLToPath } from 'url';
|
|
72
|
-
import {
|
|
73
|
-
pushFileToCloud,
|
|
74
|
-
deleteFileFromCloud,
|
|
75
|
-
pushFolderToCloud,
|
|
76
|
-
deleteFolderFromCloud,
|
|
77
|
-
readBlob,
|
|
78
|
-
readBlobAsText,
|
|
79
|
-
extFromPath,
|
|
80
|
-
isTextMimeType,
|
|
81
|
-
mimeTypeFromExt,
|
|
82
|
-
} from './files.js';
|
|
83
|
-
|
|
84
61
|
// Max file size enforced before upload. Mirrored in storage.rules.
|
|
85
62
|
const MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
|
|
86
63
|
import { resolveConfigPath, loadConfig, saveConfig } from './config.js';
|
|
87
64
|
import { makeFilePersistence, resolveAuthFilePath } from './auth-persistence.js';
|
|
88
65
|
import { readPersistedRefreshToken, exchangeRefreshTokenForIdToken } from './pre-mint-app-check.js';
|
|
89
66
|
import { setupAuthWithPreMint } from './setup-auth-with-pre-mint.js';
|
|
90
|
-
import {
|
|
91
|
-
scopeFromLocalPath,
|
|
92
|
-
teamFolderName,
|
|
93
|
-
PRIVATE_FOLDER,
|
|
94
|
-
SHARED_WITH_ME_FOLDER,
|
|
95
|
-
SHARED_BY_ME_FOLDER,
|
|
96
|
-
} from './paths.js';
|
|
97
|
-
import { TeamRegistry } from './team-registry.js';
|
|
98
67
|
import { startOrgSync } from './org-sync.js';
|
|
99
|
-
|
|
100
68
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
101
|
-
|
|
102
69
|
// --- Firebase configs (same as the web app) ---
|
|
103
|
-
|
|
104
70
|
const firebaseConfigs = {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
71
|
+
'doubling-compound-sandbox': {
|
|
72
|
+
apiKey: "AIzaSyBnskQNh--bRaRN7tsVFbb01oll8OSdVH8",
|
|
73
|
+
authDomain: "doubling-compound-sandbox.firebaseapp.com",
|
|
74
|
+
projectId: "doubling-compound-sandbox",
|
|
75
|
+
storageBucket: "doubling-compound-sandbox.firebasestorage.app",
|
|
76
|
+
messagingSenderId: "83118259611",
|
|
77
|
+
appId: "1:83118259611:web:cf9cda61c9ecdc161b353e"
|
|
78
|
+
},
|
|
79
|
+
'doubling-compound-dev': {
|
|
80
|
+
apiKey: "AIzaSyBPKJqfy77_qmP_x9zOVzoHKuY8PHFCUm0",
|
|
81
|
+
authDomain: "doubling-compound-dev.firebaseapp.com",
|
|
82
|
+
projectId: "doubling-compound-dev",
|
|
83
|
+
storageBucket: "doubling-compound-dev.firebasestorage.app",
|
|
84
|
+
messagingSenderId: "967509355891",
|
|
85
|
+
appId: "1:967509355891:web:c253ebdb8205d3ef012906"
|
|
86
|
+
},
|
|
87
|
+
'doubling-compound-prod': {
|
|
88
|
+
apiKey: "AIzaSyDvnPV5dLxzubPE8pwq_S80gMsF8E97LaI",
|
|
89
|
+
authDomain: "compound.doubling.io",
|
|
90
|
+
projectId: "doubling-compound-prod",
|
|
91
|
+
storageBucket: "doubling-compound-prod.firebasestorage.app",
|
|
92
|
+
messagingSenderId: "831648925388",
|
|
93
|
+
appId: "1:831648925388:web:87550a55f807f2a2a1a469"
|
|
94
|
+
}
|
|
129
95
|
};
|
|
130
|
-
|
|
131
96
|
// App Check token endpoints per environment
|
|
132
97
|
const appCheckEndpoints = {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
98
|
+
'doubling-compound-prod': 'https://mintappchecktoken-oaimzbv67a-uc.a.run.app',
|
|
99
|
+
'doubling-compound-dev': 'https://mintappchecktoken-molg2wq42a-uc.a.run.app',
|
|
100
|
+
'doubling-compound-sandbox': 'https://mintappchecktoken-pbpkn3igxa-uc.a.run.app',
|
|
136
101
|
};
|
|
137
|
-
|
|
138
102
|
// --- Helpers ---
|
|
139
|
-
|
|
140
103
|
function ask(question) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
104
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
rl.question(question, (answer) => {
|
|
107
|
+
rl.close();
|
|
108
|
+
resolve(answer.trim());
|
|
109
|
+
});
|
|
146
110
|
});
|
|
147
|
-
});
|
|
148
111
|
}
|
|
149
|
-
|
|
150
112
|
function expandHome(p) {
|
|
151
|
-
|
|
152
|
-
|
|
113
|
+
if (p.startsWith('~/'))
|
|
114
|
+
return path.join(os.homedir(), p.slice(2));
|
|
115
|
+
return p;
|
|
153
116
|
}
|
|
154
|
-
|
|
155
117
|
function openBrowser(url) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
118
|
+
const cmd = process.platform === 'darwin' ? 'open' :
|
|
119
|
+
process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
120
|
+
exec(`${cmd} "${url}"`);
|
|
159
121
|
}
|
|
160
|
-
|
|
161
|
-
// --- Browser-based auth ---
|
|
162
|
-
|
|
163
122
|
function buildLoginPage(fbConfig) {
|
|
164
|
-
|
|
123
|
+
return `<!DOCTYPE html>
|
|
165
124
|
<html><head><meta charset="utf-8"><title>Compound Sync - Sign In</title>
|
|
166
125
|
<style>
|
|
167
126
|
body { font-family: -apple-system, system-ui, sans-serif; max-width: 400px; margin: 80px auto; text-align: center; }
|
|
@@ -258,240 +217,228 @@ window.emailSignIn = async function() {
|
|
|
258
217
|
</script>
|
|
259
218
|
</body></html>`;
|
|
260
219
|
}
|
|
261
|
-
|
|
262
220
|
function browserAuth(fbConfig) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
221
|
+
return new Promise((resolve, reject) => {
|
|
222
|
+
const server = http.createServer((req, res) => {
|
|
223
|
+
if (req.method === 'GET' && req.url === '/login') {
|
|
224
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
225
|
+
res.end(buildLoginPage(fbConfig));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (req.method === 'POST' && req.url === '/auth-callback') {
|
|
229
|
+
let body = '';
|
|
230
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
231
|
+
req.on('end', () => {
|
|
232
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
233
|
+
res.end('OK');
|
|
234
|
+
server.close();
|
|
235
|
+
resolve(JSON.parse(body));
|
|
236
|
+
});
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
res.writeHead(404);
|
|
240
|
+
res.end('Not found');
|
|
241
|
+
});
|
|
242
|
+
server.listen(0, () => {
|
|
243
|
+
const addr = server.address();
|
|
244
|
+
// After listen(0) succeeds, address() returns an AddressInfo
|
|
245
|
+
// object. If we get null (server closed mid-flight) or a string
|
|
246
|
+
// (unix socket path) something went wrong; fail loudly rather
|
|
247
|
+
// than hand the user a `http://localhost:0/login` URL that
|
|
248
|
+
// browsers reject and hangs the auth flow indefinitely.
|
|
249
|
+
if (typeof addr !== 'object' || !addr) {
|
|
250
|
+
server.close();
|
|
251
|
+
reject(new Error(`browserAuth: server.address() returned ${JSON.stringify(addr)}`));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const url = `http://localhost:${addr.port}/login`;
|
|
255
|
+
console.log(`Opening browser for sign-in...`);
|
|
256
|
+
console.log(`If the browser doesn't open, visit: ${url}`);
|
|
257
|
+
openBrowser(url);
|
|
279
258
|
});
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
res.writeHead(404);
|
|
284
|
-
res.end('Not found');
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
server.listen(0, () => {
|
|
288
|
-
const port = server.address().port;
|
|
289
|
-
const url = `http://localhost:${port}/login`;
|
|
290
|
-
console.log(`Opening browser for sign-in...`);
|
|
291
|
-
console.log(`If the browser doesn't open, visit: ${url}`);
|
|
292
|
-
openBrowser(url);
|
|
293
259
|
});
|
|
294
|
-
});
|
|
295
260
|
}
|
|
296
|
-
|
|
297
|
-
// --- Interactive setup ---
|
|
298
|
-
|
|
299
261
|
async function setupConfig() {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
// Browser sign-in to discover orgs
|
|
310
|
-
console.log('');
|
|
311
|
-
const authResult = await browserAuth(fbConfig);
|
|
312
|
-
const app = initializeApp(fbConfig, 'setup');
|
|
313
|
-
const auth = getAuth(app);
|
|
314
|
-
|
|
315
|
-
// Initialize App Check before re-authenticating in Node.js
|
|
316
|
-
const appCheckUrl = appCheckEndpoints[projectId];
|
|
317
|
-
if (appCheckUrl && authResult.firebaseIdToken) {
|
|
318
|
-
await initAppCheckWithIdToken(app, appCheckUrl, authResult.firebaseIdToken);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const db = getFirestore(app);
|
|
322
|
-
|
|
323
|
-
let userCredential;
|
|
324
|
-
if (authResult.method === 'google') {
|
|
325
|
-
const credential = GoogleAuthProvider.credential(authResult.idToken, authResult.accessToken);
|
|
326
|
-
userCredential = await signInWithCredential(auth, credential);
|
|
327
|
-
} else {
|
|
328
|
-
userCredential = await signInWithEmailAndPassword(auth, authResult.email, authResult.password);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const userId = userCredential.user.uid;
|
|
332
|
-
const userEmail = userCredential.user.email;
|
|
333
|
-
console.log(`Signed in as ${userEmail}`);
|
|
334
|
-
|
|
335
|
-
// List available orgs for this user
|
|
336
|
-
console.log('');
|
|
337
|
-
console.log('Fetching your organizations...');
|
|
338
|
-
const membersSnapshot = await getDocs(
|
|
339
|
-
query(collection(db, 'orgMembers'), where('userId', '==', userId))
|
|
340
|
-
);
|
|
341
|
-
|
|
342
|
-
const orgIds = [];
|
|
343
|
-
membersSnapshot.forEach(doc => {
|
|
344
|
-
orgIds.push(doc.data().orgId);
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
if (orgIds.length === 0) {
|
|
348
|
-
console.error('No organizations found for your account.');
|
|
349
|
-
process.exit(1);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const orgs = [];
|
|
353
|
-
for (const orgId of orgIds) {
|
|
354
|
-
const orgDoc = await getDoc(doc(db, 'orgs', orgId));
|
|
355
|
-
if (orgDoc.exists()) {
|
|
356
|
-
orgs.push({ id: orgDoc.id, ...orgDoc.data() });
|
|
262
|
+
console.log('');
|
|
263
|
+
console.log('Welcome to the Compound sync daemon! Let\'s set things up.');
|
|
264
|
+
console.log('');
|
|
265
|
+
// Default to prod; --env flag overrides for internal use
|
|
266
|
+
const projectId = envOverride || 'doubling-compound-prod';
|
|
267
|
+
const fbConfig = firebaseConfigs[projectId];
|
|
268
|
+
if (!fbConfig) {
|
|
269
|
+
console.error(`Unknown project: ${projectId}`);
|
|
270
|
+
process.exit(1);
|
|
357
271
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
if (
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
272
|
+
// Browser sign-in to discover orgs
|
|
273
|
+
console.log('');
|
|
274
|
+
const authResult = await browserAuth(fbConfig);
|
|
275
|
+
const app = initializeApp(fbConfig, 'setup');
|
|
276
|
+
const auth = getAuth(app);
|
|
277
|
+
// Initialize App Check before re-authenticating in Node.js
|
|
278
|
+
const appCheckUrl = appCheckEndpoints[projectId];
|
|
279
|
+
if (appCheckUrl && authResult.firebaseIdToken) {
|
|
280
|
+
await initAppCheckWithIdToken(app, appCheckUrl, authResult.firebaseIdToken);
|
|
281
|
+
}
|
|
282
|
+
const db = getFirestore(app);
|
|
283
|
+
let userCredential;
|
|
284
|
+
if (authResult.method === 'google') {
|
|
285
|
+
const credential = GoogleAuthProvider.credential(authResult.idToken, authResult.accessToken);
|
|
286
|
+
userCredential = await signInWithCredential(auth, credential);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
userCredential = await signInWithEmailAndPassword(auth, authResult.email, authResult.password);
|
|
290
|
+
}
|
|
291
|
+
const userId = userCredential.user.uid;
|
|
292
|
+
const userEmail = userCredential.user.email;
|
|
293
|
+
console.log(`Signed in as ${userEmail}`);
|
|
294
|
+
// List available orgs for this user
|
|
295
|
+
console.log('');
|
|
296
|
+
console.log('Fetching your organizations...');
|
|
297
|
+
const membersSnapshot = await getDocs(query(collection(db, 'orgMembers'), where('userId', '==', userId)));
|
|
298
|
+
const orgIds = [];
|
|
299
|
+
membersSnapshot.forEach((d) => {
|
|
300
|
+
const orgId = d.data()['orgId'];
|
|
301
|
+
if (typeof orgId === 'string' && orgId)
|
|
302
|
+
orgIds.push(orgId);
|
|
303
|
+
});
|
|
304
|
+
if (orgIds.length === 0) {
|
|
305
|
+
console.error('No organizations found for your account.');
|
|
385
306
|
process.exit(1);
|
|
386
|
-
}
|
|
387
307
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
308
|
+
const orgs = [];
|
|
309
|
+
for (const orgId of orgIds) {
|
|
310
|
+
const orgDoc = await getDoc(doc(db, 'orgs', orgId));
|
|
311
|
+
if (orgDoc.exists()) {
|
|
312
|
+
const data = orgDoc.data();
|
|
313
|
+
const name = data['name'];
|
|
314
|
+
orgs.push({ id: orgDoc.id, name: typeof name === 'string' ? name : orgDoc.id });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
394
317
|
console.log('');
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
)
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
process.exit(0);
|
|
408
|
-
}
|
|
318
|
+
console.log('Your organizations:');
|
|
319
|
+
orgs.forEach((org, i) => {
|
|
320
|
+
console.log(` ${i + 1}. ${org.name}`);
|
|
321
|
+
});
|
|
322
|
+
console.log('');
|
|
323
|
+
let selectedIndices;
|
|
324
|
+
const firstOrg = orgs[0];
|
|
325
|
+
if (orgs.length === 1 && firstOrg) {
|
|
326
|
+
const confirm = await ask(`Use "${firstOrg.name}"? [Y/n]: `) || 'y';
|
|
327
|
+
if (confirm.toLowerCase() !== 'y')
|
|
328
|
+
process.exit(0);
|
|
329
|
+
selectedIndices = [0];
|
|
409
330
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
331
|
+
else {
|
|
332
|
+
const choice = await ask(`Select organizations to sync (e.g. "1,3" or "all") [1-${orgs.length}]: `);
|
|
333
|
+
if (choice.toLowerCase() === 'all') {
|
|
334
|
+
selectedIndices = orgs.map((_, i) => i);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
selectedIndices = choice
|
|
338
|
+
.split(',')
|
|
339
|
+
.map((s) => parseInt(s.trim(), 10) - 1)
|
|
340
|
+
.filter((i) => Number.isInteger(i) && i >= 0 && i < orgs.length);
|
|
341
|
+
if (selectedIndices.length === 0) {
|
|
342
|
+
console.error('Invalid selection.');
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// Per-org local path
|
|
348
|
+
const orgEntries = [];
|
|
349
|
+
for (const idx of selectedIndices) {
|
|
350
|
+
const org = orgs[idx];
|
|
351
|
+
if (!org)
|
|
352
|
+
continue;
|
|
353
|
+
console.log('');
|
|
354
|
+
const defaultPath = path.join(os.homedir(), org.name + ' Workspace');
|
|
355
|
+
const localPath = await ask(`Local sync folder for "${org.name}" [${defaultPath}]: `) || defaultPath;
|
|
356
|
+
const resolvedPath = expandHome(localPath);
|
|
357
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
358
|
+
const create = await ask(`"${resolvedPath}" doesn't exist. Create it? [Y/n]: `) || 'y';
|
|
359
|
+
if (create.toLowerCase() === 'y') {
|
|
360
|
+
fs.mkdirSync(resolvedPath, { recursive: true });
|
|
361
|
+
console.log('Created.');
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
process.exit(0);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
orgEntries.push({ orgId: org.id, localPath: resolvedPath });
|
|
368
|
+
}
|
|
369
|
+
// Save config (no credentials stored). saveConfig creates parent dirs
|
|
370
|
+
// so paths like ~/.config/compound-sync/work.json work without manual mkdir.
|
|
371
|
+
saveConfig(CONFIG_PATH, { projectId, orgs: orgEntries });
|
|
372
|
+
console.log('');
|
|
373
|
+
console.log(`Config saved to ${CONFIG_PATH}`);
|
|
374
|
+
console.log('');
|
|
375
|
+
return {
|
|
376
|
+
projectId,
|
|
377
|
+
orgs: orgEntries,
|
|
378
|
+
_userId: userId,
|
|
379
|
+
_app: app,
|
|
380
|
+
_db: db,
|
|
381
|
+
};
|
|
427
382
|
}
|
|
428
|
-
|
|
429
|
-
// --- App Check (custom provider for CLI) ---
|
|
430
|
-
|
|
431
383
|
// Fetch an App Check token using a Firebase ID token (before Node.js sign-in)
|
|
432
384
|
async function fetchAppCheckToken(tokenEndpoint, firebaseIdToken) {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
return await response.json();
|
|
385
|
+
const response = await fetch(tokenEndpoint, {
|
|
386
|
+
method: 'POST',
|
|
387
|
+
headers: {
|
|
388
|
+
'Authorization': `Bearer ${firebaseIdToken}`,
|
|
389
|
+
'Content-Type': 'application/json',
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
if (!response.ok) {
|
|
393
|
+
const errBody = await response.text().catch(() => '');
|
|
394
|
+
console.error(`App Check token request failed (${response.status}): ${errBody}`);
|
|
395
|
+
throw new Error(`App Check token request failed: ${response.status}`);
|
|
396
|
+
}
|
|
397
|
+
return await response.json();
|
|
448
398
|
}
|
|
449
|
-
|
|
450
399
|
// Initialize App Check with a pre-fetched ID token (used on first sign-in)
|
|
451
400
|
async function initAppCheckWithIdToken(app, tokenEndpoint, firebaseIdToken) {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
console.log('App Check initialized.');
|
|
401
|
+
console.log('Initializing App Check...');
|
|
402
|
+
// Pre-fetch the first token
|
|
403
|
+
const initial = await fetchAppCheckToken(tokenEndpoint, firebaseIdToken);
|
|
404
|
+
let cachedToken = {
|
|
405
|
+
token: initial.token,
|
|
406
|
+
expireTimeMillis: Date.now() + initial.ttlMillis,
|
|
407
|
+
};
|
|
408
|
+
const customProvider = new CustomProvider({
|
|
409
|
+
getToken: async () => {
|
|
410
|
+
// If token is still valid, return cached
|
|
411
|
+
if (cachedToken.expireTimeMillis > Date.now() + 60000) {
|
|
412
|
+
return cachedToken;
|
|
413
|
+
}
|
|
414
|
+
// Otherwise refresh — at this point the user should be signed in
|
|
415
|
+
const auth = getAuth(app);
|
|
416
|
+
const user = auth.currentUser;
|
|
417
|
+
if (!user)
|
|
418
|
+
return cachedToken; // fallback
|
|
419
|
+
const idToken = await user.getIdToken();
|
|
420
|
+
const refreshed = await fetchAppCheckToken(tokenEndpoint, idToken);
|
|
421
|
+
cachedToken = {
|
|
422
|
+
token: refreshed.token,
|
|
423
|
+
expireTimeMillis: Date.now() + refreshed.ttlMillis,
|
|
424
|
+
};
|
|
425
|
+
return cachedToken;
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
initializeAppCheck(app, {
|
|
429
|
+
provider: customProvider,
|
|
430
|
+
isTokenAutoRefreshEnabled: true,
|
|
431
|
+
});
|
|
432
|
+
console.log('App Check initialized.');
|
|
485
433
|
}
|
|
486
|
-
|
|
487
434
|
// Initialize App Check with an already-signed-in user (used during setup reuse)
|
|
488
435
|
async function initAppCheck(app, auth, tokenEndpoint) {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
436
|
+
const user = auth.currentUser;
|
|
437
|
+
if (!user)
|
|
438
|
+
throw new Error('Not signed in');
|
|
439
|
+
const idToken = await user.getIdToken();
|
|
440
|
+
await initAppCheckWithIdToken(app, tokenEndpoint, idToken);
|
|
493
441
|
}
|
|
494
|
-
|
|
495
442
|
// --- Non-interactive account discovery (for the desktop app) ---
|
|
496
443
|
//
|
|
497
444
|
// Sign in (restoring a persisted session if present, else via the
|
|
@@ -502,97 +449,92 @@ async function initAppCheck(app, auth, tokenEndpoint) {
|
|
|
502
449
|
// The desktop parses that to register the account and write its
|
|
503
450
|
// per-account config. Exits when done.
|
|
504
451
|
async function discoverOrgs(projectId) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// Use the DEFAULT app name (not a named 'discover' app): Firebase
|
|
512
|
-
// derives its auth-persistence storage key from the app name, and the
|
|
513
|
-
// daemon reads the default-app key. A named app here would persist
|
|
514
|
-
// under a different key, so the daemon wouldn't find the session and
|
|
515
|
-
// would prompt for sign-in a second time. discover exits before the
|
|
516
|
-
// daemon block runs, so there's no app-name collision.
|
|
517
|
-
const app = initializeApp(fbConfig);
|
|
518
|
-
const authFilePath = resolveAuthFilePath({
|
|
519
|
-
authFileEnv: process.env.COMPOUND_AUTH_FILE,
|
|
520
|
-
homeDir: os.homedir(),
|
|
521
|
-
projectId,
|
|
522
|
-
accountKey: 'discover',
|
|
523
|
-
});
|
|
524
|
-
// Pre-mint App Check on the app, THEN initializeAuth. The ordering
|
|
525
|
-
// matters: see setup-auth-with-pre-mint.js for the call-order
|
|
526
|
-
// contract and why the 1.9.3 code (initializeAuth before pre-mint)
|
|
527
|
-
// raced the SDK's startup :lookup and lost.
|
|
528
|
-
const appCheckUrl = appCheckEndpoints[projectId];
|
|
529
|
-
const { auth } = await setupAuthWithPreMint(
|
|
530
|
-
{ app, authFilePath, apiKey: fbConfig.apiKey, appCheckUrl },
|
|
531
|
-
{
|
|
532
|
-
readPersistedRefreshToken,
|
|
533
|
-
exchangeRefreshTokenForIdToken,
|
|
534
|
-
initAppCheckWithIdToken,
|
|
535
|
-
makeFilePersistence,
|
|
536
|
-
initializeAuth,
|
|
537
|
-
},
|
|
538
|
-
);
|
|
539
|
-
|
|
540
|
-
let userId;
|
|
541
|
-
let userEmail;
|
|
542
|
-
|
|
543
|
-
if (auth.currentUser) {
|
|
544
|
-
userId = auth.currentUser.uid;
|
|
545
|
-
userEmail = auth.currentUser.email;
|
|
546
|
-
// App Check is already initialized above when we had a persisted
|
|
547
|
-
// user; no need to re-init.
|
|
548
|
-
} else {
|
|
549
|
-
const authResult = await browserAuth(fbConfig);
|
|
550
|
-
if (appCheckUrl && authResult.firebaseIdToken) {
|
|
551
|
-
await initAppCheckWithIdToken(app, appCheckUrl, authResult.firebaseIdToken);
|
|
452
|
+
const fbConfig = firebaseConfigs[projectId];
|
|
453
|
+
if (!fbConfig) {
|
|
454
|
+
console.error(`Unknown project: ${projectId}`);
|
|
455
|
+
process.exit(1);
|
|
552
456
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
457
|
+
// Use the DEFAULT app name (not a named 'discover' app): Firebase
|
|
458
|
+
// derives its auth-persistence storage key from the app name, and the
|
|
459
|
+
// daemon reads the default-app key. A named app here would persist
|
|
460
|
+
// under a different key, so the daemon wouldn't find the session and
|
|
461
|
+
// would prompt for sign-in a second time. discover exits before the
|
|
462
|
+
// daemon block runs, so there's no app-name collision.
|
|
463
|
+
const app = initializeApp(fbConfig);
|
|
464
|
+
const authFilePath = resolveAuthFilePath({
|
|
465
|
+
authFileEnv: process.env['COMPOUND_AUTH_FILE'],
|
|
466
|
+
homeDir: os.homedir(),
|
|
467
|
+
projectId,
|
|
468
|
+
accountKey: 'discover',
|
|
469
|
+
});
|
|
470
|
+
// Pre-mint App Check on the app, THEN initializeAuth. The ordering
|
|
471
|
+
// matters: see setup-auth-with-pre-mint.js for the call-order
|
|
472
|
+
// contract and why the 1.9.3 code (initializeAuth before pre-mint)
|
|
473
|
+
// raced the SDK's startup :lookup and lost.
|
|
474
|
+
const appCheckUrl = appCheckEndpoints[projectId];
|
|
475
|
+
const { auth } = await setupAuthWithPreMint({ app, authFilePath, apiKey: fbConfig.apiKey, appCheckUrl }, {
|
|
476
|
+
readPersistedRefreshToken,
|
|
477
|
+
exchangeRefreshTokenForIdToken,
|
|
478
|
+
initAppCheckWithIdToken,
|
|
479
|
+
makeFilePersistence,
|
|
480
|
+
initializeAuth,
|
|
481
|
+
});
|
|
482
|
+
let userId;
|
|
483
|
+
let userEmail;
|
|
484
|
+
if (auth.currentUser) {
|
|
485
|
+
userId = auth.currentUser.uid;
|
|
486
|
+
userEmail = auth.currentUser.email;
|
|
487
|
+
// App Check is already initialized above when we had a persisted
|
|
488
|
+
// user; no need to re-init.
|
|
561
489
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
490
|
+
else {
|
|
491
|
+
const authResult = await browserAuth(fbConfig);
|
|
492
|
+
if (appCheckUrl && authResult.firebaseIdToken) {
|
|
493
|
+
await initAppCheckWithIdToken(app, appCheckUrl, authResult.firebaseIdToken);
|
|
494
|
+
}
|
|
495
|
+
let cred;
|
|
496
|
+
if (authResult.method === 'google') {
|
|
497
|
+
cred = await signInWithCredential(auth, GoogleAuthProvider.credential(authResult.idToken, authResult.accessToken));
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
cred = await signInWithEmailAndPassword(auth, authResult.email, authResult.password);
|
|
501
|
+
}
|
|
502
|
+
userId = cred.user.uid;
|
|
503
|
+
userEmail = cred.user.email;
|
|
504
|
+
}
|
|
505
|
+
const db = getFirestore(app);
|
|
506
|
+
const snapshot = await getDocs(query(collection(db, 'orgMembers'), where('userId', '==', userId)));
|
|
507
|
+
const orgIds = [];
|
|
508
|
+
snapshot.forEach((d) => {
|
|
509
|
+
const orgId = d.data()['orgId'];
|
|
510
|
+
if (typeof orgId === 'string' && orgId)
|
|
511
|
+
orgIds.push(orgId);
|
|
512
|
+
});
|
|
513
|
+
const orgs = [];
|
|
514
|
+
for (const orgId of orgIds) {
|
|
515
|
+
const orgDoc = await getDoc(doc(db, 'orgs', orgId));
|
|
516
|
+
if (orgDoc.exists()) {
|
|
517
|
+
const name = orgDoc.data()['name'];
|
|
518
|
+
// Fall back to the org id when the doc has no usable `name`
|
|
519
|
+
// field; matches setupConfig's behavior and gives the desktop
|
|
520
|
+
// org picker something to render instead of a blank button.
|
|
521
|
+
orgs.push({ id: orgDoc.id, name: typeof name === 'string' && name ? name : orgDoc.id });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
console.log(`COMPOUND_DISCOVER ${JSON.stringify({ userId, email: userEmail, orgs })}`);
|
|
525
|
+
process.exit(0);
|
|
580
526
|
}
|
|
581
|
-
|
|
582
527
|
// --- Config ---
|
|
583
|
-
|
|
584
528
|
const args = process.argv.slice(2);
|
|
585
|
-
|
|
586
529
|
// Handle --version and --help early so they short-circuit before any
|
|
587
530
|
// Firebase init or browser-auth side effects.
|
|
588
531
|
if (args.includes('--version') || args.includes('-v')) {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
532
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
|
|
533
|
+
console.log(pkg.version);
|
|
534
|
+
process.exit(0);
|
|
592
535
|
}
|
|
593
|
-
|
|
594
536
|
if (args.includes('--help') || args.includes('-h')) {
|
|
595
|
-
|
|
537
|
+
console.log(`compound-sync - Doubling Compound sync daemon
|
|
596
538
|
|
|
597
539
|
Usage:
|
|
598
540
|
compound-sync Start the daemon (interactive setup on first run)
|
|
@@ -603,164 +545,171 @@ Usage:
|
|
|
603
545
|
compound-sync --help, -h Print this help and exit
|
|
604
546
|
|
|
605
547
|
More: https://github.com/Doubling-Inc/doubling-compound`);
|
|
606
|
-
|
|
548
|
+
process.exit(0);
|
|
607
549
|
}
|
|
608
|
-
|
|
609
550
|
let configFile = 'config.json';
|
|
610
551
|
let forceSetup = false;
|
|
611
552
|
let envOverride = null;
|
|
612
553
|
let discoverMode = false;
|
|
613
554
|
for (let i = 0; i < args.length; i++) {
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
555
|
+
if (args[i] === '--config' && args[i + 1]) {
|
|
556
|
+
configFile = args[++i];
|
|
557
|
+
}
|
|
558
|
+
if (args[i] === '--setup') {
|
|
559
|
+
forceSetup = true;
|
|
560
|
+
}
|
|
561
|
+
if (args[i] === '--env' && args[i + 1]) {
|
|
562
|
+
envOverride = `doubling-compound-${args[++i]}`;
|
|
563
|
+
}
|
|
564
|
+
// Non-interactive account discovery for the desktop app: sign in
|
|
565
|
+
// (persisting credentials to COMPOUND_AUTH_FILE), print the user's
|
|
566
|
+
// orgs as a COMPOUND_DISCOVER JSON line, and exit. The desktop
|
|
567
|
+
// parses it to register the account and write its config.
|
|
568
|
+
if (args[i] === '--discover') {
|
|
569
|
+
discoverMode = true;
|
|
570
|
+
}
|
|
622
571
|
}
|
|
623
|
-
|
|
624
572
|
if (discoverMode) {
|
|
625
|
-
|
|
626
|
-
|
|
573
|
+
await discoverOrgs(envOverride || 'doubling-compound-prod');
|
|
574
|
+
// discoverOrgs exits the process.
|
|
627
575
|
}
|
|
628
|
-
|
|
629
576
|
const CONFIG_PATH = resolveConfigPath(configFile, __dirname);
|
|
630
577
|
let config = null;
|
|
631
578
|
if (!forceSetup) {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
579
|
+
try {
|
|
580
|
+
config = loadConfig(CONFIG_PATH);
|
|
581
|
+
}
|
|
582
|
+
catch (err) {
|
|
583
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
584
|
+
console.error(`Failed to load ${CONFIG_PATH}: ${msg}`);
|
|
585
|
+
process.exit(1);
|
|
586
|
+
}
|
|
638
587
|
}
|
|
639
|
-
|
|
640
588
|
// Interactive setup if no config exists or --setup was passed
|
|
641
589
|
if (!config) {
|
|
642
|
-
|
|
590
|
+
config = await setupConfig();
|
|
643
591
|
}
|
|
644
|
-
|
|
645
592
|
// --- Initialize Firebase client SDK ---
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
process.exit(1);
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
app = initializeApp(fbConfig);
|
|
669
|
-
db = getFirestore(app);
|
|
670
|
-
|
|
671
|
-
// File-backed auth persistence. A previously-signed-in session is
|
|
672
|
-
// restored here with no browser. The desktop app sets
|
|
673
|
-
// COMPOUND_AUTH_FILE per account; the CLI derives a per-(project,
|
|
674
|
-
// account) path under ~/.config/compound-sync. initializeAuth must
|
|
675
|
-
// run before any getAuth(app) for this app.
|
|
676
|
-
const authFilePath = resolveAuthFilePath({
|
|
677
|
-
authFileEnv: process.env.COMPOUND_AUTH_FILE,
|
|
678
|
-
homeDir: os.homedir(),
|
|
679
|
-
projectId: config.projectId,
|
|
680
|
-
accountKey: config.email || config.orgId,
|
|
681
|
-
});
|
|
682
|
-
// Pre-mint App Check on the app, THEN initializeAuth. The ordering
|
|
683
|
-
// matters: see setup-auth-with-pre-mint.js for the call-order
|
|
684
|
-
// contract and why the 1.9.3 code (initializeAuth before pre-mint)
|
|
685
|
-
// raced the SDK's startup :lookup and lost.
|
|
686
|
-
const appCheckUrl = appCheckEndpoints[config.projectId];
|
|
687
|
-
const { auth, appCheckPreMinted } = await setupAuthWithPreMint(
|
|
688
|
-
{ app, authFilePath, apiKey: fbConfig.apiKey, appCheckUrl },
|
|
689
|
-
{
|
|
690
|
-
readPersistedRefreshToken,
|
|
691
|
-
exchangeRefreshTokenForIdToken,
|
|
692
|
-
initAppCheckWithIdToken,
|
|
693
|
-
makeFilePersistence,
|
|
694
|
-
initializeAuth,
|
|
695
|
-
},
|
|
696
|
-
);
|
|
697
|
-
|
|
698
|
-
if (auth.currentUser) {
|
|
699
|
-
// Silent re-auth: session restored from persisted credentials, no
|
|
700
|
-
// browser. App Check was already pre-minted above when we had a
|
|
701
|
-
// persisted user, so no need to re-init.
|
|
702
|
-
userId = auth.currentUser.uid;
|
|
703
|
-
if (appCheckUrl && !appCheckPreMinted) {
|
|
704
|
-
// Defensive: persisted user existed but pre-mint failed; still try
|
|
705
|
-
// to bring App Check up via the current user's ID token so the
|
|
706
|
-
// rest of the daemon's calls aren't blocked.
|
|
707
|
-
const idToken = await auth.currentUser.getIdToken();
|
|
708
|
-
await initAppCheckWithIdToken(app, appCheckUrl, idToken);
|
|
593
|
+
// Type predicate: did setupConfig produce the "hot handles" result
|
|
594
|
+
// (with _app/_db/_userId already initialized), or did we load a
|
|
595
|
+
// vanilla NormalizedConfig from disk?
|
|
596
|
+
function hasHotHandles(c) {
|
|
597
|
+
return '_app' in c;
|
|
598
|
+
}
|
|
599
|
+
let app;
|
|
600
|
+
let db;
|
|
601
|
+
let userId;
|
|
602
|
+
if (hasHotHandles(config)) {
|
|
603
|
+
// Reuse app and auth from setup
|
|
604
|
+
app = config._app;
|
|
605
|
+
db = config._db;
|
|
606
|
+
userId = config._userId;
|
|
607
|
+
// Initialize App Check if endpoint exists for this env
|
|
608
|
+
const appCheckUrl = appCheckEndpoints[config.projectId];
|
|
609
|
+
if (appCheckUrl) {
|
|
610
|
+
const auth = getAuth(app);
|
|
611
|
+
await initAppCheck(app, auth, appCheckUrl);
|
|
709
612
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
// Initialize App Check BEFORE signing in on the Node.js side, using
|
|
718
|
-
// the Firebase ID token from the browser.
|
|
719
|
-
if (appCheckUrl && authResult.firebaseIdToken) {
|
|
720
|
-
await initAppCheckWithIdToken(app, appCheckUrl, authResult.firebaseIdToken);
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
const fbConfig = firebaseConfigs[config.projectId];
|
|
616
|
+
if (!fbConfig) {
|
|
617
|
+
console.error(`Unknown project: ${config.projectId}`);
|
|
618
|
+
process.exit(1);
|
|
721
619
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
620
|
+
app = initializeApp(fbConfig);
|
|
621
|
+
db = getFirestore(app);
|
|
622
|
+
// File-backed auth persistence. A previously-signed-in session is
|
|
623
|
+
// restored here with no browser. The desktop app sets
|
|
624
|
+
// COMPOUND_AUTH_FILE per account; the CLI derives a per-(project,
|
|
625
|
+
// account) path under ~/.config/compound-sync. initializeAuth must
|
|
626
|
+
// run before any getAuth(app) for this app.
|
|
627
|
+
//
|
|
628
|
+
// accountKey: use the first org's id as a stable per-config
|
|
629
|
+
// discriminator. NormalizedConfig drops the legacy top-level `email`
|
|
630
|
+
// and `orgId` fields, so the prior `config.email || config.orgId`
|
|
631
|
+
// lookup collapsed to undefined and every CLI config under the same
|
|
632
|
+
// project collided on the same `<project>__default.json` auth file.
|
|
633
|
+
// The first org id is always present (normalizeConfig rejects empty
|
|
634
|
+
// orgs[]) and uniquely identifies a config across the multi-org
|
|
635
|
+
// schema. The desktop app overrides this entirely via
|
|
636
|
+
// COMPOUND_AUTH_FILE, so this only matters for the bare CLI path.
|
|
637
|
+
const accountKey = config.orgs[0]?.orgId;
|
|
638
|
+
const authFilePath = resolveAuthFilePath({
|
|
639
|
+
authFileEnv: process.env['COMPOUND_AUTH_FILE'],
|
|
640
|
+
homeDir: os.homedir(),
|
|
641
|
+
projectId: config.projectId,
|
|
642
|
+
accountKey,
|
|
643
|
+
});
|
|
644
|
+
// Pre-mint App Check on the app, THEN initializeAuth. The ordering
|
|
645
|
+
// matters: see setup-auth-with-pre-mint.js for the call-order
|
|
646
|
+
// contract and why the 1.9.3 code (initializeAuth before pre-mint)
|
|
647
|
+
// raced the SDK's startup :lookup and lost.
|
|
648
|
+
const appCheckUrl = appCheckEndpoints[config.projectId];
|
|
649
|
+
const { auth, appCheckPreMinted } = await setupAuthWithPreMint({ app, authFilePath, apiKey: fbConfig.apiKey, appCheckUrl }, {
|
|
650
|
+
readPersistedRefreshToken,
|
|
651
|
+
exchangeRefreshTokenForIdToken,
|
|
652
|
+
initAppCheckWithIdToken,
|
|
653
|
+
makeFilePersistence,
|
|
654
|
+
initializeAuth,
|
|
655
|
+
});
|
|
656
|
+
if (auth.currentUser) {
|
|
657
|
+
// Silent re-auth: session restored from persisted credentials, no
|
|
658
|
+
// browser. App Check was already pre-minted above when we had a
|
|
659
|
+
// persisted user, so no need to re-init.
|
|
660
|
+
userId = auth.currentUser.uid;
|
|
661
|
+
if (appCheckUrl && !appCheckPreMinted) {
|
|
662
|
+
// Defensive: persisted user existed but pre-mint failed; still try
|
|
663
|
+
// to bring App Check up via the current user's ID token so the
|
|
664
|
+
// rest of the daemon's calls aren't blocked.
|
|
665
|
+
const idToken = await auth.currentUser.getIdToken();
|
|
666
|
+
await initAppCheckWithIdToken(app, appCheckUrl, idToken);
|
|
667
|
+
}
|
|
668
|
+
console.log(`Signed in (silent) as ${auth.currentUser.email || userId}`);
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
// First run for this account: interactive browser sign-in. The
|
|
672
|
+
// result is persisted, so later launches take the silent path.
|
|
673
|
+
console.log('');
|
|
674
|
+
const authResult = await browserAuth(fbConfig);
|
|
675
|
+
// Initialize App Check BEFORE signing in on the Node.js side, using
|
|
676
|
+
// the Firebase ID token from the browser.
|
|
677
|
+
if (appCheckUrl && authResult.firebaseIdToken) {
|
|
678
|
+
await initAppCheckWithIdToken(app, appCheckUrl, authResult.firebaseIdToken);
|
|
679
|
+
}
|
|
680
|
+
if (authResult.method === 'google') {
|
|
681
|
+
const credential = GoogleAuthProvider.credential(authResult.idToken, authResult.accessToken);
|
|
682
|
+
const cred = await signInWithCredential(auth, credential);
|
|
683
|
+
userId = cred.user.uid;
|
|
684
|
+
console.log(`Signed in as ${cred.user.email}`);
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
const cred = await signInWithEmailAndPassword(auth, authResult.email, authResult.password);
|
|
688
|
+
userId = cred.user.uid;
|
|
689
|
+
console.log(`Signed in as ${cred.user.email}`);
|
|
690
|
+
}
|
|
732
691
|
}
|
|
733
|
-
}
|
|
734
|
-
|
|
735
692
|
}
|
|
736
|
-
|
|
737
693
|
const USER_ID = userId;
|
|
738
694
|
const storage = getStorage(app);
|
|
739
|
-
|
|
740
695
|
// Read the bundled package version so the log line confirms which
|
|
741
696
|
// daemon copy is actually running. Saves the "did the rebuild include
|
|
742
697
|
// the fix?" diagnostic round-trip when a desktop bundle is mid-update.
|
|
743
|
-
const __syncPkgVersion = JSON.parse(
|
|
744
|
-
fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'),
|
|
745
|
-
).version;
|
|
746
|
-
|
|
698
|
+
const __syncPkgVersion = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8')).version;
|
|
747
699
|
console.log(`Sync daemon starting (compound-sync ${__syncPkgVersion})...`);
|
|
748
700
|
console.log(` Env: ${config.projectId}`);
|
|
749
701
|
console.log(` User: ${USER_ID}`);
|
|
750
702
|
console.log(` Orgs: ${config.orgs.length}`);
|
|
751
|
-
|
|
752
703
|
// Per-org runtime, extracted to ./org-sync.js so it is importable and
|
|
753
704
|
// testable against the emulator (DOU-156). Inject the module-level
|
|
754
705
|
// db / storage / userId here.
|
|
755
706
|
function setupOrgSync({ orgId, localPath }) {
|
|
756
|
-
|
|
707
|
+
return startOrgSync({ db, storage, userId: USER_ID, orgId, localPath: expandHome(localPath) });
|
|
757
708
|
}
|
|
758
|
-
|
|
759
709
|
// --- Main ---
|
|
760
|
-
|
|
761
710
|
for (const orgEntry of config.orgs) {
|
|
762
|
-
|
|
711
|
+
await setupOrgSync(orgEntry);
|
|
763
712
|
}
|
|
764
|
-
|
|
765
713
|
console.log('');
|
|
766
714
|
console.log('Sync running. Press Ctrl+C to stop.');
|
|
715
|
+
//# sourceMappingURL=sync.js.map
|