codehere 1.0.1 → 1.0.2

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 (163) hide show
  1. package/README.md +36 -192
  2. package/dist/application/use-cases/check-claimed-tests.js +81 -0
  3. package/dist/application/use-cases/detect-test-weakening.js +237 -0
  4. package/dist/application/use-cases/rerun-tests.js +110 -7
  5. package/dist/index.js +38 -1212
  6. package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -323
  7. package/dist/presentation/cli/commands/check-command.js +178 -23
  8. package/dist/presentation/cli/commands/help-command.js +21 -285
  9. package/dist/ui.js +5 -236
  10. package/dist/utils/version.js +0 -6
  11. package/package.json +9 -38
  12. package/dist/application/services/verdict-service.js +0 -154
  13. package/dist/application/use-cases/assess-cross-vendor.js +0 -126
  14. package/dist/application/use-cases/build-cross-vendor-verdict.js +0 -118
  15. package/dist/application/use-cases/domain-trust.js +0 -110
  16. package/dist/application/use-cases/export-aat.js +0 -107
  17. package/dist/application/use-cases/generate-acceptance-criteria.js +0 -223
  18. package/dist/application/use-cases/generate-final-report.js +0 -259
  19. package/dist/application/use-cases/generate-narration.js +0 -130
  20. package/dist/application/use-cases/generate-thread-title.js +0 -181
  21. package/dist/application/use-cases/narrator-prompt-template.js +0 -158
  22. package/dist/application/use-cases/plan-auto-review.js +0 -57
  23. package/dist/application/use-cases/reconcile-audit.js +0 -100
  24. package/dist/application/use-cases/search-sessions.js +0 -70
  25. package/dist/application/use-cases/should-auto-review.js +0 -71
  26. package/dist/application/use-cases/thread-export.js +0 -245
  27. package/dist/application/use-cases/verify-claim.js +0 -456
  28. package/dist/application/use-cases/write-pass.js +0 -277
  29. package/dist/domain/audit.js +0 -11
  30. package/dist/domain/services/codebase-detector.js +0 -85
  31. package/dist/domain/task.js +0 -91
  32. package/dist/error-handler.js +0 -139
  33. package/dist/infrastructure/completion/bash-completion.js +0 -29
  34. package/dist/infrastructure/completion/fish-completion.js +0 -17
  35. package/dist/infrastructure/completion/zsh-completion.js +0 -25
  36. package/dist/infrastructure/config/config-manager.js +0 -174
  37. package/dist/infrastructure/config/product-context.js +0 -49
  38. package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
  39. package/dist/infrastructure/context/adapters/figma-source.js +0 -198
  40. package/dist/infrastructure/context/adapters/github-issue-source.js +0 -160
  41. package/dist/infrastructure/context/adapters/linear-source.js +0 -190
  42. package/dist/infrastructure/context/context-resolver.js +0 -144
  43. package/dist/infrastructure/context/context-source.js +0 -114
  44. package/dist/infrastructure/context/github-api.js +0 -110
  45. package/dist/infrastructure/context/github-device-flow.js +0 -65
  46. package/dist/infrastructure/context/oauth-flow.js +0 -207
  47. package/dist/infrastructure/context/oauth-store.js +0 -122
  48. package/dist/infrastructure/context/registry-bootstrap.js +0 -95
  49. package/dist/infrastructure/context/write-pass-resolver.js +0 -106
  50. package/dist/infrastructure/cost/budget-config.js +0 -114
  51. package/dist/infrastructure/cost/cost-router.js +0 -97
  52. package/dist/infrastructure/cost/spend-aggregator.js +0 -222
  53. package/dist/infrastructure/cost/team-ledger.js +0 -355
  54. package/dist/infrastructure/discovery/discovery-log.js +0 -85
  55. package/dist/infrastructure/hooks/audit-tip-signature.js +0 -336
  56. package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -1018
  57. package/dist/infrastructure/hooks/hook-mode.js +0 -87
  58. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -49
  59. package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
  60. package/dist/infrastructure/hooks/session-analyzer.js +0 -177
  61. package/dist/infrastructure/observability/execution-tracer.js +0 -227
  62. package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
  63. package/dist/infrastructure/orchestration/agent-installer.js +0 -101
  64. package/dist/infrastructure/orchestration/agents-config-seed.js +0 -68
  65. package/dist/infrastructure/orchestration/aider-adapter.js +0 -611
  66. package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
  67. package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -592
  68. package/dist/infrastructure/orchestration/codex-adapter.js +0 -574
  69. package/dist/infrastructure/orchestration/conductor-adapter.js +0 -416
  70. package/dist/infrastructure/orchestration/context-handoff.js +0 -162
  71. package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
  72. package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
  73. package/dist/infrastructure/orchestration/detect-ollama.js +0 -75
  74. package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
  75. package/dist/infrastructure/orchestration/failover-config.js +0 -67
  76. package/dist/infrastructure/orchestration/failover-policy.js +0 -106
  77. package/dist/infrastructure/orchestration/gemini-adapter.js +0 -663
  78. package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -689
  79. package/dist/infrastructure/orchestration/graphify-context.js +0 -209
  80. package/dist/infrastructure/orchestration/memory-injector.js +0 -380
  81. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -388
  82. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
  83. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
  84. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
  85. package/dist/infrastructure/orchestration/opencode-adapter.js +0 -580
  86. package/dist/infrastructure/orchestration/progress-aggregator.js +0 -906
  87. package/dist/infrastructure/orchestration/readiness.js +0 -103
  88. package/dist/infrastructure/orchestration/session-manager.js +0 -1590
  89. package/dist/infrastructure/orchestration/session-persistence.js +0 -394
  90. package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
  91. package/dist/infrastructure/orchestration/write-pass-store.js +0 -38
  92. package/dist/infrastructure/protocols/cloud-sync.js +0 -207
  93. package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
  94. package/dist/infrastructure/protocols/mcp-server-impl.js +0 -3935
  95. package/dist/infrastructure/protocols/mcp-tools.js +0 -401
  96. package/dist/infrastructure/protocols/probe-port.js +0 -36
  97. package/dist/infrastructure/protocols/thread-phase.js +0 -105
  98. package/dist/infrastructure/protocols/ui/app-script-artifacts.js +0 -121
  99. package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +0 -44
  100. package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +0 -56
  101. package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +0 -588
  102. package/dist/infrastructure/protocols/ui/app-script-context-sources.js +0 -168
  103. package/dist/infrastructure/protocols/ui/app-script-conversation.js +0 -67
  104. package/dist/infrastructure/protocols/ui/app-script-drawer.js +0 -115
  105. package/dist/infrastructure/protocols/ui/app-script-failover.js +0 -133
  106. package/dist/infrastructure/protocols/ui/app-script-github.js +0 -113
  107. package/dist/infrastructure/protocols/ui/app-script-observer.js +0 -104
  108. package/dist/infrastructure/protocols/ui/app-script-onboarding.js +0 -80
  109. package/dist/infrastructure/protocols/ui/app-script-overlay.js +0 -203
  110. package/dist/infrastructure/protocols/ui/app-script-recovery.js +0 -136
  111. package/dist/infrastructure/protocols/ui/app-script-render.js +0 -166
  112. package/dist/infrastructure/protocols/ui/app-script-share.js +0 -78
  113. package/dist/infrastructure/protocols/ui/app-script-sidebar.js +0 -194
  114. package/dist/infrastructure/protocols/ui/app-script-tour.js +0 -78
  115. package/dist/infrastructure/protocols/ui/app-script-wpass.js +0 -312
  116. package/dist/infrastructure/protocols/ui/app-script.js +0 -2998
  117. package/dist/infrastructure/protocols/ui/font-faces.js +0 -7
  118. package/dist/infrastructure/protocols/ui/styles.js +0 -2147
  119. package/dist/infrastructure/protocols/ui/tokens.js +0 -96
  120. package/dist/infrastructure/protocols/ui-page-v2.js +0 -320
  121. package/dist/infrastructure/protocols/verify-page.js +0 -1677
  122. package/dist/infrastructure/security/file-collection.js +0 -118
  123. package/dist/infrastructure/security/fix-code.js +0 -144
  124. package/dist/infrastructure/security/fix-secret.js +0 -89
  125. package/dist/infrastructure/security/permissions-config.js +0 -235
  126. package/dist/infrastructure/security/scan-types.js +0 -10
  127. package/dist/infrastructure/security/ship-scan.js +0 -195
  128. package/dist/infrastructure/security/trust-scorer.js +0 -724
  129. package/dist/infrastructure/security/vibe-checks.js +0 -99
  130. package/dist/infrastructure/storage/atomic-write.js +0 -86
  131. package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
  132. package/dist/infrastructure/storage/project-thread-migration.js +0 -237
  133. package/dist/infrastructure/storage/project-thread-mirror.js +0 -671
  134. package/dist/infrastructure/storage/project-thread-reader.js +0 -409
  135. package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
  136. package/dist/monitoring.js +0 -212
  137. package/dist/presentation/cli/commands/audit-command.js +0 -201
  138. package/dist/presentation/cli/commands/budget-command.js +0 -79
  139. package/dist/presentation/cli/commands/config-command.js +0 -125
  140. package/dist/presentation/cli/commands/context-source-command.js +0 -217
  141. package/dist/presentation/cli/commands/hook-command.js +0 -265
  142. package/dist/presentation/cli/commands/init-command.js +0 -590
  143. package/dist/presentation/cli/commands/memory-command.js +0 -1031
  144. package/dist/presentation/cli/commands/narrator-command.js +0 -101
  145. package/dist/presentation/cli/commands/permission-command.js +0 -105
  146. package/dist/presentation/cli/commands/protocols-builder.js +0 -254
  147. package/dist/presentation/cli/commands/protocols-command.js +0 -84
  148. package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
  149. package/dist/presentation/cli/commands/report-command.js +0 -403
  150. package/dist/presentation/cli/commands/scan-command.js +0 -52
  151. package/dist/presentation/cli/commands/scan-tool-command.js +0 -110
  152. package/dist/presentation/cli/commands/search-command.js +0 -51
  153. package/dist/presentation/cli/commands/setup-command.js +0 -330
  154. package/dist/presentation/cli/commands/status-command.js +0 -614
  155. package/dist/presentation/cli/commands/team-command.js +0 -256
  156. package/dist/presentation/cli/commands/thread-command.js +0 -370
  157. package/dist/presentation/cli/commands/trust-command.js +0 -328
  158. package/dist/presentation/cli/commands/verify-command.js +0 -298
  159. package/dist/presentation/cli/error-display.js +0 -328
  160. package/dist/presentation/cli/repl.js +0 -805
  161. package/dist/presentation/cli/task-deliverable.js +0 -153
  162. package/dist/testing/cassette.js +0 -125
  163. package/dist/testing/replay-adapter.js +0 -147
@@ -1,110 +0,0 @@
1
- /**
2
- * Minimal GitHub REST layer for the integration — reads an issue and turns it
3
- * into a task prompt. Uses the OAuth token persisted by github-device-flow via
4
- * oauth-store ('github'). HTTP is injectable so tests run without network.
5
- *
6
- * This is the "issues -> tasks" piece of Track 2: fetch the issue, format a
7
- * prompt; the existing POST /api/task does the actual spawn (one proven path).
8
- */
9
- const defaultGet = async (url, headers) => {
10
- const res = await fetch(url, { headers });
11
- return { status: res.status, json: () => res.json() };
12
- };
13
- /** Accepts an issue URL, "owner/repo#123", or "owner/repo" (number then optional). */
14
- export function parseRepoRef(input) {
15
- const s = (input || '').trim();
16
- const url = s.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/i);
17
- if (url)
18
- return { owner: url[1], repo: url[2], number: Number(url[3]) };
19
- const hash = s.match(/^([^/\s]+)\/([^/\s#]+)#(\d+)$/);
20
- if (hash)
21
- return { owner: hash[1], repo: hash[2], number: Number(hash[3]) };
22
- const plain = s.match(/^([^/\s]+)\/([^/\s#]+)$/);
23
- if (plain)
24
- return { owner: plain[1], repo: plain[2] };
25
- return undefined;
26
- }
27
- export async function fetchIssue(token, owner, repo, number, http = defaultGet) {
28
- const res = await http(`https://api.github.com/repos/${owner}/${repo}/issues/${number}`, {
29
- Authorization: `Bearer ${token}`,
30
- Accept: 'application/vnd.github+json',
31
- 'User-Agent': 'codehere',
32
- });
33
- if (res.status === 404)
34
- throw new Error(`github issue not found: ${owner}/${repo}#${number}`);
35
- if (res.status === 401 || res.status === 403)
36
- throw new Error('github auth failed or token lacks repo scope');
37
- if (res.status >= 400)
38
- throw new Error(`github api error ${res.status}`);
39
- const d = (await res.json());
40
- return {
41
- number: Number(d.number ?? number),
42
- title: String(d.title ?? ''),
43
- body: typeof d.body === 'string' ? d.body : '',
44
- htmlUrl: String(d.html_url ?? ''),
45
- state: String(d.state ?? 'open'),
46
- };
47
- }
48
- /** List open issues for a repo so the user can PICK one instead of typing
49
- * "owner/repo#123" from memory. Filters out pull requests (the issues endpoint
50
- * returns PRs too — they carry a `pull_request` field). */
51
- export async function listIssues(token, owner, repo, http = defaultGet) {
52
- const res = await http(`https://api.github.com/repos/${owner}/${repo}/issues?state=open&per_page=50`, {
53
- Authorization: `Bearer ${token}`,
54
- Accept: 'application/vnd.github+json',
55
- 'User-Agent': 'codehere',
56
- });
57
- if (res.status === 404)
58
- throw new Error(`github repo not found: ${owner}/${repo}`);
59
- if (res.status === 401 || res.status === 403)
60
- throw new Error('github auth failed or token lacks repo scope');
61
- if (res.status >= 400)
62
- throw new Error(`github api error ${res.status}`);
63
- const arr = (await res.json());
64
- return (Array.isArray(arr) ? arr : [])
65
- .filter((d) => !d.pull_request)
66
- .map((d) => ({
67
- number: Number(d.number ?? 0),
68
- title: String(d.title ?? ''),
69
- body: typeof d.body === 'string' ? d.body : '',
70
- htmlUrl: String(d.html_url ?? ''),
71
- state: String(d.state ?? 'open'),
72
- }));
73
- }
74
- /** Format an issue as a task prompt an agent can act on. */
75
- export function issueToTaskPrompt(issue) {
76
- const body = issue.body && issue.body.trim() ? `\n\n${issue.body.trim()}` : '';
77
- return `Work on GitHub issue #${issue.number}: ${issue.title}${body}\n\nIssue: ${issue.htmlUrl}`;
78
- }
79
- /** Pull the issue ref out of a task prompt built by issueToTaskPrompt (the
80
- * "Issue: <url>" line), so a finished task knows which issue to report back to. */
81
- export function issueRefFromText(text) {
82
- const m = (text || '').match(/github\.com\/[^\s)]+\/issues\/\d+/i);
83
- if (!m)
84
- return undefined;
85
- const ref = parseRepoRef(m[0]);
86
- return ref && ref.number !== undefined ? { owner: ref.owner, repo: ref.repo, number: ref.number } : undefined;
87
- }
88
- const defaultSend = async (url, method, headers, body) => {
89
- const res = await fetch(url, { method, headers, body });
90
- return { status: res.status, json: () => res.json() };
91
- };
92
- /** Post the agent's result back onto the issue as a comment. Non-destructive —
93
- * closes the issue -> task -> result loop without touching any branches. */
94
- export async function commentOnIssue(token, owner, repo, number, body, http = defaultSend) {
95
- const res = await http(`https://api.github.com/repos/${owner}/${repo}/issues/${number}/comments`, 'POST', {
96
- Authorization: `Bearer ${token}`,
97
- Accept: 'application/vnd.github+json',
98
- 'Content-Type': 'application/json',
99
- 'User-Agent': 'codehere',
100
- }, JSON.stringify({ body }));
101
- if (res.status === 401 || res.status === 403)
102
- throw new Error('github auth failed or token lacks repo scope');
103
- if (res.status === 404)
104
- throw new Error('github issue not found (or no access)');
105
- if (res.status >= 400)
106
- throw new Error(`github comment failed: ${res.status}`);
107
- const d = (await res.json());
108
- return { htmlUrl: String(d.html_url ?? '') };
109
- }
110
- //# sourceMappingURL=github-api.js.map
@@ -1,65 +0,0 @@
1
- const DEVICE_CODE_URL = 'https://github.com/login/device/code';
2
- const TOKEN_URL = 'https://github.com/login/oauth/access_token';
3
- const DEFAULT_SCOPE = 'repo read:org';
4
- const GRANT = 'urn:ietf:params:oauth:grant-type:device_code';
5
- /** The public client_id of codehere's registered GitHub OAuth App. This is a
6
- * PUBLIC value (device flow needs no secret), so it ships as the default — the
7
- * same way the `gh` CLI bundles its own. Set this once the OAuth App with Device
8
- * Flow enabled is registered; until then GitHub connect stays inert and the UI
9
- * shows the "configure your own" fallback. CODEHERE_GITHUB_CLIENT_ID overrides
10
- * it (for users who prefer their own app). */
11
- const BUNDLED_GITHUB_CLIENT_ID = '';
12
- export function githubClientId() {
13
- const env = (process.env.CODEHERE_GITHUB_CLIENT_ID || '').trim();
14
- if (env)
15
- return env;
16
- const bundled = BUNDLED_GITHUB_CLIENT_ID.trim();
17
- return bundled || undefined;
18
- }
19
- const defaultHttp = async (url, body) => {
20
- const res = await fetch(url, {
21
- method: 'POST',
22
- headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
23
- body: JSON.stringify(body),
24
- });
25
- return (await res.json());
26
- };
27
- /** Step 1: ask GitHub for a device + user code. The user enters userCode at
28
- * verificationUri to approve; we then poll for the token. */
29
- export async function requestDeviceCode(clientId, scope = DEFAULT_SCOPE, http = defaultHttp) {
30
- const d = await http(DEVICE_CODE_URL, { client_id: clientId, scope });
31
- if (!d.device_code) {
32
- throw new Error('github device-code request failed' + (d.error ? `: ${String(d.error)}` : ''));
33
- }
34
- return {
35
- deviceCode: String(d.device_code),
36
- userCode: String(d.user_code ?? ''),
37
- verificationUri: String(d.verification_uri ?? 'https://github.com/login/device'),
38
- intervalSec: Number(d.interval ?? 5),
39
- expiresInSec: Number(d.expires_in ?? 900),
40
- };
41
- }
42
- /** Step 2: poll once for the token. Caller repeats at `intervalSec` until it
43
- * resolves to connected or error (RFC 8628: authorization_pending = keep going,
44
- * slow_down = back off). */
45
- export async function pollForToken(clientId, deviceCode, http = defaultHttp) {
46
- const d = await http(TOKEN_URL, { client_id: clientId, device_code: deviceCode, grant_type: GRANT });
47
- if (d.access_token) {
48
- return {
49
- status: 'connected',
50
- token: {
51
- provider: 'github',
52
- accessToken: String(d.access_token),
53
- expiresAt: null, // GitHub OAuth App tokens don't expire unless revoked
54
- scope: d.scope ? String(d.scope) : null,
55
- },
56
- };
57
- }
58
- const err = String(d.error ?? 'unknown');
59
- if (err === 'authorization_pending')
60
- return { status: 'pending' };
61
- if (err === 'slow_down')
62
- return { status: 'slow_down', intervalSec: Number(d.interval ?? 10) };
63
- return { status: 'error', error: err };
64
- }
65
- //# sourceMappingURL=github-device-flow.js.map
@@ -1,207 +0,0 @@
1
- /**
2
- * OAuth 2.0 + PKCE flow for context-source providers (v0.8 S2.5).
3
- *
4
- * Provider-agnostic state machine that handles the four steps of the
5
- * code-grant flow with PKCE (RFC 7636) so we don't need a client secret on
6
- * the user's machine:
7
- *
8
- * 1. begin(provider) · build authorize URL · stash state + verifier
9
- * 2. (browser) user approves · provider redirects to our callback
10
- * 3. complete(code, state) · exchange code for {access_token, refresh_token, user}
11
- * 4. refresh(refresh_token) · swap for a fresh access_token
12
- *
13
- * Tokens persist in ~/.codehere/context-sources.json (the same file manual
14
- * configs use). User can revoke from the provider's web UI any time; our
15
- * disconnect command just deletes the local config.
16
- *
17
- * Provider client IDs come from env vars (CODEHERE_LINEAR_CLIENT_ID,
18
- * CODEHERE_FIGMA_CLIENT_ID) so binaries can ship without secrets baked in.
19
- * The codehere app registrations are public client IDs · PKCE removes the
20
- * need for a client secret on the user's machine. Anyone running codehere
21
- * locally uses the same `codehere-local` app at each provider, no per-user
22
- * registration needed.
23
- *
24
- * IMPORTANT · this module is pure logic. All I/O (httpPost, filesystem,
25
- * clock) is injected. The daemon's HTTP route at /oauth/callback supplies
26
- * the production wiring (registry-bootstrap.ts) and tests use mocks.
27
- */
28
- import { createHash, randomBytes } from 'crypto';
29
- const DEFAULT_REDIRECT = 'http://127.0.0.1:3773/oauth/callback';
30
- export const PROVIDERS = {
31
- linear: {
32
- id: 'linear',
33
- label: 'Linear',
34
- authorizeUrl: 'https://linear.app/oauth/authorize',
35
- tokenUrl: 'https://api.linear.app/oauth/token',
36
- scopes: ['read'],
37
- clientIdEnv: 'CODEHERE_LINEAR_CLIENT_ID',
38
- redirectUri: DEFAULT_REDIRECT,
39
- tokenPostMode: 'form',
40
- userInfoUrl: 'https://api.linear.app/graphql', // POST { query: '{ viewer { name email } }' }
41
- },
42
- figma: {
43
- id: 'figma',
44
- label: 'Figma',
45
- authorizeUrl: 'https://www.figma.com/oauth',
46
- tokenUrl: 'https://api.figma.com/v1/oauth/token',
47
- scopes: ['file_read'],
48
- clientIdEnv: 'CODEHERE_FIGMA_CLIENT_ID',
49
- redirectUri: DEFAULT_REDIRECT,
50
- tokenPostMode: 'form',
51
- userInfoUrl: 'https://api.figma.com/v1/me',
52
- },
53
- };
54
- export function listProviders() {
55
- return Object.values(PROVIDERS);
56
- }
57
- export function getProvider(id) {
58
- return PROVIDERS[id];
59
- }
60
- /* ──────────────────────────────────────────────────────────────────────── */
61
- /* PKCE helpers */
62
- /* ──────────────────────────────────────────────────────────────────────── */
63
- function base64UrlEncode(buf) {
64
- return buf.toString('base64').replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
65
- }
66
- /** Random 43-128 char string · we pick 64 bytes (86 chars after b64url). */
67
- export function generateCodeVerifier() {
68
- return base64UrlEncode(randomBytes(64));
69
- }
70
- export function deriveCodeChallenge(verifier) {
71
- return base64UrlEncode(createHash('sha256').update(verifier).digest());
72
- }
73
- /** Opaque CSRF defense · returned by provider unchanged · we verify on callback. */
74
- export function generateState() {
75
- return base64UrlEncode(randomBytes(24));
76
- }
77
- export function begin(provider, ctx) {
78
- if (!ctx.clientId) {
79
- return { kind: 'not_registered', provider: provider.id, envVar: provider.clientIdEnv };
80
- }
81
- const codeVerifier = ctx.rng?.verifier() ?? generateCodeVerifier();
82
- const codeChallenge = deriveCodeChallenge(codeVerifier);
83
- const state = ctx.rng?.state() ?? generateState();
84
- const startedAt = ctx.now ? ctx.now() : Date.now();
85
- const params = new URLSearchParams({
86
- response_type: 'code',
87
- client_id: ctx.clientId,
88
- redirect_uri: provider.redirectUri,
89
- scope: provider.scopes.join(' '),
90
- state,
91
- code_challenge: codeChallenge,
92
- code_challenge_method: 'S256',
93
- });
94
- return {
95
- authorizeUrl: `${provider.authorizeUrl}?${params.toString()}`,
96
- pending: { provider: provider.id, state, codeVerifier, startedAt },
97
- };
98
- }
99
- export async function complete(code, state, ctx) {
100
- const pending = ctx.pending.get(state);
101
- if (!pending) {
102
- return { kind: 'state_unknown', detail: `no pending flow for state '${state.slice(0, 8)}...'` };
103
- }
104
- ctx.pending.delete(state); // single-use
105
- const provider = getProvider(pending.provider);
106
- if (!provider) {
107
- return { kind: 'state_unknown', detail: `unknown provider '${pending.provider}'` };
108
- }
109
- const maxAge = ctx.maxAgeMs ?? 10 * 60 * 1000;
110
- const now = ctx.now ? ctx.now() : Date.now();
111
- if (now - pending.startedAt > maxAge) {
112
- return { kind: 'state_expired', detail: `pending flow older than ${Math.round(maxAge / 1000)}s` };
113
- }
114
- if (!ctx.clientId) {
115
- return { kind: 'not_registered', detail: provider.clientIdEnv };
116
- }
117
- const params = new URLSearchParams({
118
- grant_type: 'authorization_code',
119
- code,
120
- redirect_uri: provider.redirectUri,
121
- client_id: ctx.clientId,
122
- code_verifier: pending.codeVerifier,
123
- });
124
- const headers = {
125
- 'Content-Type': 'application/x-www-form-urlencoded',
126
- Accept: 'application/json',
127
- };
128
- const res = await ctx.httpPost(provider.tokenUrl, params.toString(), headers);
129
- if (res.status !== 200) {
130
- return {
131
- kind: 'provider_error',
132
- detail: `${provider.label} token endpoint returned ${res.status}: ${res.body.slice(0, 200)}`,
133
- };
134
- }
135
- let payload;
136
- try {
137
- payload = JSON.parse(res.body);
138
- }
139
- catch (err) {
140
- return {
141
- kind: 'parse_error',
142
- detail: `token response not JSON: ${err instanceof Error ? err.message : String(err)}`,
143
- };
144
- }
145
- if (!payload.access_token) {
146
- return { kind: 'parse_error', detail: 'token response missing access_token' };
147
- }
148
- const token = {
149
- provider: provider.id,
150
- accessToken: payload.access_token,
151
- refreshToken: payload.refresh_token,
152
- expiresAt: typeof payload.expires_in === 'number' ? now + payload.expires_in * 1000 : null,
153
- scope: payload.scope ?? provider.scopes.join(' '),
154
- };
155
- // Best-effort user-info fetch · failure here doesn't undo the connect
156
- if (ctx.fetchUserInfo && provider.userInfoUrl) {
157
- try {
158
- const u = await ctx.fetchUserInfo(token.accessToken, provider.userInfoUrl);
159
- if (u) {
160
- const label = u.email || u.name;
161
- if (label)
162
- token.userLabel = label;
163
- }
164
- }
165
- catch {
166
- // ignore · token is valid, user label is decoration
167
- }
168
- }
169
- return token;
170
- }
171
- export async function refresh(provider, refreshToken, ctx) {
172
- if (!ctx.clientId)
173
- return { kind: 'not_registered', detail: provider.clientIdEnv };
174
- const params = new URLSearchParams({
175
- grant_type: 'refresh_token',
176
- refresh_token: refreshToken,
177
- client_id: ctx.clientId,
178
- });
179
- const res = await ctx.httpPost(provider.tokenUrl, params.toString(), {
180
- 'Content-Type': 'application/x-www-form-urlencoded',
181
- Accept: 'application/json',
182
- });
183
- if (res.status !== 200) {
184
- return { kind: 'provider_error', detail: `${provider.label} refresh returned ${res.status}` };
185
- }
186
- let payload;
187
- try {
188
- payload = JSON.parse(res.body);
189
- }
190
- catch (err) {
191
- return {
192
- kind: 'parse_error',
193
- detail: `refresh response not JSON: ${err instanceof Error ? err.message : String(err)}`,
194
- };
195
- }
196
- if (!payload.access_token)
197
- return { kind: 'parse_error', detail: 'refresh missing access_token' };
198
- const now = ctx.now ? ctx.now() : Date.now();
199
- return {
200
- provider: provider.id,
201
- accessToken: payload.access_token,
202
- refreshToken: payload.refresh_token ?? refreshToken,
203
- expiresAt: typeof payload.expires_in === 'number' ? now + payload.expires_in * 1000 : null,
204
- scope: payload.scope ?? null,
205
- };
206
- }
207
- //# sourceMappingURL=oauth-flow.js.map
@@ -1,122 +0,0 @@
1
- /**
2
- * OAuth token storage · v0.8 S2.5.
3
- *
4
- * Persists OAuth tokens to ~/.codehere/context-sources.json next to manual
5
- * configs. File shape evolved · backward-compatible:
6
- *
7
- * {
8
- * "linear": {
9
- * "apiKey": "...", // manual mode · still supported
10
- * "oauth": { // OAuth mode · NEW
11
- * "accessToken": "...",
12
- * "refreshToken": "...",
13
- * "expiresAt": 1700000000000,
14
- * "scope": "read",
15
- * "userLabel": "ega@example.com",
16
- * "connectedAt": 1700000000000
17
- * }
18
- * },
19
- * "figma": { ... }
20
- * }
21
- *
22
- * The linear-source / figma-source adapters read EITHER apiKey OR
23
- * oauth.accessToken (in S3 we'll merge them under one access path; for now
24
- * S2.5 just lands the storage shape so the OAuth flow has somewhere to put
25
- * the token).
26
- *
27
- * File permissions · 0600 on write so a multi-user machine can't read each
28
- * other's tokens.
29
- */
30
- import { chmodSync, existsSync, mkdirSync, readFileSync } from 'fs';
31
- import { writeFileAtomic } from '../storage/atomic-write.js';
32
- import { homedir } from 'os';
33
- import { dirname, join } from 'path';
34
- function configPath() {
35
- const home = process.env.CODEHERE_HOME || homedir();
36
- return join(home, '.codehere', 'context-sources.json');
37
- }
38
- export function readAllConfig(path) {
39
- const target = path ?? configPath();
40
- if (!existsSync(target))
41
- return {};
42
- try {
43
- const raw = readFileSync(target, 'utf-8');
44
- const parsed = JSON.parse(raw);
45
- if (typeof parsed !== 'object' || parsed === null)
46
- return {};
47
- return parsed;
48
- }
49
- catch {
50
- return {};
51
- }
52
- }
53
- function writeAllConfig(cfg, path) {
54
- const target = path ?? configPath();
55
- mkdirSync(dirname(target), { recursive: true });
56
- // mode on the TEMP file, so the credentials are never briefly world-readable
57
- // the way write-then-chmod leaves them. The chmod below stays as belt-and-braces.
58
- writeFileAtomic(target, JSON.stringify(cfg, null, 2) + '\n', { mode: 0o600 });
59
- try {
60
- chmodSync(target, 0o600);
61
- }
62
- catch {
63
- // chmod can fail on FATs · the local-first promise still holds; we just
64
- // can't enforce 0600. Real-world users on macOS/Linux always succeed.
65
- }
66
- }
67
- /** Persist a freshly-minted OAuth token for the given provider. */
68
- export function saveToken(token, path) {
69
- const cfg = readAllConfig(path);
70
- const existing = cfg[token.provider] ?? {};
71
- cfg[token.provider] = {
72
- ...existing,
73
- oauth: {
74
- accessToken: token.accessToken,
75
- refreshToken: token.refreshToken,
76
- expiresAt: token.expiresAt,
77
- scope: token.scope,
78
- userLabel: token.userLabel,
79
- connectedAt: Date.now(),
80
- },
81
- };
82
- writeAllConfig(cfg, path);
83
- }
84
- /** Disconnect · drops only the oauth subtree, preserves any manual apiKey. */
85
- export function clearToken(providerId, path) {
86
- const cfg = readAllConfig(path);
87
- const entry = cfg[providerId];
88
- if (!entry || !entry.oauth)
89
- return false;
90
- delete entry.oauth;
91
- // If the entry is now empty (no manual apiKey either), drop the whole row.
92
- const isEmpty = Object.keys(entry).filter((k) => k !== 'oauth').length === 0;
93
- if (isEmpty)
94
- delete cfg[providerId];
95
- writeAllConfig(cfg, path);
96
- return true;
97
- }
98
- /** Read the persisted OAuth token for a provider (undefined if not connected). */
99
- export function loadToken(providerId, path) {
100
- const cfg = readAllConfig(path);
101
- return cfg[providerId]?.oauth;
102
- }
103
- export function connectionStatus(providerId, path) {
104
- const cfg = readAllConfig(path);
105
- const entry = cfg[providerId];
106
- if (!entry)
107
- return { status: 'disconnected' };
108
- if (entry.oauth?.accessToken) {
109
- const exp = entry.oauth.expiresAt;
110
- const expired = typeof exp === 'number' && exp < Date.now();
111
- return {
112
- status: expired ? 'expired' : 'connected',
113
- userLabel: entry.oauth.userLabel,
114
- connectedAt: entry.oauth.connectedAt,
115
- expiresAt: exp,
116
- };
117
- }
118
- if (entry.apiKey || entry.token)
119
- return { status: 'manual' };
120
- return { status: 'disconnected' };
121
- }
122
- //# sourceMappingURL=oauth-store.js.map
@@ -1,95 +0,0 @@
1
- /**
2
- * Registry bootstrap · v0.8 S2.
3
- *
4
- * Builds the default ContextSourceRegistry at daemon startup with our 1st-
5
- * party adapters (Linear, Figma), loads adapter config from
6
- * `~/.codehere/context-sources.json`, and provides a default
7
- * ContextSourceFetchContext that wraps node's fetch() for the network path
8
- * and the audit chain for telemetry.
9
- *
10
- * This file is the ONLY place where context-sources touch process.env, the
11
- * filesystem, and global fetch · everything else is pure code + injected
12
- * primitives. Makes tests trivial and the security boundary explicit.
13
- */
14
- import { existsSync, readFileSync } from 'fs';
15
- import { homedir } from 'os';
16
- import { join } from 'path';
17
- import { createRegistry, register, } from './context-source.js';
18
- import { linearSource } from './adapters/linear-source.js';
19
- import { figmaSource } from './adapters/figma-source.js';
20
- import { drawioSource } from './adapters/drawio-source.js';
21
- import { githubIssueSource } from './adapters/github-issue-source.js';
22
- /** Built once per daemon · in-memory · re-read from disk on demand via `reload`. */
23
- let cached = null;
24
- export function getDefaultRegistry() {
25
- if (cached)
26
- return cached;
27
- const reg = createRegistry();
28
- register(reg, linearSource);
29
- register(reg, figmaSource);
30
- register(reg, drawioSource);
31
- register(reg, githubIssueSource);
32
- cached = reg;
33
- return reg;
34
- }
35
- /**
36
- * Resolve config path · respects CODEHERE_HOME env so tests can isolate.
37
- * File format: a JSON object keyed by adapter id, e.g.
38
- * { "linear": { "apiKey": "..." }, "figma": { "token": "..." } }
39
- */
40
- function configPath() {
41
- const home = process.env.CODEHERE_HOME || homedir();
42
- return join(home, '.codehere', 'context-sources.json');
43
- }
44
- function loadConfig() {
45
- const path = configPath();
46
- if (!existsSync(path))
47
- return {};
48
- try {
49
- const raw = readFileSync(path, 'utf-8');
50
- const parsed = JSON.parse(raw);
51
- if (typeof parsed !== 'object' || parsed === null)
52
- return {};
53
- return parsed;
54
- }
55
- catch {
56
- return {};
57
- }
58
- }
59
- /**
60
- * Real-network FetchContext · used by the daemon at runtime. Tests inject
61
- * a fake context so this code path never runs in CI.
62
- */
63
- export function createDefaultFetchContext(opts) {
64
- const config = opts.configOverride ?? loadConfig();
65
- return {
66
- config,
67
- async httpGet(target, headers) {
68
- const r = await fetch(target, { method: 'GET', headers: headers ?? {} });
69
- const body = await r.text();
70
- const hOut = {};
71
- r.headers.forEach((v, k) => {
72
- hOut[k.toLowerCase()] = v;
73
- });
74
- return { status: r.status, body, headers: hOut };
75
- },
76
- emitAuditLine: opts.emitAuditLine,
77
- now: () => Date.now(),
78
- };
79
- }
80
- export function createDefaultFetchContextWithPost(opts) {
81
- const base = createDefaultFetchContext(opts);
82
- return {
83
- ...base,
84
- async httpPost(target, body, headers) {
85
- const r = await fetch(target, { method: 'POST', headers: headers ?? {}, body });
86
- const text = await r.text();
87
- const hOut = {};
88
- r.headers.forEach((v, k) => {
89
- hOut[k.toLowerCase()] = v;
90
- });
91
- return { status: r.status, body: text, headers: hOut };
92
- },
93
- };
94
- }
95
- //# sourceMappingURL=registry-bootstrap.js.map