@crouton-kit/crouter 0.3.40 → 0.3.42

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 (195) hide show
  1. package/dist/build-root.js +1 -0
  2. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +472 -9
  3. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +284 -76
  4. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/strip-skills-docs.ts +31 -24
  5. package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +52 -15
  6. package/dist/clients/attach/attach-cmd.js +720 -747
  7. package/dist/clients/attach/chat-view.js +11 -0
  8. package/dist/clients/attach/view-socket.d.ts +8 -1
  9. package/dist/clients/attach/view-socket.js +15 -1
  10. package/dist/clients/web/server.js +5 -28
  11. package/dist/clients/web/web-cmd.js +1 -1
  12. package/dist/commands/__tests__/revive-now-gate.test.js +60 -0
  13. package/dist/commands/canvas-rebuild-index.js +5 -5
  14. package/dist/commands/memory/shared.d.ts +7 -3
  15. package/dist/commands/memory/shared.js +35 -5
  16. package/dist/commands/memory/write.js +5 -3
  17. package/dist/commands/node-snapshot.js +9 -1
  18. package/dist/commands/node.js +37 -17
  19. package/dist/commands/push.js +8 -0
  20. package/dist/commands/revive.d.ts +10 -0
  21. package/dist/commands/revive.js +28 -10
  22. package/dist/commands/sys/__tests__/setup-core.test.js +19 -0
  23. package/dist/commands/sys/doctor.js +1 -1
  24. package/dist/commands/sys/setup-core.js +3 -2
  25. package/dist/commands/sys/setup.js +1 -1
  26. package/dist/commands/worktree.d.ts +2 -0
  27. package/dist/commands/worktree.js +94 -0
  28. package/dist/core/__tests__/boot.test.js +4 -4
  29. package/dist/core/__tests__/canvas.test.js +19 -7
  30. package/dist/core/__tests__/child-followup.test.js +15 -5
  31. package/dist/core/__tests__/daemon-boot.test.js +6 -1
  32. package/dist/core/__tests__/daemon-wedge.test.js +18 -1
  33. package/dist/core/__tests__/fault-classifier.test.js +30 -0
  34. package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -0
  35. package/dist/core/__tests__/fixtures/fake-engine.js +20 -0
  36. package/dist/core/__tests__/full/spike-harness.test.js +8 -5
  37. package/dist/core/__tests__/grace-clock.test.js +18 -2
  38. package/dist/core/__tests__/host-teardown-process-group.test.js +465 -0
  39. package/dist/core/__tests__/prune-to-limit.test.js +14 -0
  40. package/dist/core/__tests__/review-model-floor.test.js +32 -0
  41. package/dist/core/__tests__/revive.test.js +18 -4
  42. package/dist/core/__tests__/session-cycles.test.js +77 -0
  43. package/dist/core/__tests__/worktree.test.js +85 -0
  44. package/dist/core/canvas/boot.js +12 -7
  45. package/dist/core/canvas/canvas.d.ts +27 -8
  46. package/dist/core/canvas/canvas.js +54 -26
  47. package/dist/core/canvas/db.js +14 -0
  48. package/dist/core/canvas/history.js +1 -0
  49. package/dist/core/canvas/paths.d.ts +10 -9
  50. package/dist/core/canvas/paths.js +10 -9
  51. package/dist/core/canvas/pid.d.ts +155 -1
  52. package/dist/core/canvas/pid.js +306 -1
  53. package/dist/core/canvas/status-glyph.d.ts +7 -0
  54. package/dist/core/canvas/status-glyph.js +10 -1
  55. package/dist/core/canvas/types.d.ts +34 -0
  56. package/dist/core/fault-classifier.js +5 -1
  57. package/dist/core/profiles/select.d.ts +4 -2
  58. package/dist/core/profiles/select.js +30 -4
  59. package/dist/core/runtime/bearings.js +4 -0
  60. package/dist/core/runtime/branded-host.d.ts +7 -0
  61. package/dist/core/runtime/branded-host.js +44 -17
  62. package/dist/core/runtime/broker-sdk.js +28 -68
  63. package/dist/core/runtime/broker.js +35 -4
  64. package/dist/core/runtime/host.d.ts +3 -3
  65. package/dist/core/runtime/host.js +148 -33
  66. package/dist/core/runtime/launch.d.ts +15 -15
  67. package/dist/core/runtime/launch.js +58 -4
  68. package/dist/core/runtime/naming.js +3 -2
  69. package/dist/core/runtime/nodes.d.ts +3 -1
  70. package/dist/core/runtime/nodes.js +1 -0
  71. package/dist/core/runtime/pi-cli.d.ts +2 -0
  72. package/dist/core/runtime/pi-cli.js +51 -0
  73. package/dist/core/runtime/placement.d.ts +1 -1
  74. package/dist/core/runtime/placement.js +5 -2
  75. package/dist/core/runtime/recap.js +2 -1
  76. package/dist/core/runtime/revive.d.ts +21 -15
  77. package/dist/core/runtime/revive.js +87 -52
  78. package/dist/core/runtime/session-cycles.d.ts +30 -0
  79. package/dist/core/runtime/session-cycles.js +77 -0
  80. package/dist/core/runtime/spawn.d.ts +4 -0
  81. package/dist/core/runtime/spawn.js +152 -100
  82. package/dist/core/runtime/tmux.d.ts +7 -10
  83. package/dist/core/runtime/tmux.js +9 -11
  84. package/dist/core/worktree.d.ts +35 -0
  85. package/dist/core/worktree.js +158 -0
  86. package/dist/daemon/crtrd.d.ts +17 -8
  87. package/dist/daemon/crtrd.js +191 -40
  88. package/dist/index.d.ts +3 -0
  89. package/dist/index.js +7 -0
  90. package/dist/pi-extensions/canvas-context-intro.d.ts +1 -0
  91. package/dist/pi-extensions/canvas-context-intro.js +34 -23
  92. package/dist/pi-extensions/canvas-stophook.js +11 -5
  93. package/dist/web-client/assets/{index-CbO8L0mN.js → index-B00YpRQ1.js} +20 -20
  94. package/dist/web-client/assets/index-DrkcvANq.css +2 -0
  95. package/dist/web-client/index.html +2 -2
  96. package/docs/compat/hearth-crtr-v1.md +191 -0
  97. package/docs/public-api.md +75 -0
  98. package/package.json +3 -4
  99. package/dist/core/__tests__/hearth-bootstrap.test.js +0 -136
  100. package/dist/core/hearth/__tests__/model-auth-guest.test.js +0 -151
  101. package/dist/core/hearth/config.d.ts +0 -3
  102. package/dist/core/hearth/config.js +0 -108
  103. package/dist/core/hearth/guest-env.d.ts +0 -9
  104. package/dist/core/hearth/guest-env.js +0 -27
  105. package/dist/core/hearth/index.d.ts +0 -4
  106. package/dist/core/hearth/index.js +0 -4
  107. package/dist/core/hearth/model-auth-guest.d.ts +0 -8
  108. package/dist/core/hearth/model-auth-guest.js +0 -430
  109. package/dist/core/hearth/provider.d.ts +0 -36
  110. package/dist/core/hearth/provider.js +0 -10
  111. package/dist/core/hearth/providers/__tests__/sweep-and-release.test.js +0 -362
  112. package/dist/core/hearth/providers/blaxel-bootstrap.d.ts +0 -12
  113. package/dist/core/hearth/providers/blaxel-bootstrap.js +0 -147
  114. package/dist/core/hearth/providers/blaxel-home.d.ts +0 -60
  115. package/dist/core/hearth/providers/blaxel-home.js +0 -405
  116. package/dist/core/hearth/providers/blaxel.d.ts +0 -36
  117. package/dist/core/hearth/providers/blaxel.js +0 -364
  118. package/dist/core/hearth/providers/types.d.ts +0 -93
  119. package/dist/core/hearth/types.d.ts +0 -155
  120. package/dist/hearth/control-plane/__tests__/error-serialization.test.js +0 -29
  121. package/dist/hearth/control-plane/__tests__/node-message.test.js +0 -60
  122. package/dist/hearth/control-plane/__tests__/oauth-serving-marker.test.d.ts +0 -1
  123. package/dist/hearth/control-plane/__tests__/oauth-serving-marker.test.js +0 -44
  124. package/dist/hearth/control-plane/__tests__/rate-limit-recurrence.test.d.ts +0 -1
  125. package/dist/hearth/control-plane/__tests__/rate-limit-recurrence.test.js +0 -49
  126. package/dist/hearth/control-plane/__tests__/relay-security.test.d.ts +0 -1
  127. package/dist/hearth/control-plane/__tests__/relay-security.test.js +0 -314
  128. package/dist/hearth/control-plane/__tests__/scheduler-scan-loop.test.d.ts +0 -1
  129. package/dist/hearth/control-plane/__tests__/scheduler-scan-loop.test.js +0 -133
  130. package/dist/hearth/control-plane/__tests__/trigger-delivery.test.d.ts +0 -1
  131. package/dist/hearth/control-plane/__tests__/trigger-delivery.test.js +0 -170
  132. package/dist/hearth/control-plane/__tests__/wake-roll.test.d.ts +0 -1
  133. package/dist/hearth/control-plane/__tests__/wake-roll.test.js +0 -230
  134. package/dist/hearth/control-plane/__tests__/webhook-ingress.test.d.ts +0 -1
  135. package/dist/hearth/control-plane/__tests__/webhook-ingress.test.js +0 -167
  136. package/dist/hearth/control-plane/config.d.ts +0 -21
  137. package/dist/hearth/control-plane/config.js +0 -77
  138. package/dist/hearth/control-plane/db.d.ts +0 -30
  139. package/dist/hearth/control-plane/db.js +0 -561
  140. package/dist/hearth/control-plane/hearth-target.d.ts +0 -23
  141. package/dist/hearth/control-plane/hearth-target.js +0 -68
  142. package/dist/hearth/control-plane/ingress/rate-limit.d.ts +0 -24
  143. package/dist/hearth/control-plane/ingress/rate-limit.js +0 -100
  144. package/dist/hearth/control-plane/ingress/route-store.d.ts +0 -31
  145. package/dist/hearth/control-plane/ingress/route-store.js +0 -61
  146. package/dist/hearth/control-plane/ingress/webhook-delivery-store.d.ts +0 -41
  147. package/dist/hearth/control-plane/ingress/webhook-delivery-store.js +0 -69
  148. package/dist/hearth/control-plane/ingress/webhook-route.d.ts +0 -55
  149. package/dist/hearth/control-plane/ingress/webhook-route.js +0 -285
  150. package/dist/hearth/control-plane/main.d.ts +0 -1
  151. package/dist/hearth/control-plane/main.js +0 -88
  152. package/dist/hearth/control-plane/node-message.d.ts +0 -31
  153. package/dist/hearth/control-plane/node-message.js +0 -98
  154. package/dist/hearth/control-plane/register.d.ts +0 -15
  155. package/dist/hearth/control-plane/register.js +0 -34
  156. package/dist/hearth/control-plane/registry.d.ts +0 -22
  157. package/dist/hearth/control-plane/registry.js +0 -168
  158. package/dist/hearth/control-plane/relay.d.ts +0 -44
  159. package/dist/hearth/control-plane/relay.js +0 -711
  160. package/dist/hearth/control-plane/scheduler/fire-store.d.ts +0 -36
  161. package/dist/hearth/control-plane/scheduler/fire-store.js +0 -73
  162. package/dist/hearth/control-plane/scheduler/recurrence.d.ts +0 -7
  163. package/dist/hearth/control-plane/scheduler/recurrence.js +0 -58
  164. package/dist/hearth/control-plane/scheduler/scan-loop.d.ts +0 -38
  165. package/dist/hearth/control-plane/scheduler/scan-loop.js +0 -138
  166. package/dist/hearth/control-plane/scheduler/schedule-store.d.ts +0 -32
  167. package/dist/hearth/control-plane/scheduler/schedule-store.js +0 -66
  168. package/dist/hearth/control-plane/secrets.d.ts +0 -31
  169. package/dist/hearth/control-plane/secrets.js +0 -134
  170. package/dist/hearth/control-plane/server.d.ts +0 -27
  171. package/dist/hearth/control-plane/server.js +0 -482
  172. package/dist/hearth/control-plane/serving.d.ts +0 -15
  173. package/dist/hearth/control-plane/serving.js +0 -106
  174. package/dist/hearth/control-plane/session.d.ts +0 -68
  175. package/dist/hearth/control-plane/session.js +0 -273
  176. package/dist/hearth/control-plane/triggers/acl.d.ts +0 -14
  177. package/dist/hearth/control-plane/triggers/acl.js +0 -52
  178. package/dist/hearth/control-plane/triggers/audit-store.d.ts +0 -38
  179. package/dist/hearth/control-plane/triggers/audit-store.js +0 -79
  180. package/dist/hearth/control-plane/triggers/deliver.d.ts +0 -43
  181. package/dist/hearth/control-plane/triggers/deliver.js +0 -76
  182. package/dist/hearth/control-plane/triggers/envelope.d.ts +0 -29
  183. package/dist/hearth/control-plane/triggers/envelope.js +0 -38
  184. package/dist/hearth/control-plane/types.d.ts +0 -86
  185. package/dist/hearth/control-plane/types.js +0 -1
  186. package/dist/hearth/control-plane/wake.d.ts +0 -86
  187. package/dist/hearth/control-plane/wake.js +0 -550
  188. package/dist/web-client/assets/index-DwO46Cs5.css +0 -2
  189. /package/dist/{core/__tests__/hearth-bootstrap.test.d.ts → commands/__tests__/revive-now-gate.test.d.ts} +0 -0
  190. /package/dist/{core/hearth/__tests__/model-auth-guest.test.d.ts → commands/sys/__tests__/setup-core.test.d.ts} +0 -0
  191. /package/dist/core/{hearth/providers/__tests__/sweep-and-release.test.d.ts → __tests__/fault-classifier.test.d.ts} +0 -0
  192. /package/dist/core/{hearth/providers/types.js → __tests__/host-teardown-process-group.test.d.ts} +0 -0
  193. /package/dist/core/{hearth/types.js → __tests__/review-model-floor.test.d.ts} +0 -0
  194. /package/dist/{hearth/control-plane/__tests__/error-serialization.test.d.ts → core/__tests__/session-cycles.test.d.ts} +0 -0
  195. /package/dist/{hearth/control-plane/__tests__/node-message.test.d.ts → core/__tests__/worktree.test.d.ts} +0 -0
@@ -1,430 +0,0 @@
1
- // model-auth-guest.ts — Hearth product seam: the in-guest model-auth lane.
2
- //
3
- // Handles GET/POST /__hearth/model-auth/anthropic/{status,start,finish}, mounted
4
- // by src/clients/web/server.ts (the crtr web substrate provides only routing +
5
- // relay-token enforcement there; ALL Hearth/OAuth product logic lives here).
6
- //
7
- // This handler runs IN the guest: it performs the PKCE/OAuth exchange itself
8
- // and talks to pi's AuthStorage directly to read/write the credential — no
9
- // exec round-trip to another process. There is no tenantId/owner-auth
10
- // resolution: the relay token already gates the whole lane one layer up, and
11
- // a guest serves exactly one home.
12
- //
13
- // This module must not import anything from src/clients/web (the seam runs the
14
- // other direction: server.ts imports `handleHearthModelAuthRequest` from here)
15
- // — it is a standalone, self-contained Hearth handler.
16
- import { createHash, randomBytes as nodeRandomBytes } from 'node:crypto';
17
- import { AuthStorage } from '@earendil-works/pi-coding-agent';
18
- const ANTHROPIC_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
19
- const ANTHROPIC_AUTHORIZE_URL = 'https://claude.ai/oauth/authorize';
20
- const ANTHROPIC_TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
21
- const ANTHROPIC_REDIRECT_URI = 'http://localhost:53692/callback';
22
- const ANTHROPIC_SCOPES = 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload';
23
- const ANTHROPIC_SESSION_TTL_MS = 10 * 60_000;
24
- const ANTHROPIC_CALLBACK_MAX_URL_LENGTH = 2048;
25
- const ANTHROPIC_TOKEN_TIMEOUT_MS = 30_000;
26
- const ANTHROPIC_TOKEN_SKEW_MS = 5 * 60_000;
27
- const BODY_LIMIT_BYTES = 16 * 1024;
28
- // ── Redaction ────────────────────────────────────────────────────────────
29
- // Never let a raw access/refresh token, bearer header, auth.json path, or
30
- // OAuth query param reach a log line or an HTTP response body.
31
- const REDIRECT_URL_RE = /https?:\/\/localhost:53692\/callback[^\s"'`<>)]*/gi;
32
- const BEARER_RE = /\bBearer\s+[A-Za-z0-9._~+/-]+=?=?/gi;
33
- const TOKEN_QUERY_RE = /([?&](?:token|code|state|code_verifier)=)[^&#\s]+/gi;
34
- const JSON_TOKEN_RE = /("(?:access_token|refresh_token|access|refresh|code|state|code_verifier)"\s*:\s*")[^"]+(")/gi;
35
- const ASSIGN_TOKEN_RE = /\b(?:access_token|refresh_token|access|refresh|code|state|code_verifier)\s*[:=]\s*[^\s,&}]+/gi;
36
- const AUTH_JSON_RE = /\bauth\.json\b/gi;
37
- const SHA256_RE = /\bsha256:[0-9a-f]{64}\b/gi;
38
- function redact(message) {
39
- return message
40
- .replace(REDIRECT_URL_RE, '[redacted redirect url]')
41
- .replace(BEARER_RE, 'Bearer [redacted]')
42
- .replace(TOKEN_QUERY_RE, '$1[redacted]')
43
- .replace(JSON_TOKEN_RE, '$1[redacted]$2')
44
- .replace(ASSIGN_TOKEN_RE, (value) => value.replace(/[:=]\s*[^\s,&}]+$/, ': [redacted]'))
45
- .replace(AUTH_JSON_RE, '[redacted auth.json]')
46
- .replace(SHA256_RE, 'sha256:[redacted]');
47
- }
48
- function redactedErrorMessage(error) {
49
- return redact(error instanceof Error ? error.message : String(error));
50
- }
51
- let currentSession;
52
- function defaultRandomBytes(size) {
53
- return nodeRandomBytes(size);
54
- }
55
- function defaultSha256(value) {
56
- return createHash('sha256').update(value, 'utf8').digest();
57
- }
58
- function purgeExpiredSession(now) {
59
- if (currentSession !== undefined && currentSession.expiresAt <= now) {
60
- currentSession = undefined;
61
- }
62
- }
63
- function buildAuthUrl(session, challenge) {
64
- const params = new URLSearchParams({
65
- // claude.ai/oauth/authorize requires `code=true` (the Claude Code authorize
66
- // flow param); omitting it makes the endpoint reject with "Invalid request
67
- // format". Claude Code also requires `state` to be the PKCE verifier so the
68
- // authorize request satisfies `code_challenge === S256(state)`; the
69
- // installed @earendil-works/pi-ai adapter does the same (`state: verifier`).
70
- code: 'true',
71
- client_id: ANTHROPIC_CLIENT_ID,
72
- response_type: 'code',
73
- redirect_uri: ANTHROPIC_REDIRECT_URI,
74
- scope: ANTHROPIC_SCOPES,
75
- state: session.state,
76
- code_challenge: challenge,
77
- code_challenge_method: 'S256',
78
- });
79
- return `${ANTHROPIC_AUTHORIZE_URL}?${params.toString()}`;
80
- }
81
- function failure(status, error) {
82
- return { ok: false, status, error: redact(error) };
83
- }
84
- function parseCallbackUrl(raw) {
85
- if (raw.length > ANTHROPIC_CALLBACK_MAX_URL_LENGTH) {
86
- return failure(400, 'Anthropic callback URL is too long');
87
- }
88
- let url;
89
- try {
90
- url = new URL(raw);
91
- }
92
- catch {
93
- return failure(400, 'Anthropic callback URL is invalid');
94
- }
95
- if (url.protocol !== 'http:' || url.hostname !== 'localhost' || url.port !== '53692' || url.pathname !== '/callback') {
96
- return failure(400, 'Anthropic callback URL must be http://localhost:53692/callback');
97
- }
98
- const error = url.searchParams.get('error');
99
- if (error !== null && error.trim() !== '') {
100
- return failure(400, `Anthropic callback reported error=${error}`);
101
- }
102
- const code = url.searchParams.get('code')?.trim() ?? '';
103
- const state = url.searchParams.get('state')?.trim() ?? '';
104
- if (code === '' || state === '') {
105
- return failure(400, 'Anthropic callback is missing code or state');
106
- }
107
- return { code, state };
108
- }
109
- async function postTokenExchange(body) {
110
- const controller = new AbortController();
111
- const timeout = setTimeout(() => {
112
- controller.abort(new Error('Anthropic OAuth token exchange timed out'));
113
- }, ANTHROPIC_TOKEN_TIMEOUT_MS);
114
- timeout.unref();
115
- try {
116
- const response = await fetch(ANTHROPIC_TOKEN_URL, {
117
- method: 'POST',
118
- headers: { 'content-type': 'application/json', accept: 'application/json' },
119
- body: JSON.stringify(body),
120
- signal: controller.signal,
121
- });
122
- const responseBody = await response.text();
123
- if (!response.ok) {
124
- return failure(502, `Anthropic token exchange failed with HTTP ${response.status} ${response.statusText}${responseBody.trim() === '' ? '' : `: ${responseBody}`}`);
125
- }
126
- return { ok: true, body: responseBody };
127
- }
128
- catch (error) {
129
- return failure(502, `Anthropic token exchange request failed: ${redactedErrorMessage(error)}`);
130
- }
131
- finally {
132
- clearTimeout(timeout);
133
- }
134
- }
135
- function validateTokenExchangeResponse(body, now) {
136
- let parsed;
137
- try {
138
- parsed = JSON.parse(body);
139
- }
140
- catch {
141
- return failure(502, `Anthropic token exchange returned invalid JSON: ${body}`);
142
- }
143
- const access = typeof parsed.access_token === 'string' ? parsed.access_token.trim() : '';
144
- const refresh = typeof parsed.refresh_token === 'string' ? parsed.refresh_token.trim() : '';
145
- const expiresIn = typeof parsed.expires_in === 'number' ? parsed.expires_in : Number.NaN;
146
- if (access === '' || refresh === '' || !Number.isFinite(expiresIn) || expiresIn <= 0) {
147
- return failure(502, 'Anthropic token exchange response is missing access_token, refresh_token, or positive expires_in');
148
- }
149
- return { type: 'oauth', access, refresh, expires: now + expiresIn * 1000 - ANTHROPIC_TOKEN_SKEW_MS };
150
- }
151
- function startAnthropicOAuthSession(now) {
152
- purgeExpiredSession(now);
153
- const sessionId = defaultRandomBytes(16).toString('base64url');
154
- const verifier = defaultRandomBytes(32).toString('base64url');
155
- const session = {
156
- sessionId,
157
- state: verifier,
158
- verifier,
159
- createdAt: now,
160
- expiresAt: now + ANTHROPIC_SESSION_TTL_MS,
161
- };
162
- const challenge = defaultSha256(verifier).toString('base64url');
163
- currentSession = session;
164
- return { sessionId, authUrl: buildAuthUrl(session, challenge), expiresAt: session.expiresAt };
165
- }
166
- async function finishAnthropicOAuthSession(sessionId, redirectUrl) {
167
- const now = Date.now();
168
- purgeExpiredSession(now);
169
- const session = currentSession;
170
- if (session === undefined) {
171
- return failure(400, 'Anthropic OAuth session not found or expired');
172
- }
173
- // Delete on first use regardless of outcome below — a session is single-shot.
174
- currentSession = undefined;
175
- if (session.sessionId !== sessionId) {
176
- return failure(400, 'Anthropic OAuth session id does not match the active session');
177
- }
178
- const parsed = parseCallbackUrl(redirectUrl);
179
- if ('status' in parsed)
180
- return parsed;
181
- if (parsed.state !== session.state) {
182
- return failure(400, 'Anthropic OAuth callback state mismatch');
183
- }
184
- const result = await postTokenExchange({
185
- grant_type: 'authorization_code',
186
- client_id: ANTHROPIC_CLIENT_ID,
187
- code: parsed.code,
188
- state: parsed.state,
189
- redirect_uri: ANTHROPIC_REDIRECT_URI,
190
- code_verifier: session.verifier,
191
- });
192
- if ('status' in result)
193
- return result;
194
- const credential = validateTokenExchangeResponse(result.body, now);
195
- if ('status' in credential)
196
- return credential;
197
- return { ok: true, sessionId: session.sessionId, credential };
198
- }
199
- // ── AuthStorage (direct, in-guest) ──────────────────────────────────────────
200
- function readAnthropicStatus() {
201
- const storage = AuthStorage.create();
202
- const credential = storage.get('anthropic');
203
- const errors = storage.drainErrors();
204
- if (errors.length > 0) {
205
- throw new Error(errors.map((error) => error.message).join('; '));
206
- }
207
- const adapterConfigured = storage.getOAuthProviders().some((provider) => provider.id === 'anthropic');
208
- const storedOauth = credential?.type === 'oauth';
209
- const expiresAt = storedOauth ? credential.expires : null;
210
- return { storedOauth, expiresAt, adapterConfigured };
211
- }
212
- function writeAnthropicCredential(credential) {
213
- const storage = AuthStorage.create();
214
- // pi-ai's `OAuthCredentials` carries a `[key: string]: unknown` index
215
- // signature for adapter-specific extra fields; our narrower
216
- // AnthropicOAuthCredential is otherwise structurally identical (`type`,
217
- // `refresh`, `access`, `expires`), so this is a safe widen, not a cast away
218
- // from a real type mismatch.
219
- storage.set('anthropic', credential);
220
- const errors = storage.drainErrors();
221
- if (errors.length > 0) {
222
- throw new Error(errors.map((error) => error.message).join('; '));
223
- }
224
- // set() mutates in-memory state before persistence — re-read through a FRESH
225
- // AuthStorage instance to verify the write actually round-tripped to disk.
226
- const reread = AuthStorage.create();
227
- const stored = reread.get('anthropic');
228
- const rereadErrors = reread.drainErrors();
229
- if (rereadErrors.length > 0) {
230
- throw new Error(rereadErrors.map((error) => error.message).join('; '));
231
- }
232
- if (stored === undefined ||
233
- stored.type !== 'oauth' ||
234
- stored.refresh !== credential.refresh ||
235
- stored.access !== credential.access ||
236
- stored.expires !== credential.expires) {
237
- throw new Error('stored anthropic oauth credential did not round-trip');
238
- }
239
- return { storedOauth: true, expiresAt: stored.expires };
240
- }
241
- // ── HTTP plumbing ───────────────────────────────────────────────────────
242
- function respondJson(res, statusCode, value) {
243
- const body = `${JSON.stringify(value, null, 2)}\n`;
244
- res.writeHead(statusCode, {
245
- 'content-type': 'application/json; charset=utf-8',
246
- 'content-length': Buffer.byteLength(body),
247
- 'cache-control': 'no-store',
248
- });
249
- res.end(body);
250
- }
251
- function respondText(res, statusCode, message) {
252
- const body = `${redact(message)}\n`;
253
- res.writeHead(statusCode, {
254
- 'content-type': 'text/plain; charset=utf-8',
255
- 'content-length': Buffer.byteLength(body),
256
- 'cache-control': 'no-store',
257
- });
258
- res.end(body);
259
- }
260
- /** Read a request body up to `limitBytes`, rejecting (and destroying the
261
- * socket) if it grows past the cap. */
262
- function readJsonBody(req, limitBytes) {
263
- return new Promise((resolve, reject) => {
264
- const chunks = [];
265
- let bytes = 0;
266
- const cleanup = () => {
267
- req.off('data', onData);
268
- req.off('end', onEnd);
269
- req.off('error', onError);
270
- req.off('aborted', onAbort);
271
- };
272
- const onData = (chunk) => {
273
- bytes += chunk.byteLength;
274
- if (bytes > limitBytes) {
275
- cleanup();
276
- req.destroy();
277
- reject(new Error(`request body too large (limit ${limitBytes} bytes)`));
278
- return;
279
- }
280
- chunks.push(chunk);
281
- };
282
- const onEnd = () => {
283
- cleanup();
284
- try {
285
- const text = Buffer.concat(chunks).toString('utf8');
286
- resolve(text === '' ? {} : JSON.parse(text));
287
- }
288
- catch (error) {
289
- reject(error instanceof Error ? error : new Error(String(error)));
290
- }
291
- };
292
- const onError = (error) => {
293
- cleanup();
294
- reject(error instanceof Error ? error : new Error(String(error)));
295
- };
296
- const onAbort = () => {
297
- cleanup();
298
- reject(new Error('request aborted while reading body'));
299
- };
300
- req.on('data', onData);
301
- req.once('end', onEnd);
302
- req.once('error', onError);
303
- req.once('aborted', onAbort);
304
- });
305
- }
306
- function isPlainObject(value) {
307
- return value !== null && typeof value === 'object' && !Array.isArray(value);
308
- }
309
- function parseFinishBody(body) {
310
- if (!isPlainObject(body))
311
- return null;
312
- if (Object.keys(body).length !== 2)
313
- return null;
314
- const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
315
- const redirectUrl = typeof body.redirectUrl === 'string' ? body.redirectUrl.trim() : '';
316
- if (sessionId === '' || redirectUrl === '')
317
- return null;
318
- return { sessionId, redirectUrl };
319
- }
320
- async function handleStatus(req, res) {
321
- if (req.method !== 'GET') {
322
- respondText(res, 405, 'method not allowed');
323
- return;
324
- }
325
- try {
326
- respondJson(res, 200, readAnthropicStatus());
327
- }
328
- catch (error) {
329
- respondJson(res, 502, { error: redactedErrorMessage(error) });
330
- }
331
- }
332
- async function handleStart(req, res) {
333
- if (req.method !== 'POST') {
334
- respondText(res, 405, 'method not allowed');
335
- return;
336
- }
337
- let body;
338
- try {
339
- body = await readJsonBody(req, BODY_LIMIT_BYTES);
340
- }
341
- catch (error) {
342
- respondJson(res, 400, { error: redactedErrorMessage(error) });
343
- return;
344
- }
345
- if (!isPlainObject(body)) {
346
- respondJson(res, 400, { error: 'start request must be empty or a JSON object' });
347
- return;
348
- }
349
- try {
350
- const started = startAnthropicOAuthSession(Date.now());
351
- respondJson(res, 200, {
352
- provider: 'anthropic',
353
- sessionId: started.sessionId,
354
- authUrl: started.authUrl,
355
- expiresAt: started.expiresAt,
356
- });
357
- }
358
- catch (error) {
359
- respondJson(res, 502, { error: redactedErrorMessage(error) });
360
- }
361
- }
362
- async function handleFinish(req, res) {
363
- if (req.method !== 'POST') {
364
- respondText(res, 405, 'method not allowed');
365
- return;
366
- }
367
- let body;
368
- try {
369
- body = await readJsonBody(req, BODY_LIMIT_BYTES);
370
- }
371
- catch (error) {
372
- respondJson(res, 400, { error: redactedErrorMessage(error) });
373
- return;
374
- }
375
- const parsed = parseFinishBody(body);
376
- if (parsed === null) {
377
- respondJson(res, 400, { error: 'finish request must be { sessionId, redirectUrl }' });
378
- return;
379
- }
380
- try {
381
- const finished = await finishAnthropicOAuthSession(parsed.sessionId, parsed.redirectUrl);
382
- if (!finished.ok) {
383
- respondJson(res, finished.status, { error: finished.error });
384
- return;
385
- }
386
- const stored = writeAnthropicCredential(finished.credential);
387
- respondJson(res, 200, {
388
- ok: true,
389
- provider: 'anthropic',
390
- sessionId: finished.sessionId,
391
- storedOauth: stored.storedOauth,
392
- expiresAt: stored.expiresAt,
393
- });
394
- }
395
- catch (error) {
396
- respondJson(res, 502, { error: redactedErrorMessage(error) });
397
- }
398
- }
399
- /** Dispatch one `/__hearth/model-auth/anthropic/{status,start,finish}` request.
400
- * The caller (src/clients/web/server.ts) has already matched the path prefix
401
- * and enforced the relay token — this function assumes both are done and
402
- * handles ONLY routing-within-the-lane, method checks, and the Hearth/OAuth
403
- * product logic. Always writes a response; never throws (errors become a
404
- * redacted 502/500 JSON body). */
405
- export async function handleHearthModelAuthRequest(req, res) {
406
- const pathname = new URL(req.url ?? '/', 'http://127.0.0.1').pathname;
407
- try {
408
- if (pathname === '/__hearth/model-auth/anthropic/status') {
409
- await handleStatus(req, res);
410
- return;
411
- }
412
- if (pathname === '/__hearth/model-auth/anthropic/start') {
413
- await handleStart(req, res);
414
- return;
415
- }
416
- if (pathname === '/__hearth/model-auth/anthropic/finish') {
417
- await handleFinish(req, res);
418
- return;
419
- }
420
- respondText(res, 404, 'not found');
421
- }
422
- catch (error) {
423
- if (!res.headersSent) {
424
- respondJson(res, 500, { error: redactedErrorMessage(error) });
425
- }
426
- else {
427
- res.destroy();
428
- }
429
- }
430
- }
@@ -1,36 +0,0 @@
1
- import type { HomeProviderDescriptor, HomeProviderRefresh, MachineId, MachineRef, M0Config } from './types.js';
2
- import type { ExecInput, ExecResult } from './providers/types.js';
3
- /**
4
- * The Hearth home backend seam. The wake-proxy front door (token auth, origin checks,
5
- * SSE/WS forwarding, the source gate) is vendor-neutral and drives a home purely through
6
- * this interface — never a concrete sandbox SDK.
7
- */
8
- export interface HomeBackend {
9
- /**
10
- * Bring a CP-resolved home up and serving — warm-resume or recreate as the backend decides —
11
- * and return refreshed provider pointers for the CP registry to persist.
12
- */
13
- wake(home: HomeProviderDescriptor): Promise<HomeProviderRefresh>;
14
- /**
15
- * Targeted destroy→recreate of a home onto a SPECIFIC image, reattaching the same durable
16
- * volume (auto-upgrade roll and rollback, design §4/§5). Force-destroys the sandbox first so
17
- * the recreate genuinely lands on `imageRef` (the volume is never touched), reboots the guest,
18
- * and reports `templateVersion` for the CP to record. The caller independently asserts the
19
- * running `crtr sys version` in-guest before trusting the flip.
20
- */
21
- recreateOnImage(home: HomeProviderDescriptor, imageRef: string, templateVersion: string): Promise<HomeProviderRefresh>;
22
- /** Pause the home and return refreshed provider pointers for the CP registry to persist. */
23
- suspend(home: HomeProviderDescriptor): Promise<HomeProviderRefresh>;
24
- /** Machine resources (cpu/mem) for status, or null when unavailable. */
25
- getMachine(machineId: MachineId): Promise<MachineRef | null>;
26
- /**
27
- * The P2.6 stdin-capable provider exec seam, exposed through this vendor-neutral interface
28
- * so CP code (P4.4 `wakeAndExecNodeMessage`) never imports a concrete sandbox SDK to run an
29
- * in-guest command. Callers pass untrusted bodies via `input.stdin`, never `input.command`.
30
- */
31
- exec(machineId: MachineId, input: ExecInput): Promise<ExecResult>;
32
- }
33
- /**
34
- * Resolve the configured home backend.
35
- */
36
- export declare function getHomeBackend(config: M0Config): HomeBackend;
@@ -1,10 +0,0 @@
1
- import { general } from '../errors.js';
2
- import { BlaxelHomeBackend } from './providers/blaxel-home.js';
3
- /**
4
- * Resolve the configured home backend.
5
- */
6
- export function getHomeBackend(config) {
7
- if (config.providerName === 'blaxel')
8
- return new BlaxelHomeBackend(config);
9
- throw general('no Hearth home provider configured — Blaxel backend pending', { status: 503 });
10
- }