@indigoai-us/hq-cloud 5.33.0 → 5.34.0
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.d.ts +9 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +43 -9
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +69 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/share.d.ts +60 -4
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +103 -6
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +78 -0
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts +20 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +259 -6
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +469 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/ignore.d.ts.map +1 -1
- package/dist/ignore.js +7 -1
- package/dist/ignore.js.map +1 -1
- package/dist/ignore.test.js +19 -3
- package/dist/ignore.test.js.map +1 -1
- package/dist/s3.d.ts +21 -0
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +69 -2
- package/dist/s3.js.map +1 -1
- package/dist/s3.test.js +129 -2
- package/dist/s3.test.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner.test.ts +85 -0
- package/src/bin/sync-runner.ts +52 -9
- package/src/cli/share.test.ts +89 -0
- package/src/cli/share.ts +122 -6
- package/src/cli/sync.test.ts +529 -0
- package/src/cli/sync.ts +294 -7
- package/src/ignore.test.ts +20 -3
- package/src/ignore.ts +7 -1
- package/src/s3.test.ts +142 -2
- package/src/s3.ts +71 -2
package/src/s3.ts
CHANGED
|
@@ -160,6 +160,28 @@ export function decodeSymlinkMetadataValue(value: string): string {
|
|
|
160
160
|
*/
|
|
161
161
|
export const SYMLINK_BODY_PREFIX = "hq-symlink:";
|
|
162
162
|
|
|
163
|
+
/**
|
|
164
|
+
* S3 user-metadata key carrying the source-side file mode (permission bits
|
|
165
|
+
* only — \`mode & 0o777\`) as an octal string ("755", "640", etc.). On
|
|
166
|
+
* download, downloadFile parses this with \`parseInt(value, 8)\` and chmods
|
|
167
|
+
* the file to the exact source mode after the byte write.
|
|
168
|
+
*
|
|
169
|
+
* Bug #5 in the 5.33.0 deep-test was originally reported as "exec bit lost
|
|
170
|
+
* on sync" but the verification report broadened it: ALL modes (0600 / 0640
|
|
171
|
+
* / 0700 / 0750 / 0755) collapsed to the receiver's umask default (0644)
|
|
172
|
+
* because no mode signal crossed the wire at all. Stamping the mode in
|
|
173
|
+
* metadata is the smallest schema change that preserves the full
|
|
174
|
+
* permission bitfield without a per-host umask negotiation.
|
|
175
|
+
*
|
|
176
|
+
* Symlinks: skipped at upload time (symlink mode is OS-controlled
|
|
177
|
+
* lrwxrwxrwx) and skipped on download (\`fs.chmodSync\` follows symlinks
|
|
178
|
+
* and would mutate the target's mode instead).
|
|
179
|
+
*
|
|
180
|
+
* Back-compat: legacy uploads have no \`hq-mode\` header — the receiver
|
|
181
|
+
* leaves the umask default in place, matching pre-fix behavior.
|
|
182
|
+
*/
|
|
183
|
+
export const FILE_MODE_META_KEY = "hq-mode";
|
|
184
|
+
|
|
163
185
|
/**
|
|
164
186
|
* Encode/decode the symlink wire body. Kept as exported helpers so the
|
|
165
187
|
* format is centrally defined and tests can probe both sides without
|
|
@@ -178,6 +200,20 @@ export async function uploadFile(
|
|
|
178
200
|
const client = buildClient(ctx);
|
|
179
201
|
const body = fs.readFileSync(localPath);
|
|
180
202
|
|
|
203
|
+
// Capture source-side file mode (permission bits only) for Bug #5 — see
|
|
204
|
+
// FILE_MODE_META_KEY doc. Best-effort: lstat failure (raced rm, EPERM)
|
|
205
|
+
// falls through to "no mode header" and the receiver keeps its umask
|
|
206
|
+
// default — same as the legacy back-compat path.
|
|
207
|
+
let modeOctal: string | undefined;
|
|
208
|
+
try {
|
|
209
|
+
const lstat = fs.lstatSync(localPath);
|
|
210
|
+
if (!lstat.isSymbolicLink()) {
|
|
211
|
+
modeOctal = (lstat.mode & 0o777).toString(8);
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
// Leave modeOctal undefined; receiver applies its umask default.
|
|
215
|
+
}
|
|
216
|
+
|
|
181
217
|
// Preserve the original `created-at` across re-uploads when the object
|
|
182
218
|
// already exists with author metadata — same convention the hq-console
|
|
183
219
|
// upload route uses, so the NEW-pill ageing window doesn't reset on every
|
|
@@ -198,7 +234,10 @@ export async function uploadFile(
|
|
|
198
234
|
}
|
|
199
235
|
}
|
|
200
236
|
|
|
201
|
-
const Metadata
|
|
237
|
+
const Metadata: Record<string, string> = {
|
|
238
|
+
...(author ? buildAuthorMetadata(author, createdAt) : {}),
|
|
239
|
+
...(modeOctal ? { [FILE_MODE_META_KEY]: modeOctal } : {}),
|
|
240
|
+
};
|
|
202
241
|
|
|
203
242
|
const response = await client.send(
|
|
204
243
|
new PutObjectCommand({
|
|
@@ -206,7 +245,7 @@ export async function uploadFile(
|
|
|
206
245
|
Key: key,
|
|
207
246
|
Body: body,
|
|
208
247
|
ContentType: getMimeType(key),
|
|
209
|
-
...(
|
|
248
|
+
...(Object.keys(Metadata).length > 0 ? { Metadata } : {}),
|
|
210
249
|
}),
|
|
211
250
|
);
|
|
212
251
|
|
|
@@ -404,6 +443,36 @@ export async function downloadFile(
|
|
|
404
443
|
chunks.push(Buffer.from(chunk));
|
|
405
444
|
}
|
|
406
445
|
fs.writeFileSync(localPath, Buffer.concat(chunks));
|
|
446
|
+
|
|
447
|
+
// Bug #5 — apply source-side mode after the byte write. See
|
|
448
|
+
// FILE_MODE_META_KEY for the metadata contract. Parses defensively:
|
|
449
|
+
// a malformed value falls through with no chmod so the umask default
|
|
450
|
+
// applies, matching the legacy back-compat path. fs.chmodSync
|
|
451
|
+
// follows symlinks — that's fine here because we're on the regular-
|
|
452
|
+
// file branch (the symlink branch above already returned).
|
|
453
|
+
//
|
|
454
|
+
// Codex P2 (PR #24 round 3): strict octal-only regex BEFORE parseInt.
|
|
455
|
+
// parseInt(modeOctal, 8) accepts partial-prefix garbage — "755junk"
|
|
456
|
+
// parses to 0o755 instead of NaN — so tampered or malformed metadata
|
|
457
|
+
// could still change local permissions unexpectedly. The regex
|
|
458
|
+
// requires 1–4 pure octal digits (`[0-7]{1,4}$`), which matches what
|
|
459
|
+
// the upload side stamps (`(mode & 0o777).toString(8)` → at most
|
|
460
|
+
// three digits, all 0–7) and rejects everything else.
|
|
461
|
+
const modeOctal = response.Metadata?.[FILE_MODE_META_KEY];
|
|
462
|
+
if (typeof modeOctal === "string" && /^[0-7]{1,4}$/.test(modeOctal)) {
|
|
463
|
+
const parsed = parseInt(modeOctal, 8);
|
|
464
|
+
if (Number.isFinite(parsed) && parsed >= 0 && parsed <= 0o777) {
|
|
465
|
+
try {
|
|
466
|
+
fs.chmodSync(localPath, parsed);
|
|
467
|
+
} catch {
|
|
468
|
+
// chmod failure (read-only FS, EPERM) is non-fatal — the file
|
|
469
|
+
// is materialized, just with the umask default. Surface via
|
|
470
|
+
// S3-side metadata being present but the file not matching;
|
|
471
|
+
// a future operator-side audit can reconcile.
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
407
476
|
return { metadata: response.Metadata };
|
|
408
477
|
}
|
|
409
478
|
|