@indigoai-us/hq-cli 5.5.0 → 5.5.2-sentry-rc.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 (139) hide show
  1. package/dist/bin/hq-auth-refresh.js +22 -12
  2. package/dist/bin/hq-auth-refresh.js.map +1 -1
  3. package/dist/commands/_patterns.d.ts +3 -0
  4. package/dist/commands/_patterns.d.ts.map +1 -0
  5. package/dist/commands/_patterns.js +3 -0
  6. package/dist/commands/_patterns.js.map +1 -0
  7. package/dist/commands/add.d.ts.map +1 -1
  8. package/dist/commands/add.js +2 -1
  9. package/dist/commands/add.js.map +1 -1
  10. package/dist/commands/auth.d.ts +0 -3
  11. package/dist/commands/auth.d.ts.map +1 -1
  12. package/dist/commands/auth.js +0 -3
  13. package/dist/commands/auth.js.map +1 -1
  14. package/dist/commands/cloud.d.ts +12 -2
  15. package/dist/commands/cloud.d.ts.map +1 -1
  16. package/dist/commands/cloud.js +112 -80
  17. package/dist/commands/cloud.js.map +1 -1
  18. package/dist/commands/groups.d.ts +3 -0
  19. package/dist/commands/groups.d.ts.map +1 -0
  20. package/dist/commands/groups.js +341 -0
  21. package/dist/commands/groups.js.map +1 -0
  22. package/dist/commands/list.d.ts.map +1 -1
  23. package/dist/commands/list.js +18 -1
  24. package/dist/commands/list.js.map +1 -1
  25. package/dist/commands/login.d.ts +1 -1
  26. package/dist/commands/login.d.ts.map +1 -1
  27. package/dist/commands/login.js +28 -9
  28. package/dist/commands/login.js.map +1 -1
  29. package/dist/commands/logout.d.ts +1 -1
  30. package/dist/commands/logout.d.ts.map +1 -1
  31. package/dist/commands/logout.js +8 -3
  32. package/dist/commands/logout.js.map +1 -1
  33. package/dist/commands/onboard.d.ts +23 -0
  34. package/dist/commands/onboard.d.ts.map +1 -0
  35. package/dist/commands/onboard.js +142 -0
  36. package/dist/commands/onboard.js.map +1 -0
  37. package/dist/commands/pack-install.d.ts +44 -0
  38. package/dist/commands/pack-install.d.ts.map +1 -0
  39. package/dist/commands/pack-install.js +513 -0
  40. package/dist/commands/pack-install.js.map +1 -0
  41. package/dist/commands/pkg-install.d.ts.map +1 -1
  42. package/dist/commands/pkg-install.js +20 -8
  43. package/dist/commands/pkg-install.js.map +1 -1
  44. package/dist/commands/pkg-list.js +4 -4
  45. package/dist/commands/pkg-list.js.map +1 -1
  46. package/dist/commands/pkg-update.js +3 -3
  47. package/dist/commands/pkg-update.js.map +1 -1
  48. package/dist/commands/secrets.d.ts +12 -0
  49. package/dist/commands/secrets.d.ts.map +1 -0
  50. package/dist/commands/secrets.js +756 -0
  51. package/dist/commands/secrets.js.map +1 -0
  52. package/dist/commands/sync.d.ts.map +1 -1
  53. package/dist/commands/sync.js +10 -2
  54. package/dist/commands/sync.js.map +1 -1
  55. package/dist/commands/team-sync.d.ts.map +1 -1
  56. package/dist/commands/team-sync.js +6 -6
  57. package/dist/commands/team-sync.js.map +1 -1
  58. package/dist/commands/whoami.d.ts +1 -1
  59. package/dist/commands/whoami.d.ts.map +1 -1
  60. package/dist/commands/whoami.js +23 -7
  61. package/dist/commands/whoami.js.map +1 -1
  62. package/dist/index.js +24 -2
  63. package/dist/index.js.map +1 -1
  64. package/dist/sentry-before-send.d.ts +3 -0
  65. package/dist/sentry-before-send.d.ts.map +1 -0
  66. package/dist/sentry-before-send.js +216 -0
  67. package/dist/sentry-before-send.js.map +1 -0
  68. package/dist/sentry-dsn.generated.d.ts +2 -0
  69. package/dist/sentry-dsn.generated.d.ts.map +1 -0
  70. package/dist/sentry-dsn.generated.js.map +1 -0
  71. package/dist/sentry.d.ts +4 -0
  72. package/dist/sentry.d.ts.map +1 -0
  73. package/dist/sentry.js +19 -0
  74. package/dist/sentry.js.map +1 -0
  75. package/dist/strategies/link.d.ts +2 -2
  76. package/dist/strategies/link.d.ts.map +1 -1
  77. package/dist/strategies/link.js.map +1 -1
  78. package/dist/strategies/merge.d.ts +2 -2
  79. package/dist/strategies/merge.d.ts.map +1 -1
  80. package/dist/strategies/merge.js.map +1 -1
  81. package/dist/types.d.ts +37 -3
  82. package/dist/types.d.ts.map +1 -1
  83. package/dist/types.js +6 -0
  84. package/dist/types.js.map +1 -1
  85. package/dist/utils/cognito-session.d.ts +18 -7
  86. package/dist/utils/cognito-session.d.ts.map +1 -1
  87. package/dist/utils/cognito-session.js +35 -8
  88. package/dist/utils/cognito-session.js.map +1 -1
  89. package/dist/utils/manifest.d.ts +13 -0
  90. package/dist/utils/manifest.d.ts.map +1 -1
  91. package/dist/utils/manifest.js +24 -1
  92. package/dist/utils/manifest.js.map +1 -1
  93. package/dist/utils/secrets-cache.d.ts +7 -0
  94. package/dist/utils/secrets-cache.d.ts.map +1 -0
  95. package/dist/utils/secrets-cache.js +135 -0
  96. package/dist/utils/secrets-cache.js.map +1 -0
  97. package/package.json +17 -6
  98. package/scripts/generate-dsn.mjs +26 -0
  99. package/src/bin/hq-auth-refresh.ts +23 -16
  100. package/src/commands/_patterns.ts +2 -0
  101. package/src/commands/add.ts +7 -6
  102. package/src/commands/auth.ts +0 -3
  103. package/src/commands/cloud.ts +214 -101
  104. package/src/commands/groups.ts +357 -0
  105. package/src/commands/list.ts +20 -1
  106. package/src/commands/login.ts +28 -9
  107. package/src/commands/logout.ts +8 -3
  108. package/src/commands/onboard.ts +193 -0
  109. package/src/commands/pack-install.ts +667 -0
  110. package/src/commands/pkg-install.ts +34 -16
  111. package/src/commands/pkg-list.ts +4 -4
  112. package/src/commands/pkg-update.ts +3 -3
  113. package/src/commands/secrets.ts +950 -0
  114. package/src/commands/sync.ts +20 -4
  115. package/src/commands/team-sync.ts +7 -8
  116. package/src/commands/whoami.ts +22 -7
  117. package/src/index.ts +26 -2
  118. package/src/sentry-before-send.test.ts +359 -0
  119. package/src/sentry-before-send.ts +221 -0
  120. package/src/sentry.test.ts +24 -0
  121. package/src/sentry.ts +19 -0
  122. package/src/strategies/link.ts +2 -2
  123. package/src/strategies/merge.ts +2 -2
  124. package/src/types.ts +54 -4
  125. package/src/utils/cognito-session.ts +46 -14
  126. package/src/utils/manifest.test.ts +148 -0
  127. package/src/utils/manifest.ts +22 -1
  128. package/src/utils/secrets-cache.ts +130 -0
  129. package/tsconfig.json +2 -1
  130. package/dist/utils/auth.d.ts +0 -27
  131. package/dist/utils/auth.d.ts.map +0 -1
  132. package/dist/utils/auth.js +0 -155
  133. package/dist/utils/auth.js.map +0 -1
  134. package/dist/utils/token-store.d.ts +0 -28
  135. package/dist/utils/token-store.d.ts.map +0 -1
  136. package/dist/utils/token-store.js +0 -68
  137. package/dist/utils/token-store.js.map +0 -1
  138. package/src/utils/auth.ts +0 -193
  139. package/src/utils/token-store.ts +0 -83
@@ -0,0 +1,130 @@
1
+ import * as crypto from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import * as os from "node:os";
5
+
6
+ const CACHE_DIR = path.join(os.homedir(), ".hq", "secrets-cache");
7
+ const KEY_PATH = path.join(CACHE_DIR, ".key");
8
+ const TTL_MS = 5 * 60 * 1000;
9
+ const ALGORITHM = "aes-256-gcm";
10
+ const IV_BYTES = 12;
11
+ const AUTH_TAG_BYTES = 16;
12
+
13
+ function ensureCacheDir(companyUid: string): void {
14
+ const dir = path.join(CACHE_DIR, companyUid);
15
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
16
+ }
17
+
18
+ function validateInputs(companyUid: string, name: string): boolean {
19
+ if (!companyUid || companyUid.includes("/") || companyUid.includes("..")) return false;
20
+ if (!/^[A-Z][A-Z0-9_]*(?:\/[A-Z][A-Z0-9_]+)*$/.test(name)) return false;
21
+ return true;
22
+ }
23
+
24
+ function getOrCreateKey(): Buffer {
25
+ try {
26
+ const key = fs.readFileSync(KEY_PATH);
27
+ if (key.length === 32) {
28
+ const mode = fs.statSync(KEY_PATH).mode & 0o777;
29
+ if (mode !== 0o600) {
30
+ process.stderr.write(`Warning: secrets cache key has permissions ${mode.toString(8)}, expected 600\n`);
31
+ }
32
+ return key;
33
+ }
34
+ } catch {
35
+ // Key doesn't exist or is unreadable — generate a new one
36
+ }
37
+ fs.mkdirSync(CACHE_DIR, { recursive: true, mode: 0o700 });
38
+ const key = crypto.randomBytes(32);
39
+ const tmpPath = `${KEY_PATH}.tmp.${process.pid}`;
40
+ fs.writeFileSync(tmpPath, key, { mode: 0o600 });
41
+ fs.renameSync(tmpPath, KEY_PATH);
42
+ return fs.readFileSync(KEY_PATH);
43
+ }
44
+
45
+ export function readCache(companyUid: string, name: string): string | null {
46
+ if (!validateInputs(companyUid, name)) return null;
47
+ const filePath = path.join(CACHE_DIR, companyUid, name);
48
+ let raw: Buffer;
49
+ try {
50
+ raw = fs.readFileSync(filePath);
51
+ } catch {
52
+ return null;
53
+ }
54
+
55
+ // Format: [8 bytes timestamp][12 bytes IV][16 bytes authTag][...ciphertext]
56
+ const headerLen = 8 + IV_BYTES + AUTH_TAG_BYTES;
57
+ if (raw.length < headerLen) return null;
58
+
59
+ const timestampMs = Number(raw.readBigInt64BE(0));
60
+ if (Date.now() - timestampMs > TTL_MS) {
61
+ try { fs.unlinkSync(filePath); } catch { /* ok */ }
62
+ return null;
63
+ }
64
+
65
+ const iv = raw.subarray(8, 8 + IV_BYTES);
66
+ const authTag = raw.subarray(8 + IV_BYTES, headerLen);
67
+ const ciphertext = raw.subarray(headerLen);
68
+
69
+ let key: Buffer;
70
+ try {
71
+ key = getOrCreateKey();
72
+ } catch {
73
+ return null;
74
+ }
75
+
76
+ try {
77
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
78
+ decipher.setAuthTag(authTag);
79
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
80
+ return decrypted.toString("utf8");
81
+ } catch {
82
+ try { fs.unlinkSync(filePath); } catch { /* ok */ }
83
+ return null;
84
+ }
85
+ }
86
+
87
+ export function writeCache(companyUid: string, name: string, value: string): void {
88
+ try {
89
+ if (!validateInputs(companyUid, name)) return;
90
+ ensureCacheDir(companyUid);
91
+ const key = getOrCreateKey();
92
+ const iv = crypto.randomBytes(IV_BYTES);
93
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
94
+ const encrypted = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
95
+ const authTag = cipher.getAuthTag();
96
+
97
+ const timestamp = Buffer.alloc(8);
98
+ timestamp.writeBigInt64BE(BigInt(Date.now()));
99
+
100
+ const out = Buffer.concat([timestamp, iv, authTag, encrypted]);
101
+ const filePath = path.join(CACHE_DIR, companyUid, name);
102
+ fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
103
+ const tmpPath = `${filePath}.tmp.${process.pid}`;
104
+ fs.writeFileSync(tmpPath, out, { mode: 0o600 });
105
+ fs.renameSync(tmpPath, filePath);
106
+ } catch {
107
+ // Cache write failure is non-fatal
108
+ }
109
+ }
110
+
111
+ export function removeCacheEntry(companyUid: string, name: string): void {
112
+ if (!validateInputs(companyUid, name)) return;
113
+ const filePath = path.join(CACHE_DIR, companyUid, name);
114
+ try {
115
+ fs.unlinkSync(filePath);
116
+ } catch {
117
+ // Cache entry may not exist
118
+ }
119
+ }
120
+
121
+ export function clearAllCache(): { removed: number } {
122
+ let removed = 0;
123
+ try {
124
+ fs.rmSync(CACHE_DIR, { recursive: true, force: true });
125
+ removed = 1;
126
+ } catch {
127
+ // Cache dir may not exist
128
+ }
129
+ return { removed };
130
+ }
package/tsconfig.json CHANGED
@@ -4,5 +4,6 @@
4
4
  "outDir": "dist",
5
5
  "rootDir": "src"
6
6
  },
7
- "include": ["src"]
7
+ "include": ["src"],
8
+ "exclude": ["src/**/*.test.ts"]
8
9
  }
@@ -1,27 +0,0 @@
1
- /**
2
- * Auth utilities — OAuth PKCE flow for Clerk (US-004)
3
- *
4
- * Token values are NEVER written to stdout or logs.
5
- */
6
- import type { AuthToken } from './token-store.js';
7
- /**
8
- * Start the OAuth PKCE auth flow:
9
- * 1. Generate PKCE code_verifier + code_challenge
10
- * 2. Start local HTTP server on a random port for the callback
11
- * 3. Open browser to Clerk auth URL
12
- * 4. Wait for callback with auth code
13
- * 5. Exchange code for token via registry /auth/token endpoint
14
- * 6. Return AuthToken
15
- */
16
- export declare function startAuthFlow(registryUrl: string): Promise<AuthToken>;
17
- /**
18
- * Get a valid auth token:
19
- * 1. Load from store
20
- * 2. If expired or within 5 min buffer: try refresh
21
- * 3. If refresh fails: throw with message to re-login
22
- * 4. Return valid token
23
- *
24
- * Token values are NEVER written to stdout.
25
- */
26
- export declare function getAuthToken(): Promise<AuthToken>;
27
- //# sourceMappingURL=auth.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/utils/auth.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAgClD;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CACjC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,SAAS,CAAC,CA6FpB;AAED;;;;;;;;GAQG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,SAAS,CAAC,CAkCvD"}
@@ -1,155 +0,0 @@
1
- /**
2
- * Auth utilities — OAuth PKCE flow for Clerk (US-004)
3
- *
4
- * Token values are NEVER written to stdout or logs.
5
- */
6
- import * as crypto from 'crypto';
7
- import * as http from 'http';
8
- import { exec } from 'child_process';
9
- import { saveToken, loadToken, isTokenExpired } from './token-store.js';
10
- import { getRegistryUrl } from './registry-client.js';
11
- /**
12
- * Generate a cryptographically random PKCE code verifier (43-128 chars, URL-safe).
13
- */
14
- function generateCodeVerifier() {
15
- return crypto.randomBytes(32).toString('base64url');
16
- }
17
- /**
18
- * Derive the PKCE code challenge from the verifier (S256).
19
- */
20
- function generateCodeChallenge(verifier) {
21
- return crypto.createHash('sha256').update(verifier).digest('base64url');
22
- }
23
- /**
24
- * Open a URL in the default browser (macOS / Linux).
25
- */
26
- function openBrowser(url) {
27
- const command = process.platform === 'darwin' ? `open "${url}"` : `xdg-open "${url}"`;
28
- exec(command, (err) => {
29
- if (err) {
30
- // Non-fatal — user can manually open the URL
31
- console.error(`Could not open browser automatically. Please visit:\n ${url}`);
32
- }
33
- });
34
- }
35
- /**
36
- * Start the OAuth PKCE auth flow:
37
- * 1. Generate PKCE code_verifier + code_challenge
38
- * 2. Start local HTTP server on a random port for the callback
39
- * 3. Open browser to Clerk auth URL
40
- * 4. Wait for callback with auth code
41
- * 5. Exchange code for token via registry /auth/token endpoint
42
- * 6. Return AuthToken
43
- */
44
- export async function startAuthFlow(registryUrl) {
45
- const codeVerifier = generateCodeVerifier();
46
- const codeChallenge = generateCodeChallenge(codeVerifier);
47
- return new Promise((resolve, reject) => {
48
- const server = http.createServer(async (req, res) => {
49
- try {
50
- const url = new URL(req.url ?? '/', `http://localhost`);
51
- if (url.pathname !== '/callback') {
52
- res.writeHead(404);
53
- res.end('Not found');
54
- return;
55
- }
56
- const code = url.searchParams.get('code');
57
- const error = url.searchParams.get('error');
58
- if (error) {
59
- res.writeHead(200, { 'Content-Type': 'text/html' });
60
- res.end('<html><body><h2>Authentication failed</h2><p>You can close this window.</p></body></html>');
61
- server.close();
62
- reject(new Error(`Auth failed: ${error}`));
63
- return;
64
- }
65
- if (!code) {
66
- res.writeHead(400, { 'Content-Type': 'text/html' });
67
- res.end('<html><body><h2>Missing auth code</h2><p>You can close this window.</p></body></html>');
68
- server.close();
69
- reject(new Error('No authorization code received'));
70
- return;
71
- }
72
- // Exchange code for token
73
- const tokenResponse = await fetch(`${registryUrl}/auth/token`, {
74
- method: 'POST',
75
- headers: { 'Content-Type': 'application/json' },
76
- body: JSON.stringify({
77
- code,
78
- code_verifier: codeVerifier,
79
- redirect_uri: `http://localhost:${server.address().port}/callback`,
80
- }),
81
- });
82
- if (!tokenResponse.ok) {
83
- const body = await tokenResponse.text();
84
- res.writeHead(200, { 'Content-Type': 'text/html' });
85
- res.end('<html><body><h2>Token exchange failed</h2><p>You can close this window.</p></body></html>');
86
- server.close();
87
- reject(new Error(`Token exchange failed (${tokenResponse.status}): ${body}`));
88
- return;
89
- }
90
- const tokenData = (await tokenResponse.json());
91
- res.writeHead(200, { 'Content-Type': 'text/html' });
92
- res.end('<html><body><h2>Authenticated!</h2><p>You can close this window and return to the terminal.</p></body></html>');
93
- server.close();
94
- resolve(tokenData);
95
- }
96
- catch (err) {
97
- server.close();
98
- reject(err);
99
- }
100
- });
101
- // Listen on random port
102
- server.listen(0, '127.0.0.1', () => {
103
- const addr = server.address();
104
- const redirectUri = `http://localhost:${addr.port}/callback`;
105
- const authUrl = `${registryUrl}/auth/login?redirect_uri=${encodeURIComponent(redirectUri)}&code_challenge=${codeChallenge}&code_challenge_method=S256`;
106
- console.log('Opening browser for authentication...');
107
- openBrowser(authUrl);
108
- console.log(`\nIf the browser doesn't open, visit:\n ${authUrl}\n`);
109
- });
110
- // Timeout after 2 minutes
111
- setTimeout(() => {
112
- server.close();
113
- reject(new Error('Authentication timed out after 2 minutes'));
114
- }, 120_000);
115
- });
116
- }
117
- /**
118
- * Get a valid auth token:
119
- * 1. Load from store
120
- * 2. If expired or within 5 min buffer: try refresh
121
- * 3. If refresh fails: throw with message to re-login
122
- * 4. Return valid token
123
- *
124
- * Token values are NEVER written to stdout.
125
- */
126
- export async function getAuthToken() {
127
- const token = await loadToken();
128
- if (!token) {
129
- throw new Error("Not logged in. Run 'hq login' to authenticate.");
130
- }
131
- if (!isTokenExpired(token)) {
132
- return token;
133
- }
134
- // Attempt refresh
135
- try {
136
- const registryUrl = getRegistryUrl();
137
- const refreshResponse = await fetch(`${registryUrl}/auth/refresh`, {
138
- method: 'POST',
139
- headers: { 'Content-Type': 'application/json' },
140
- body: JSON.stringify({
141
- clerk_session_token: token.clerk_session_token,
142
- }),
143
- });
144
- if (refreshResponse.ok) {
145
- const refreshedToken = (await refreshResponse.json());
146
- await saveToken(refreshedToken);
147
- return refreshedToken;
148
- }
149
- }
150
- catch {
151
- // Refresh failed — fall through to error
152
- }
153
- throw new Error("Session expired. Run 'hq login' to re-authenticate.");
154
- }
155
- //# sourceMappingURL=auth.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/utils/auth.ts"],"names":[],"mappings":"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"}
@@ -1,28 +0,0 @@
1
- /**
2
- * Auth token store — manages ~/.hq/auth.json with secure file permissions (US-004)
3
- */
4
- export interface AuthToken {
5
- clerk_session_token: string;
6
- user_id: string;
7
- email: string;
8
- expires_at: string;
9
- }
10
- /**
11
- * Save auth token to ~/.hq/auth.json with 0600 permissions.
12
- * Token content is never logged or written to stdout.
13
- */
14
- export declare function saveToken(token: AuthToken): Promise<void>;
15
- /**
16
- * Load auth token from ~/.hq/auth.json.
17
- * Returns null if file is missing, unreadable, or contains invalid JSON.
18
- */
19
- export declare function loadToken(): Promise<AuthToken | null>;
20
- /**
21
- * Delete the auth token file.
22
- */
23
- export declare function clearToken(): Promise<void>;
24
- /**
25
- * Check if a token is expired or within 5 minutes of expiry.
26
- */
27
- export declare function isTokenExpired(token: AuthToken): boolean;
28
- //# sourceMappingURL=token-store.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../../src/utils/token-store.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,MAAM,WAAW,SAAS;IACxB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB;AAWD;;;GAGG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAI/D;AAED;;;GAGG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAsB3D;AAED;;GAEG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAQhD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAIxD"}
@@ -1,68 +0,0 @@
1
- /**
2
- * Auth token store — manages ~/.hq/auth.json with secure file permissions (US-004)
3
- */
4
- import * as fs from 'fs';
5
- import * as path from 'path';
6
- import * as os from 'os';
7
- const HQ_DIR = path.join(os.homedir(), '.hq');
8
- const AUTH_FILE = path.join(HQ_DIR, 'auth.json');
9
- function ensureHqDir() {
10
- if (!fs.existsSync(HQ_DIR)) {
11
- fs.mkdirSync(HQ_DIR, { recursive: true, mode: 0o700 });
12
- }
13
- }
14
- /**
15
- * Save auth token to ~/.hq/auth.json with 0600 permissions.
16
- * Token content is never logged or written to stdout.
17
- */
18
- export async function saveToken(token) {
19
- ensureHqDir();
20
- const content = JSON.stringify(token, null, 2);
21
- fs.writeFileSync(AUTH_FILE, content, { mode: 0o600 });
22
- }
23
- /**
24
- * Load auth token from ~/.hq/auth.json.
25
- * Returns null if file is missing, unreadable, or contains invalid JSON.
26
- */
27
- export async function loadToken() {
28
- try {
29
- if (!fs.existsSync(AUTH_FILE)) {
30
- return null;
31
- }
32
- const content = fs.readFileSync(AUTH_FILE, 'utf-8');
33
- const parsed = JSON.parse(content);
34
- // Validate required fields
35
- if (typeof parsed.clerk_session_token !== 'string' ||
36
- typeof parsed.user_id !== 'string' ||
37
- typeof parsed.email !== 'string' ||
38
- typeof parsed.expires_at !== 'string') {
39
- return null;
40
- }
41
- return parsed;
42
- }
43
- catch {
44
- return null;
45
- }
46
- }
47
- /**
48
- * Delete the auth token file.
49
- */
50
- export async function clearToken() {
51
- try {
52
- if (fs.existsSync(AUTH_FILE)) {
53
- fs.unlinkSync(AUTH_FILE);
54
- }
55
- }
56
- catch {
57
- // Ignore errors — file may already be gone
58
- }
59
- }
60
- /**
61
- * Check if a token is expired or within 5 minutes of expiry.
62
- */
63
- export function isTokenExpired(token) {
64
- const expiresAt = new Date(token.expires_at).getTime();
65
- const bufferMs = 5 * 60 * 1000; // 5 minutes
66
- return Date.now() >= expiresAt - bufferMs;
67
- }
68
- //# sourceMappingURL=token-store.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"token-store.js","sourceRoot":"","sources":["../../src/utils/token-store.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AASzB,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC;AAC9C,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AAEjD,SAAS,WAAW;IAClB,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3B,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACzD,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,KAAgB;IAC9C,WAAW,EAAE,CAAC;IACd,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAC/C,EAAE,CAAC,aAAa,CAAC,SAAS,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AACxD,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS;IAC7B,IAAI,CAAC;QACH,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC9B,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACpD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAEnC,2BAA2B;QAC3B,IACE,OAAO,MAAM,CAAC,mBAAmB,KAAK,QAAQ;YAC9C,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ;YAClC,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ;YAChC,OAAO,MAAM,CAAC,UAAU,KAAK,QAAQ,EACrC,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,MAAmB,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU;IAC9B,IAAI,CAAC;QACH,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC7B,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,2CAA2C;IAC7C,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,KAAgB;IAC7C,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE,CAAC;IACvD,MAAM,QAAQ,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,YAAY;IAC5C,OAAO,IAAI,CAAC,GAAG,EAAE,IAAI,SAAS,GAAG,QAAQ,CAAC;AAC5C,CAAC"}
package/src/utils/auth.ts DELETED
@@ -1,193 +0,0 @@
1
- /**
2
- * Auth utilities — OAuth PKCE flow for Clerk (US-004)
3
- *
4
- * Token values are NEVER written to stdout or logs.
5
- */
6
-
7
- import * as crypto from 'crypto';
8
- import * as http from 'http';
9
- import { exec } from 'child_process';
10
- import { saveToken, loadToken, isTokenExpired } from './token-store.js';
11
- import { getRegistryUrl } from './registry-client.js';
12
- import type { AuthToken } from './token-store.js';
13
-
14
- /**
15
- * Generate a cryptographically random PKCE code verifier (43-128 chars, URL-safe).
16
- */
17
- function generateCodeVerifier(): string {
18
- return crypto.randomBytes(32).toString('base64url');
19
- }
20
-
21
- /**
22
- * Derive the PKCE code challenge from the verifier (S256).
23
- */
24
- function generateCodeChallenge(verifier: string): string {
25
- return crypto.createHash('sha256').update(verifier).digest('base64url');
26
- }
27
-
28
- /**
29
- * Open a URL in the default browser (macOS / Linux).
30
- */
31
- function openBrowser(url: string): void {
32
- const command =
33
- process.platform === 'darwin' ? `open "${url}"` : `xdg-open "${url}"`;
34
- exec(command, (err) => {
35
- if (err) {
36
- // Non-fatal — user can manually open the URL
37
- console.error(
38
- `Could not open browser automatically. Please visit:\n ${url}`
39
- );
40
- }
41
- });
42
- }
43
-
44
- /**
45
- * Start the OAuth PKCE auth flow:
46
- * 1. Generate PKCE code_verifier + code_challenge
47
- * 2. Start local HTTP server on a random port for the callback
48
- * 3. Open browser to Clerk auth URL
49
- * 4. Wait for callback with auth code
50
- * 5. Exchange code for token via registry /auth/token endpoint
51
- * 6. Return AuthToken
52
- */
53
- export async function startAuthFlow(
54
- registryUrl: string
55
- ): Promise<AuthToken> {
56
- const codeVerifier = generateCodeVerifier();
57
- const codeChallenge = generateCodeChallenge(codeVerifier);
58
-
59
- return new Promise<AuthToken>((resolve, reject) => {
60
- const server = http.createServer(async (req, res) => {
61
- try {
62
- const url = new URL(req.url ?? '/', `http://localhost`);
63
-
64
- if (url.pathname !== '/callback') {
65
- res.writeHead(404);
66
- res.end('Not found');
67
- return;
68
- }
69
-
70
- const code = url.searchParams.get('code');
71
- const error = url.searchParams.get('error');
72
-
73
- if (error) {
74
- res.writeHead(200, { 'Content-Type': 'text/html' });
75
- res.end(
76
- '<html><body><h2>Authentication failed</h2><p>You can close this window.</p></body></html>'
77
- );
78
- server.close();
79
- reject(new Error(`Auth failed: ${error}`));
80
- return;
81
- }
82
-
83
- if (!code) {
84
- res.writeHead(400, { 'Content-Type': 'text/html' });
85
- res.end(
86
- '<html><body><h2>Missing auth code</h2><p>You can close this window.</p></body></html>'
87
- );
88
- server.close();
89
- reject(new Error('No authorization code received'));
90
- return;
91
- }
92
-
93
- // Exchange code for token
94
- const tokenResponse = await fetch(`${registryUrl}/auth/token`, {
95
- method: 'POST',
96
- headers: { 'Content-Type': 'application/json' },
97
- body: JSON.stringify({
98
- code,
99
- code_verifier: codeVerifier,
100
- redirect_uri: `http://localhost:${(server.address() as { port: number }).port}/callback`,
101
- }),
102
- });
103
-
104
- if (!tokenResponse.ok) {
105
- const body = await tokenResponse.text();
106
- res.writeHead(200, { 'Content-Type': 'text/html' });
107
- res.end(
108
- '<html><body><h2>Token exchange failed</h2><p>You can close this window.</p></body></html>'
109
- );
110
- server.close();
111
- reject(
112
- new Error(`Token exchange failed (${tokenResponse.status}): ${body}`)
113
- );
114
- return;
115
- }
116
-
117
- const tokenData = (await tokenResponse.json()) as AuthToken;
118
-
119
- res.writeHead(200, { 'Content-Type': 'text/html' });
120
- res.end(
121
- '<html><body><h2>Authenticated!</h2><p>You can close this window and return to the terminal.</p></body></html>'
122
- );
123
- server.close();
124
- resolve(tokenData);
125
- } catch (err) {
126
- server.close();
127
- reject(err);
128
- }
129
- });
130
-
131
- // Listen on random port
132
- server.listen(0, '127.0.0.1', () => {
133
- const addr = server.address() as { port: number };
134
- const redirectUri = `http://localhost:${addr.port}/callback`;
135
- const authUrl = `${registryUrl}/auth/login?redirect_uri=${encodeURIComponent(redirectUri)}&code_challenge=${codeChallenge}&code_challenge_method=S256`;
136
-
137
- console.log('Opening browser for authentication...');
138
- openBrowser(authUrl);
139
- console.log(`\nIf the browser doesn't open, visit:\n ${authUrl}\n`);
140
- });
141
-
142
- // Timeout after 2 minutes
143
- setTimeout(() => {
144
- server.close();
145
- reject(new Error('Authentication timed out after 2 minutes'));
146
- }, 120_000);
147
- });
148
- }
149
-
150
- /**
151
- * Get a valid auth token:
152
- * 1. Load from store
153
- * 2. If expired or within 5 min buffer: try refresh
154
- * 3. If refresh fails: throw with message to re-login
155
- * 4. Return valid token
156
- *
157
- * Token values are NEVER written to stdout.
158
- */
159
- export async function getAuthToken(): Promise<AuthToken> {
160
- const token = await loadToken();
161
-
162
- if (!token) {
163
- throw new Error("Not logged in. Run 'hq login' to authenticate.");
164
- }
165
-
166
- if (!isTokenExpired(token)) {
167
- return token;
168
- }
169
-
170
- // Attempt refresh
171
- try {
172
- const registryUrl = getRegistryUrl();
173
- const refreshResponse = await fetch(`${registryUrl}/auth/refresh`, {
174
- method: 'POST',
175
- headers: { 'Content-Type': 'application/json' },
176
- body: JSON.stringify({
177
- clerk_session_token: token.clerk_session_token,
178
- }),
179
- });
180
-
181
- if (refreshResponse.ok) {
182
- const refreshedToken = (await refreshResponse.json()) as AuthToken;
183
- await saveToken(refreshedToken);
184
- return refreshedToken;
185
- }
186
- } catch {
187
- // Refresh failed — fall through to error
188
- }
189
-
190
- throw new Error(
191
- "Session expired. Run 'hq login' to re-authenticate."
192
- );
193
- }