@indigoai-us/hq-cli 5.77.13 → 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 (307) hide show
  1. package/CHANGELOG.md +34 -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 +51 -2
  33. package/dist/commands/pkg-install.js +43 -0
  34. package/dist/commands/run.js +8 -0
  35. package/dist/commands/secrets.js +125 -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 -2247
  144. package/src/commands/pack-update-cache.test.ts +0 -149
  145. package/src/commands/packs.test.ts +0 -88
  146. package/src/commands/packs.ts +0 -622
  147. package/src/commands/people.test.ts +0 -807
  148. package/src/commands/people.ts +0 -422
  149. package/src/commands/pkg-install.dispatch.test.ts +0 -71
  150. package/src/commands/pkg-install.ts +0 -215
  151. package/src/commands/pkg-list.test.ts +0 -79
  152. package/src/commands/pkg-list.ts +0 -13
  153. package/src/commands/pkg-remove.ts +0 -71
  154. package/src/commands/pkg-update.ts +0 -189
  155. package/src/commands/publish.test.ts +0 -650
  156. package/src/commands/publish.ts +0 -580
  157. package/src/commands/reindex.test.ts +0 -259
  158. package/src/commands/reindex.ts +0 -274
  159. package/src/commands/rescue.test.ts +0 -39
  160. package/src/commands/rescue.ts +0 -210
  161. package/src/commands/run.env-local.test.ts +0 -88
  162. package/src/commands/run.test.ts +0 -45
  163. package/src/commands/run.ts +0 -184
  164. package/src/commands/safe-extract.test.ts +0 -459
  165. package/src/commands/safe-extract.ts +0 -444
  166. package/src/commands/secrets-scope.test.ts +0 -56
  167. package/src/commands/secrets-scope.ts +0 -32
  168. package/src/commands/secrets.parse-destination.test.ts +0 -38
  169. package/src/commands/secrets.test.ts +0 -2037
  170. package/src/commands/secrets.ts +0 -2240
  171. package/src/commands/signals.ts +0 -351
  172. package/src/commands/skill.test.ts +0 -770
  173. package/src/commands/skill.ts +0 -796
  174. package/src/commands/sources.ts +0 -362
  175. package/src/commands/sync-mode.test.ts +0 -366
  176. package/src/commands/sync-mode.ts +0 -387
  177. package/src/commands/sync-narrow.test.ts +0 -633
  178. package/src/commands/sync-narrow.ts +0 -555
  179. package/src/commands/sync.ts +0 -165
  180. package/src/commands/team-sync.ts +0 -628
  181. package/src/commands/update.ts +0 -71
  182. package/src/commands/whoami.ts +0 -83
  183. package/src/commands/workers.test.ts +0 -158
  184. package/src/commands/workers.ts +0 -301
  185. package/src/index.test.ts +0 -32
  186. package/src/index.ts +0 -20
  187. package/src/lib/db/control-plane.test.ts +0 -59
  188. package/src/lib/db/control-plane.ts +0 -113
  189. package/src/lib/db/local.test.ts +0 -81
  190. package/src/lib/db/local.ts +0 -148
  191. package/src/lib/db/migrate.test.ts +0 -133
  192. package/src/lib/db/migrate.ts +0 -137
  193. package/src/lib/db/paths.test.ts +0 -112
  194. package/src/lib/db/paths.ts +0 -128
  195. package/src/lib/db/remote-engine.test.ts +0 -44
  196. package/src/lib/db/remote-engine.ts +0 -148
  197. package/src/lib/db/remote-sql.test.ts +0 -32
  198. package/src/lib/db/remote-sql.ts +0 -62
  199. package/src/lib/db/sql.test.ts +0 -106
  200. package/src/lib/db/sql.ts +0 -192
  201. package/src/lib/hq-cloud-dep.smoke.test.ts +0 -75
  202. package/src/lib/local-tree-diff.test.ts +0 -301
  203. package/src/lib/local-tree-diff.ts +0 -362
  204. package/src/lib/narrow-hint-banner.test.ts +0 -235
  205. package/src/lib/narrow-hint-banner.ts +0 -212
  206. package/src/lib/plan-limit-nag.test.ts +0 -317
  207. package/src/lib/plan-limit-nag.ts +0 -264
  208. package/src/main.ts +0 -371
  209. package/src/node-preflight.test.ts +0 -60
  210. package/src/node-preflight.ts +0 -67
  211. package/src/outpost/session-heartbeat-bounds.test.ts +0 -195
  212. package/src/outpost/session-heartbeat-guard.test.ts +0 -105
  213. package/src/outpost/session-heartbeat-publisher.test.ts +0 -178
  214. package/src/outpost/session-heartbeat-publisher.ts +0 -186
  215. package/src/outpost/session-heartbeat-retain-guard.test.ts +0 -126
  216. package/src/outpost/session-heartbeat.test.ts +0 -459
  217. package/src/outpost/session-heartbeat.ts +0 -877
  218. package/src/packaging.test.ts +0 -109
  219. package/src/run/__fixtures__/discover-schemas/example.env.schema +0 -4
  220. package/src/run/discover-schemas.test.ts +0 -153
  221. package/src/run/discover-schemas.ts +0 -79
  222. package/src/run/hq-plugin.test.ts +0 -371
  223. package/src/run/hq-plugin.ts +0 -277
  224. package/src/run/varlock-shape.test.ts +0 -57
  225. package/src/schemas/hq-package.schema.json +0 -155
  226. package/src/sentry-before-send.test.ts +0 -359
  227. package/src/sentry-before-send.ts +0 -221
  228. package/src/sentry-epipe.test.ts +0 -37
  229. package/src/sentry-release.test.ts +0 -54
  230. package/src/sentry.test.ts +0 -24
  231. package/src/sentry.ts +0 -54
  232. package/src/strategies/link.ts +0 -62
  233. package/src/strategies/merge.ts +0 -142
  234. package/src/types.ts +0 -146
  235. package/src/utils/__fixtures__/scan-packages.generated-block.sh +0 -23
  236. package/src/utils/auth-error.test.ts +0 -40
  237. package/src/utils/auth-error.ts +0 -42
  238. package/src/utils/billing-gate.test.ts +0 -95
  239. package/src/utils/billing-gate.ts +0 -228
  240. package/src/utils/breadcrumb-buffer.ts +0 -18
  241. package/src/utils/cli-telemetry.test.ts +0 -153
  242. package/src/utils/cli-telemetry.ts +0 -61
  243. package/src/utils/cognito-session.machine.test.ts +0 -109
  244. package/src/utils/cognito-session.refresh.test.ts +0 -84
  245. package/src/utils/cognito-session.test.ts +0 -328
  246. package/src/utils/cognito-session.ts +0 -448
  247. package/src/utils/company-selection-error.test.ts +0 -42
  248. package/src/utils/company-selection-error.ts +0 -45
  249. package/src/utils/contribution-table.ts +0 -83
  250. package/src/utils/environmental-error.test.ts +0 -45
  251. package/src/utils/environmental-error.ts +0 -39
  252. package/src/utils/epipe.test.ts +0 -28
  253. package/src/utils/epipe.ts +0 -29
  254. package/src/utils/expected-cli-error.test.ts +0 -28
  255. package/src/utils/expected-cli-error.ts +0 -39
  256. package/src/utils/feedback-diagnostics.test.ts +0 -183
  257. package/src/utils/feedback-diagnostics.ts +0 -123
  258. package/src/utils/feedback-screenshots.test.ts +0 -134
  259. package/src/utils/feedback-screenshots.ts +0 -124
  260. package/src/utils/feedback-versions.test.ts +0 -98
  261. package/src/utils/feedback-versions.ts +0 -68
  262. package/src/utils/git.ts +0 -74
  263. package/src/utils/integrity.ts +0 -59
  264. package/src/utils/intercepted-process-exit.test.ts +0 -37
  265. package/src/utils/intercepted-process-exit.ts +0 -36
  266. package/src/utils/login-provider.test.ts +0 -42
  267. package/src/utils/login-provider.ts +0 -30
  268. package/src/utils/manifest.test.ts +0 -167
  269. package/src/utils/manifest.ts +0 -131
  270. package/src/utils/pack-contributions.test.ts +0 -603
  271. package/src/utils/pack-contributions.ts +0 -450
  272. package/src/utils/people.ts +0 -215
  273. package/src/utils/registry-client.ts +0 -204
  274. package/src/utils/registry.ts +0 -90
  275. package/src/utils/resolve-vault-credential.test.ts +0 -69
  276. package/src/utils/resolve-vault-credential.ts +0 -60
  277. package/src/utils/sandbox-runner-client.test.ts +0 -279
  278. package/src/utils/sandbox-runner-client.ts +0 -260
  279. package/src/utils/secrets-cache.ts +0 -183
  280. package/src/utils/sentry-identity.ts +0 -45
  281. package/src/utils/settle-with-timeout.test.ts +0 -21
  282. package/src/utils/settle-with-timeout.ts +0 -22
  283. package/src/utils/user-yaml-error.test.ts +0 -24
  284. package/src/utils/user-yaml-error.ts +0 -30
  285. package/src/utils/vault-api.test.ts +0 -536
  286. package/src/utils/vault-api.ts +0 -382
  287. package/src/utils/version-check.test.ts +0 -176
  288. package/src/utils/version-check.ts +0 -155
  289. package/src/utils/version-gate.test.ts +0 -1024
  290. package/src/utils/version-gate.ts +0 -702
  291. package/test/commands/db-tenant-isolation.test.ts +0 -94
  292. package/test/commands/db.test.ts +0 -85
  293. package/test/commands/signals.test.ts +0 -207
  294. package/test/commands/sources.test.ts +0 -231
  295. package/test/e2e/smoke-install-mcp.sh +0 -113
  296. package/test/fixtures/hq-pack-smoke-mcp/mcp/smoke-http.json +0 -1
  297. package/test/fixtures/hq-pack-smoke-mcp/package.yaml +0 -11
  298. package/test/fixtures/signals/action_item/sample.md +0 -16
  299. package/test/fixtures/signals/summary/sample.md +0 -12
  300. package/test/fixtures/sources/meetings/sample.md +0 -25
  301. package/test/helpers/cli-runner.ts +0 -150
  302. package/test/helpers/s3-list-mock.ts +0 -79
  303. package/test/helpers/vault-service-mock.ts +0 -295
  304. package/test/sources-signals/smoke.test.ts +0 -228
  305. package/tsconfig.json +0 -20
  306. package/vitest.config.ts +0 -11
  307. 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
- }