@indigoai-us/hq-cli 5.77.14 → 5.78.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.
Files changed (308) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/assets/scaffold/core/scripts/archive-old-threads.sh +86 -0
  3. package/assets/scaffold/core/scripts/backfill-company-skill-mirrors.sh +73 -0
  4. package/assets/scaffold/core/scripts/backfill-workspace-mirror.sh +51 -0
  5. package/assets/scaffold/core/scripts/codex-skill-bridge.sh +550 -0
  6. package/assets/scaffold/core/scripts/detect-stale-core-policy-mirror.sh +185 -0
  7. package/assets/scaffold/core/scripts/hq-status-summary.sh +187 -0
  8. package/assets/scaffold/core/scripts/ontology-readme-drift.sh +264 -0
  9. package/assets/scaffold/core/scripts/qmd-reindex-after-sync.sh +93 -0
  10. package/assets/scaffold/core/scripts/rebuild-all-indexes.sh +69 -0
  11. package/assets/scaffold/core/scripts/rebuild-companies-index.sh +206 -0
  12. package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +127 -0
  13. package/assets/scaffold/core/scripts/rebuild-orchestrator-index.sh +86 -0
  14. package/assets/scaffold/core/scripts/rebuild-projects-index.sh +105 -0
  15. package/assets/scaffold/core/scripts/rebuild-public-knowledge-index.sh +83 -0
  16. package/assets/scaffold/core/scripts/rebuild-reports-index.sh +114 -0
  17. package/assets/scaffold/core/scripts/rebuild-social-drafts-index.sh +135 -0
  18. package/assets/scaffold/core/scripts/rebuild-threads-index.sh +82 -0
  19. package/assets/scaffold/core/scripts/rebuild-workers-index.sh +97 -0
  20. package/assets/scaffold/core/scripts/resize-screenshot.sh +47 -0
  21. package/assets/scaffold/core/scripts/token-usage-report.sh +254 -0
  22. package/assets/scaffold/core/scripts/worktree.sh +136 -0
  23. package/dist/commands/agents.js +28 -12
  24. package/dist/commands/api-keys.js +100 -20
  25. package/dist/commands/core.d.ts +90 -0
  26. package/dist/commands/core.js +279 -0
  27. package/dist/commands/files-browse.d.ts +21 -0
  28. package/dist/commands/files-browse.js +145 -24
  29. package/dist/commands/files.js +33 -7
  30. package/dist/commands/members.js +16 -5
  31. package/dist/commands/pack-install.d.ts +14 -0
  32. package/dist/commands/pack-install.js +42 -2
  33. package/dist/commands/pkg-install.js +39 -2
  34. package/dist/commands/run.js +6 -2
  35. package/dist/commands/secrets.js +120 -65
  36. package/dist/main.js +6 -0
  37. package/dist/utils/api-key-command-gate.d.ts +45 -0
  38. package/dist/utils/api-key-command-gate.js +51 -0
  39. package/dist/utils/hq-roots.d.ts +91 -0
  40. package/dist/utils/hq-roots.js +212 -0
  41. package/dist/utils/resolve-vault-credential.d.ts +63 -3
  42. package/dist/utils/resolve-vault-credential.js +152 -6
  43. package/dist/utils/run-bundled-script.d.ts +66 -0
  44. package/dist/utils/run-bundled-script.js +80 -0
  45. package/dist/utils/vault-api.d.ts +21 -0
  46. package/dist/utils/vault-api.js +74 -3
  47. package/package.json +7 -1
  48. package/.claude/policies/hq-cli-interactive-reads-use-sts-vend.md +0 -49
  49. package/.github/workflows/ci.yml +0 -46
  50. package/.github/workflows/publish.yml +0 -36
  51. package/.github/workflows/scripts/smoke-test-pkg.sh +0 -97
  52. package/e2e/cli.test.ts +0 -128
  53. package/eslint.config.js +0 -37
  54. package/pnpm-workspace.yaml +0 -4
  55. package/scripts/chmod-bins.mjs +0 -49
  56. package/scripts/generate-dsn.mjs +0 -26
  57. package/scripts/generate-scan-packages-table.mjs +0 -113
  58. package/scripts/smoke-sources-signals.sh +0 -103
  59. package/src/bin/hq-auth-refresh.ts +0 -43
  60. package/src/cli-version.ts +0 -13
  61. package/src/commands/__fixtures__/make-tar.ts +0 -126
  62. package/src/commands/_patterns.ts +0 -10
  63. package/src/commands/add.ts +0 -75
  64. package/src/commands/agents.test.ts +0 -527
  65. package/src/commands/agents.ts +0 -923
  66. package/src/commands/api-keys.test.ts +0 -291
  67. package/src/commands/api-keys.ts +0 -382
  68. package/src/commands/artifact-verify.test.ts +0 -177
  69. package/src/commands/auth.test.ts +0 -82
  70. package/src/commands/auth.ts +0 -213
  71. package/src/commands/billing.test.ts +0 -158
  72. package/src/commands/billing.ts +0 -146
  73. package/src/commands/channels.test.ts +0 -74
  74. package/src/commands/channels.ts +0 -91
  75. package/src/commands/cloud-demote.test.ts +0 -401
  76. package/src/commands/cloud-demote.ts +0 -278
  77. package/src/commands/cloud-provision.test.ts +0 -1117
  78. package/src/commands/cloud-provision.ts +0 -942
  79. package/src/commands/cloud.pull-all.test.ts +0 -711
  80. package/src/commands/cloud.pull-per-company.test.ts +0 -188
  81. package/src/commands/cloud.push-all.test.ts +0 -485
  82. package/src/commands/cloud.scope-excluded-warning.test.ts +0 -22
  83. package/src/commands/cloud.selectors.test.ts +0 -208
  84. package/src/commands/cloud.status.test.ts +0 -160
  85. package/src/commands/cloud.test.ts +0 -41
  86. package/src/commands/cloud.ts +0 -2130
  87. package/src/commands/company.test.ts +0 -202
  88. package/src/commands/company.ts +0 -148
  89. package/src/commands/creators.test.ts +0 -60
  90. package/src/commands/creators.ts +0 -117
  91. package/src/commands/crm.test.ts +0 -179
  92. package/src/commands/crm.ts +0 -236
  93. package/src/commands/db-migrate.ts +0 -55
  94. package/src/commands/db-provision.ts +0 -114
  95. package/src/commands/db-sql.ts +0 -124
  96. package/src/commands/db-status.ts +0 -108
  97. package/src/commands/db.ts +0 -26
  98. package/src/commands/dm.test.ts +0 -748
  99. package/src/commands/dm.ts +0 -1226
  100. package/src/commands/feedback.test.ts +0 -413
  101. package/src/commands/feedback.ts +0 -169
  102. package/src/commands/files-browse.test.ts +0 -1249
  103. package/src/commands/files-browse.ts +0 -1506
  104. package/src/commands/files-delete.test.ts +0 -484
  105. package/src/commands/files-recovery.test.ts +0 -370
  106. package/src/commands/files.test.ts +0 -722
  107. package/src/commands/files.ts +0 -1446
  108. package/src/commands/group-grants.test.ts +0 -330
  109. package/src/commands/group-grants.ts +0 -455
  110. package/src/commands/groups.test.ts +0 -44
  111. package/src/commands/groups.ts +0 -368
  112. package/src/commands/integrations.test.ts +0 -633
  113. package/src/commands/integrations.ts +0 -507
  114. package/src/commands/list.ts +0 -85
  115. package/src/commands/login.ts +0 -52
  116. package/src/commands/logout.ts +0 -30
  117. package/src/commands/marketplace-install.test.ts +0 -543
  118. package/src/commands/marketplace-security.test.ts +0 -646
  119. package/src/commands/mcp-registration.test.ts +0 -2787
  120. package/src/commands/mcp-registration.ts +0 -2612
  121. package/src/commands/mcp-status.test.ts +0 -483
  122. package/src/commands/mcp-status.ts +0 -575
  123. package/src/commands/mcp-status.us011.test.ts +0 -243
  124. package/src/commands/meetings.test.ts +0 -522
  125. package/src/commands/meetings.ts +0 -789
  126. package/src/commands/members.test.ts +0 -1474
  127. package/src/commands/members.ts +0 -1181
  128. package/src/commands/onboard-identity-guard.test.ts +0 -99
  129. package/src/commands/onboard-identity-guard.ts +0 -103
  130. package/src/commands/onboard-join.test.ts +0 -96
  131. package/src/commands/onboard-join.ts +0 -101
  132. package/src/commands/onboard-warning.test.ts +0 -26
  133. package/src/commands/onboard-warning.ts +0 -12
  134. package/src/commands/onboard.ts +0 -267
  135. package/src/commands/outposts-heartbeat.test.ts +0 -299
  136. package/src/commands/outposts-heartbeat.ts +0 -310
  137. package/src/commands/outposts-replica-sync.test.ts +0 -272
  138. package/src/commands/outposts-self-deploy.test.ts +0 -243
  139. package/src/commands/outposts.test.ts +0 -1039
  140. package/src/commands/outposts.ts +0 -1935
  141. package/src/commands/pack-install-secret-authorization.test.ts +0 -115
  142. package/src/commands/pack-install.test.ts +0 -1469
  143. package/src/commands/pack-install.ts +0 -2256
  144. package/src/commands/pack-update-cache.test.ts +0 -149
  145. package/src/commands/packs-update-api-key.test.ts +0 -105
  146. package/src/commands/packs.test.ts +0 -88
  147. package/src/commands/packs.ts +0 -622
  148. package/src/commands/people.test.ts +0 -807
  149. package/src/commands/people.ts +0 -422
  150. package/src/commands/pkg-install.dispatch.test.ts +0 -103
  151. package/src/commands/pkg-install.ts +0 -221
  152. package/src/commands/pkg-list.test.ts +0 -79
  153. package/src/commands/pkg-list.ts +0 -13
  154. package/src/commands/pkg-remove.ts +0 -71
  155. package/src/commands/pkg-update.ts +0 -189
  156. package/src/commands/publish.test.ts +0 -650
  157. package/src/commands/publish.ts +0 -580
  158. package/src/commands/reindex.test.ts +0 -259
  159. package/src/commands/reindex.ts +0 -274
  160. package/src/commands/rescue.test.ts +0 -39
  161. package/src/commands/rescue.ts +0 -210
  162. package/src/commands/run.env-local.test.ts +0 -88
  163. package/src/commands/run.test.ts +0 -45
  164. package/src/commands/run.ts +0 -190
  165. package/src/commands/safe-extract.test.ts +0 -459
  166. package/src/commands/safe-extract.ts +0 -444
  167. package/src/commands/secrets-scope.test.ts +0 -56
  168. package/src/commands/secrets-scope.ts +0 -32
  169. package/src/commands/secrets.parse-destination.test.ts +0 -38
  170. package/src/commands/secrets.test.ts +0 -2050
  171. package/src/commands/secrets.ts +0 -2249
  172. package/src/commands/signals.ts +0 -351
  173. package/src/commands/skill.test.ts +0 -770
  174. package/src/commands/skill.ts +0 -796
  175. package/src/commands/sources.ts +0 -362
  176. package/src/commands/sync-mode.test.ts +0 -366
  177. package/src/commands/sync-mode.ts +0 -387
  178. package/src/commands/sync-narrow.test.ts +0 -633
  179. package/src/commands/sync-narrow.ts +0 -555
  180. package/src/commands/sync.ts +0 -165
  181. package/src/commands/team-sync.ts +0 -628
  182. package/src/commands/update.ts +0 -71
  183. package/src/commands/whoami.ts +0 -83
  184. package/src/commands/workers.test.ts +0 -158
  185. package/src/commands/workers.ts +0 -301
  186. package/src/index.test.ts +0 -32
  187. package/src/index.ts +0 -20
  188. package/src/lib/db/control-plane.test.ts +0 -59
  189. package/src/lib/db/control-plane.ts +0 -113
  190. package/src/lib/db/local.test.ts +0 -81
  191. package/src/lib/db/local.ts +0 -148
  192. package/src/lib/db/migrate.test.ts +0 -133
  193. package/src/lib/db/migrate.ts +0 -137
  194. package/src/lib/db/paths.test.ts +0 -112
  195. package/src/lib/db/paths.ts +0 -128
  196. package/src/lib/db/remote-engine.test.ts +0 -44
  197. package/src/lib/db/remote-engine.ts +0 -148
  198. package/src/lib/db/remote-sql.test.ts +0 -32
  199. package/src/lib/db/remote-sql.ts +0 -62
  200. package/src/lib/db/sql.test.ts +0 -106
  201. package/src/lib/db/sql.ts +0 -192
  202. package/src/lib/hq-cloud-dep.smoke.test.ts +0 -75
  203. package/src/lib/local-tree-diff.test.ts +0 -301
  204. package/src/lib/local-tree-diff.ts +0 -362
  205. package/src/lib/narrow-hint-banner.test.ts +0 -235
  206. package/src/lib/narrow-hint-banner.ts +0 -212
  207. package/src/lib/plan-limit-nag.test.ts +0 -317
  208. package/src/lib/plan-limit-nag.ts +0 -264
  209. package/src/main.ts +0 -371
  210. package/src/node-preflight.test.ts +0 -60
  211. package/src/node-preflight.ts +0 -67
  212. package/src/outpost/session-heartbeat-bounds.test.ts +0 -195
  213. package/src/outpost/session-heartbeat-guard.test.ts +0 -105
  214. package/src/outpost/session-heartbeat-publisher.test.ts +0 -178
  215. package/src/outpost/session-heartbeat-publisher.ts +0 -186
  216. package/src/outpost/session-heartbeat-retain-guard.test.ts +0 -126
  217. package/src/outpost/session-heartbeat.test.ts +0 -459
  218. package/src/outpost/session-heartbeat.ts +0 -877
  219. package/src/packaging.test.ts +0 -109
  220. package/src/run/__fixtures__/discover-schemas/example.env.schema +0 -4
  221. package/src/run/discover-schemas.test.ts +0 -153
  222. package/src/run/discover-schemas.ts +0 -79
  223. package/src/run/hq-plugin.test.ts +0 -371
  224. package/src/run/hq-plugin.ts +0 -277
  225. package/src/run/varlock-shape.test.ts +0 -57
  226. package/src/schemas/hq-package.schema.json +0 -155
  227. package/src/sentry-before-send.test.ts +0 -359
  228. package/src/sentry-before-send.ts +0 -221
  229. package/src/sentry-epipe.test.ts +0 -37
  230. package/src/sentry-release.test.ts +0 -54
  231. package/src/sentry.test.ts +0 -24
  232. package/src/sentry.ts +0 -54
  233. package/src/strategies/link.ts +0 -62
  234. package/src/strategies/merge.ts +0 -142
  235. package/src/types.ts +0 -146
  236. package/src/utils/__fixtures__/scan-packages.generated-block.sh +0 -23
  237. package/src/utils/auth-error.test.ts +0 -40
  238. package/src/utils/auth-error.ts +0 -42
  239. package/src/utils/billing-gate.test.ts +0 -95
  240. package/src/utils/billing-gate.ts +0 -228
  241. package/src/utils/breadcrumb-buffer.ts +0 -18
  242. package/src/utils/cli-telemetry.test.ts +0 -153
  243. package/src/utils/cli-telemetry.ts +0 -61
  244. package/src/utils/cognito-session.machine.test.ts +0 -109
  245. package/src/utils/cognito-session.refresh.test.ts +0 -84
  246. package/src/utils/cognito-session.test.ts +0 -328
  247. package/src/utils/cognito-session.ts +0 -448
  248. package/src/utils/company-selection-error.test.ts +0 -42
  249. package/src/utils/company-selection-error.ts +0 -45
  250. package/src/utils/contribution-table.ts +0 -83
  251. package/src/utils/environmental-error.test.ts +0 -45
  252. package/src/utils/environmental-error.ts +0 -39
  253. package/src/utils/epipe.test.ts +0 -28
  254. package/src/utils/epipe.ts +0 -29
  255. package/src/utils/expected-cli-error.test.ts +0 -28
  256. package/src/utils/expected-cli-error.ts +0 -39
  257. package/src/utils/feedback-diagnostics.test.ts +0 -183
  258. package/src/utils/feedback-diagnostics.ts +0 -123
  259. package/src/utils/feedback-screenshots.test.ts +0 -134
  260. package/src/utils/feedback-screenshots.ts +0 -124
  261. package/src/utils/feedback-versions.test.ts +0 -98
  262. package/src/utils/feedback-versions.ts +0 -68
  263. package/src/utils/git.ts +0 -74
  264. package/src/utils/integrity.ts +0 -59
  265. package/src/utils/intercepted-process-exit.test.ts +0 -37
  266. package/src/utils/intercepted-process-exit.ts +0 -36
  267. package/src/utils/login-provider.test.ts +0 -42
  268. package/src/utils/login-provider.ts +0 -30
  269. package/src/utils/manifest.test.ts +0 -167
  270. package/src/utils/manifest.ts +0 -131
  271. package/src/utils/pack-contributions.test.ts +0 -603
  272. package/src/utils/pack-contributions.ts +0 -450
  273. package/src/utils/people.ts +0 -215
  274. package/src/utils/registry-client.ts +0 -204
  275. package/src/utils/registry.ts +0 -90
  276. package/src/utils/resolve-vault-credential.test.ts +0 -69
  277. package/src/utils/resolve-vault-credential.ts +0 -60
  278. package/src/utils/sandbox-runner-client.test.ts +0 -279
  279. package/src/utils/sandbox-runner-client.ts +0 -260
  280. package/src/utils/secrets-cache.ts +0 -183
  281. package/src/utils/sentry-identity.ts +0 -45
  282. package/src/utils/settle-with-timeout.test.ts +0 -21
  283. package/src/utils/settle-with-timeout.ts +0 -22
  284. package/src/utils/user-yaml-error.test.ts +0 -24
  285. package/src/utils/user-yaml-error.ts +0 -30
  286. package/src/utils/vault-api.test.ts +0 -536
  287. package/src/utils/vault-api.ts +0 -382
  288. package/src/utils/version-check.test.ts +0 -176
  289. package/src/utils/version-check.ts +0 -155
  290. package/src/utils/version-gate.test.ts +0 -1024
  291. package/src/utils/version-gate.ts +0 -702
  292. package/test/commands/db-tenant-isolation.test.ts +0 -94
  293. package/test/commands/db.test.ts +0 -85
  294. package/test/commands/signals.test.ts +0 -207
  295. package/test/commands/sources.test.ts +0 -231
  296. package/test/e2e/smoke-install-mcp.sh +0 -113
  297. package/test/fixtures/hq-pack-smoke-mcp/mcp/smoke-http.json +0 -1
  298. package/test/fixtures/hq-pack-smoke-mcp/package.yaml +0 -11
  299. package/test/fixtures/signals/action_item/sample.md +0 -16
  300. package/test/fixtures/signals/summary/sample.md +0 -12
  301. package/test/fixtures/sources/meetings/sample.md +0 -25
  302. package/test/helpers/cli-runner.ts +0 -150
  303. package/test/helpers/s3-list-mock.ts +0 -79
  304. package/test/helpers/vault-service-mock.ts +0 -295
  305. package/test/sources-signals/smoke.test.ts +0 -228
  306. package/tsconfig.json +0 -20
  307. package/vitest.config.ts +0 -11
  308. package/vitest.e2e.config.ts +0 -7
@@ -1,371 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
- import * as fs from 'node:fs';
3
- import * as os from 'node:os';
4
- import * as path from 'node:path';
5
- import { internal } from 'varlock';
6
- import {
7
- installHqPlugin,
8
- prewarmHqSecrets,
9
- type InstallHqPluginOpts,
10
- type PluginState,
11
- } from './hq-plugin.js';
12
- import { removeCacheEntry, writeCache } from '../utils/secrets-cache.js';
13
-
14
- // Prevent any test run from writing to ~/.hq/secrets-cache/.
15
- vi.mock('../utils/secrets-cache.js', () => {
16
- return {
17
- DEFAULT_SECRETS_CACHE_TTL_MS: 300000,
18
- writeCache: vi.fn((): void => {}),
19
- removeCacheEntry: vi.fn((): void => {}),
20
- clearAllCache: (): { removed: number } => ({ removed: 0 }),
21
- };
22
- });
23
-
24
- let tmpDir: string;
25
-
26
- beforeEach(() => {
27
- vi.clearAllMocks();
28
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hq-plugin-test-'));
29
- });
30
-
31
- afterEach(() => {
32
- fs.rmSync(tmpDir, { recursive: true, force: true });
33
- });
34
-
35
- function makeMocks(overrides?: Partial<InstallHqPluginOpts>): InstallHqPluginOpts {
36
- return {
37
- resolveCompanyUid: async () => `test-uid-${Math.random().toString(36).slice(2)}`,
38
- fetchBatch: async () => ({ secrets: [], errors: [] }),
39
- ...overrides,
40
- };
41
- }
42
-
43
- describe('hq-plugin', () => {
44
- it('happy path: resolves hq() vars via batch fetch and cache', async () => {
45
- const schemaPath = path.join(tmpDir, '.env.schema');
46
- // Empty line after @hqCompany is required: varlock's env-spec parser only treats
47
- // a decorator comment as a root (file-level) decorator when it is separated from the
48
- // first var by a blank line. Without it, the decorator attaches to FOO as an item decorator.
49
- fs.writeFileSync(schemaPath, `# @hqCompany("test")\n\nFOO=hq()\n`);
50
-
51
- const uid = `test-uid-${Math.random().toString(36).slice(2)}`;
52
- const mocks = makeMocks({
53
- resolveCompanyUid: async () => uid,
54
- fetchBatch: async (_uid, names) => ({
55
- secrets: names.map((name) => ({ name, value: 'fixture-value' })),
56
- errors: [],
57
- }),
58
- });
59
-
60
- let state!: PluginState;
61
- const graph = await internal.loadEnvGraph({
62
- entryFilePaths: [schemaPath],
63
- afterInit: async (g) => {
64
- state = installHqPlugin(g, mocks);
65
- },
66
- });
67
- await prewarmHqSecrets(graph, mocks, state);
68
- await graph.resolveEnvValues();
69
- expect(graph.getResolvedEnvObject().FOO).toBe('fixture-value');
70
- });
71
-
72
- it('zero-cache secrets still resolve from in-memory prewarm state within a single run', async () => {
73
- const schemaPath = path.join(tmpDir, '.env.schema');
74
- fs.writeFileSync(schemaPath, `# @hqCompany("test")\n\nFOO=hq()\n`);
75
-
76
- const uid = `test-uid-${Math.random().toString(36).slice(2)}`;
77
- const mocks = makeMocks({
78
- resolveCompanyUid: async () => uid,
79
- fetchBatch: async (_uid, names) => ({
80
- secrets: names.map((name) => ({
81
- name,
82
- value: 'memory-only-value',
83
- cacheTtlMs: 0,
84
- })),
85
- errors: [],
86
- }),
87
- });
88
-
89
- let state!: PluginState;
90
- const graph = await internal.loadEnvGraph({
91
- entryFilePaths: [schemaPath],
92
- afterInit: async (g) => {
93
- state = installHqPlugin(g, mocks);
94
- },
95
- });
96
- await prewarmHqSecrets(graph, mocks, state);
97
- await graph.resolveEnvValues();
98
-
99
- expect(graph.getResolvedEnvObject().FOO).toBe('memory-only-value');
100
- expect(removeCacheEntry).toHaveBeenCalledWith(uid, 'FOO');
101
- });
102
-
103
- it('controlled policy metadata overrides a malformed positive cache TTL', async () => {
104
- const schemaPath = path.join(tmpDir, '.env.schema');
105
- fs.writeFileSync(schemaPath, `# @hqCompany("test")\n\nFOO=hq()\n`);
106
-
107
- const uid = `test-uid-${Math.random().toString(36).slice(2)}`;
108
- const mocks = makeMocks({
109
- resolveCompanyUid: async () => uid,
110
- fetchBatch: async () => ({
111
- secrets: [{
112
- name: 'FOO',
113
- value: 'memory-only-value',
114
- tier: 'sensitive',
115
- scriptLock: { mode: 'enforced' },
116
- cacheTtlMs: 300000,
117
- }],
118
- errors: [],
119
- }),
120
- });
121
-
122
- let state!: PluginState;
123
- const graph = await internal.loadEnvGraph({
124
- entryFilePaths: [schemaPath],
125
- afterInit: async (g) => {
126
- state = installHqPlugin(g, mocks);
127
- },
128
- });
129
- await prewarmHqSecrets(graph, mocks, state);
130
- await graph.resolveEnvValues();
131
-
132
- expect(graph.getResolvedEnvObject().FOO).toBe('memory-only-value');
133
- expect(writeCache).not.toHaveBeenCalled();
134
- expect(removeCacheEntry).toHaveBeenCalledWith(uid, 'FOO');
135
- });
136
-
137
- it('fails closed and evicts cache when the server omits a requested secret', async () => {
138
- const schemaPath = path.join(tmpDir, '.env.schema');
139
- fs.writeFileSync(schemaPath, `# @hqCompany("test")\n\nFOO=hq()\n`);
140
-
141
- const uid = `test-uid-${Math.random().toString(36).slice(2)}`;
142
- const mocks = makeMocks({
143
- resolveCompanyUid: async () => uid,
144
- fetchBatch: async () => ({ secrets: [], errors: [] }),
145
- });
146
-
147
- let state!: PluginState;
148
- const graph = await internal.loadEnvGraph({
149
- entryFilePaths: [schemaPath],
150
- afterInit: async (g) => {
151
- state = installHqPlugin(g, mocks);
152
- },
153
- });
154
- await prewarmHqSecrets(graph, mocks, state);
155
- await graph.resolveEnvValues();
156
-
157
- const fooErrors = (graph as any).configSchema.FOO.errors as Array<{
158
- message: string;
159
- }>;
160
- expect(fooErrors.some((e) => e.message.includes('not returned by vault'))).toBe(true);
161
- expect(removeCacheEntry).toHaveBeenCalledWith(uid, 'FOO');
162
- });
163
-
164
- it('fails closed and evicts cache when the server returns metadata without plaintext', async () => {
165
- const schemaPath = path.join(tmpDir, '.env.schema');
166
- fs.writeFileSync(schemaPath, `# @hqCompany("test")\n\nFOO=hq()\n`);
167
-
168
- const uid = `test-uid-${Math.random().toString(36).slice(2)}`;
169
- const mocks = makeMocks({
170
- resolveCompanyUid: async () => uid,
171
- fetchBatch: async () => ({
172
- secrets: [{ name: 'FOO', cacheTtlMs: 0 }],
173
- errors: [],
174
- }),
175
- });
176
-
177
- let state!: PluginState;
178
- const graph = await internal.loadEnvGraph({
179
- entryFilePaths: [schemaPath],
180
- afterInit: async (g) => {
181
- state = installHqPlugin(g, mocks);
182
- },
183
- });
184
- await prewarmHqSecrets(graph, mocks, state);
185
- await graph.resolveEnvValues();
186
-
187
- const fooErrors = (graph as any).configSchema.FOO.errors as Array<{
188
- message: string;
189
- }>;
190
- expect(fooErrors.some((e) => e.message.includes('not returned by vault'))).toBe(true);
191
- expect(removeCacheEntry).toHaveBeenCalledWith(uid, 'FOO');
192
- });
193
-
194
- it('evicts every requested cache entry when batch authorization throws', async () => {
195
- const schemaPath = path.join(tmpDir, '.env.schema');
196
- fs.writeFileSync(
197
- schemaPath,
198
- `# @hqCompany("test")\n\nFOO=hq()\nBAR=hq()\n`,
199
- );
200
-
201
- const uid = `test-uid-${Math.random().toString(36).slice(2)}`;
202
- const mocks = makeMocks({
203
- resolveCompanyUid: async () => uid,
204
- fetchBatch: async () => {
205
- throw new Error('Forbidden: batch authorization denied');
206
- },
207
- });
208
-
209
- let state!: PluginState;
210
- const graph = await internal.loadEnvGraph({
211
- entryFilePaths: [schemaPath],
212
- afterInit: async (g) => {
213
- state = installHqPlugin(g, mocks);
214
- },
215
- });
216
-
217
- await expect(prewarmHqSecrets(graph, mocks, state)).rejects.toThrow(
218
- 'Forbidden: batch authorization denied',
219
- );
220
- expect(removeCacheEntry).toHaveBeenCalledWith(uid, 'FOO');
221
- expect(removeCacheEntry).toHaveBeenCalledWith(uid, 'BAR');
222
- });
223
-
224
- it('rejects mixed success/error rows, clears memory, and evicts cache', async () => {
225
- const schemaPath = path.join(tmpDir, '.env.schema');
226
- fs.writeFileSync(schemaPath, `# @hqCompany("test")\n\nFOO=hq()\n`);
227
-
228
- const uid = `test-uid-${Math.random().toString(36).slice(2)}`;
229
- const mocks = makeMocks({
230
- resolveCompanyUid: async () => uid,
231
- fetchBatch: async () => ({
232
- secrets: [{ name: 'FOO', value: 'sentinel-secret-value' }],
233
- errors: [{ name: 'FOO', code: 'forbidden', message: 'Denied' }],
234
- }),
235
- });
236
-
237
- let state!: PluginState;
238
- const graph = await internal.loadEnvGraph({
239
- entryFilePaths: [schemaPath],
240
- afterInit: async (g) => {
241
- state = installHqPlugin(g, mocks);
242
- },
243
- });
244
-
245
- await expect(prewarmHqSecrets(graph, mocks, state)).rejects.toThrow(
246
- 'Invalid secret load response from vault',
247
- );
248
- expect(state.loadedSecretsByName.size).toBe(0);
249
- expect(removeCacheEntry).toHaveBeenCalledWith(uid, 'FOO');
250
- });
251
-
252
- it('rejects malformed response envelopes and evicts every requested entry', async () => {
253
- const schemaPath = path.join(tmpDir, '.env.schema');
254
- fs.writeFileSync(
255
- schemaPath,
256
- `# @hqCompany("test")\n\nFOO=hq()\nBAR=hq()\n`,
257
- );
258
-
259
- const uid = `test-uid-${Math.random().toString(36).slice(2)}`;
260
- const mocks = makeMocks({
261
- resolveCompanyUid: async () => uid,
262
- fetchBatch: async () => ({
263
- secrets: [],
264
- errors: undefined as unknown as [],
265
- }),
266
- });
267
-
268
- let state!: PluginState;
269
- const graph = await internal.loadEnvGraph({
270
- entryFilePaths: [schemaPath],
271
- afterInit: async (g) => {
272
- state = installHqPlugin(g, mocks);
273
- },
274
- });
275
-
276
- await expect(prewarmHqSecrets(graph, mocks, state)).rejects.toThrow(
277
- 'Invalid secret load response from vault',
278
- );
279
- expect(removeCacheEntry).toHaveBeenCalledWith(uid, 'FOO');
280
- expect(removeCacheEntry).toHaveBeenCalledWith(uid, 'BAR');
281
- });
282
-
283
- it('missing-company-error: throws when no @hqCompany and no companyOverride', async () => {
284
- const schemaPath = path.join(tmpDir, '.env.schema');
285
- // Schema has hq() resolver but no @hqCompany annotation and no opts.companyOverride
286
- fs.writeFileSync(schemaPath, `FOO=hq()\n`);
287
-
288
- const mocks = makeMocks();
289
-
290
- let state!: PluginState;
291
- const graph = await internal.loadEnvGraph({
292
- entryFilePaths: [schemaPath],
293
- afterInit: async (g) => {
294
- state = installHqPlugin(g, mocks);
295
- },
296
- });
297
- await expect(prewarmHqSecrets(graph, mocks, state)).rejects.toThrow(
298
- '@hqCompany("...") not declared and --company not passed',
299
- );
300
- });
301
-
302
- it('batch-error-passthrough-403: forbidden error is surfaced as ResolutionError', async () => {
303
- const schemaPath = path.join(tmpDir, '.env.schema');
304
- // Empty line after @hqCompany is required: varlock's env-spec parser only treats
305
- // a decorator comment as a root (file-level) decorator when it is separated from the
306
- // first var by a blank line. Without it, the decorator attaches to FOO as an item decorator.
307
- fs.writeFileSync(schemaPath, `# @hqCompany("test")\n\nFOO=hq()\n`);
308
-
309
- const uid = `test-uid-${Math.random().toString(36).slice(2)}`;
310
- const mocks = makeMocks({
311
- resolveCompanyUid: async () => uid,
312
- fetchBatch: async () => ({
313
- secrets: [],
314
- errors: [{ name: 'FOO', code: 'forbidden' }],
315
- }),
316
- });
317
-
318
- let state!: PluginState;
319
- const graph = await internal.loadEnvGraph({
320
- entryFilePaths: [schemaPath],
321
- afterInit: async (g) => {
322
- state = installHqPlugin(g, mocks);
323
- },
324
- });
325
- await prewarmHqSecrets(graph, mocks, state);
326
- await graph.resolveEnvValues();
327
- // varlock collects ResolutionErrors on ConfigItem.errors (not re-thrown from resolveEnvValues).
328
- // We verify the per-item error contains the forbidden diagnostic.
329
- const fooErrors = (graph as any).configSchema['FOO'].errors as Array<{ message: string }>;
330
- expect(fooErrors.some((e) => e.message.includes('No read permission for secret "FOO"'))).toBe(true);
331
- });
332
-
333
- // US-003 (secrets-server-proxy): the server refuses to batch-load a
334
- // high-security ("nuclear") secret (code: high_security_denied, no plaintext).
335
- // `hq run` injects plaintext into the child env, so it must surface this as a
336
- // clear, actionable error pointing at the proxy — and never resolve a value.
337
- it('high-security-denied: nuclear secret is surfaced as a proxy-pointing ResolutionError, no value', async () => {
338
- const schemaPath = path.join(tmpDir, '.env.schema');
339
- fs.writeFileSync(schemaPath, `# @hqCompany("test")\n\nANTHROPIC_API_KEY=hq()\n`);
340
-
341
- const uid = `test-uid-${Math.random().toString(36).slice(2)}`;
342
- const mocks = makeMocks({
343
- resolveCompanyUid: async () => uid,
344
- fetchBatch: async () => ({
345
- secrets: [],
346
- errors: [
347
- {
348
- name: 'ANTHROPIC_API_KEY',
349
- code: 'high_security_denied',
350
- message: 'high-security; use the proxy',
351
- },
352
- ],
353
- }),
354
- });
355
-
356
- let state!: PluginState;
357
- const graph = await internal.loadEnvGraph({
358
- entryFilePaths: [schemaPath],
359
- afterInit: async (g) => {
360
- state = installHqPlugin(g, mocks);
361
- },
362
- });
363
- await prewarmHqSecrets(graph, mocks, state);
364
- await graph.resolveEnvValues();
365
-
366
- const errs = (graph as any).configSchema['ANTHROPIC_API_KEY'].errors as Array<{ message: string }>;
367
- expect(errs.some((e) => /high-security/i.test(e.message) && /proxy/i.test(e.message))).toBe(true);
368
- // No plaintext value was resolved for the nuclear secret.
369
- expect((graph.getResolvedEnvObject() as Record<string, unknown>).ANTHROPIC_API_KEY).toBeUndefined();
370
- });
371
- });
@@ -1,277 +0,0 @@
1
- import { ResolutionError } from 'varlock/plugin-lib';
2
- import type { Resolver } from 'varlock/plugin-lib';
3
- import {
4
- DEFAULT_SECRETS_CACHE_TTL_MS,
5
- writeCache,
6
- removeCacheEntry,
7
- } from '../utils/secrets-cache.js';
8
- import type { SecretLoadResponse, SecretUsage } from '../commands/secrets.js';
9
-
10
- function normalizeCacheTtlMs(secret: SecretLoadResponse['secrets'][number]): number {
11
- if (
12
- secret.tier === 'sensitive' ||
13
- secret.tier === 'nuclear' ||
14
- secret.scriptLock?.mode === 'enforced'
15
- ) {
16
- return 0;
17
- }
18
- if (secret.cacheTtlMs === undefined) {
19
- return DEFAULT_SECRETS_CACHE_TTL_MS;
20
- }
21
- return typeof secret.cacheTtlMs === 'number' &&
22
- Number.isFinite(secret.cacheTtlMs) &&
23
- secret.cacheTtlMs > 0
24
- ? secret.cacheTtlMs
25
- : 0;
26
- }
27
-
28
- export interface InstallHqPluginOpts {
29
- companyOverride?: string;
30
- resolveCompanyUid: (slug: string) => Promise<string>;
31
- usage?: SecretUsage;
32
- fetchBatch: (
33
- uid: string,
34
- names: string[],
35
- usage?: SecretUsage,
36
- ) => Promise<SecretLoadResponse>;
37
- }
38
-
39
- export interface PluginState {
40
- schemaCompanySlug: string | null;
41
- uid: string | null;
42
- errorsByName: Map<string, { code: string; message?: string }>;
43
- loadedSecretsByName: Map<string, string>;
44
- }
45
-
46
- export function installHqPlugin(graph: any /* EnvGraph */, opts: InstallHqPluginOpts) {
47
- const pluginState: PluginState = {
48
- schemaCompanySlug: null,
49
- uid: null,
50
- errorsByName: new Map(),
51
- loadedSecretsByName: new Map(),
52
- };
53
-
54
- // varlock@1.0.0's plugin-lib.js omits the Resolver export (d.ts/JS mismatch);
55
- // extract it at runtime from any already-registered built-in resolver's prototype.
56
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
- let RuntimeResolver: any;
58
- try {
59
- const fns = graph.registeredResolverFunctions as Record<string, unknown>;
60
- const first = Object.values(fns)[0];
61
- if (first == null) throw new Error('registeredResolverFunctions is empty');
62
- const proto = Object.getPrototypeOf(first) as { prototype?: { process?: unknown } } | null;
63
- if (proto == null || typeof proto.prototype?.process !== 'function') {
64
- throw new Error('prototype has no process method');
65
- }
66
- RuntimeResolver = proto;
67
- } catch (e) {
68
- throw new Error(
69
- 'varlock Resolver base class extraction failed — the varlock@1.0.0 d.ts/JS mismatch ' +
70
- 'may have been resolved; switch to `import { Resolver } from "varlock/plugin-lib"`. ' +
71
- `Underlying: ${e instanceof Error ? e.message : String(e)}`,
72
- );
73
- }
74
-
75
- // HqResolver is declared INSIDE installHqPlugin so its static def.resolve
76
- // closes over `pluginState`. Module-scope declaration is forbidden — resolve()
77
- // would hit `ReferenceError: pluginState is not defined`.
78
- class HqResolver extends RuntimeResolver {
79
- static def = {
80
- name: 'hq',
81
- impliesSensitive: true,
82
- argsSchema: { type: 'array' as const, arrayMaxLength: 1 },
83
- resolve: async function (this: HqResolver) {
84
- // `pluginState` is captured by this inner-class closure;
85
- // `prewarmHqSecrets(graph, opts, state)` server-authorizes and populates
86
- // the in-memory values before `graph.resolveEnvValues()` calls us.
87
- const explicit = this.arrArgs?.[0]?.staticValue;
88
- const secretName = (typeof explicit === 'string' && explicit) ? explicit : this._ownerKey;
89
- if (!secretName) {
90
- throw new ResolutionError('hq() resolver could not determine secret name (missing owner key)');
91
- }
92
- const err = pluginState.errorsByName.get(secretName);
93
- if (err) {
94
- if (err.code === 'forbidden') {
95
- throw new ResolutionError(`No read permission for secret "${secretName}" — ask an admin to share it via \`hq secrets share ${secretName} --with <you> --permission read\``);
96
- }
97
- if (err.code === 'not_found') {
98
- throw new ResolutionError(`Secret "${secretName}" does not exist in company`);
99
- }
100
- // High-security ("nuclear") secret: the server refuses to vend it on
101
- // the local-injection path. It can ONLY be used through the
102
- // server-side proxy, so `hq run` (which injects plaintext into the
103
- // child env) can never load it. Surface a clear, actionable error.
104
- if (err.code === 'high_security_denied') {
105
- throw new ResolutionError(`Secret "${secretName}" is high-security and cannot be injected locally — it can only be used via the HQ secret proxy (POST /secrets/{companyUid}/proxy/{path}), which keeps the plaintext server-side. Remove it from this schema's locally-injected vars.`);
106
- }
107
- throw new ResolutionError(`Failed to load secret "${secretName}": ${err.message ?? err.code}`);
108
- }
109
- if (pluginState.uid == null) {
110
- throw new ResolutionError('Internal error: prewarmHqSecrets was not called before resolveEnvValues');
111
- }
112
- const inMemory = pluginState.loadedSecretsByName.get(secretName);
113
- if (inMemory != null) {
114
- return inMemory;
115
- }
116
- throw new ResolutionError(
117
- `Secret "${secretName}" was not returned by vault after server authorization`,
118
- );
119
- },
120
- };
121
-
122
- // Captured during process(parent); used by resolve() to fall back to the var key.
123
- private _ownerKey?: string;
124
-
125
- process(parent?: Parameters<Resolver['process']>[0]) {
126
- super.process(parent);
127
- if (parent != null && typeof (parent as { key?: unknown }).key === 'string') {
128
- this._ownerKey = (parent as { key: string }).key;
129
- }
130
- }
131
- }
132
-
133
- // varlock@1.0.0's env-spec parser rejects dots in decorator names ([a-zA-Z0-9_] only),
134
- // so `@hq.company` is not valid syntax. We register as `@hqCompany` (camelCase) instead.
135
- // Schema files must use `# @hqCompany("slug")` followed by a blank line so the parser
136
- // treats it as a file-level root decorator rather than an item decorator for the next var.
137
- graph.registerRootDecorator({
138
- name: 'hqCompany',
139
- isFunction: true,
140
- process: (decoratorValue: any) => {
141
- const slug = decoratorValue.arrArgs?.[0]?.staticValue;
142
- return typeof slug === 'string' ? slug : null;
143
- },
144
- execute: (slug: string | null) => {
145
- if (slug) pluginState.schemaCompanySlug = slug;
146
- },
147
- });
148
-
149
- graph.registerResolver(HqResolver);
150
-
151
- // Returned so `prewarmHqSecrets(graph, opts, state)` can read schemaCompanySlug
152
- // and write `uid` + `errorsByName`. The state is held by the closure; the returned handle
153
- // is purely for the prewarm helper.
154
- return pluginState;
155
- }
156
-
157
- export async function prewarmHqSecrets(
158
- graph: any /* EnvGraph */,
159
- opts: InstallHqPluginOpts,
160
- state: PluginState,
161
- ): Promise<void> {
162
- const queue: Array<{ key: string; secretName: string }> = [];
163
- for (const [key, item] of Object.entries(graph.configSchema)) {
164
- const resolver = (item as any).valueResolver;
165
- if (resolver?.def?.name === 'hq') {
166
- const explicit = resolver.arrArgs?.[0]?.staticValue;
167
- const secretName = (typeof explicit === 'string' && explicit) ? explicit : key;
168
- queue.push({ key, secretName });
169
- }
170
- }
171
-
172
- if (queue.length === 0) {
173
- return;
174
- }
175
-
176
- let slug: string;
177
- if (state.schemaCompanySlug) {
178
- slug = state.schemaCompanySlug;
179
- } else if (opts.companyOverride) {
180
- slug = opts.companyOverride;
181
- } else {
182
- throw new Error('@hqCompany("...") not declared and --company not passed');
183
- }
184
-
185
- const uid = await opts.resolveCompanyUid(slug);
186
-
187
- const uniqueNames = [...new Set(queue.map((q) => q.secretName))];
188
-
189
- if (uniqueNames.length > 100) {
190
- throw new Error(
191
- `hq run supports at most 100 hq() resolvers per schema; got ${uniqueNames.length}`,
192
- );
193
- }
194
- state.loadedSecretsByName.clear();
195
- state.errorsByName = new Map();
196
-
197
- let result: SecretLoadResponse;
198
- try {
199
- result = await opts.fetchBatch(uid, uniqueNames, opts.usage);
200
- if (!Array.isArray(result.secrets) || !Array.isArray(result.errors)) {
201
- throw new Error('Invalid secret load response from vault');
202
- }
203
-
204
- const errorsByName = new Map<string, { code: string; message?: string }>();
205
- const returnedNames = new Set<string>();
206
- const requestedNames = new Set(uniqueNames);
207
- const seenNames = new Set<string>();
208
- for (const rawSecret of result.secrets) {
209
- if (!rawSecret || typeof rawSecret !== 'object') {
210
- throw new Error('Invalid secret load response from vault');
211
- }
212
- const s = rawSecret as SecretLoadResponse['secrets'][number];
213
- if (
214
- typeof s.name !== 'string' ||
215
- !requestedNames.has(s.name) ||
216
- seenNames.has(s.name) ||
217
- (s.value != null && typeof s.value !== 'string')
218
- ) {
219
- throw new Error('Invalid secret load response from vault');
220
- }
221
- seenNames.add(s.name);
222
- if (s.value == null) {
223
- errorsByName.set(s.name, {
224
- code: 'not_returned',
225
- message: 'not returned by vault after server authorization',
226
- });
227
- removeCacheEntry(uid, s.name);
228
- continue;
229
- }
230
- returnedNames.add(s.name);
231
- state.loadedSecretsByName.set(s.name, s.value);
232
- const cacheTtlMs = normalizeCacheTtlMs(s);
233
- if (cacheTtlMs > 0) {
234
- writeCache(uid, s.name, s.value, cacheTtlMs);
235
- } else {
236
- removeCacheEntry(uid, s.name);
237
- }
238
- }
239
- for (const rawError of result.errors) {
240
- if (!rawError || typeof rawError !== 'object') {
241
- throw new Error('Invalid secret load response from vault');
242
- }
243
- const e = rawError as { name?: unknown; code?: unknown; message?: unknown };
244
- if (
245
- typeof e.name !== 'string' ||
246
- !requestedNames.has(e.name) ||
247
- seenNames.has(e.name) ||
248
- typeof e.code !== 'string' ||
249
- (e.message !== undefined && typeof e.message !== 'string')
250
- ) {
251
- throw new Error('Invalid secret load response from vault');
252
- }
253
- seenNames.add(e.name);
254
- errorsByName.set(e.name, { code: e.code, message: e.message });
255
- state.loadedSecretsByName.delete(e.name);
256
- removeCacheEntry(uid, e.name);
257
- }
258
- for (const name of uniqueNames) {
259
- if (!returnedNames.has(name) && !errorsByName.has(name)) {
260
- errorsByName.set(name, {
261
- code: 'not_returned',
262
- message: 'not returned by vault after server authorization',
263
- });
264
- removeCacheEntry(uid, name);
265
- }
266
- }
267
- state.errorsByName = errorsByName;
268
- state.uid = uid;
269
- } catch (err) {
270
- state.loadedSecretsByName.clear();
271
- state.errorsByName = new Map();
272
- for (const name of uniqueNames) {
273
- removeCacheEntry(uid, name);
274
- }
275
- throw err;
276
- }
277
- }