@indigoai-us/hq-cloud 6.14.46 → 6.14.48
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/cli/share.d.ts +11 -3
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +214 -22
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +595 -28
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts +9 -5
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +16 -3
- package/dist/cli/sync.js.map +1 -1
- package/dist/ignore.d.ts +38 -0
- package/dist/ignore.d.ts.map +1 -1
- package/dist/ignore.js +55 -0
- package/dist/ignore.js.map +1 -1
- package/dist/ignore.test.js +50 -0
- package/dist/ignore.test.js.map +1 -1
- package/dist/journal.d.ts +56 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +49 -1
- package/dist/journal.js.map +1 -1
- package/dist/journal.test.js +37 -2
- package/dist/journal.test.js.map +1 -1
- package/dist/s3.test.js +6 -0
- package/dist/s3.test.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/share.test.ts +670 -28
- package/src/cli/share.ts +234 -34
- package/src/cli/sync.ts +29 -15
- package/src/ignore.test.ts +55 -0
- package/src/ignore.ts +57 -0
- package/src/journal.test.ts +44 -1
- package/src/journal.ts +69 -4
- package/src/s3.test.ts +6 -0
package/src/journal.test.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
readJournal,
|
|
17
17
|
writeJournal,
|
|
18
18
|
updateEntry,
|
|
19
|
+
PrematureJournalEntryError,
|
|
19
20
|
migrateToV2,
|
|
20
21
|
normalizeJournalKeys,
|
|
21
22
|
appendPullRecord,
|
|
@@ -208,12 +209,54 @@ describe("journal", () => {
|
|
|
208
209
|
describe("updateEntry", () => {
|
|
209
210
|
it("stamps lastSync and the per-file syncedAt", () => {
|
|
210
211
|
const j: SyncJournal = { version: "1", lastSync: "", files: {} };
|
|
211
|
-
|
|
212
|
+
const abs = path.join(stateDir, "foo.md");
|
|
213
|
+
fs.writeFileSync(abs, "x");
|
|
214
|
+
updateEntry(j, "foo.md", "hash", 10, "up", abs);
|
|
212
215
|
expect(j.files["foo.md"]?.hash).toBe("hash");
|
|
213
216
|
expect(j.files["foo.md"]?.direction).toBe("up");
|
|
214
217
|
expect(j.lastSync).not.toBe("");
|
|
215
218
|
expect(j.files["foo.md"]?.syncedAt).not.toBe("");
|
|
216
219
|
});
|
|
220
|
+
|
|
221
|
+
// Load-bearing once delete propagation authorizes on ETag currency alone:
|
|
222
|
+
// an entry whose file was never written is indistinguishable from "the
|
|
223
|
+
// user deleted this", and the next push would issue a remote DeleteObject
|
|
224
|
+
// for what was only premature bookkeeping.
|
|
225
|
+
it("refuses to journal a path that is not on disk", () => {
|
|
226
|
+
const j: SyncJournal = { version: "1", lastSync: "", files: {} };
|
|
227
|
+
const missing = path.join(stateDir, "never-written.md");
|
|
228
|
+
expect(() => updateEntry(j, "never-written.md", "hash", 10, "down", missing)).toThrow(
|
|
229
|
+
PrematureJournalEntryError,
|
|
230
|
+
);
|
|
231
|
+
expect(j.files).not.toHaveProperty("never-written.md");
|
|
232
|
+
expect(j.lastSync).toBe("");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("accepts a dangling symlink — the link itself is the synced object", () => {
|
|
236
|
+
const j: SyncJournal = { version: "1", lastSync: "", files: {} };
|
|
237
|
+
const link = path.join(stateDir, "dangling-link");
|
|
238
|
+
fs.symlinkSync(path.join(stateDir, "no-such-target"), link);
|
|
239
|
+
expect(() =>
|
|
240
|
+
updateEntry(j, "dangling-link", "hash", 0, "down", link, { kind: "symlink" }),
|
|
241
|
+
).not.toThrow();
|
|
242
|
+
expect(j.files["dangling-link"]?.kind).toBe("symlink");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Regression: `kind` and `createdBySub` were adjacent trailing optionals
|
|
246
|
+
// and two call sites passed kind into the createdBySub slot, leaving
|
|
247
|
+
// entry.kind unset. entry.kind gates delete-intent minting, so those
|
|
248
|
+
// entries could never be deleted. Named options make it unrepresentable.
|
|
249
|
+
it("records kind and createdBySub into their own fields", () => {
|
|
250
|
+
const j: SyncJournal = { version: "1", lastSync: "", files: {} };
|
|
251
|
+
const abs = path.join(stateDir, "typed.md");
|
|
252
|
+
fs.writeFileSync(abs, "x");
|
|
253
|
+
updateEntry(j, "typed.md", "hash", 1, "down", abs, {
|
|
254
|
+
kind: "file",
|
|
255
|
+
createdBySub: "sub-123",
|
|
256
|
+
});
|
|
257
|
+
expect(j.files["typed.md"]?.kind).toBe("file");
|
|
258
|
+
expect(j.files["typed.md"]?.createdBySub).toBe("sub-123");
|
|
259
|
+
});
|
|
217
260
|
});
|
|
218
261
|
|
|
219
262
|
// ── Journal v2 (US-005): pulls, tombstones, GC, migration ─────────────────
|
package/src/journal.ts
CHANGED
|
@@ -416,17 +416,82 @@ export function hashSymlinkTarget(target: string): string {
|
|
|
416
416
|
* via `hq files get` are pulled — `direction:"down"` — but ride the pin union
|
|
417
417
|
* in the caller's inclusion prefixSet, so they are likewise never pruned.)
|
|
418
418
|
*/
|
|
419
|
+
/**
|
|
420
|
+
* Thrown when a caller tries to journal a path that is not on disk.
|
|
421
|
+
*
|
|
422
|
+
* Deliberately loud rather than a silent skip: a caller reaching this has a
|
|
423
|
+
* real ordering bug, and swallowing it would reintroduce exactly the silent
|
|
424
|
+
* drift this guard exists to prevent.
|
|
425
|
+
*/
|
|
426
|
+
export class PrematureJournalEntryError extends Error {
|
|
427
|
+
constructor(readonly relativePath: string, readonly absolutePath: string) {
|
|
428
|
+
super(
|
|
429
|
+
`refusing to journal '${relativePath}': no file at '${absolutePath}'. ` +
|
|
430
|
+
`A journal entry must be written only AFTER its file is on disk.`,
|
|
431
|
+
);
|
|
432
|
+
this.name = "PrematureJournalEntryError";
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Optional fields of a journal entry.
|
|
438
|
+
*
|
|
439
|
+
* These are an OBJECT rather than trailing positional parameters because they
|
|
440
|
+
* were four adjacent optionals, two of them `string`, and callers drifted:
|
|
441
|
+
* two conflict-resolution sites in `cli/sync.ts` passed their `"file" |
|
|
442
|
+
* "symlink"` value into the `createdBySub` slot, so `entry.kind` was silently
|
|
443
|
+
* never set on any conflict-resolved entry. That is not cosmetic — `entry.kind`
|
|
444
|
+
* gates delete-intent minting (`markLocalDeleteIntent` requires it to match,
|
|
445
|
+
* and the watcher skips any entry without one), so a file that had ever been
|
|
446
|
+
* through conflict resolution could never be deleted, even with a live watcher.
|
|
447
|
+
* Named fields make that class of mistake unrepresentable.
|
|
448
|
+
*/
|
|
449
|
+
export interface UpdateEntryOptions {
|
|
450
|
+
remoteEtag?: string;
|
|
451
|
+
mtimeMs?: number;
|
|
452
|
+
/** Object's `created-by-sub` S3 metadata. Download path only. */
|
|
453
|
+
createdBySub?: string;
|
|
454
|
+
kind?: "file" | "symlink";
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Record a journal entry for a file that is CONFIRMED present on disk.
|
|
459
|
+
*
|
|
460
|
+
* `verifyAbsolutePath` is required, and the entry is written only after an
|
|
461
|
+
* `lstat` proves the file exists. This is a load-bearing invariant, not a
|
|
462
|
+
* defensive nicety: a journal entry says "this key was synced and the local
|
|
463
|
+
* copy is at this hash". Once delete propagation authorizes on ETag currency
|
|
464
|
+
* alone (no watcher-minted intent), an entry whose file was never actually
|
|
465
|
+
* written becomes indistinguishable from "the user deleted this file" — and
|
|
466
|
+
* the next push issues a remote DeleteObject for a file that was only ever
|
|
467
|
+
* premature bookkeeping.
|
|
468
|
+
*
|
|
469
|
+
* Every existing caller already had a `fs.lstatSync` in hand and passed values
|
|
470
|
+
* derived from it, so this makes an existing convention structural. The point
|
|
471
|
+
* is that a FUTURE caller cannot get it wrong: with the check inside this
|
|
472
|
+
* function there is no longer a way to add a call site that records an entry
|
|
473
|
+
* for a file that is not there.
|
|
474
|
+
*
|
|
475
|
+
* `lstat` (not `stat`) so a dangling symlink still counts as present — the
|
|
476
|
+
* link itself is the synced object, and its target may legitimately be absent.
|
|
477
|
+
*
|
|
478
|
+
* @throws PrematureJournalEntryError when nothing exists at `verifyAbsolutePath`.
|
|
479
|
+
*/
|
|
419
480
|
export function updateEntry(
|
|
420
481
|
journal: SyncJournal,
|
|
421
482
|
relativePath: string,
|
|
422
483
|
hash: string,
|
|
423
484
|
size: number,
|
|
424
485
|
direction: "up" | "down",
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
createdBySub?: string,
|
|
428
|
-
kind?: "file" | "symlink",
|
|
486
|
+
verifyAbsolutePath: string,
|
|
487
|
+
opts: UpdateEntryOptions = {},
|
|
429
488
|
): void {
|
|
489
|
+
try {
|
|
490
|
+
fs.lstatSync(verifyAbsolutePath);
|
|
491
|
+
} catch {
|
|
492
|
+
throw new PrematureJournalEntryError(relativePath, verifyAbsolutePath);
|
|
493
|
+
}
|
|
494
|
+
const { remoteEtag, mtimeMs, createdBySub, kind } = opts;
|
|
430
495
|
const entry: JournalEntry = {
|
|
431
496
|
hash,
|
|
432
497
|
size,
|
package/src/s3.test.ts
CHANGED
|
@@ -1679,6 +1679,12 @@ describe("validateVaultUploadKey — direct-S3 key poisoning guard (incident 202
|
|
|
1679
1679
|
["a/../b.md", "INVALID_KEY_DOT_COMPONENT"],
|
|
1680
1680
|
["a/./b.md", "INVALID_KEY_DOT_COMPONENT"],
|
|
1681
1681
|
["bad\x00key.md", "INVALID_KEY_CONTROL_CHARS"],
|
|
1682
|
+
// macOS Finder custom-icon file. Named explicitly because this is the
|
|
1683
|
+
// spelling that reaches the validator in the wild: Finder writes one into
|
|
1684
|
+
// every directory it renders a custom icon for, and the push walker now
|
|
1685
|
+
// excludes them precisely because this throw is unavoidable.
|
|
1686
|
+
["Icon\r", "INVALID_KEY_CONTROL_CHARS"],
|
|
1687
|
+
["docs/Icon\r", "INVALID_KEY_CONTROL_CHARS"],
|
|
1682
1688
|
])(
|
|
1683
1689
|
"validateVaultUploadKey mirrors the server rule set: %j → %s",
|
|
1684
1690
|
(key, code) => {
|