@doubling/compound-sync 1.12.4 → 1.12.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.
@@ -0,0 +1,22 @@
1
+ export interface ResolveAuthFilePathInput {
2
+ authFileEnv?: string | null | undefined;
3
+ homeDir: string;
4
+ projectId?: string | undefined;
5
+ accountKey?: string | undefined;
6
+ }
7
+ export interface FilePersistenceClass {
8
+ new (): {
9
+ type: 'LOCAL';
10
+ _isAvailable(): Promise<boolean>;
11
+ _set(key: string, value: unknown): Promise<void>;
12
+ _get(key: string): Promise<unknown>;
13
+ _remove(key: string): Promise<void>;
14
+ _addListener(key: string, listener: unknown): void;
15
+ _removeListener(key: string, listener: unknown): void;
16
+ };
17
+ type: 'LOCAL';
18
+ unfreeze(): void;
19
+ }
20
+ export declare function resolveAuthFilePath({ authFileEnv, homeDir, projectId, accountKey, }: ResolveAuthFilePathInput): string;
21
+ export declare function makeFilePersistence(filePath: string): FilePersistenceClass;
22
+ //# sourceMappingURL=auth-persistence.d.ts.map
@@ -14,98 +14,94 @@
14
14
  // desktop app give each account its own credentials file.
15
15
  //
16
16
  // Security: the file holds a refresh token, so it's written 0600.
17
-
18
17
  import fs from 'node:fs';
19
18
  import path from 'node:path';
20
-
21
19
  // Where to store an account's persisted auth. The desktop app passes
22
20
  // an explicit per-account path via the COMPOUND_AUTH_FILE env var
23
21
  // (each account gets its own file). The CLI falls back to a stable
24
22
  // per-(project, account) path under the user's config dir, keyed by
25
23
  // email (or orgId) so two accounts on the same project don't collide.
26
- export function resolveAuthFilePath({ authFileEnv, homeDir, projectId, accountKey }) {
27
- if (authFileEnv) return authFileEnv;
28
- const safeAccount = String(accountKey || 'default').replace(/[^a-zA-Z0-9._@-]/g, '_');
29
- const safeProject = String(projectId || 'default').replace(/[^a-zA-Z0-9._-]/g, '_');
30
- return path.join(homeDir, '.config', 'compound-sync', `${safeProject}__${safeAccount}.json`);
24
+ export function resolveAuthFilePath({ authFileEnv, homeDir, projectId, accountKey, }) {
25
+ if (authFileEnv)
26
+ return authFileEnv;
27
+ const safeAccount = String(accountKey || 'default').replace(/[^a-zA-Z0-9._@-]/g, '_');
28
+ const safeProject = String(projectId || 'default').replace(/[^a-zA-Z0-9._-]/g, '_');
29
+ return path.join(homeDir, '.config', 'compound-sync', `${safeProject}__${safeAccount}.json`);
31
30
  }
32
-
33
31
  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
-
46
- function readStore() {
47
- try {
48
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
49
- } catch {
50
- // Missing or unreadable file: start from an empty store.
51
- return {};
32
+ // Freeze guard: starts true. During daemon startup the Firebase Auth
33
+ // SDK validates the cached refresh token against the backend. With
34
+ // App Check enforcement on Authentication enabled, that validation
35
+ // request lacks an App Check header (App Check is initialized after
36
+ // authStateReady today), the backend rejects, and the SDK calls
37
+ // _remove(userKey) on our persistence, wiping credentials. While
38
+ // frozen, _remove is a no-op. The daemon calls FilePersistence.unfreeze()
39
+ // after authStateReady resolves, restoring normal sign-out behavior.
40
+ // Per factory call (per account): unfreezing one account's persistence
41
+ // must not unfreeze another's.
42
+ let frozen = true;
43
+ function readStore() {
44
+ try {
45
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
46
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
47
+ return parsed;
48
+ }
49
+ return {};
50
+ }
51
+ catch {
52
+ // Missing or unreadable file: start from an empty store.
53
+ return {};
54
+ }
52
55
  }
53
- }
54
-
55
- function writeStore(store) {
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);
67
- }
68
-
69
- return class FilePersistence {
70
- // Read by the SDK both as a static and as an instance property in
71
- // different code paths; set both. 'LOCAL' = survives restarts.
72
- static type = 'LOCAL';
73
- type = 'LOCAL';
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;
56
+ function writeStore(store) {
57
+ // Atomic write: temp file + rename. fs.writeFileSync is not atomic,
58
+ // so a crash or kill mid-write would truncate the credentials file
59
+ // and the next launch would read it as an empty store (parse fail
60
+ // = {}), forcing a browser popup. rename(2) on the same filesystem
61
+ // is atomic, so on-disk state is always either the prior valid
62
+ // JSON or the new valid JSON.
63
+ const dir = path.dirname(filePath);
64
+ fs.mkdirSync(dir, { recursive: true });
65
+ const tmp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.tmp`);
66
+ fs.writeFileSync(tmp, JSON.stringify(store), { mode: 0o600 });
67
+ fs.renameSync(tmp, filePath);
80
68
  }
81
-
82
- async _isAvailable() {
83
- return true;
84
- }
85
-
86
- async _set(key, value) {
87
- const store = readStore();
88
- store[key] = value;
89
- writeStore(store);
90
- }
91
-
92
- async _get(key) {
93
- const store = readStore();
94
- return Object.prototype.hasOwnProperty.call(store, key) ? store[key] : null;
95
- }
96
-
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;
102
- const store = readStore();
103
- delete store[key];
104
- writeStore(store);
105
- }
106
-
107
- // Cross-tab change listeners are a browser concern; no-ops in Node.
108
- _addListener(_key, _listener) {}
109
- _removeListener(_key, _listener) {}
110
- };
69
+ return class FilePersistence {
70
+ // Read by the SDK both as a static and as an instance property in
71
+ // different code paths; set both. 'LOCAL' = survives restarts.
72
+ static type = 'LOCAL';
73
+ type = 'LOCAL';
74
+ // Called by the daemon once auth restore is past the validation
75
+ // window (after authStateReady). Sign-out _remove calls after this
76
+ // point are honored normally.
77
+ static unfreeze() {
78
+ frozen = false;
79
+ }
80
+ async _isAvailable() {
81
+ return true;
82
+ }
83
+ async _set(key, value) {
84
+ const store = readStore();
85
+ store[key] = value;
86
+ writeStore(store);
87
+ }
88
+ async _get(key) {
89
+ const store = readStore();
90
+ return Object.prototype.hasOwnProperty.call(store, key) ? store[key] : null;
91
+ }
92
+ async _remove(key) {
93
+ // See the freeze comment above the closure. During init we keep
94
+ // the cached credential rather than letting the SDK's startup
95
+ // validation wipe it.
96
+ if (frozen)
97
+ return;
98
+ const store = readStore();
99
+ delete store[key];
100
+ writeStore(store);
101
+ }
102
+ // Cross-tab change listeners are a browser concern; no-ops in Node.
103
+ _addListener(_key, _listener) { }
104
+ _removeListener(_key, _listener) { }
105
+ };
111
106
  }
107
+ //# sourceMappingURL=auth-persistence.js.map
package/config.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ export interface OrgConfig {
2
+ orgId: string;
3
+ localPath: string;
4
+ }
5
+ export interface NormalizedConfig {
6
+ projectId: string;
7
+ orgs: OrgConfig[];
8
+ }
9
+ export declare function resolveConfigPath(arg: string, defaultDir: string): string;
10
+ export declare function normalizeConfig(raw: unknown): NormalizedConfig;
11
+ export declare function loadConfig(absPath: string): NormalizedConfig | null;
12
+ export declare function saveConfig(absPath: string, configObject: NormalizedConfig): void;
13
+ //# sourceMappingURL=config.d.ts.map
package/config.js CHANGED
@@ -19,85 +19,86 @@
19
19
  // }
20
20
  //
21
21
  // `normalizeConfig` produces the multi-org shape from either.
22
-
23
22
  import fs from 'node:fs';
24
23
  import os from 'node:os';
25
24
  import path from 'node:path';
26
-
27
25
  function expandHome(p) {
28
- if (typeof p !== 'string') return p;
29
- if (p === '~') return os.homedir();
30
- if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
31
- return p;
26
+ if (p === '~')
27
+ return os.homedir();
28
+ if (p.startsWith('~/'))
29
+ return path.join(os.homedir(), p.slice(2));
30
+ return p;
32
31
  }
33
-
34
32
  // Resolve a --config arg to an absolute path.
35
33
  // Absolute paths are returned as-is. ~/ is expanded to the user home.
36
34
  // Otherwise the arg is treated as relative to defaultDir.
37
35
  export function resolveConfigPath(arg, defaultDir) {
38
- const expanded = expandHome(arg);
39
- if (path.isAbsolute(expanded)) return expanded;
40
- return path.join(defaultDir, expanded);
36
+ const expanded = expandHome(arg);
37
+ if (path.isAbsolute(expanded))
38
+ return expanded;
39
+ return path.join(defaultDir, expanded);
41
40
  }
42
-
43
41
  // Normalize a raw config object into the multi-org shape.
44
42
  // Throws if required fields are missing or contradictory.
45
43
  export function normalizeConfig(raw) {
46
- if (!raw || typeof raw !== 'object') {
47
- throw new Error('Config must be an object');
48
- }
49
- if (!raw.projectId) {
50
- throw new Error('Config is missing required field: projectId');
51
- }
52
-
53
- let orgs;
54
- if (Array.isArray(raw.orgs)) {
55
- orgs = raw.orgs;
56
- } else if (raw.orgId) {
57
- orgs = [{ orgId: raw.orgId, localPath: raw.localPath }];
58
- } else {
59
- throw new Error('Config must define either orgs[] or orgId');
60
- }
61
-
62
- if (orgs.length === 0) {
63
- throw new Error('Config must define at least one org');
64
- }
65
-
66
- const seenPaths = new Set();
67
- const normalizedOrgs = orgs.map((entry, idx) => {
68
- if (!entry || typeof entry !== 'object') {
69
- throw new Error(`orgs[${idx}] must be an object`);
44
+ if (!raw || typeof raw !== 'object') {
45
+ throw new Error('Config must be an object');
70
46
  }
71
- if (!entry.orgId) {
72
- throw new Error(`orgs[${idx}] is missing required field: orgId`);
47
+ const r = raw;
48
+ const projectId = r['projectId'];
49
+ if (typeof projectId !== 'string' || !projectId) {
50
+ throw new Error('Config is missing required field: projectId');
73
51
  }
74
- if (!entry.localPath) {
75
- throw new Error(`orgs[${idx}] is missing required field: localPath`);
52
+ let orgs;
53
+ if (Array.isArray(r['orgs'])) {
54
+ orgs = r['orgs'];
76
55
  }
77
- const localPath = expandHome(entry.localPath);
78
- if (seenPaths.has(localPath)) {
79
- throw new Error(`orgs[${idx}] has a duplicate localPath: ${localPath}`);
56
+ else if (r['orgId']) {
57
+ orgs = [{ orgId: r['orgId'], localPath: r['localPath'] }];
80
58
  }
81
- seenPaths.add(localPath);
82
- return { orgId: entry.orgId, localPath };
83
- });
84
-
85
- return { projectId: raw.projectId, orgs: normalizedOrgs };
59
+ else {
60
+ throw new Error('Config must define either orgs[] or orgId');
61
+ }
62
+ if (orgs.length === 0) {
63
+ throw new Error('Config must define at least one org');
64
+ }
65
+ const seenPaths = new Set();
66
+ const normalizedOrgs = orgs.map((entry, idx) => {
67
+ if (!entry || typeof entry !== 'object') {
68
+ throw new Error(`orgs[${idx}] must be an object`);
69
+ }
70
+ const e = entry;
71
+ const orgId = e['orgId'];
72
+ if (typeof orgId !== 'string' || !orgId) {
73
+ throw new Error(`orgs[${idx}] is missing required field: orgId`);
74
+ }
75
+ const rawLocalPath = e['localPath'];
76
+ if (typeof rawLocalPath !== 'string' || !rawLocalPath) {
77
+ throw new Error(`orgs[${idx}] is missing required field: localPath`);
78
+ }
79
+ const localPath = expandHome(rawLocalPath);
80
+ if (seenPaths.has(localPath)) {
81
+ throw new Error(`orgs[${idx}] has a duplicate localPath: ${localPath}`);
82
+ }
83
+ seenPaths.add(localPath);
84
+ return { orgId, localPath };
85
+ });
86
+ return { projectId, orgs: normalizedOrgs };
86
87
  }
87
-
88
88
  // Load a config file from disk and return it normalized.
89
89
  // Returns null if the file doesn't exist (caller can trigger setup).
90
90
  // Throws if the file exists but is invalid JSON or fails normalization.
91
91
  export function loadConfig(absPath) {
92
- if (!fs.existsSync(absPath)) return null;
93
- const raw = JSON.parse(fs.readFileSync(absPath, 'utf-8'));
94
- return normalizeConfig(raw);
92
+ if (!fs.existsSync(absPath))
93
+ return null;
94
+ const raw = JSON.parse(fs.readFileSync(absPath, 'utf-8'));
95
+ return normalizeConfig(raw);
95
96
  }
96
-
97
97
  // Write a config object to disk. Creates the parent directory chain if
98
98
  // it doesn't already exist (so callers can target paths like
99
99
  // ~/.config/compound-sync/work.json without manual mkdir).
100
100
  export function saveConfig(absPath, configObject) {
101
- fs.mkdirSync(path.dirname(absPath), { recursive: true });
102
- fs.writeFileSync(absPath, JSON.stringify(configObject, null, 2) + '\n');
101
+ fs.mkdirSync(path.dirname(absPath), { recursive: true });
102
+ fs.writeFileSync(absPath, JSON.stringify(configObject, null, 2) + '\n');
103
103
  }
104
+ //# sourceMappingURL=config.js.map
package/files.d.ts ADDED
@@ -0,0 +1,221 @@
1
+ import { type CollectionReference, type DocumentData } from 'firebase/firestore';
2
+ import { type FirebaseStorage } from 'firebase/storage';
3
+ import { type StorageData } from './storage-helpers.js';
4
+ export { storagePathFor, extFromPath, contentByteSize, computeContentHash, isTextMimeType, mimeTypeFromExt, } from './storage-helpers.js';
5
+ interface BlobLocation {
6
+ storage: FirebaseStorage;
7
+ orgId: string;
8
+ fileId: string;
9
+ ext: string;
10
+ }
11
+ export interface UploadBlobInput extends BlobLocation {
12
+ data: StorageData;
13
+ mimeType?: string;
14
+ }
15
+ interface ScopedFolderQuery {
16
+ filesRef: CollectionReference<DocumentData>;
17
+ scope: string;
18
+ teamId?: string | null | undefined;
19
+ userId?: string | undefined;
20
+ }
21
+ export interface EnsureFolderChainInput extends ScopedFolderQuery {
22
+ docPath: string;
23
+ now?: string | undefined;
24
+ }
25
+ export interface PushFolderInput extends ScopedFolderQuery {
26
+ docPath: string;
27
+ now?: string | undefined;
28
+ }
29
+ export type PushFolderOutcome = {
30
+ action: 'skipped';
31
+ reason: 'shared-scope';
32
+ } | {
33
+ action: 'ensured';
34
+ folderId: string | null;
35
+ };
36
+ export interface DeleteFolderInput {
37
+ filesRef: CollectionReference<DocumentData>;
38
+ storage: FirebaseStorage;
39
+ orgId: string;
40
+ scope: string;
41
+ teamId?: string | null | undefined;
42
+ userId?: string | undefined;
43
+ docPath: string;
44
+ }
45
+ export type DeleteFolderOutcome = {
46
+ action: 'skipped';
47
+ reason: 'shared-scope';
48
+ } | {
49
+ action: 'not-found';
50
+ } | {
51
+ action: 'deleted';
52
+ folderId: string | null;
53
+ childCount: number;
54
+ };
55
+ export interface PushFileInput {
56
+ filesRef: CollectionReference<DocumentData>;
57
+ storage: FirebaseStorage;
58
+ orgId: string;
59
+ userId: string;
60
+ scope: string;
61
+ teamId?: string | null | undefined;
62
+ docPath: string;
63
+ data: StorageData;
64
+ mimeType?: string | undefined;
65
+ now?: string | undefined;
66
+ localModifiedAt?: string | null | undefined;
67
+ }
68
+ export interface PushedFileData {
69
+ type: 'file';
70
+ name: string;
71
+ parentId: string | null;
72
+ path: string;
73
+ createdAt: string;
74
+ updatedAt: string;
75
+ scope: string;
76
+ teamId: string | null;
77
+ ownerId: string | null;
78
+ sharedWith: string[];
79
+ storagePath: string;
80
+ contentHash: string;
81
+ size: number;
82
+ mimeType: string;
83
+ }
84
+ export type PushFileToCloudOutcome = {
85
+ action: 'skipped';
86
+ reason: 'shared-scope';
87
+ } | {
88
+ action: 'noop';
89
+ fileId: string;
90
+ } | {
91
+ action: 'skipped-stale';
92
+ fileId: string;
93
+ } | {
94
+ action: 'created';
95
+ fileId: string;
96
+ fileData: PushedFileData;
97
+ storageError: unknown | null;
98
+ } | {
99
+ action: 'updated';
100
+ fileId: string;
101
+ storageError: unknown | null;
102
+ };
103
+ export interface DeleteFileInput {
104
+ filesRef: CollectionReference<DocumentData>;
105
+ storage: FirebaseStorage;
106
+ orgId: string;
107
+ userId: string;
108
+ scope: string;
109
+ teamId?: string | null | undefined;
110
+ docPath: string;
111
+ }
112
+ export type DeleteFileOutcome = {
113
+ action: 'skipped';
114
+ reason: 'shared-scope';
115
+ } | {
116
+ action: 'not-found';
117
+ fileId?: string;
118
+ } | {
119
+ action: 'deleted';
120
+ fileId: string;
121
+ };
122
+ export declare function uploadBlob({ storage, orgId, fileId, ext, data, mimeType }: UploadBlobInput): Promise<void>;
123
+ export declare function deleteBlob({ storage, orgId, fileId, ext }: BlobLocation): Promise<void>;
124
+ /**
125
+ * Read a blob from Cloud Storage as raw bytes. Throws on any failure
126
+ * (missing blob, permission error, network).
127
+ */
128
+ export declare function readBlob({ storage, orgId, fileId, ext }: BlobLocation): Promise<Buffer>;
129
+ /**
130
+ * Read a blob and decode as UTF-8 text. Convenience for text/markdown
131
+ * callers.
132
+ */
133
+ export declare function readBlobAsText(input: BlobLocation): Promise<string>;
134
+ /**
135
+ * Ensure folder docs exist for every segment of `docPath` and return
136
+ * the id of the leaf folder. Idempotent: existing folder docs are
137
+ * reused; missing ones are created; folder docs missing the `path`
138
+ * field are backfilled.
139
+ *
140
+ * `docPath` is the FULL slash-joined path of the folder itself (NOT a
141
+ * file path). Pass `'A/B'` to ensure both `A` and `A/B` exist; the
142
+ * returned id is for `A/B`.
143
+ *
144
+ * Returns null if `docPath` is empty (root).
145
+ */
146
+ export declare function ensureFolderChainInCloud({ filesRef, scope, teamId, userId, docPath, now, }: EnsureFolderChainInput): Promise<string | null>;
147
+ /**
148
+ * Find or create the folder doc at `docPath`. Convenience wrapper
149
+ * around ensureFolderChainInCloud for callers that want to upsert a
150
+ * folder explicitly (e.g., chokidar `addDir` events).
151
+ *
152
+ * Returns:
153
+ * { action: 'skipped', reason: 'shared-scope' }
154
+ * { action: 'ensured', folderId }
155
+ */
156
+ export declare function pushFolderToCloud({ filesRef, scope, teamId, userId, docPath, now, }: PushFolderInput): Promise<PushFolderOutcome>;
157
+ /**
158
+ * Delete the folder doc at `docPath` AND every descendant (files and
159
+ * sub-folders) in the same scope. Cascade is authoritative: it does
160
+ * not rely on per-child `unlink` events to clean up children first.
161
+ *
162
+ * Pre-DOU-223 this function deleted only the folder doc, on the
163
+ * (wrong) assumption that chokidar would emit `unlink` for every
164
+ * child before the `unlinkDir` for the folder. macOS FSEvents
165
+ * coalescing and burst-event ordering broke that assumption,
166
+ * leaving orphan file docs that reconstituted the folder on the web
167
+ * via `getFoldersAndFiles` (which derives folder names from paths).
168
+ * The cascade closes the gap; if children's `unlink` events also
169
+ * fire in time, the cascade is a no-op for them.
170
+ *
171
+ * Storage blobs for descendant files are deleted before their
172
+ * Firestore docs so the Storage rule (which reads the doc to
173
+ * authorize) still passes.
174
+ *
175
+ * Returns:
176
+ * { action: 'skipped', reason: 'shared-scope' }
177
+ * { action: 'not-found' }
178
+ * { action: 'deleted', folderId, childCount }
179
+ */
180
+ export declare function deleteFolderFromCloud({ filesRef, storage, orgId, scope, teamId, userId, docPath, }: DeleteFolderInput): Promise<DeleteFolderOutcome>;
181
+ /**
182
+ * Push a file's content to the cloud: ensure the parent folder chain
183
+ * exists, upsert the Firestore metadata doc (with canonical fields
184
+ * including `type: 'file'`, `name`, `parentId`), and upload the blob.
185
+ *
186
+ * Args:
187
+ * - data: the file's content. String, Buffer, or Uint8Array.
188
+ * - mimeType: the file's mime type. Stored on the metadata doc and
189
+ * used as the blob's Content-Type. Optional; if absent
190
+ * for an existing file the existing mime is preserved,
191
+ * and for new files defaults to 'text/markdown' to keep
192
+ * legacy callers working.
193
+ *
194
+ * - localModifiedAt: ISO timestamp of the local file's last
195
+ * modification (mtime). When provided, a push that would
196
+ * overwrite a server copy whose `updatedAt` is strictly
197
+ * newer is skipped instead, so an older local file cannot
198
+ * clobber a newer remote edit (last-write-wins, symmetric
199
+ * with the firestore -> local pull guard). Omit it to
200
+ * force the push regardless of timestamps (legacy callers).
201
+ *
202
+ * Returns one of:
203
+ * { action: 'skipped', reason: 'shared-scope' }
204
+ * { action: 'noop', fileId }
205
+ * { action: 'skipped-stale', fileId } // server copy is newer
206
+ * { action: 'created', fileId, fileData, storageError? }
207
+ * { action: 'updated', fileId, storageError? }
208
+ */
209
+ export declare function pushFileToCloud({ filesRef, storage, orgId, userId, scope, teamId, docPath, data, mimeType, now, localModifiedAt, }: PushFileInput): Promise<PushFileToCloudOutcome>;
210
+ /**
211
+ * Delete a file from Firestore and the corresponding Storage blob.
212
+ *
213
+ * Returns one of:
214
+ * { action: 'skipped', reason: 'shared-scope' }
215
+ * { action: 'not-found' }
216
+ * { action: 'deleted', fileId }
217
+ *
218
+ * Blob deletion tolerates `storage/object-not-found`.
219
+ */
220
+ export declare function deleteFileFromCloud({ filesRef, storage, orgId, userId, scope, teamId, docPath, }: DeleteFileInput): Promise<DeleteFileOutcome>;
221
+ //# sourceMappingURL=files.d.ts.map