@indigoai-us/hq-cli 5.345.137 → 5.345.139
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/CHANGELOG.md +12 -0
- package/dist/commands/storage-offload.d.ts +154 -42
- package/dist/commands/storage-offload.js +469 -111
- package/dist/commands/storage.d.ts +12 -1
- package/dist/commands/storage.js +97 -17
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +577 -441
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/package.json +2 -2
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
import { Command } from "commander";
|
|
37
37
|
import { type ReclaimReport } from "./storage-core.js";
|
|
38
|
-
import { ListObjectVersionsCommand, DeleteObjectsCommand, type ListObjectVersionsCommandOutput, type DeleteObjectsCommandOutput } from "@aws-sdk/client-s3";
|
|
38
|
+
import { S3Client, ListObjectVersionsCommand, DeleteObjectsCommand, type ListObjectVersionsCommandOutput, type DeleteObjectsCommandOutput } from "@aws-sdk/client-s3";
|
|
39
39
|
import { type OffloadResult, type OffloadStatus } from "./storage-offload.js";
|
|
40
40
|
export type TrancheId = "7d" | "30d" | "90d" | "365d" | "older";
|
|
41
41
|
export declare const TRANCHES: readonly {
|
|
@@ -265,6 +265,17 @@ export declare const NEEDS_YES = "No terminal to confirm in. Re-run with --yes t
|
|
|
265
265
|
export declare function confirmOnTty(message: string, input?: NodeJS.ReadableStream & {
|
|
266
266
|
isTTY?: boolean;
|
|
267
267
|
}, output?: NodeJS.WritableStream): Promise<boolean>;
|
|
268
|
+
/**
|
|
269
|
+
* An S3 client for one vault whose credentials are re-vended before they
|
|
270
|
+
* expire, plus `revend`, which vends now and returns a client on the new
|
|
271
|
+
* credentials (the SDK caches credentials per client, so a forced refresh
|
|
272
|
+
* needs a new client).
|
|
273
|
+
*/
|
|
274
|
+
export declare function vendedS3(vend: () => Promise<VendedCredentials>, bucket: string, makeClient?: (credentials: ReturnType<typeof refreshingVendedCredentials>) => S3Client): {
|
|
275
|
+
bucket: string;
|
|
276
|
+
client: S3Client;
|
|
277
|
+
revend: () => Promise<S3Client>;
|
|
278
|
+
};
|
|
268
279
|
/** STS credentials from one vend, with the time they stop working. */
|
|
269
280
|
export interface VendedCredentials {
|
|
270
281
|
accessKeyId: string;
|
package/dist/commands/storage.js
CHANGED
|
@@ -48,7 +48,7 @@ import { vaultApiFetch } from "../utils/vault-api.js";
|
|
|
48
48
|
import { findHqRoot } from "../utils/manifest.js";
|
|
49
49
|
import { maxLength } from "../utils/table-width.js";
|
|
50
50
|
import { resolveCanonicalPersonUid } from "./cloud.js";
|
|
51
|
-
import { DEFAULT_IDLE_DAYS, currentAvailability, defaultTmpDir, getOffloadStatus, gitExportBlob, gitListPlaceholders,
|
|
51
|
+
import { DEFAULT_IDLE_DAYS, currentAvailability, defaultTmpDir, getOffloadStatus, gitExportBlob, gitListPlaceholders, credentialRetryingClient, resolveMinBytes, runFetch, runFetchGitBlob, runOffload, s3BlobStore, s3VaultObjectStore, syncExcludesOffload, } from "./storage-offload.js";
|
|
52
52
|
export const TRANCHES = [
|
|
53
53
|
{ id: "7d", label: "Last 7 days", maxAgeDays: 7 },
|
|
54
54
|
{ id: "30d", label: "8-30 days", maxAgeDays: 30 },
|
|
@@ -838,12 +838,16 @@ export function formatOffload(r) {
|
|
|
838
838
|
const CURRENT_ACTION_LABEL = {
|
|
839
839
|
"point-at-vault": "point at vault",
|
|
840
840
|
upload: "upload",
|
|
841
|
+
"upload+offload": "upload+offload",
|
|
841
842
|
skip: "skip",
|
|
842
843
|
};
|
|
843
844
|
/** One `--current` plan line: path, size, owning vault, sync state, action. */
|
|
844
845
|
function formatCurrentFile(f) {
|
|
845
846
|
const vault = f.vault === "company" ? `company ${f.company ?? "?"}` : "personal vault";
|
|
846
847
|
const parts = [f.path, formatBytes(f.bytes), vault, f.synced ? "synced" : "not synced", CURRENT_ACTION_LABEL[f.action]];
|
|
848
|
+
if (f.action === "upload+offload") {
|
|
849
|
+
parts.push(`(${f.bytes} bytes to the company ${f.company ?? "?"} vault at ${f.vault_key ?? f.path})`);
|
|
850
|
+
}
|
|
847
851
|
if (f.action === "skip" && f.reason)
|
|
848
852
|
parts.push(`(${f.reason})`);
|
|
849
853
|
return parts.join(" ");
|
|
@@ -1014,8 +1018,14 @@ async function buildOffloadDeps() {
|
|
|
1014
1018
|
}
|
|
1015
1019
|
if (!entity.bucketName)
|
|
1016
1020
|
throw new Error("Your personal vault has no storage yet. Run `hq onboard`.");
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1021
|
+
const uid = entity.uid;
|
|
1022
|
+
// A run can outlast one STS session, so every client re-vends before the
|
|
1023
|
+
// credentials expire, and once more when S3 refuses them anyway.
|
|
1024
|
+
return vendedS3(async () => {
|
|
1025
|
+
const { vault } = await signedInVault();
|
|
1026
|
+
const vend = await vault.sts.vendSelf({ personUid: uid });
|
|
1027
|
+
return { ...vend.credentials, expiresAt: vend.expiresAt };
|
|
1028
|
+
}, entity.bucketName);
|
|
1019
1029
|
};
|
|
1020
1030
|
const activeCompanies = async () => {
|
|
1021
1031
|
const { token } = await signedInVault();
|
|
@@ -1026,6 +1036,30 @@ async function buildOffloadDeps() {
|
|
|
1026
1036
|
return companiesFromMemberships(await res.json());
|
|
1027
1037
|
};
|
|
1028
1038
|
const journals = new Map();
|
|
1039
|
+
const journalSlug = (target) => target.vault === "company" ? target.slug : hqCloud.PERSONAL_VAULT_JOURNAL_SLUG;
|
|
1040
|
+
const companyVault = async (slug) => {
|
|
1041
|
+
const company = (await activeCompanies()).find((c) => c.slug === slug);
|
|
1042
|
+
if (!company)
|
|
1043
|
+
throw new Error(`You are not an active member of '${slug}'.`);
|
|
1044
|
+
const { vault } = await signedInVault();
|
|
1045
|
+
const entity = await vault.entity.get(company.uid);
|
|
1046
|
+
const bucket = entity.bucketName ?? `hq-vault-cmp-${company.slug}`;
|
|
1047
|
+
const s3 = vendedS3(async () => {
|
|
1048
|
+
const { vault } = await signedInVault();
|
|
1049
|
+
const vend = await vault.sts.vend({ companyUid: company.uid });
|
|
1050
|
+
return { ...vend.credentials, expiresAt: vend.expiresAt };
|
|
1051
|
+
}, bucket);
|
|
1052
|
+
return { company, ...s3 };
|
|
1053
|
+
};
|
|
1054
|
+
// Company upload, the ignore explainer and the cloud-only journal row ship
|
|
1055
|
+
// in a newer @indigoai-us/hq-cloud. Until the bundled one exports them,
|
|
1056
|
+
// company files over the sync limit report "update HQ sync".
|
|
1057
|
+
const cloud = hqCloud;
|
|
1058
|
+
const explainIgnore = cloud.createIgnoreExplainer;
|
|
1059
|
+
const uploadLarge = cloud.uploadLargeObjectVerified;
|
|
1060
|
+
const multipartClient = cloud.s3MultipartClient;
|
|
1061
|
+
const recordOffloaded = cloud.recordOffloadedObject;
|
|
1062
|
+
let explainer;
|
|
1029
1063
|
return {
|
|
1030
1064
|
hqRoot,
|
|
1031
1065
|
now: () => new Date(),
|
|
@@ -1037,8 +1071,8 @@ async function buildOffloadDeps() {
|
|
|
1037
1071
|
tmpDir: defaultTmpDir,
|
|
1038
1072
|
syncExcludesOffload: () => syncExcludesOffload(PERSONAL_VAULT_EXCLUDED_TOP_LEVEL),
|
|
1039
1073
|
openPersonal: async () => {
|
|
1040
|
-
const { bucket,
|
|
1041
|
-
return
|
|
1074
|
+
const { bucket, client, revend } = await personalVault();
|
|
1075
|
+
return s3BlobStore(bucket, client, { where: "personal vault", revend });
|
|
1042
1076
|
},
|
|
1043
1077
|
activeCompanySlugs: async () => (await activeCompanies()).map((c) => c.slug),
|
|
1044
1078
|
// Current-file offload replaces a file with a placeholder only when the
|
|
@@ -1047,9 +1081,9 @@ async function buildOffloadDeps() {
|
|
|
1047
1081
|
syncHoldsPlaceholders: () => hqCloud.CLOUD_PLACEHOLDER_HOLDS_ANY_SIBLING === true,
|
|
1048
1082
|
// What HQ sync last stored for a file, read from its local journal.
|
|
1049
1083
|
// Company journal keys are relative to the company folder; personal keys
|
|
1050
|
-
// are relative to the HQ root.
|
|
1084
|
+
// are relative to the HQ root. The only journal write is recordCloudOnly.
|
|
1051
1085
|
syncRecord: async (target) => {
|
|
1052
|
-
const slug = target
|
|
1086
|
+
const slug = journalSlug(target);
|
|
1053
1087
|
let journal = journals.get(slug);
|
|
1054
1088
|
if (!journal) {
|
|
1055
1089
|
journal = hqCloud.readJournal(slug);
|
|
@@ -1064,17 +1098,63 @@ async function buildOffloadDeps() {
|
|
|
1064
1098
|
// come from the existing membership-scoped /sts/vend path.
|
|
1065
1099
|
openVaultObjects: async (target) => {
|
|
1066
1100
|
if (target.vault === "personal") {
|
|
1067
|
-
const { bucket,
|
|
1068
|
-
return s3VaultObjectStore(bucket,
|
|
1101
|
+
const { bucket, client, revend } = await personalVault();
|
|
1102
|
+
return s3VaultObjectStore(bucket, client, { where: "personal vault", revend });
|
|
1069
1103
|
}
|
|
1070
|
-
const company
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1104
|
+
const { company, bucket, client, revend } = await companyVault(target.slug);
|
|
1105
|
+
return s3VaultObjectStore(bucket, client, { where: `company vault '${company.slug}'`, revend });
|
|
1106
|
+
},
|
|
1107
|
+
syncMaxFileBytes: typeof cloud.SYNC_MAX_FILE_BYTES === "number" ? cloud.SYNC_MAX_FILE_BYTES : undefined,
|
|
1108
|
+
syncExclusion: explainIgnore
|
|
1109
|
+
? (rel) => {
|
|
1110
|
+
explainer ??= explainIgnore(hqRoot);
|
|
1111
|
+
return explainer(rel);
|
|
1112
|
+
}
|
|
1113
|
+
: undefined,
|
|
1114
|
+
// Multipart upload into the company vault, through the caller's
|
|
1115
|
+
// membership-scoped /sts/vend credentials (re-vended before they expire,
|
|
1116
|
+
// since a multi-GB upload can outlast one STS session).
|
|
1117
|
+
openCompanyUploader: uploadLarge && multipartClient && recordOffloaded
|
|
1118
|
+
? async (slug) => {
|
|
1119
|
+
const { company, bucket, client: s3, revend } = await companyVault(slug);
|
|
1120
|
+
if (bucket.startsWith("hq-vault-prs-")) {
|
|
1121
|
+
throw new Error(`'${bucket}' is a personal vault, not the company vault for '${slug}'.`);
|
|
1122
|
+
}
|
|
1123
|
+
// Each multipart request re-vends once if S3 still refuses the
|
|
1124
|
+
// credentials; a part refused twice leaves the upload resumable.
|
|
1125
|
+
const client = credentialRetryingClient(multipartClient(s3, bucket), `company vault '${company.slug}'`, async () => multipartClient(await revend(), bucket));
|
|
1126
|
+
return {
|
|
1127
|
+
slug: company.slug,
|
|
1128
|
+
bucket,
|
|
1129
|
+
upload: ({ key, file, stateFile }) => uploadLarge({ client, key, filePath: file, stateFile }),
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
: undefined,
|
|
1133
|
+
recordCloudOnly: recordOffloaded
|
|
1134
|
+
? async (target, object) => {
|
|
1135
|
+
const slug = journalSlug(target);
|
|
1136
|
+
recordOffloaded({ journalSlug: slug, key: target.key, ...object });
|
|
1137
|
+
// The cached journal is stale now; reread it on the next lookup.
|
|
1138
|
+
journals.delete(slug);
|
|
1139
|
+
}
|
|
1140
|
+
: undefined,
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* An S3 client for one vault whose credentials are re-vended before they
|
|
1145
|
+
* expire, plus `revend`, which vends now and returns a client on the new
|
|
1146
|
+
* credentials (the SDK caches credentials per client, so a forced refresh
|
|
1147
|
+
* needs a new client).
|
|
1148
|
+
*/
|
|
1149
|
+
export function vendedS3(vend, bucket, makeClient = (credentials) => new S3Client({ region: DEFAULT_COGNITO.region, credentials })) {
|
|
1150
|
+
return {
|
|
1151
|
+
bucket,
|
|
1152
|
+
client: makeClient(refreshingVendedCredentials(vend)),
|
|
1153
|
+
revend: async () => {
|
|
1154
|
+
const credentials = refreshingVendedCredentials(vend);
|
|
1155
|
+
// Vend now, so a failed vend is reported as one rather than as a refused request.
|
|
1156
|
+
await credentials();
|
|
1157
|
+
return makeClient(credentials);
|
|
1078
1158
|
},
|
|
1079
1159
|
};
|
|
1080
1160
|
}
|