/** * HQ Cloud Sync Types */ export interface SyncConfig { bucket: string; region: string; userId: string; prefix: string; } export interface Credentials { accessKeyId: string; secretAccessKey: string; sessionToken?: string; expiration?: string; refreshToken: string; userId: string; bucket: string; region: string; teamId?: string; } export interface JournalEntry { hash: string; size: number; syncedAt: string; direction: "up" | "down"; /** Optional note supplied by the caller that shared this entry. */ message?: string; /** Kind of local object represented by this entry. */ kind?: "file" | "symlink"; /** * Cognito `sub` of the file's author, captured from the object's * `created-by-sub` S3 user-metadata at download time (zero extra network — * the GET response already carries it). Powers the scope-shrink authorship * guard: a scope shrink must never orphan content the caller authored, so * sync mode only ever governs whether you ALSO mirror *other people's* * files. Optional for backwards compatibility — entries written before this * field existed (or uploaded without author metadata) leave it `undefined`, * which the automatic prune path treats conservatively (never auto-delete an * unknown-author orphan). */ createdBySub?: string; /** * S3 ETag of the remote object as of last successful sync, normalized (no * surrounding quotes). Optional for backwards compatibility: entries * written before this field existed won't have it, in which case * conflict detection falls back to comparing remote `lastModified` * against `syncedAt`. */ remoteEtag?: string; /** * Local mtime (epoch ms) recorded with the last synced content hash. * * Half of the stat pair that lets a push skip a file without reading it. * `mtimeMs` alone is not content identity — `cp -p`, `touch -r` and * `rsync --times` all put an old mtime back on new bytes — which is why the * skip gate also requires `ctimeMs`. Absent on entries written before stat * tracking; such an entry is hashed once and then stamped. */ mtimeMs?: number; /** * Local ctime (epoch ms) recorded with the last synced content hash. * * The inode-change stamp, and the reason the size+mtime skip is safe. * Userspace can set mtime to any value it likes but cannot forge ctime: any * write, rename, chmod or utimes call moves it to now. So the same-length * rewrite with a restored mtime — the case that makes a naive size+mtime * check skip an edit forever — still lands here as a mismatch and is * re-hashed. Absent on entries written before stat tracking. */ ctimeMs?: number; /** * Tombstone marker (Journal v2, US-005). When set, this entry represents * a file that was pruned by a scope shrink — either implicitly (next pull * after the membership's effective scope narrowed) or explicitly (via * `hq sync narrow --apply`). Tombstones are kept for `TOMBSTONE_TTL_MS` * (30d) so subsequent pulls under the same shrunk scope don't re-flag * the same paths as orphans, then garbage-collected. */ removedAt?: string; removedReason?: "scope_shrink" | "narrow_apply" | "manual" | "local-delete"; /** * Explicit local-delete intent. Unlike the legacy `removedReason` marker, * this is bound to the exact synced remote revision and local object shape * that the user approved for deletion. */ localDeleteIntent?: { version: 1; remoteEtag: string; localHash: string; localKind: "file" | "symlink"; }; /** * Durable automatic-pull retention marker. Set when a scope shrink keeps an * out-of-scope caller-authored or unknown-author entry on disk instead of * pruning it. Subsequent pulls under the already-narrowed scope use this to * exclude the survivor from remote-missing local deletes; it is cleared if * the entry becomes in-scope again. */ outOfScopeProtected?: boolean; /** * The canonical skill upload landed but registry metadata reconciliation * still needs to complete. Retried only while this entry's hash still * matches disk, so an unuploaded edit cannot become metadata authority. */ skillMetadataPending?: boolean; /** * Journal-honesty marker. Set when a conflict was resolved by KEEPing a local * copy that diverges from the remote: the entry still records the remote * `remoteEtag` (so the conflict can't re-fire — the #137 invariant), but the * local bytes never matched that remote. The currency-gated delete planner * refuses to propagate a delete for such an entry — its currency would * falsely "match" on HEAD, and deleting would destroy the divergent remote * version. Cleared by any genuine download (which makes local == remote). */ localDiverges?: boolean; } /** * Per-pull boundary record (Journal v2, US-005). * * Recorded at the end of every per-company sync leg so the next pull can * detect "scope changed" against `prefixSet` and compute orphans * deterministically. `pulls[]` is a bounded tail: writers keep the most * recent `MAX_PULLS_PER_COMPANY` records per company and drop the oldest. */ export interface PullRecord { /** ULID-shaped id (crockford base32, lexically sortable). */ pullId: string; companyUid: string; startedAt: string; completedAt: string; /** Effective sync-mode at pull start. */ syncMode: "all" | "shared" | "custom"; /** * Coalesced prefixes used to drive ListObjectsV2 for this pull. Empty for * v1-migrated records (no recorded scope; treated as full-bucket "all"). */ prefixSet: string[]; scopeChangeDetected: boolean; orphansRemoved: number; orphansBlocked: number; } export interface SyncJournal { /** * Schema version. `"1"` is the pre-US-005 shape (no `pulls`, no * tombstone fields on entries). `"2"` adds per-pull records and * tombstone markers; readers MUST tolerate either and migrate v1 → v2 * in place on the first v2 write. */ version: "1" | "2"; lastSync: string; files: Record; /** * Per-pull boundary records (v2 only). Always present on v2 journals. * v1 journals do not carry this field — readers should treat absence as * "no recorded history; treat last scope as 'all'/empty". Writers cap the * list per company at `MAX_PULLS_PER_COMPANY`, keeping the newest records. */ pulls?: PullRecord[]; } /** * The legacy v2 journal payload once migrated into the v3 framed state store. * The journal shape deliberately stays v2-compatible while its durability and * recovery semantics are owned by the v3 snapshot/WAL container. */ export interface V3JournalState { journal: SyncJournal; } /** Payload of the journal's append-only v3 WAL record. */ export interface V3JournalWalPayload { journal: SyncJournal; } /** A bounded v3 WAL mutation; unchanged journal entries are not repeated. */ export interface V3JournalDeltaPayload { version: "2"; /** Omitted when this writer did not change the completion marker. */ lastSync?: string; upserts: Record; deletes: string[]; /** Omitted when this writer did not append or rewrite pull history. */ pulls?: PullRecord[]; } export interface SyncStatus { running: boolean; lastSync: string | null; fileCount: number; bucket: string | null; errors: string[]; } export interface PushResult { filesUploaded: number; bytesUploaded: number; } export interface PullResult { filesDownloaded: number; bytesDownloaded: number; } export interface DaemonState { pid: number; startedAt: string; hqRoot: string; } /** * Entity-aware context for vault-backed S3 operations (VLT-5). * Resolved from vault-service entity registry + STS vending. */ export interface EntityContext { /** Entity UID (cmp_*) */ uid: string; /** Entity slug (human-readable, stable key for per-company local state). */ slug: string; /** S3 bucket name for this entity */ bucketName: string; /** AWS region */ region: string; /** * STS-scoped credentials. * * Absent for presign-only COMPANY contexts (HQ-59): when a run uses the * presigned-URL transport for company vaults (`companyVaultUsesPresign`), * `resolveEntityContext` skips the `POST /sts/vend` call entirely — the * presign transport authorizes each object server-side and never reads these * creds, and a compliant sync-runner must NOT hit the company vend route * (the route-presence signal the hq-pro min-version gate keys on). The only * reader of these creds is the direct-S3 (`S3SdkObjectIO`) path, which is * used for personal vaults (`prs_*`, still vended) and pre-presign clients — * never for a `cmp_*` context built on the presign path. That path throws * loudly if it ever receives a credential-less context. */ credentials?: VaultCredentials; /** * When the credentials expire (ISO 8601). For a presign-only company context * (no STS vend) this is a far-future sentinel so `isExpiringSoon` is always * false and no auto-refresh ever re-vends the skipped company route. */ expiresAt: string; } export interface VaultCredentials { accessKeyId: string; secretAccessKey: string; sessionToken: string; } /** * Caller identification stamped on every request to hq-cloud-api so the server * can attribute traffic and gate on minimum versions. */ export interface ClientInfo { /** Package name, e.g. "@indigoai-us/hq-cli" */ name: string; /** Package version, e.g. "5.15.0" */ version: string; /** * `hqVersion` from `core/core.yaml` — set when the caller is running inside * an hq-core checkout. Lets the server see scaffold-generation skew. */ hqCoreVersion?: string; /** Arbitrary extra key/value pairs forwarded as `x-hq-client-` headers. */ extra?: Record; } /** * Configuration for connecting to the vault-service API. */ export interface VaultServiceConfig { /** Vault API base URL (e.g. https://vault-api.example.com) */ apiUrl: string; /** * Cognito JWT token for authentication. Either a static string OR an async * getter that returns the current token on every call. Long-running consumers * (e.g. `hq-sync-runner`'s multi-company fanout) MUST pass a getter — a * captured string can outlive the 60-min Cognito access token TTL, which * causes mid-flight `refreshEntityContext` calls to 401 against * API Gateway's JWT authorizer even though `~/.hq/cognito-tokens.json` was * refreshed under them by another process (e.g. the menubar). The getter * lets every request resolve the latest token via `getValidAccessToken`. */ authToken: string | (() => string | Promise); /** AWS region for S3 client (defaults to entity region or us-east-1) */ region?: string; /** * Identifies the calling package + version on every outbound request. * Optional for back-compat, but all first-party clients should pass it. */ clientInfo?: ClientInfo; /** * When true, COMPANY vaults (`cmp_*`) use the presigned-URL transport, so * `resolveEntityContext` SKIPS `POST /sts/vend` for them and returns a * credential-less context with a far-future `expiresAt` (HQ-59). Personal * vaults (`prs_*`) are unaffected — they always vend self via `/sts/vend-self`. * * This flag and the installed `ObjectIO` factory are two halves of one * decision and must always agree. `ensureCompanyVaultTransport` * (src/company-vault-transport.ts) is the single supported way to make that * decision: it installs the transport and sets this flag from the same value. * Every caller that pushes or pulls a company vault routes through it — the * `hq-sync-runner` AND the one-shot `share()` / `sync()` paths that hq-cli and * standalone `hq sync` drive. (Before those one-shot paths were wired through * it, a company push from hq-cli kept vending STS write creds and aborted on * the `POLICY_WRITE_SCOPE_TRUNCATED` 422 — Sentry hq-cli 7711917661.) * * Tri-state: `undefined` means "no transport decision has been made yet" — the * signal `ensureCompanyVaultTransport` acts on. `true` / `false` means a caller * already decided (the runner mutates it on the same object it hands down), and * the helper is then a strict no-op. A caller that vended its own credentials * out of band (AppBar's pre-vended `entityContext` flow) never sets this and * never installs a factory. */ companyVaultUsesPresign?: boolean; } export interface ConflictIndexEntry { id: string; originalPath: string; /** * hq-root-relative path of the preserved (losing) body. New rows point under * `.hq/conflict-backups/`; rows written by older engines point at a legacy * sibling twin (`.conflict--.`). Absent only on rows * whose loser could not be preserved — readers must tolerate that. */ conflictPath?: string; detectedAt: string; side: "push" | "pull"; machineId: string; localHash: string; remoteHash: string; /** * Which side's bytes the `conflictPath` sidecar actually holds. * * The version-aware winner adopts one body at the original name and parks * the other under `conflictPath`. Absent on entries written before this * field existed, which are all `"remote"` — readers must default accordingly. */ preserved?: "local" | "remote"; /** * How the live body was chosen when the engine decided automatically * (`pickWinner`): which side won and why (`version` = higher frontmatter * `version:`, `mtime` = newer modification time, `default` = nothing to * compare, local kept). Absent on rows written by older engines and on * holds where no side was adopted. */ decision?: { winner: "local" | "remote"; reason: "version" | "mtime" | "identical" | "default"; localVersion?: number; remoteVersion?: number; }; /** S3 VersionId when known (present for VersionId-aware buckets). */ remoteVersionId?: string; /** Last-known parent VersionId from journal, when known. */ lastKnownVersionId?: string | null; } export interface ConflictIndex { version: 1; conflicts: ConflictIndexEntry[]; } //# sourceMappingURL=types.d.ts.map