@indigoai-us/hq-cloud 6.11.11 → 6.11.13
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/dist/bin/sync-runner-company.d.ts +35 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -0
- package/dist/bin/sync-runner-company.js +290 -0
- package/dist/bin/sync-runner-company.js.map +1 -0
- package/dist/bin/sync-runner-events.d.ts +12 -0
- package/dist/bin/sync-runner-events.d.ts.map +1 -0
- package/dist/bin/sync-runner-events.js +12 -0
- package/dist/bin/sync-runner-events.js.map +1 -0
- package/dist/bin/sync-runner-planning.d.ts +53 -0
- package/dist/bin/sync-runner-planning.d.ts.map +1 -0
- package/dist/bin/sync-runner-planning.js +59 -0
- package/dist/bin/sync-runner-planning.js.map +1 -0
- package/dist/bin/sync-runner-rollup.d.ts +24 -0
- package/dist/bin/sync-runner-rollup.d.ts.map +1 -0
- package/dist/bin/sync-runner-rollup.js +46 -0
- package/dist/bin/sync-runner-rollup.js.map +1 -0
- package/dist/bin/sync-runner-telemetry.d.ts +5 -0
- package/dist/bin/sync-runner-telemetry.d.ts.map +1 -0
- package/dist/bin/sync-runner-telemetry.js +5 -0
- package/dist/bin/sync-runner-telemetry.js.map +1 -0
- package/dist/bin/sync-runner-watch-loop.d.ts +17 -0
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -0
- package/dist/bin/sync-runner-watch-loop.js +372 -0
- package/dist/bin/sync-runner-watch-loop.js.map +1 -0
- package/dist/bin/sync-runner-watch-routes.d.ts +25 -0
- package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -0
- package/dist/bin/sync-runner-watch-routes.js +74 -0
- package/dist/bin/sync-runner-watch-routes.js.map +1 -0
- package/dist/bin/sync-runner.d.ts +5 -54
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +76 -978
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +265 -11
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/reindex.d.ts.map +1 -1
- package/dist/cli/reindex.js +34 -17
- package/dist/cli/reindex.js.map +1 -1
- package/dist/cli/reindex.test.js +39 -5
- package/dist/cli/reindex.test.js.map +1 -1
- package/dist/cli/rescue-classify-ordering.test.js +75 -0
- package/dist/cli/rescue-classify-ordering.test.js.map +1 -1
- package/dist/cli/rescue-core.d.ts +45 -0
- package/dist/cli/rescue-core.d.ts.map +1 -1
- package/dist/cli/rescue-core.js +320 -170
- package/dist/cli/rescue-core.js.map +1 -1
- package/dist/cli/share.d.ts +2 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +276 -660
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +30 -0
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts +28 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +541 -748
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +382 -1
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/cognito-auth.d.ts.map +1 -1
- package/dist/cognito-auth.js +55 -10
- package/dist/cognito-auth.js.map +1 -1
- package/dist/cognito-auth.test.js +61 -0
- package/dist/cognito-auth.test.js.map +1 -1
- package/dist/daemon-worker.d.ts +2 -2
- package/dist/daemon-worker.js +3 -3
- package/dist/daemon-worker.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +93 -6
- package/dist/journal.js.map +1 -1
- package/dist/journal.test.js +59 -0
- package/dist/journal.test.js.map +1 -1
- package/dist/machine-auth.test.js +60 -2
- package/dist/machine-auth.test.js.map +1 -1
- package/dist/object-io.d.ts +37 -1
- package/dist/object-io.d.ts.map +1 -1
- package/dist/object-io.js +149 -30
- package/dist/object-io.js.map +1 -1
- package/dist/object-io.test.js +121 -0
- package/dist/object-io.test.js.map +1 -1
- package/dist/operation-lock.d.ts +8 -8
- package/dist/operation-lock.d.ts.map +1 -1
- package/dist/operation-lock.js +99 -32
- package/dist/operation-lock.js.map +1 -1
- package/dist/operation-lock.test.js +51 -4
- package/dist/operation-lock.test.js.map +1 -1
- package/dist/personal-vault.d.ts.map +1 -1
- package/dist/personal-vault.js +8 -2
- package/dist/personal-vault.js.map +1 -1
- package/dist/personal-vault.test.js +34 -0
- package/dist/personal-vault.test.js.map +1 -1
- package/dist/prefix-coalesce.d.ts +20 -9
- package/dist/prefix-coalesce.d.ts.map +1 -1
- package/dist/prefix-coalesce.js +124 -28
- package/dist/prefix-coalesce.js.map +1 -1
- package/dist/prefix-coalesce.test.js +57 -2
- package/dist/prefix-coalesce.test.js.map +1 -1
- package/dist/remote-pull.d.ts +8 -3
- package/dist/remote-pull.d.ts.map +1 -1
- package/dist/remote-pull.js +85 -16
- package/dist/remote-pull.js.map +1 -1
- package/dist/remote-pull.test.js +213 -2
- package/dist/remote-pull.test.js.map +1 -1
- package/dist/s3.d.ts +2 -0
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +197 -116
- package/dist/s3.js.map +1 -1
- package/dist/s3.test.js +109 -0
- package/dist/s3.test.js.map +1 -1
- package/dist/scope-shrink.d.ts +3 -2
- package/dist/scope-shrink.d.ts.map +1 -1
- package/dist/scope-shrink.js +1 -1
- package/dist/scope-shrink.js.map +1 -1
- package/dist/skill-telemetry.d.ts +1 -1
- package/dist/skill-telemetry.d.ts.map +1 -1
- package/dist/skill-telemetry.js +69 -9
- package/dist/skill-telemetry.js.map +1 -1
- package/dist/skill-telemetry.test.js +86 -0
- package/dist/skill-telemetry.test.js.map +1 -1
- package/dist/sync/event-sync.d.ts +6 -0
- package/dist/sync/event-sync.d.ts.map +1 -1
- package/dist/sync/event-sync.js +34 -1
- package/dist/sync/event-sync.js.map +1 -1
- package/dist/sync/event-sync.test.js +73 -0
- package/dist/sync/event-sync.test.js.map +1 -1
- package/dist/sync/metrics.d.ts +17 -1
- package/dist/sync/metrics.d.ts.map +1 -1
- package/dist/sync/metrics.js +32 -1
- package/dist/sync/metrics.js.map +1 -1
- package/dist/sync/metrics.test.js +74 -1
- package/dist/sync/metrics.test.js.map +1 -1
- package/dist/sync/pull-scope.d.ts.map +1 -1
- package/dist/sync/pull-scope.js +15 -7
- package/dist/sync/pull-scope.js.map +1 -1
- package/dist/sync/push-receiver.d.ts +12 -5
- package/dist/sync/push-receiver.d.ts.map +1 -1
- package/dist/sync/push-receiver.js +45 -17
- package/dist/sync/push-receiver.js.map +1 -1
- package/dist/sync/push-receiver.test.js +67 -1
- package/dist/sync/push-receiver.test.js.map +1 -1
- package/dist/sync-core.d.ts +27 -0
- package/dist/sync-core.d.ts.map +1 -0
- package/dist/sync-core.js +54 -0
- package/dist/sync-core.js.map +1 -0
- package/dist/telemetry.d.ts +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +59 -6
- package/dist/telemetry.js.map +1 -1
- package/dist/telemetry.test.js +74 -0
- package/dist/telemetry.test.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vault-client.d.ts.map +1 -1
- package/dist/vault-client.js +284 -36
- package/dist/vault-client.js.map +1 -1
- package/dist/vault-client.test.js +59 -0
- package/dist/vault-client.test.js.map +1 -1
- package/dist/watcher.d.ts +38 -20
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +155 -143
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +103 -0
- package/dist/watcher.test.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner-company.ts +350 -0
- package/src/bin/sync-runner-events.ts +25 -0
- package/src/bin/sync-runner-planning.ts +121 -0
- package/src/bin/sync-runner-rollup.ts +72 -0
- package/src/bin/sync-runner-telemetry.ts +8 -0
- package/src/bin/sync-runner-watch-loop.ts +443 -0
- package/src/bin/sync-runner-watch-routes.ts +86 -0
- package/src/bin/sync-runner.test.ts +298 -11
- package/src/bin/sync-runner.ts +99 -1054
- package/src/cli/reindex.test.ts +41 -3
- package/src/cli/reindex.ts +35 -19
- package/src/cli/rescue-classify-ordering.test.ts +81 -0
- package/src/cli/rescue-core.ts +400 -165
- package/src/cli/share.test.ts +38 -0
- package/src/cli/share.ts +420 -693
- package/src/cli/sync.test.ts +460 -1
- package/src/cli/sync.ts +788 -825
- package/src/cognito-auth.test.ts +77 -0
- package/src/cognito-auth.ts +73 -11
- package/src/daemon-worker.ts +3 -3
- package/src/index.ts +8 -0
- package/src/journal.test.ts +72 -0
- package/src/journal.ts +95 -8
- package/src/machine-auth.test.ts +64 -2
- package/src/object-io.test.ts +142 -0
- package/src/object-io.ts +183 -31
- package/src/operation-lock.test.ts +63 -4
- package/src/operation-lock.ts +99 -31
- package/src/personal-vault.test.ts +42 -0
- package/src/personal-vault.ts +8 -2
- package/src/prefix-coalesce.test.ts +71 -1
- package/src/prefix-coalesce.ts +155 -30
- package/src/remote-pull.test.ts +235 -1
- package/src/remote-pull.ts +106 -18
- package/src/s3.test.ts +126 -0
- package/src/s3.ts +237 -122
- package/src/scope-shrink.ts +6 -3
- package/src/skill-telemetry.test.ts +109 -0
- package/src/skill-telemetry.ts +82 -14
- package/src/sync/event-sync.test.ts +75 -0
- package/src/sync/event-sync.ts +54 -1
- package/src/sync/metrics.test.ts +81 -0
- package/src/sync/metrics.ts +59 -4
- package/src/sync/pull-scope.ts +23 -7
- package/src/sync/push-receiver.test.ts +73 -1
- package/src/sync/push-receiver.ts +56 -20
- package/src/sync-core.ts +58 -0
- package/src/telemetry.test.ts +85 -0
- package/src/telemetry.ts +69 -6
- package/src/types.ts +8 -0
- package/src/vault-client.test.ts +74 -0
- package/src/vault-client.ts +395 -43
- package/src/watcher.test.ts +117 -0
- package/src/watcher.ts +215 -174
package/src/remote-pull.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* sync-runner.ts trivial: list S3 → call `decideRemotePulls` → drive S3 +
|
|
9
9
|
* filesystem from the result.
|
|
10
10
|
*
|
|
11
|
-
* Pairs with
|
|
11
|
+
* Pairs with the TreeWatcher push path — together they implement the
|
|
12
12
|
* bidirectional auto-sync the Settings toggle exposes.
|
|
13
13
|
*/
|
|
14
14
|
import type { RemoteFile } from "./s3.js";
|
|
@@ -29,7 +29,13 @@ import type {
|
|
|
29
29
|
ExplicitGrant,
|
|
30
30
|
MembershipSyncConfig,
|
|
31
31
|
} from "./vault-client.js";
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
coalescePrefixes,
|
|
34
|
+
isCoveredByAny,
|
|
35
|
+
pathToScopePrefix,
|
|
36
|
+
toScopePrefixEntries,
|
|
37
|
+
type ScopePrefixInput,
|
|
38
|
+
} from "./prefix-coalesce.js";
|
|
33
39
|
import {
|
|
34
40
|
applyScopeShrink,
|
|
35
41
|
buildScopeShrinkPlan,
|
|
@@ -73,12 +79,18 @@ export interface DecideRemotePullsInput {
|
|
|
73
79
|
* conflict resolution can't be silently overwritten.
|
|
74
80
|
*/
|
|
75
81
|
conflictKeys: Set<string>;
|
|
82
|
+
/**
|
|
83
|
+
* Journal keys intentionally retained by scope-shrink authorship guards
|
|
84
|
+
* even though they are outside the current remote listing scope.
|
|
85
|
+
*/
|
|
86
|
+
protectedMissingKeys?: Set<string>;
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
export function decideRemotePulls({
|
|
79
90
|
remoteFiles,
|
|
80
91
|
journal,
|
|
81
92
|
conflictKeys,
|
|
93
|
+
protectedMissingKeys = new Set<string>(),
|
|
82
94
|
}: DecideRemotePullsInput): RemotePullDecision {
|
|
83
95
|
const download: RemoteFile[] = [];
|
|
84
96
|
const skip: SkippedKey[] = [];
|
|
@@ -111,6 +123,11 @@ export function decideRemotePulls({
|
|
|
111
123
|
// Tombstone pass: anything in the journal that's no longer remote.
|
|
112
124
|
for (const relativePath of Object.keys(journal.files)) {
|
|
113
125
|
if (seenRemote.has(relativePath)) continue;
|
|
126
|
+
if (journal.files[relativePath]?.removedAt) continue;
|
|
127
|
+
if (protectedMissingKeys.has(relativePath)) {
|
|
128
|
+
skip.push({ key: relativePath });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
114
131
|
if (conflictKeys.has(relativePath)) {
|
|
115
132
|
// Remote tombstone for a file the user is conflict-resolving locally.
|
|
116
133
|
// Skip — record so callers can log/report, but do NOT delete.
|
|
@@ -143,7 +160,7 @@ export const VEND_PATH_CAP = 10;
|
|
|
143
160
|
*/
|
|
144
161
|
export const POST_FILTER_THRESHOLD = 50;
|
|
145
162
|
|
|
146
|
-
/** Bounded parallelism for vend fan-out (5 concurrent
|
|
163
|
+
/** Bounded parallelism for vend fan-out (5 concurrent vends/list paginators). */
|
|
147
164
|
export const VEND_FANOUT_CONCURRENCY = 5;
|
|
148
165
|
|
|
149
166
|
/**
|
|
@@ -204,12 +221,12 @@ export function resolveCompanyScope(
|
|
|
204
221
|
};
|
|
205
222
|
}
|
|
206
223
|
|
|
207
|
-
let raw:
|
|
224
|
+
let raw: ScopePrefixInput[];
|
|
208
225
|
if (syncConfig.syncMode === "custom") {
|
|
209
|
-
raw = syncConfig.customPaths ?? [];
|
|
226
|
+
raw = (syncConfig.customPaths ?? []).map(pathToScopePrefix);
|
|
210
227
|
} else {
|
|
211
228
|
// 'shared'
|
|
212
|
-
raw = (explicitGrants ?? []).map((g) => g.path);
|
|
229
|
+
raw = (explicitGrants ?? []).map((g) => pathToScopePrefix(g.path));
|
|
213
230
|
}
|
|
214
231
|
const prefixSet = coalescePrefixes(raw);
|
|
215
232
|
|
|
@@ -239,9 +256,10 @@ export function batchPrefixesForVend(
|
|
|
239
256
|
cap: number = VEND_PATH_CAP,
|
|
240
257
|
): string[][] {
|
|
241
258
|
if (cap <= 0) throw new Error(`batchPrefixesForVend: cap must be > 0`);
|
|
259
|
+
const vendPrefixes = toScopePrefixEntries(prefixes).map((entry) => entry.prefix);
|
|
242
260
|
const batches: string[][] = [];
|
|
243
|
-
for (let i = 0; i <
|
|
244
|
-
batches.push(
|
|
261
|
+
for (let i = 0; i < vendPrefixes.length; i += cap) {
|
|
262
|
+
batches.push(vendPrefixes.slice(i, i + cap));
|
|
245
263
|
}
|
|
246
264
|
return batches;
|
|
247
265
|
}
|
|
@@ -273,6 +291,27 @@ async function mapWithConcurrency<T, R>(
|
|
|
273
291
|
return results;
|
|
274
292
|
}
|
|
275
293
|
|
|
294
|
+
function createConcurrencyLimiter(
|
|
295
|
+
concurrency: number,
|
|
296
|
+
): <R>(fn: () => Promise<R>) => Promise<R> {
|
|
297
|
+
const limit = Math.max(1, concurrency);
|
|
298
|
+
let active = 0;
|
|
299
|
+
const waiters: Array<() => void> = [];
|
|
300
|
+
|
|
301
|
+
return async function runLimited<R>(fn: () => Promise<R>): Promise<R> {
|
|
302
|
+
if (active >= limit) {
|
|
303
|
+
await new Promise<void>((resolve) => waiters.push(resolve));
|
|
304
|
+
}
|
|
305
|
+
active += 1;
|
|
306
|
+
try {
|
|
307
|
+
return await fn();
|
|
308
|
+
} finally {
|
|
309
|
+
active -= 1;
|
|
310
|
+
waiters.shift()?.();
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
276
315
|
export interface ListRemoteForScopeInput {
|
|
277
316
|
ctx: EntityContext;
|
|
278
317
|
scope: CompanyScope;
|
|
@@ -320,14 +359,16 @@ export async function listRemoteForScope(
|
|
|
320
359
|
|
|
321
360
|
if (scope.strategy === "broad-postfilter") {
|
|
322
361
|
const all = await list(ctx);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
);
|
|
362
|
+
const scopeEntries = toScopePrefixEntries(scope.prefixSet);
|
|
363
|
+
return all.filter((f) => isCoveredByAny(f.key, scopeEntries));
|
|
326
364
|
}
|
|
327
365
|
|
|
328
366
|
// vend-fanout
|
|
329
367
|
if (scope.prefixSet.length === 0) return [];
|
|
330
|
-
const
|
|
368
|
+
const scopeEntries = toScopePrefixEntries(scope.prefixSet);
|
|
369
|
+
const listPrefixes = scopeEntries.map((entry) => entry.prefix);
|
|
370
|
+
const batches = batchPrefixesForVend(listPrefixes);
|
|
371
|
+
const listWithLimit = createConcurrencyLimiter(VEND_FANOUT_CONCURRENCY);
|
|
331
372
|
const perBatch = await mapWithConcurrency(
|
|
332
373
|
batches,
|
|
333
374
|
VEND_FANOUT_CONCURRENCY,
|
|
@@ -338,12 +379,17 @@ export async function listRemoteForScope(
|
|
|
338
379
|
// For a coalesced batch we issue one ListObjectsV2 per prefix in the
|
|
339
380
|
// batch. We can't issue one ListObjectsV2 across N prefixes (the API
|
|
340
381
|
// takes a single Prefix); the per-batch grouping exists for the STS
|
|
341
|
-
// session policy ceiling, not the list call itself.
|
|
342
|
-
|
|
382
|
+
// session policy ceiling, not the list call itself. The shared limiter
|
|
383
|
+
// keeps the total active prefix paginators bounded across all batches.
|
|
384
|
+
const lists = await Promise.all(
|
|
385
|
+
paths.map((p) => listWithLimit(() => list(batchCtx, p))),
|
|
386
|
+
);
|
|
343
387
|
return lists.flat();
|
|
344
388
|
},
|
|
345
389
|
);
|
|
346
|
-
return dedupByKey(
|
|
390
|
+
return dedupByKey(
|
|
391
|
+
perBatch.flat().filter((f) => isCoveredByAny(f.key, scopeEntries)),
|
|
392
|
+
);
|
|
347
393
|
}
|
|
348
394
|
|
|
349
395
|
function dedupByKey(files: RemoteFile[]): RemoteFile[] {
|
|
@@ -434,11 +480,12 @@ export async function pullCompany(
|
|
|
434
480
|
// `all` -> `shared` flip this correctly flags shared-mode orphans.
|
|
435
481
|
[companyPrefixOf(input.scope, last)];
|
|
436
482
|
|
|
483
|
+
const currentPrefixSet = input.scope.prefixSet;
|
|
437
484
|
const scopeShrinkPlan = buildScopeShrinkPlan({
|
|
438
485
|
journal: input.journal,
|
|
439
486
|
hqRoot: input.hqRoot,
|
|
440
487
|
lastPrefixSet,
|
|
441
|
-
currentPrefixSet
|
|
488
|
+
currentPrefixSet,
|
|
442
489
|
callerSub: input.callerSub,
|
|
443
490
|
// Background runner pull: protect the caller's own work and don't make a
|
|
444
491
|
// destructive guess about unknown-author (legacy) orphans. The explicit
|
|
@@ -479,6 +526,13 @@ export async function pullCompany(
|
|
|
479
526
|
remoteFiles,
|
|
480
527
|
journal: input.journal,
|
|
481
528
|
conflictKeys,
|
|
529
|
+
protectedMissingKeys: collectScopeProtectedMissingKeys({
|
|
530
|
+
journal: input.journal,
|
|
531
|
+
lastPrefixSet,
|
|
532
|
+
currentPrefixSet,
|
|
533
|
+
callerSub: input.callerSub,
|
|
534
|
+
protectUnknownAuthors: true,
|
|
535
|
+
}),
|
|
482
536
|
});
|
|
483
537
|
|
|
484
538
|
const completedAt = now().toISOString();
|
|
@@ -509,6 +563,39 @@ export async function pullCompany(
|
|
|
509
563
|
};
|
|
510
564
|
}
|
|
511
565
|
|
|
566
|
+
function collectScopeProtectedMissingKeys(input: {
|
|
567
|
+
journal: SyncJournal;
|
|
568
|
+
lastPrefixSet: readonly ScopePrefixInput[];
|
|
569
|
+
currentPrefixSet: readonly ScopePrefixInput[];
|
|
570
|
+
callerSub?: string;
|
|
571
|
+
protectUnknownAuthors: boolean;
|
|
572
|
+
}): Set<string> {
|
|
573
|
+
const protectedKeys = new Set<string>();
|
|
574
|
+
for (const [relPath, entry] of Object.entries(input.journal.files)) {
|
|
575
|
+
if (entry.removedAt) continue;
|
|
576
|
+
if (entry.direction !== "down") continue;
|
|
577
|
+
if (isCoveredByAny(relPath, input.currentPrefixSet)) {
|
|
578
|
+
if (entry.outOfScopeProtected) delete entry.outOfScopeProtected;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (entry.outOfScopeProtected) {
|
|
582
|
+
protectedKeys.add(relPath);
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
if (!isCoveredByAny(relPath, input.lastPrefixSet)) continue;
|
|
586
|
+
if (input.callerSub && entry.createdBySub === input.callerSub) {
|
|
587
|
+
entry.outOfScopeProtected = true;
|
|
588
|
+
protectedKeys.add(relPath);
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
if (input.protectUnknownAuthors && entry.createdBySub === undefined) {
|
|
592
|
+
entry.outOfScopeProtected = true;
|
|
593
|
+
protectedKeys.add(relPath);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return protectedKeys;
|
|
597
|
+
}
|
|
598
|
+
|
|
512
599
|
/**
|
|
513
600
|
* Recover the "company prefix" for a v1-migrated record with no recorded
|
|
514
601
|
* `prefixSet`. We derive it from the current scope's first prefix's parent
|
|
@@ -521,10 +608,11 @@ function companyPrefixOf(
|
|
|
521
608
|
_last: PullRecord | undefined,
|
|
522
609
|
): string {
|
|
523
610
|
// For `all` mode, scope.prefixSet[0] IS the company prefix.
|
|
524
|
-
|
|
611
|
+
const firstEntry = toScopePrefixEntries(scope.prefixSet)[0];
|
|
612
|
+
if (scope.strategy === "all" && firstEntry) return firstEntry.prefix;
|
|
525
613
|
// Otherwise, derive `companies/{slug}/` from the first prefix. ACL grant
|
|
526
614
|
// paths always start with `companies/{slug}/...`.
|
|
527
|
-
const first =
|
|
615
|
+
const first = firstEntry?.prefix ?? "";
|
|
528
616
|
const m = first.match(/^(companies\/[^/]+\/)/);
|
|
529
617
|
return m ? m[1]! : first;
|
|
530
618
|
}
|
package/src/s3.test.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
|
10
10
|
import * as fs from "fs";
|
|
11
11
|
import * as os from "os";
|
|
12
12
|
import * as path from "path";
|
|
13
|
+
import * as crypto from "crypto";
|
|
13
14
|
|
|
14
15
|
// Capture every command sent to the S3Client across the test suite. Cleared
|
|
15
16
|
// in beforeEach so per-test assertions don't leak from neighbours.
|
|
@@ -96,6 +97,7 @@ import {
|
|
|
96
97
|
import {
|
|
97
98
|
setObjectIOFactory,
|
|
98
99
|
presignObjectIOFactory,
|
|
100
|
+
type ObjectIO,
|
|
99
101
|
type PresignTransportClient,
|
|
100
102
|
} from "./object-io.js";
|
|
101
103
|
import type { PresignResultRow } from "./vault-client.js";
|
|
@@ -896,6 +898,130 @@ describe("downloadFile", () => {
|
|
|
896
898
|
expect(fs.readlinkSync(localPath)).toBe("fresh-target.md");
|
|
897
899
|
});
|
|
898
900
|
|
|
901
|
+
it("F11: failed symlink downloads preserve the previous local file", async () => {
|
|
902
|
+
const localPath = path.join(tmpRoot, "preserve-on-failure.md");
|
|
903
|
+
fs.writeFileSync(localPath, "existing local copy");
|
|
904
|
+
|
|
905
|
+
nextGetObjectResponse = {
|
|
906
|
+
Body: (async function* () {
|
|
907
|
+
yield new TextEncoder().encode(SYMLINK_BODY_PREFIX + "x".repeat(10_000));
|
|
908
|
+
})(),
|
|
909
|
+
Metadata: { "hq-symlink-target": SYMLINK_MARKER_META_VALUE },
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
await expect(
|
|
913
|
+
downloadFile(makeCtx(), "preserve-on-failure.md", localPath),
|
|
914
|
+
).rejects.toThrow();
|
|
915
|
+
|
|
916
|
+
expect(fs.existsSync(localPath)).toBe(true);
|
|
917
|
+
expect(fs.lstatSync(localPath).isSymbolicLink()).toBe(false);
|
|
918
|
+
expect(fs.readFileSync(localPath, "utf-8")).toBe("existing local copy");
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
it("R-F11: downloads to a near component-limit basename via a bounded temp name", async () => {
|
|
922
|
+
nextGetObjectResponse = {
|
|
923
|
+
Body: (async function* () {
|
|
924
|
+
yield Buffer.from("near-limit bytes");
|
|
925
|
+
})(),
|
|
926
|
+
Metadata: {},
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
const localPath = path.join(tmpRoot, "x".repeat(240));
|
|
930
|
+
await downloadFile(makeCtx(), "near-limit.bin", localPath);
|
|
931
|
+
|
|
932
|
+
expect(fs.readFileSync(localPath, "utf-8")).toBe("near-limit bytes");
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
it("F20: regular downloads do not decode the full body as UTF-8", async () => {
|
|
936
|
+
const body = Buffer.alloc(64 * 1024, 0x61);
|
|
937
|
+
const originalToString = body.toString;
|
|
938
|
+
body.toString = ((encoding?: BufferEncoding, start?: number, end?: number) => {
|
|
939
|
+
const sliceStart = start ?? 0;
|
|
940
|
+
const sliceEnd = end ?? body.length;
|
|
941
|
+
if (sliceEnd - sliceStart > SYMLINK_BODY_PREFIX.length) {
|
|
942
|
+
throw new Error("full-body UTF-8 decode attempted");
|
|
943
|
+
}
|
|
944
|
+
return originalToString.call(body, encoding, start, end);
|
|
945
|
+
}) as Buffer["toString"];
|
|
946
|
+
|
|
947
|
+
const io: ObjectIO = {
|
|
948
|
+
putObject: async () => ({ etag: '"unused"' }),
|
|
949
|
+
getObject: async () => ({ body, metadata: {} }),
|
|
950
|
+
listObjects: async () => ({ objects: [] }),
|
|
951
|
+
deleteObject: async () => undefined,
|
|
952
|
+
headObject: async () => null,
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
const localPath = path.join(tmpRoot, "large-regular.bin");
|
|
956
|
+
setObjectIOFactory(() => io);
|
|
957
|
+
try {
|
|
958
|
+
await downloadFile(makeCtx(), "large-regular.bin", localPath);
|
|
959
|
+
} finally {
|
|
960
|
+
setObjectIOFactory(null);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const written = fs.readFileSync(localPath);
|
|
964
|
+
expect(written.length).toBe(body.length);
|
|
965
|
+
expect(written[0]).toBe(0x61);
|
|
966
|
+
expect(written[written.length - 1]).toBe(0x61);
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
it("R-F20: streams regular downloads with hash metadata and still detects symlink records", async () => {
|
|
970
|
+
const regularChunks = [
|
|
971
|
+
Buffer.from("alpha-"),
|
|
972
|
+
Buffer.from("beta-"),
|
|
973
|
+
Buffer.from("gamma"),
|
|
974
|
+
];
|
|
975
|
+
const regularBody = Buffer.concat(regularChunks);
|
|
976
|
+
const linkTarget = "../target.md";
|
|
977
|
+
|
|
978
|
+
const io: ObjectIO = {
|
|
979
|
+
putObject: async () => ({ etag: '"unused"' }),
|
|
980
|
+
getObject: async () => {
|
|
981
|
+
throw new Error("buffered getObject attempted");
|
|
982
|
+
},
|
|
983
|
+
getObjectStream: async (key: string) => {
|
|
984
|
+
if (key === "streamed-link") {
|
|
985
|
+
return {
|
|
986
|
+
body: (async function* () {
|
|
987
|
+
yield Buffer.from("hq-");
|
|
988
|
+
yield Buffer.from("symlink:");
|
|
989
|
+
yield Buffer.from(linkTarget);
|
|
990
|
+
})(),
|
|
991
|
+
metadata: {},
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
return {
|
|
995
|
+
body: (async function* () {
|
|
996
|
+
for (const chunk of regularChunks) yield chunk;
|
|
997
|
+
})(),
|
|
998
|
+
metadata: {},
|
|
999
|
+
};
|
|
1000
|
+
},
|
|
1001
|
+
listObjects: async () => ({ objects: [] }),
|
|
1002
|
+
deleteObject: async () => undefined,
|
|
1003
|
+
headObject: async () => null,
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
setObjectIOFactory(() => io);
|
|
1007
|
+
try {
|
|
1008
|
+
const localPath = path.join(tmpRoot, "streamed.bin");
|
|
1009
|
+
const result = await downloadFile(makeCtx(), "streamed.bin", localPath);
|
|
1010
|
+
expect(fs.readFileSync(localPath)).toEqual(regularBody);
|
|
1011
|
+
expect(result.contentHash).toBe(
|
|
1012
|
+
crypto.createHash("sha256").update(regularBody).digest("hex"),
|
|
1013
|
+
);
|
|
1014
|
+
expect(result.contentSize).toBe(regularBody.length);
|
|
1015
|
+
|
|
1016
|
+
const linkPath = path.join(tmpRoot, "streamed-link");
|
|
1017
|
+
await downloadFile(makeCtx(), "streamed-link", linkPath);
|
|
1018
|
+
expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
|
|
1019
|
+
expect(fs.readlinkSync(linkPath)).toBe(linkTarget);
|
|
1020
|
+
} finally {
|
|
1021
|
+
setObjectIOFactory(null);
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
|
|
899
1025
|
it("applies hq-mode metadata via chmod after byte write (Bug #5 — preserve permissions)", async () => {
|
|
900
1026
|
// Round-trip pair to the s3.upload test: source-side mode lives in
|
|
901
1027
|
// \`Metadata['hq-mode']\` as an octal string; the receiver must chmod
|