@doubling/compound-sync 1.7.0 → 1.9.5
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.js +34 -2
- package/files.js +21 -4
- package/org-sync.js +607 -0
- package/package.json +7 -3
- package/pre-mint-app-check.js +84 -0
- package/setup-auth-with-pre-mint.js +84 -0
- package/sync-state.js +32 -0
- package/sync.js +134 -455
package/auth-persistence.js
CHANGED
|
@@ -31,6 +31,18 @@ export function resolveAuthFilePath({ authFileEnv, homeDir, projectId, accountKe
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export function makeFilePersistence(filePath) {
|
|
34
|
+
// Freeze guard: starts true. During daemon startup the Firebase Auth
|
|
35
|
+
// SDK validates the cached refresh token against the backend. With
|
|
36
|
+
// App Check enforcement on Authentication enabled, that validation
|
|
37
|
+
// request lacks an App Check header (App Check is initialized after
|
|
38
|
+
// authStateReady today), the backend rejects, and the SDK calls
|
|
39
|
+
// _remove(userKey) on our persistence, wiping credentials. While
|
|
40
|
+
// frozen, _remove is a no-op. The daemon calls FilePersistence.unfreeze()
|
|
41
|
+
// after authStateReady resolves, restoring normal sign-out behavior.
|
|
42
|
+
// Per factory call (per account): unfreezing one account's persistence
|
|
43
|
+
// must not unfreeze another's.
|
|
44
|
+
let frozen = true;
|
|
45
|
+
|
|
34
46
|
function readStore() {
|
|
35
47
|
try {
|
|
36
48
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
@@ -41,8 +53,17 @@ export function makeFilePersistence(filePath) {
|
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
function writeStore(store) {
|
|
44
|
-
fs.
|
|
45
|
-
|
|
56
|
+
// Atomic write: temp file + rename. fs.writeFileSync is not atomic,
|
|
57
|
+
// so a crash or kill mid-write would truncate the credentials file
|
|
58
|
+
// and the next launch would read it as an empty store (parse fail
|
|
59
|
+
// = {}), forcing a browser popup. rename(2) on the same filesystem
|
|
60
|
+
// is atomic, so on-disk state is always either the prior valid
|
|
61
|
+
// JSON or the new valid JSON.
|
|
62
|
+
const dir = path.dirname(filePath);
|
|
63
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
64
|
+
const tmp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.tmp`);
|
|
65
|
+
fs.writeFileSync(tmp, JSON.stringify(store), { mode: 0o600 });
|
|
66
|
+
fs.renameSync(tmp, filePath);
|
|
46
67
|
}
|
|
47
68
|
|
|
48
69
|
return class FilePersistence {
|
|
@@ -51,6 +72,13 @@ export function makeFilePersistence(filePath) {
|
|
|
51
72
|
static type = 'LOCAL';
|
|
52
73
|
type = 'LOCAL';
|
|
53
74
|
|
|
75
|
+
// Called by the daemon once auth restore is past the validation
|
|
76
|
+
// window (after authStateReady). Sign-out _remove calls after this
|
|
77
|
+
// point are honored normally.
|
|
78
|
+
static unfreeze() {
|
|
79
|
+
frozen = false;
|
|
80
|
+
}
|
|
81
|
+
|
|
54
82
|
async _isAvailable() {
|
|
55
83
|
return true;
|
|
56
84
|
}
|
|
@@ -67,6 +95,10 @@ export function makeFilePersistence(filePath) {
|
|
|
67
95
|
}
|
|
68
96
|
|
|
69
97
|
async _remove(key) {
|
|
98
|
+
// See the freeze comment above the closure. During init we keep
|
|
99
|
+
// the cached credential rather than letting the SDK's startup
|
|
100
|
+
// validation wipe it.
|
|
101
|
+
if (frozen) return;
|
|
70
102
|
const store = readStore();
|
|
71
103
|
delete store[key];
|
|
72
104
|
writeStore(store);
|
package/files.js
CHANGED
|
@@ -264,11 +264,20 @@ export async function deleteFolderFromCloud({
|
|
|
264
264
|
* and for new files defaults to 'text/markdown' to keep
|
|
265
265
|
* legacy callers working.
|
|
266
266
|
*
|
|
267
|
+
* - localModifiedAt: ISO timestamp of the local file's last
|
|
268
|
+
* modification (mtime). When provided, a push that would
|
|
269
|
+
* overwrite a server copy whose `updatedAt` is strictly
|
|
270
|
+
* newer is skipped instead, so an older local file cannot
|
|
271
|
+
* clobber a newer remote edit (last-write-wins, symmetric
|
|
272
|
+
* with the firestore -> local pull guard). Omit it to
|
|
273
|
+
* force the push regardless of timestamps (legacy callers).
|
|
274
|
+
*
|
|
267
275
|
* Returns one of:
|
|
268
|
-
* { action: 'skipped',
|
|
269
|
-
* { action: 'noop',
|
|
270
|
-
* { action: '
|
|
271
|
-
* { action: '
|
|
276
|
+
* { action: 'skipped', reason: 'shared-scope' }
|
|
277
|
+
* { action: 'noop', fileId }
|
|
278
|
+
* { action: 'skipped-stale', fileId } // server copy is newer
|
|
279
|
+
* { action: 'created', fileId, fileData, storageError? }
|
|
280
|
+
* { action: 'updated', fileId, storageError? }
|
|
272
281
|
*/
|
|
273
282
|
export async function pushFileToCloud({
|
|
274
283
|
filesRef,
|
|
@@ -281,6 +290,7 @@ export async function pushFileToCloud({
|
|
|
281
290
|
data,
|
|
282
291
|
mimeType,
|
|
283
292
|
now = new Date().toISOString(),
|
|
293
|
+
localModifiedAt = null,
|
|
284
294
|
}) {
|
|
285
295
|
if (scope === 'shared-with-me' || scope === 'shared-by-me') {
|
|
286
296
|
return { action: 'skipped', reason: 'shared-scope' };
|
|
@@ -362,6 +372,13 @@ export async function pushFileToCloud({
|
|
|
362
372
|
&& (existing.parentId == null ? null : existing.parentId) === parentId) {
|
|
363
373
|
return { action: 'noop', fileId: docSnap.id };
|
|
364
374
|
}
|
|
375
|
+
// Last-write-wins: never let an older local file overwrite a newer
|
|
376
|
+
// server edit. The caller (sync daemon) passes the local file's mtime;
|
|
377
|
+
// if the server doc was updated more recently, leave it for the
|
|
378
|
+
// firestore -> local pull to bring down instead.
|
|
379
|
+
if (localModifiedAt && existing.updatedAt && existing.updatedAt > localModifiedAt) {
|
|
380
|
+
return { action: 'skipped-stale', fileId: docSnap.id };
|
|
381
|
+
}
|
|
365
382
|
const resolvedMime = mimeType || existing.mimeType || 'text/markdown';
|
|
366
383
|
await updateDoc(docSnap.ref, {
|
|
367
384
|
type: 'file',
|