@indigoai-us/hq-cli 5.345.154 → 5.345.155

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.
@@ -1,7 +1,8 @@
1
1
  import { Command } from "commander";
2
2
  import { type CloudPlaceholder } from "@indigoai-us/hq-cloud";
3
3
  import { type SearchCollection } from "../lib/search-index/index.js";
4
- import { type LibraryAddResult } from "./library-add.js";
4
+ import { type CompaniesManifestMap } from "../lib/work-context/repo-remote.js";
5
+ import { type LibraryCompany, type LibraryAddResult } from "./library-add.js";
5
6
  /** Mirrors hq-pro's MAX_ASSET_SEARCH_TAGS / tag length bound. */
6
7
  export declare const MAX_LIBRARY_SEARCH_TAGS = 20;
7
8
  export interface LibrarySearchOptions {
@@ -68,5 +69,29 @@ export declare function searchLocalAssets(query: string, options: LibrarySearchO
68
69
  export declare function listLibraryAssets(hqRoot: string, directory: string, tags: readonly string[], env?: Pick<LocalSearchEnv, "readFile" | "listPlaceholders">): LibraryAssetHit[];
69
70
  export declare function formatLibraryResult(result: LibrarySearchResult): string;
70
71
  export declare function formatLibraryAddResult(result: LibraryAddResult): string;
72
+ /**
73
+ * The company for a library command run on an HQ_API_KEY, from a slug or a
74
+ * cmp_ uid. A key cannot read memberships (hq-pro serves no keyed
75
+ * /membership/me) and is bound to one company, which hq-pro checks on every
76
+ * keyed presign. So the slug ↔ uid mapping comes from the HQ tree's
77
+ * companies/manifest.yaml (`<slug>: { cloud_uid }`), as `hq secrets` does,
78
+ * and a key minted for another company is refused by the server.
79
+ * `folderSlug` is the companies/<slug>/ of an explicit `--to`.
80
+ */
81
+ export declare function libraryCompanyForApiKey(manifest: CompaniesManifestMap | null, ref: string, folderSlug?: string): LibraryCompany;
82
+ /**
83
+ * The company uid for `hq library link` on an HQ_API_KEY. A cmp_ uid passes
84
+ * through (hq-pro checks it against the key). A slug, from --company or from
85
+ * the placeholder's folder, maps through companies/manifest.yaml. With
86
+ * neither --company nor a placeholder, or with HQ_COMPANY_UID set and no
87
+ * --company, the existing key default applies (`fallback`). An explicit
88
+ * --company always wins: `--company <slug>` maps through the manifest even
89
+ * when HQ_COMPANY_UID is set, and the env uid is ignored.
90
+ */
91
+ export declare function libraryLinkCompanyForApiKey(ref: string | undefined, placeholderSlug: string | undefined, deps: {
92
+ manifest: () => CompaniesManifestMap | null;
93
+ envCompanyUid?: string;
94
+ fallback: () => Promise<string>;
95
+ }): Promise<string>;
71
96
  export declare function registerLibraryCommand(program: Command): void;
72
97
  //# sourceMappingURL=library.d.ts.map
@@ -1,7 +1,8 @@
1
1
  /**
2
- * `hq library search|add|tag|ls|fetch|describe|link` — the company media library.
2
+ * `hq library search|add|tag|ls|fetch|describe|link|rm` — the company media library.
3
3
  * `add` and `tag` live in ./library-add.ts; AI captions (`add` and `describe`)
4
- * live in ./library-caption.ts; `link` lives in ./library-link.ts; `fetch`
4
+ * live in ./library-caption.ts; `link` lives in ./library-link.ts; `rm` lives in
5
+ * ./library-remove.ts; `fetch`
5
6
  * wraps `hq storage fetch`.
6
7
  *
7
8
  * `hq library search <query> [--tag t] [--json]` — find library assets.
@@ -34,10 +35,12 @@ import { ASSET_COLLECTION_MASK, deriveCollections, runQmd, } from "../lib/search
34
35
  import { findHqRoot } from "../utils/manifest.js";
35
36
  import { networkTransportErrorCode } from "../utils/network-transport-error.js";
36
37
  import { resolveVaultCredentialForCapability } from "../utils/resolve-vault-credential.js";
37
- import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
38
+ import { getCompanyUid, looksLikeCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
39
+ import { readCompaniesManifestMap } from "../lib/work-context/repo-remote.js";
38
40
  import { defaultLibraryIo, resolveLibrarySyncScope, runLibraryAdd, runLibraryTag, } from "./library-add.js";
39
41
  import { captionAddedAsset, extractCaptionFrames, runLibraryDescribe, runLocalClaude } from "./library-caption.js";
40
42
  import { LIBRARY_LINK_DEFAULT_MINUTES, LIBRARY_LINK_MAX_MINUTES, runLibraryLink } from "./library-link.js";
43
+ import { registerLibraryRemoveCommand } from "./library-remove.js";
41
44
  /** Mirrors hq-pro's MAX_ASSET_SEARCH_TAGS / tag length bound. */
42
45
  export const MAX_LIBRARY_SEARCH_TAGS = 20;
43
46
  const MAX_TAG_LENGTH = 100;
@@ -335,17 +338,92 @@ async function downloadPresigned(url) {
335
338
  throw new Error(`Could not download the asset (HTTP ${res.status}).`);
336
339
  return Readable.fromWeb(res.body);
337
340
  }
338
- /** The active company membership for `slug`, via /membership/me. */
339
- async function libraryCompany(token, slug) {
341
+ /** The active company membership for `ref` (slug or cmp_ uid), via /membership/me. */
342
+ async function libraryCompany(token, ref) {
340
343
  const { companiesFromMemberships } = await import("./storage.js");
341
344
  const res = await vaultApiFetch({ token, path: "/membership/me" });
342
345
  if (!res.ok)
343
346
  throw new Error(`Could not load your companies (HTTP ${res.status}). Run \`hq login\`.`);
344
- const company = companiesFromMemberships(await res.json()).find((c) => c.slug === slug);
347
+ const company = companiesFromMemberships(await res.json()).find((c) => c.slug === ref || c.uid === ref);
345
348
  if (!company)
346
- throw new Error(`You are not an active member of '${slug}'.`);
349
+ throw new Error(`You are not an active member of '${ref}'.`);
347
350
  return { uid: company.uid, slug: company.slug };
348
351
  }
352
+ /**
353
+ * The company for a library command run on an HQ_API_KEY, from a slug or a
354
+ * cmp_ uid. A key cannot read memberships (hq-pro serves no keyed
355
+ * /membership/me) and is bound to one company, which hq-pro checks on every
356
+ * keyed presign. So the slug ↔ uid mapping comes from the HQ tree's
357
+ * companies/manifest.yaml (`<slug>: { cloud_uid }`), as `hq secrets` does,
358
+ * and a key minted for another company is refused by the server.
359
+ * `folderSlug` is the companies/<slug>/ of an explicit `--to`.
360
+ */
361
+ export function libraryCompanyForApiKey(manifest, ref, folderSlug) {
362
+ const uidFor = (slug) => {
363
+ const uid = manifest?.[slug]?.cloud_uid;
364
+ return typeof uid === "string" && looksLikeCompanyUid(uid) ? uid : undefined;
365
+ };
366
+ // A missing or unparsable manifest is its own problem: say so rather than
367
+ // report a missing cloud_uid. A uid with an explicit --to folder needs no
368
+ // lookup, so it still goes through.
369
+ if (manifest === null && (!looksLikeCompanyUid(ref) || folderSlug === undefined)) {
370
+ throw new Error(`HQ_API_KEY is set and companies/manifest.yaml could not be read (missing or not valid YAML), ` +
371
+ `so '${ref}' cannot be matched to a company. Fix the manifest, or pass --company <cmp_uid> ` +
372
+ "(for library add, with --to companies/<slug>/<dir>).");
373
+ }
374
+ if (!looksLikeCompanyUid(ref)) {
375
+ const uid = uidFor(ref);
376
+ if (!uid) {
377
+ throw new Error(`HQ_API_KEY is set and companies/manifest.yaml has no cloud_uid for '${ref}'. ` +
378
+ `Pass --company <cmp_uid> (for library add, with --to companies/${ref}/<dir>).`);
379
+ }
380
+ return { uid, slug: ref };
381
+ }
382
+ if (folderSlug !== undefined) {
383
+ const known = uidFor(folderSlug);
384
+ if (known !== undefined && known !== ref) {
385
+ throw new Error(`--to is inside companies/${folderSlug}/, which is ${known}, but --company is ${ref}.`);
386
+ }
387
+ return { uid: ref, slug: folderSlug };
388
+ }
389
+ const slugs = Object.keys(manifest ?? {}).filter((slug) => !looksLikeCompanyUid(slug) && uidFor(slug) === ref);
390
+ if (slugs.length !== 1) {
391
+ throw new Error(`HQ_API_KEY is set and companies/manifest.yaml ${slugs.length === 0 ? "names no company folder" : `names ${slugs.length} company folders`} for ${ref}. Pass --company <slug> or --to companies/<slug>/<dir>.`);
392
+ }
393
+ return { uid: ref, slug: slugs[0] };
394
+ }
395
+ /**
396
+ * The company uid for `hq library link` on an HQ_API_KEY. A cmp_ uid passes
397
+ * through (hq-pro checks it against the key). A slug, from --company or from
398
+ * the placeholder's folder, maps through companies/manifest.yaml. With
399
+ * neither --company nor a placeholder, or with HQ_COMPANY_UID set and no
400
+ * --company, the existing key default applies (`fallback`). An explicit
401
+ * --company always wins: `--company <slug>` maps through the manifest even
402
+ * when HQ_COMPANY_UID is set, and the env uid is ignored.
403
+ */
404
+ export async function libraryLinkCompanyForApiKey(ref, placeholderSlug, deps) {
405
+ if (ref !== undefined && looksLikeCompanyUid(ref))
406
+ return ref;
407
+ const slug = ref ?? (deps.envCompanyUid ? undefined : placeholderSlug);
408
+ if (slug === undefined)
409
+ return deps.fallback();
410
+ return libraryCompanyForApiKey(deps.manifest(), slug).uid;
411
+ }
412
+ function manifestOrNull() {
413
+ try {
414
+ return readCompaniesManifestMap(findHqRoot());
415
+ }
416
+ catch {
417
+ return null;
418
+ }
419
+ }
420
+ /** Company resolver for `add` and `describe`: memberships on a session, the manifest on a key. */
421
+ function libraryCompanyResolver(credential, hqRoot) {
422
+ if (credential.kind === "api-key") {
423
+ return async (ref, folderSlug) => libraryCompanyForApiKey(readCompaniesManifestMap(hqRoot), ref, folderSlug);
424
+ }
425
+ return (ref) => libraryCompany(credential.token, ref);
426
+ }
349
427
  export function registerLibraryCommand(program) {
350
428
  const library = program
351
429
  .command("library")
@@ -392,7 +470,7 @@ export function registerLibraryCommand(program) {
392
470
  .option("--tags <tags>", "Tags (comma-separated, repeatable)", collectTag, [])
393
471
  .option("--title <title>", "Asset title (default: the file name)")
394
472
  .option("--note <text>", "Asset description")
395
- .option("--company <slug>", "Company whose vault receives the files")
473
+ .option("--company <company>", "Company slug or uid (cmp_…) whose vault receives the files")
396
474
  .option("--no-caption", "Skip the AI description and suggested tags for images and videos")
397
475
  .option("--json", "Print machine-readable JSON")
398
476
  .action(async (sources, options) => {
@@ -411,7 +489,7 @@ export function registerLibraryCommand(program) {
411
489
  }, {
412
490
  hqRoot,
413
491
  now: () => new Date(),
414
- resolveCompany: (slug) => libraryCompany(token, slug),
492
+ resolveCompany: libraryCompanyResolver(credential, hqRoot),
415
493
  presign: (body) => vaultApiFetch({ token, path: "/v1/files/presign", method: "POST", body }),
416
494
  ...defaultLibraryIo,
417
495
  ...(await librarySyncScopeDeps(credential, hqRoot)),
@@ -482,13 +560,15 @@ export function registerLibraryCommand(program) {
482
560
  .option("--json", "Print machine-readable JSON")
483
561
  .action(async (target, options) => {
484
562
  try {
485
- const { token } = await resolveVaultCredentialForCapability("files:read", "library describe", {
563
+ const credential = await resolveVaultCredentialForCapability("files:read", "library describe", {
486
564
  interactive: true,
487
565
  });
566
+ const { token } = credential;
567
+ const hqRoot = findHqRoot();
488
568
  const result = await runLibraryDescribe(target, {
489
- hqRoot: findHqRoot(),
569
+ hqRoot,
490
570
  now: () => new Date(),
491
- resolveCompany: (slug) => libraryCompany(token, slug),
571
+ resolveCompany: libraryCompanyResolver(credential, hqRoot),
492
572
  presign: (body) => vaultApiFetch({ token, path: "/v1/files/presign", method: "POST", body }),
493
573
  download: downloadPresigned,
494
574
  runLocal: runLocalClaude,
@@ -532,7 +612,7 @@ export function registerLibraryCommand(program) {
532
612
  .command("link")
533
613
  .description("Mint a short-lived download URL for a library asset (needs files:read)")
534
614
  .argument("<target>", "A .hqcloud placeholder, its original path, or a vault key (.hq-offload/…)")
535
- .option("--company <company>", "Company uid (cmp_…), or a slug on a session; default: the placeholder's company or the key's")
615
+ .option("--company <company>", "Company slug or uid (cmp_…); default: the placeholder's company or the key's")
536
616
  .option("--expires <minutes>", `Link lifetime in minutes, 1-${LIBRARY_LINK_MAX_MINUTES} (default ${LIBRARY_LINK_DEFAULT_MINUTES})`, (value) => Number(value))
537
617
  .option("--json", "Print machine-readable JSON")
538
618
  .action(async (target, options) => {
@@ -544,7 +624,13 @@ export function registerLibraryCommand(program) {
544
624
  const result = await runLibraryLink(target, { company: options.company, expiresMinutes: options.expires }, {
545
625
  hqRoot: () => findHqRoot(),
546
626
  apiKey: credential.kind === "api-key",
547
- resolveCompany: (ref) => getCompanyUid(token, ref),
627
+ resolveCompany: credential.kind === "api-key"
628
+ ? (ref, placeholderSlug) => libraryLinkCompanyForApiKey(ref, placeholderSlug, {
629
+ manifest: manifestOrNull,
630
+ envCompanyUid: process.env.HQ_COMPANY_UID?.trim() || undefined,
631
+ fallback: () => getCompanyUid(token, undefined),
632
+ })
633
+ : (ref) => getCompanyUid(token, ref),
548
634
  presign: (body) => vaultApiFetch({ token, path: "/v1/files/presign", method: "POST", body }),
549
635
  });
550
636
  // The URL is the only stdout line (or the JSON), so scripts can capture it.
@@ -557,5 +643,6 @@ export function registerLibraryCommand(program) {
557
643
  process.exitCode = 1;
558
644
  }
559
645
  });
646
+ registerLibraryRemoveCommand(library);
560
647
  }
561
648
  //# sourceMappingURL=library.js.map
@@ -4,6 +4,7 @@ import * as yaml from "js-yaml";
4
4
  import { assertApiKeyCompany, buildSecretUsage, extractApiMessage, highSecuritySandboxOnlyMessage, loadRevealedSecrets, normalizeScriptLockMode, normalizeSecretTier, parseSecretNameList, requestedApiKeyCompany, runSecretsEnvCommand, scopeOpts, secretsCredentialOptions, } from "../secrets-env-action.js";
5
5
  import { readSessionCompanyLock } from "../lib/session-company-lock.js";
6
6
  import { sessionIdFromEnv } from "../lib/core-utils/session-id.js";
7
+ import { appendPersonalSecretsAudit, isEntityScopeRef, PERSONAL_SCOPE_READ_ONLY_MESSAGE, } from "../lib/personal-secrets-audit.js";
7
8
  export { readSessionCompanyLock } from "../lib/session-company-lock.js";
8
9
  export { buildSecretUsage, emitEnvExportsAndExit, loadRevealedSecrets, parseSecretNameList, runSecretsEnvCommand, } from "../secrets-env-action.js";
9
10
  function companyUidForSlug(root, slug) {
@@ -53,7 +54,7 @@ async function resolveApiKeyCompanyWithinLock(input) {
53
54
  throw new Error("HQ_API_KEY is not bound to a company in this session's locked company set.");
54
55
  }
55
56
  import chalk from "chalk";
56
- import { ensureCognitoToken } from "../utils/cognito-session.js";
57
+ import { cognitoSubjectFromToken, ensureCognitoToken } from "../utils/cognito-session.js";
57
58
  /**
58
59
  * Exit code for `hq secrets exec` run with no session id and no explicit
59
60
  * company. 78 is EX_CONFIG from sysexits.h: the caller's invocation is missing
@@ -508,6 +509,35 @@ export function registerSecretsCommand(program) {
508
509
  .description("Manage secrets in HQ vault (SSM Parameter Store)")
509
510
  .option("--company <slug>", "Company slug (resolves to companyUid)")
510
511
  .option("--personal", "Operate on the caller's personal vault (no sharing)");
512
+ // In a session locked to a company, the caller's personal vault is read-only
513
+ // and reachable only through `secrets exec` (which audit-logs each use).
514
+ // Writes, sharing, listing and printing values stay refused.
515
+ secrets.hook("preAction", async (_parent, actionCommand) => {
516
+ if (actionCommand.parent === secrets && actionCommand.name() === "exec")
517
+ return;
518
+ const parentOpts = secrets.opts();
519
+ const actionOpts = actionCommand.opts();
520
+ const companyRef = actionOpts.company ?? parentOpts.company ?? process.env.HQ_COMPANY_UID?.trim();
521
+ // --company prs_…/agt_… names a personal vault: same read-only rule.
522
+ const personal = Boolean(parentOpts.personal) || Boolean(actionOpts.personal)
523
+ || (companyRef !== undefined && isEntityScopeRef(companyRef));
524
+ if (!personal || !sessionIdFromEnv(process.env))
525
+ return;
526
+ if (!(await fastFlagEnabledDefaultOn(MULTI_COMPANY_SESSION_LOCK_FLAG)))
527
+ return;
528
+ let lock;
529
+ try {
530
+ lock = readSessionCompanyLock(process.env);
531
+ }
532
+ catch (err) {
533
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
534
+ process.exit(1);
535
+ }
536
+ if (!lock || lock.companies.length === 0 || lock.companies.includes("personal"))
537
+ return;
538
+ console.error(chalk.red("Error:"), PERSONAL_SCOPE_READ_ONLY_MESSAGE);
539
+ process.exit(1);
540
+ });
511
541
  secrets
512
542
  .command("set <name>")
513
543
  .description("Create or update a secret")
@@ -1181,9 +1211,9 @@ export function registerSecretsCommand(program) {
1181
1211
  process.exit(1);
1182
1212
  }
1183
1213
  const keys = parseSecretNameList(_opts.only);
1184
- const parentScope = secrets.opts();
1214
+ const parentScope = { ...secrets.opts() };
1185
1215
  const inheritedCompany = parentScope.company;
1186
- const requestedCompany = _opts.company ?? inheritedCompany;
1216
+ let requestedCompany = _opts.company ?? inheritedCompany;
1187
1217
  // A launchd or cron job has no session id. Session resolution would then
1188
1218
  // fall back to workspace/sessions/.current, which names whichever
1189
1219
  // interactive session last ran a hook, and the job would read that
@@ -1200,6 +1230,17 @@ export function registerSecretsCommand(program) {
1200
1230
  const sessionLock = multiCompanyLockEnabled && hasCallerSession ? readSessionCompanyLock(process.env) : undefined;
1201
1231
  const hasLockedCompanies = (sessionLock?.companies.length ?? 0) > 0;
1202
1232
  const lockRoot = hasLockedCompanies ? resolveDefaultHqRoot({ env: process.env }) : undefined;
1233
+ // A prs_/agt_ value passed as --company names a personal vault. In a
1234
+ // locked session it gets exactly the --personal rules: one locked
1235
+ // company, exec only, the caller's own uid only, and the audit line.
1236
+ const entityScopeRef = hasLockedCompanies && requestedCompany && isEntityScopeRef(requestedCompany)
1237
+ ? requestedCompany
1238
+ : undefined;
1239
+ if (entityScopeRef) {
1240
+ requestedCompany = undefined;
1241
+ delete parentScope.company;
1242
+ parentScope.personal = true;
1243
+ }
1203
1244
  const selectedCompany = (hasLockedCompanies && requestedCompany && looksLikeCompanyUid(requestedCompany)
1204
1245
  ? companySlugForUid(lockRoot, sessionLock.companies, requestedCompany) ?? requestedCompany
1205
1246
  : requestedCompany)
@@ -1207,8 +1248,14 @@ export function registerSecretsCommand(program) {
1207
1248
  if (hasLockedCompanies && selectedCompany && !sessionLock.companies.includes(selectedCompany)) {
1208
1249
  throw new Error(`Company '${selectedCompany}' is outside this session's locked company set.`);
1209
1250
  }
1210
- if (hasLockedCompanies && parentScope.personal && !sessionLock.companies.includes("personal")) {
1211
- throw new Error("Personal scope is outside this session's locked company set.");
1251
+ // Owner decision dq_20261020234444_f1b1ea4185: the caller's own personal
1252
+ // vault may be read next to exactly one locked company, through exec
1253
+ // only, and every use is audit-logged below.
1254
+ const personalNextToCompany = hasLockedCompanies
1255
+ && Boolean(parentScope.personal)
1256
+ && !sessionLock.companies.includes("personal");
1257
+ if (personalNextToCompany && sessionLock.companies.length !== 1) {
1258
+ throw new Error(`Personal scope can be read next to exactly one locked company; this session locks ${sessionLock.companies.length}.`);
1212
1259
  }
1213
1260
  if (hasLockedCompanies && !selectedCompany && !parentScope.personal) {
1214
1261
  throw new Error("This session has no locked company scope for secrets exec.");
@@ -1220,6 +1267,9 @@ export function registerSecretsCommand(program) {
1220
1267
  ? scopeOpts({ personal: true })
1221
1268
  : scopeOpts({ ...parentScope, ...(selectedCompany ? { company: selectedCompany } : {}) });
1222
1269
  const cred = await resolveVaultCredentialForCapability("secrets:read", "secrets exec", secretsCredentialOptions());
1270
+ if ((personalNextToCompany || entityScopeRef) && cred.kind !== "cognito") {
1271
+ throw new Error("Personal scope in a company-locked session needs your own HQ sign-in, not an API key.");
1272
+ }
1223
1273
  const apiKeyCompany = cred.kind === "api-key" && hasLockedCompanies
1224
1274
  ? await resolveApiKeyCompanyWithinLock({
1225
1275
  token: cred.token,
@@ -1240,6 +1290,23 @@ export function registerSecretsCommand(program) {
1240
1290
  ? await assertApiKeyCompany(cred.token, unlockedApiKeyCompanyRef, "secrets exec")
1241
1291
  : "__api_key__"))
1242
1292
  : await getEntityUid(cred.token, secretScope, { allowOfflineScopeFallback: true });
1293
+ if (entityScopeRef && companyUid !== entityScopeRef) {
1294
+ throw new Error("Another person's personal vault is outside this session's scope; refusing.");
1295
+ }
1296
+ if (personalNextToCompany) {
1297
+ // Only the caller's own scope: getEntityUid({ personal }) resolves the
1298
+ // token's own entity. Refuse anything that is not a person/agent-self
1299
+ // uid, or that disagrees with the token's entity claim.
1300
+ const claimedUid = cognitoSubjectFromToken(cred.token).entityUid;
1301
+ if (!/^(prs|agt)_/.test(companyUid) || (claimedUid && claimedUid !== companyUid)) {
1302
+ throw new Error("Personal scope did not resolve to your own vault; refusing.");
1303
+ }
1304
+ appendPersonalSecretsAudit(lockRoot, {
1305
+ session_id: sessionLock.sessionId,
1306
+ locked_company: sessionLock.companies[0],
1307
+ secret_names: keys,
1308
+ });
1309
+ }
1243
1310
  const readSecrets = async () => loadRevealedSecrets(cred.token, companyUid, keys, cred.kind === "cognito"
1244
1311
  ? await buildSecretUsage("exec", _opts.script, _opts.scriptId)
1245
1312
  : undefined, { allowOfflineFallback: true });
@@ -23,7 +23,10 @@
23
23
  * recorded in the sync journal as synced and cloud-only, then the
24
24
  * placeholder is written, then the local file is removed. A path the
25
25
  * sync ignore rules exclude is refused and the excluding rule is named
26
- * (for `.hqinclude`, with the exact line to add). Other unsynced
26
+ * (for `.hqinclude`, with the exact line to add). A library original
27
+ * restored by `hq library fetch`, which the journal does not know, is
28
+ * pointed back at its verified blob `.hq-offload/blobs/<sha256>` in the
29
+ * company vault (`point-at-library`), uploading nothing. Other unsynced
27
30
  * company files are skipped with "run hq sync first".
28
31
  * - Any other file points at its personal-vault sync object the same
29
32
  * way, or is uploaded to the personal vault's `.hq-offload/blobs/`.
@@ -65,6 +68,12 @@ export declare const RUN_SYNC_FIRST = "run hq sync first";
65
68
  export declare const SYNC_MAX_FILE_BYTES: number;
66
69
  /** Object metadata key holding the sha256 of a company-vault offload upload. */
67
70
  export declare const VAULT_SHA256_METADATA_KEY = "hq-sha256";
71
+ /**
72
+ * Object metadata key on library blobs (`hq library add`). hq-pro's put
73
+ * presign binds it to the `x-amz-checksum-sha256` it signs, and S3 checks that
74
+ * checksum against the bytes, so it is the stored bytes' sha256.
75
+ */
76
+ export declare const CONTENT_SHA256_METADATA_KEY = "hq-content-sha256";
68
77
  /** Top-level HQ-root folder that holds offloaded blobs in the personal vault. */
69
78
  export declare const OFFLOAD_TOP_LEVEL = ".hq-offload";
70
79
  /**
@@ -197,11 +206,13 @@ export interface VaultObjectStore {
197
206
  /**
198
207
  * Size and unquoted ETag of `key`, or null when it is not there. `sha256` is
199
208
  * the object's `hq-sha256` metadata, set on company-vault offload uploads.
209
+ * `contentSha256` is its `hq-content-sha256` metadata, set on library blobs.
200
210
  */
201
211
  head(key: string): Promise<{
202
212
  size: number;
203
213
  etag: string;
204
214
  sha256?: string;
215
+ contentSha256?: string;
205
216
  } | null>;
206
217
  /** Download `key` to `dest` only if its ETag is `etag`; returns the served ETag. */
207
218
  download(key: string, dest: string, etag: string): Promise<string>;
@@ -403,10 +414,17 @@ export interface CurrentFilePlan {
403
414
  /**
404
415
  * upload+offload: a company file over the sync limit, uploaded to the
405
416
  * company vault at `vault_key`, then replaced by a placeholder.
417
+ * point-at-library: a restored library original whose verified blob is
418
+ * already in the company vault at `vault_key`; nothing is uploaded.
406
419
  */
407
- action: "point-at-vault" | "upload" | "upload+offload" | "skip";
408
- /** Company vault key for upload+offload. */
420
+ action: "point-at-vault" | "upload" | "upload+offload" | "point-at-library" | "skip";
421
+ /** Company vault key for upload+offload and point-at-library. */
409
422
  vault_key?: string;
423
+ /**
424
+ * point-at-library only: "kept" when the library placeholder already sits
425
+ * beside the file (it is left as is), "written" when offload writes it.
426
+ */
427
+ placeholder?: "kept" | "written";
410
428
  /** The HQ sync ignore layer that excludes this path. */
411
429
  excluded_by?: string;
412
430
  reason?: string;
@@ -416,7 +434,7 @@ export interface CurrentOffloaded {
416
434
  bytes: number;
417
435
  vault: "personal" | "company";
418
436
  company?: string;
419
- action: "point-at-vault" | "upload" | "upload+offload";
437
+ action: "point-at-vault" | "upload" | "upload+offload" | "point-at-library";
420
438
  }
421
439
  export interface OffloadOptions {
422
440
  minBytes: number;