@inneranimalmedia/agentsam-sdk 2.6.1 → 2.6.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.
package/src/lib/auth.js CHANGED
@@ -1,71 +1,334 @@
1
1
  /**
2
- * Browser OAuth for SDK init — one click IAM login + Cloudflare connect.
2
+ * RFC 8252 native-app OAuth for AgentSam CLI.
3
+ *
4
+ * The CLI is a public client: authorization code + PKCE over a loopback
5
+ * redirect. Browser OAuth sessions are machine-local and remain separate from
6
+ * reusable AGENTSAM_API_KEY (aak_*) credentials.
3
7
  */
4
8
  import http from 'node:http';
5
- import { randomBytes } from 'node:crypto';
6
- import { postJson } from './core-client.js';
9
+ import { createHash, randomBytes } from 'node:crypto';
10
+ import { resolveIamIssuer } from '../../packages/identity/src/contracts/auth-config.js';
7
11
  import { promptToOpenUrl } from './open-url.js';
8
- import { saveAccountSession } from './account-session.js';
12
+ import {
13
+ isBrowserSessionExpired,
14
+ readAccountSession,
15
+ resolveAccountApiKey,
16
+ saveAccountSession,
17
+ } from './account-session.js';
9
18
 
10
- function randomState() {
11
- return randomBytes(16).toString('hex');
19
+ export const AGENTSAM_NATIVE_OAUTH_CLIENT_ID = 'iam_cli_agentsam';
20
+ export const AGENTSAM_NATIVE_OAUTH_SCOPE = 'openid profile email offline_access';
21
+ export const AGENTSAM_OAUTH_CALLBACK_PATH = '/callback';
22
+
23
+ function clean(value) { return value == null ? '' : String(value).trim(); }
24
+ function base64url(value) {
25
+ return Buffer.from(value).toString('base64')
26
+ .replace(/=/g, '')
27
+ .replace(/\+/g, '-')
28
+ .replace(/\//g, '_');
29
+ }
30
+ function randomUrlSafe(bytes = 32, randomBytesImpl = randomBytes) {
31
+ return base64url(randomBytesImpl(bytes));
32
+ }
33
+ function oauthErrorMessage(body, status) {
34
+ const code = clean(body?.error);
35
+ const description = clean(body?.error_description || body?.message);
36
+ if (code && description) return `${code}: ${description}`;
37
+ return code || description || `OAuth token HTTP ${status}`;
38
+ }
39
+
40
+ export function createPkcePair(options = {}) {
41
+ const verifier = randomUrlSafe(32, options.randomBytesImpl || randomBytes);
42
+ const challenge = base64url(createHash('sha256').update(verifier, 'ascii').digest());
43
+ return Object.freeze({ verifier, challenge, method: 'S256' });
44
+ }
45
+
46
+ export function buildNativeAuthorizationUrl(options = {}) {
47
+ const env = options.env || process.env;
48
+ const issuer = resolveIamIssuer(env, options.issuer || '');
49
+ const clientId = clean(options.clientId) || AGENTSAM_NATIVE_OAUTH_CLIENT_ID;
50
+ const redirectUri = clean(options.redirectUri);
51
+ const state = clean(options.state);
52
+ const codeChallenge = clean(options.codeChallenge);
53
+ const scope = clean(options.scope ?? AGENTSAM_NATIVE_OAUTH_SCOPE);
54
+ if (!redirectUri || !state || !codeChallenge) throw new Error('oauth_authorization_parameters_required');
55
+
56
+ const url = new URL('/api/oauth/authorize', `${issuer}/`);
57
+ url.searchParams.set('response_type', 'code');
58
+ url.searchParams.set('client_id', clientId);
59
+ url.searchParams.set('redirect_uri', redirectUri);
60
+ url.searchParams.set('code_challenge', codeChallenge);
61
+ url.searchParams.set('code_challenge_method', 'S256');
62
+ url.searchParams.set('state', state);
63
+ if (scope) url.searchParams.set('scope', scope);
64
+ return url.toString();
65
+ }
66
+
67
+ async function oauthTokenRequest(params, options = {}) {
68
+ const env = options.env || process.env;
69
+ const issuer = resolveIamIssuer(env, options.issuer || '');
70
+ const fetchImpl = options.fetchImpl || fetch;
71
+ const body = new URLSearchParams();
72
+ for (const [key, value] of Object.entries(params || {})) {
73
+ const normalized = clean(value);
74
+ if (normalized) body.set(key, normalized);
75
+ }
76
+
77
+ const response = await fetchImpl(new URL('/api/oauth/token', `${issuer}/`).toString(), {
78
+ method: 'POST',
79
+ headers: {
80
+ Accept: 'application/json',
81
+ 'Content-Type': 'application/x-www-form-urlencoded',
82
+ },
83
+ body: body.toString(),
84
+ signal: options.signal || (typeof AbortSignal?.timeout === 'function' ? AbortSignal.timeout(15_000) : undefined),
85
+ });
86
+ const data = await response.json().catch(() => ({}));
87
+ if (!response.ok) {
88
+ const error = new Error(oauthErrorMessage(data, response.status));
89
+ error.status = response.status;
90
+ error.oauth_error = clean(data?.error) || null;
91
+ throw error;
92
+ }
93
+ if (!clean(data?.access_token)) throw new Error('oauth_token_response_missing_access_token');
94
+ return data;
95
+ }
96
+
97
+ export async function exchangeAuthorizationCode(options = {}) {
98
+ const code = clean(options.code);
99
+ const codeVerifier = clean(options.codeVerifier);
100
+ const redirectUri = clean(options.redirectUri);
101
+ if (!code || !codeVerifier || !redirectUri) throw new Error('oauth_authorization_code_exchange_parameters_required');
102
+ return oauthTokenRequest({
103
+ grant_type: 'authorization_code',
104
+ client_id: clean(options.clientId) || AGENTSAM_NATIVE_OAUTH_CLIENT_ID,
105
+ redirect_uri: redirectUri,
106
+ code,
107
+ code_verifier: codeVerifier,
108
+ }, options);
109
+ }
110
+
111
+ export async function refreshAccountSession(options = {}) {
112
+ const session = options.session || readAccountSession(options);
113
+ if (!session?.refresh_token) throw new Error('browser_oauth_refresh_unavailable');
114
+ const clientId = clean(session.client_id) || clean(options.clientId) || AGENTSAM_NATIVE_OAUTH_CLIENT_ID;
115
+ const refreshed = await oauthTokenRequest({
116
+ grant_type: 'refresh_token',
117
+ client_id: clientId,
118
+ refresh_token: session.refresh_token,
119
+ }, options);
120
+
121
+ return saveAccountSession({
122
+ ...refreshed,
123
+ refresh_token: clean(refreshed.refresh_token) || session.refresh_token,
124
+ client_id: clientId,
125
+ user_id: session.user_id,
126
+ account_id: session.account_id,
127
+ email: session.email,
128
+ }, {
129
+ ...options,
130
+ preserveRefreshToken: true,
131
+ });
12
132
  }
13
133
 
14
134
  /**
15
- * @returns {Promise<{ access_token: string, user_id: string, workspace_id: string, tenant_id: string }>}
135
+ * Canonical SDK account authority resolution.
136
+ * explicit aak_* -> AGENTSAM_API_KEY -> stored browser OAuth -> OAuth refresh.
16
137
  */
17
- export async function authenticateViaBrowser() {
18
- const state = randomState();
19
- const port = 8791 + (randomBytes(1)[0] % 20);
20
- const redirectUri = `http://127.0.0.1:${port}/callback`;
138
+ export async function resolveAccountAuthority(options = {}) {
139
+ const apiKey = resolveAccountApiKey(options);
140
+ if (apiKey.value || apiKey.error) return apiKey;
21
141
 
22
- const { auth_url: authUrl } = await postJson('/api/sdk/auth/start', {
23
- redirect_uri: redirectUri,
24
- state,
142
+ let session = readAccountSession(options);
143
+ if (!session?.access_token) return { value: '', source: null, kind: null, session: null };
144
+
145
+ if (isBrowserSessionExpired(session, options)) {
146
+ if (!session.refresh_token) {
147
+ return {
148
+ value: '',
149
+ source: 'agentsam_browser_oauth',
150
+ kind: 'browser_oauth',
151
+ session,
152
+ error: 'browser_oauth_session_expired',
153
+ };
154
+ }
155
+ try {
156
+ const refreshImpl = options.refreshImpl || refreshAccountSession;
157
+ session = await refreshImpl({ ...options, session });
158
+ } catch (error) {
159
+ return {
160
+ value: '',
161
+ source: 'agentsam_browser_oauth',
162
+ kind: 'browser_oauth',
163
+ session,
164
+ error: `browser_oauth_refresh_failed: ${error?.message || String(error)}`,
165
+ };
166
+ }
167
+ }
168
+
169
+ return {
170
+ value: session.access_token,
171
+ source: 'agentsam_browser_oauth',
172
+ kind: 'browser_oauth',
173
+ session,
174
+ };
175
+ }
176
+
177
+ export async function createLoopbackCallbackListener(options = {}) {
178
+ const host = clean(options.host) || '127.0.0.1';
179
+ const callbackPath = clean(options.callbackPath) || AGENTSAM_OAUTH_CALLBACK_PATH;
180
+ const expectedState = clean(options.state);
181
+ if (!expectedState) throw new Error('oauth_state_required');
182
+ const timeoutMs = Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 180_000;
183
+ const createServerImpl = options.createServerImpl || http.createServer;
184
+
185
+ let settle;
186
+ let settled = false;
187
+ let timer = null;
188
+ const callbackPromise = new Promise((resolve, reject) => {
189
+ settle = (error, result) => {
190
+ if (settled) return;
191
+ settled = true;
192
+ if (timer) clearTimeout(timer);
193
+ if (error) reject(error);
194
+ else resolve(result);
195
+ };
25
196
  });
26
197
 
27
- if (!authUrl) throw new Error('IAM auth did not return an authorization URL');
198
+ const server = createServerImpl((req, res) => {
199
+ try {
200
+ const requestUrl = new URL(req.url || '/', `http://${host}`);
201
+ if (requestUrl.pathname !== callbackPath) {
202
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
203
+ res.end('Not found');
204
+ return;
205
+ }
28
206
 
29
- const codePromise = new Promise((resolve, reject) => {
30
- const server = http.createServer((req, res) => {
31
- try {
32
- const u = new URL(req.url || '/', `http://127.0.0.1:${port}`);
33
- if (u.pathname !== '/callback') {
34
- res.writeHead(404);
35
- res.end('Not found');
36
- return;
37
- }
38
- const code = u.searchParams.get('code');
39
- const gotState = u.searchParams.get('state');
40
- if (!code || gotState !== state) {
41
- res.writeHead(400);
42
- res.end('Invalid callback');
43
- reject(new Error('auth callback invalid'));
44
- server.close();
45
- return;
46
- }
47
- res.writeHead(200, { 'Content-Type': 'text/html' });
48
- res.end('<html><body style="font-family:system-ui"><h1>Agent Sam</h1><p>Authentication complete. You can close this tab and return to your terminal.</p></body></html>');
49
- resolve(code);
50
- server.close();
51
- } catch (e) {
52
- reject(e);
53
- server.close();
207
+ const returnedState = clean(requestUrl.searchParams.get('state'));
208
+ const oauthError = clean(requestUrl.searchParams.get('error'));
209
+ const oauthDescription = clean(requestUrl.searchParams.get('error_description'));
210
+ const code = clean(requestUrl.searchParams.get('code'));
211
+
212
+ if (!returnedState || returnedState !== expectedState) {
213
+ res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
214
+ res.end('Invalid OAuth state. Return to the terminal and retry.');
215
+ settle(new Error('oauth_state_mismatch'));
216
+ return;
54
217
  }
55
- });
56
- server.on('error', reject);
57
- server.listen(port, '127.0.0.1');
218
+ if (oauthError) {
219
+ res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
220
+ res.end('Authorization was not completed. Return to the terminal.');
221
+ settle(new Error(oauthDescription ? `${oauthError}: ${oauthDescription}` : oauthError));
222
+ return;
223
+ }
224
+ if (!code) {
225
+ res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
226
+ res.end('Authorization code missing. Return to the terminal and retry.');
227
+ settle(new Error('oauth_authorization_code_missing'));
228
+ return;
229
+ }
230
+
231
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
232
+ res.end('<!doctype html><html><body style="font-family:system-ui"><h1>Agent Sam</h1><p>Authentication complete. You can close this tab and return to your terminal.</p></body></html>');
233
+ settle(null, { code, state: returnedState });
234
+ } catch (error) {
235
+ try {
236
+ res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
237
+ res.end('OAuth callback failed. Return to the terminal.');
238
+ } catch { /* response may already be closed */ }
239
+ settle(error);
240
+ }
58
241
  });
59
242
 
60
- await promptToOpenUrl(authUrl, {
61
- heading: 'Authenticate your InnerAnimalMedia account at:',
62
- prompt: 'Press ENTER to open InnerAnimalMedia sign-in in your browser.',
243
+ await new Promise((resolve, reject) => {
244
+ const onError = (error) => {
245
+ server.off('listening', onListening);
246
+ reject(error);
247
+ };
248
+ const onListening = () => {
249
+ server.off('error', onError);
250
+ resolve();
251
+ };
252
+ server.once('error', onError);
253
+ server.once('listening', onListening);
254
+ server.listen({ host, port: Number(options.port) || 0, exclusive: true });
63
255
  });
64
256
 
65
- const code = await codePromise;
66
- const session = await postJson('/api/sdk/auth/exchange', { code, state });
67
- if (String(session?.access_token || '').trim().startsWith('sdk_')) {
68
- saveAccountSession(session);
257
+ const address = server.address();
258
+ if (!address || typeof address === 'string') {
259
+ server.close();
260
+ throw new Error('oauth_loopback_listener_address_unavailable');
261
+ }
262
+ const redirectUri = `http://${host}:${address.port}${callbackPath}`;
263
+ timer = setTimeout(() => settle(new Error('oauth_callback_timeout')), Math.max(1, timeoutMs));
264
+ timer.unref?.();
265
+
266
+ return {
267
+ redirectUri,
268
+ waitForCallback: () => callbackPromise,
269
+ close: () => new Promise((resolve) => {
270
+ if (!server.listening) return resolve();
271
+ server.close(() => resolve());
272
+ }),
273
+ };
274
+ }
275
+
276
+ export async function authenticateViaBrowser(options = {}) {
277
+ const env = options.env || process.env;
278
+ const clientId = clean(options.clientId) || AGENTSAM_NATIVE_OAUTH_CLIENT_ID;
279
+ const state = randomUrlSafe(24, options.randomBytesImpl || randomBytes);
280
+ const pkce = createPkcePair({ randomBytesImpl: options.randomBytesImpl });
281
+ const listener = await createLoopbackCallbackListener({
282
+ state,
283
+ host: options.host,
284
+ port: options.port,
285
+ callbackPath: options.callbackPath,
286
+ timeoutMs: options.timeoutMs,
287
+ createServerImpl: options.createServerImpl,
288
+ });
289
+
290
+ try {
291
+ const authorizationUrl = buildNativeAuthorizationUrl({
292
+ env,
293
+ issuer: options.issuer,
294
+ clientId,
295
+ redirectUri: listener.redirectUri,
296
+ state,
297
+ codeChallenge: pkce.challenge,
298
+ scope: options.scope,
299
+ });
300
+
301
+ const promptImpl = options.promptToOpenUrlImpl || promptToOpenUrl;
302
+ await promptImpl(authorizationUrl, {
303
+ heading: 'Authenticate your InnerAnimalMedia account at:',
304
+ prompt: 'Press ENTER to open InnerAnimalMedia sign-in in your browser.',
305
+ input: options.input,
306
+ output: options.output,
307
+ openImpl: options.openImpl,
308
+ });
309
+
310
+ const callback = await listener.waitForCallback();
311
+ const tokenSet = await exchangeAuthorizationCode({
312
+ env,
313
+ issuer: options.issuer,
314
+ clientId,
315
+ redirectUri: listener.redirectUri,
316
+ code: callback.code,
317
+ codeVerifier: pkce.verifier,
318
+ fetchImpl: options.fetchImpl,
319
+ signal: options.signal,
320
+ });
321
+
322
+ return saveAccountSession({
323
+ ...tokenSet,
324
+ client_id: clientId,
325
+ }, {
326
+ home: options.home,
327
+ env,
328
+ nowMs: options.nowMs,
329
+ preserveRefreshToken: false,
330
+ });
331
+ } finally {
332
+ await listener.close();
69
333
  }
70
- return session;
71
334
  }
@@ -1,27 +1,67 @@
1
1
  import { resolveIamOrigin } from '../../packages/identity/src/contracts/auth-config.js';
2
+ import { resolveAccountAuthority } from './auth.js';
2
3
 
3
4
  /**
4
5
  * IAM CORE client — SDK is a delivery mechanism; intelligence lives server-side.
5
6
  *
6
- * IAM_ORIGIN is the canonical platform authority/API origin. During the
7
- * migration window IAM_CORE_URL / AGENTSAM_CORE_URL remain compatibility-only
8
- * fallbacks. Without a CORE backend, consumers get scaffold/local CLI only.
7
+ * IAM_OAUTH_ISSUER is the canonical platform authority/API origin. IAM_ORIGIN,
8
+ * IAM_CORE_URL and AGENTSAM_CORE_URL remain compatibility fallbacks.
9
9
  */
10
-
11
10
  export function coreBaseUrl(env = process.env) {
12
- const explicit = env?.IAM_ORIGIN || env?.IAM_CORE_URL || env?.AGENTSAM_CORE_URL || '';
11
+ const explicit = env?.IAM_OAUTH_ISSUER || env?.IAM_ORIGIN || env?.IAM_CORE_URL || env?.AGENTSAM_CORE_URL || '';
13
12
  return resolveIamOrigin(env, explicit);
14
13
  }
15
14
 
16
- export async function postJson(path, body, token) {
17
- const headers = { 'Content-Type': 'application/json', Accept: 'application/json' };
18
- if (token) headers.Authorization = `Bearer ${token}`;
19
- const res = await fetch(`${coreBaseUrl()}${path}`, {
15
+ function authOptions(options) {
16
+ if (typeof options === 'string') return { explicit: options };
17
+ return options && typeof options === 'object' ? options : {};
18
+ }
19
+
20
+ export async function resolveCoreAuthority(options = {}) {
21
+ const resolvedOptions = authOptions(options);
22
+ const resolver = resolvedOptions.resolveAuthorityImpl || resolveAccountAuthority;
23
+ const authority = await resolver({
24
+ env: resolvedOptions.env || process.env,
25
+ home: resolvedOptions.home,
26
+ explicit: resolvedOptions.explicit || resolvedOptions.token || '',
27
+ fetchImpl: resolvedOptions.fetchImpl,
28
+ issuer: resolvedOptions.issuer,
29
+ signal: resolvedOptions.signal,
30
+ nowMs: resolvedOptions.nowMs,
31
+ skewMs: resolvedOptions.skewMs,
32
+ refreshImpl: resolvedOptions.refreshImpl,
33
+ });
34
+ if (authority?.error) throw new Error(authority.error);
35
+ if (!authority?.value) throw new Error('account_auth_required');
36
+ return authority;
37
+ }
38
+
39
+ async function authorizedHeaders(headers, options = {}) {
40
+ const authority = await resolveCoreAuthority(options);
41
+ return {
42
+ ...headers,
43
+ Authorization: `Bearer ${authority.value}`,
44
+ };
45
+ }
46
+
47
+ async function responseJson(res) {
48
+ return res.json().catch(() => ({}));
49
+ }
50
+
51
+ export async function postJson(path, body, options = {}) {
52
+ const resolvedOptions = authOptions(options);
53
+ const fetchImpl = resolvedOptions.fetchImpl || fetch;
54
+ const headers = await authorizedHeaders({
55
+ 'Content-Type': 'application/json',
56
+ Accept: 'application/json',
57
+ }, resolvedOptions);
58
+ const res = await fetchImpl(`${coreBaseUrl(resolvedOptions.env || process.env)}${path}`, {
20
59
  method: 'POST',
21
60
  headers,
22
61
  body: JSON.stringify(body ?? {}),
62
+ signal: resolvedOptions.signal,
23
63
  });
24
- const data = await res.json().catch(() => ({}));
64
+ const data = await responseJson(res);
25
65
  if (!res.ok) {
26
66
  const msg = data?.error || data?.message || `HTTP ${res.status}`;
27
67
  throw new Error(String(msg));
@@ -29,11 +69,15 @@ export async function postJson(path, body, token) {
29
69
  return data;
30
70
  }
31
71
 
32
- export async function getJson(path, token) {
33
- const headers = { Accept: 'application/json' };
34
- if (token) headers.Authorization = `Bearer ${token}`;
35
- const res = await fetch(`${coreBaseUrl()}${path}`, { headers });
36
- const data = await res.json().catch(() => ({}));
72
+ export async function getJson(path, options = {}) {
73
+ const resolvedOptions = authOptions(options);
74
+ const fetchImpl = resolvedOptions.fetchImpl || fetch;
75
+ const headers = await authorizedHeaders({ Accept: 'application/json' }, resolvedOptions);
76
+ const res = await fetchImpl(`${coreBaseUrl(resolvedOptions.env || process.env)}${path}`, {
77
+ headers,
78
+ signal: resolvedOptions.signal,
79
+ });
80
+ const data = await responseJson(res);
37
81
  if (!res.ok) {
38
82
  const msg = data?.error || data?.message || `HTTP ${res.status}`;
39
83
  throw new Error(String(msg));
@@ -44,18 +88,21 @@ export async function getJson(path, token) {
44
88
  /**
45
89
  * Stream NDJSON from POST /api/sdk/scaffold — calls onEvent for each line.
46
90
  */
47
- export async function streamScaffold(body, token, onEvent) {
48
- const res = await fetch(`${coreBaseUrl()}/api/sdk/scaffold`, {
91
+ export async function streamScaffold(body, onEvent, options = {}) {
92
+ const resolvedOptions = authOptions(options);
93
+ const fetchImpl = resolvedOptions.fetchImpl || fetch;
94
+ const headers = await authorizedHeaders({
95
+ 'Content-Type': 'application/json',
96
+ Accept: 'application/x-ndjson',
97
+ }, resolvedOptions);
98
+ const res = await fetchImpl(`${coreBaseUrl(resolvedOptions.env || process.env)}/api/sdk/scaffold`, {
49
99
  method: 'POST',
50
- headers: {
51
- 'Content-Type': 'application/json',
52
- Accept: 'application/x-ndjson',
53
- Authorization: `Bearer ${token}`,
54
- },
100
+ headers,
55
101
  body: JSON.stringify(body),
102
+ signal: resolvedOptions.signal,
56
103
  });
57
104
  if (!res.ok) {
58
- const data = await res.json().catch(() => ({}));
105
+ const data = await responseJson(res);
59
106
  throw new Error(data?.error || `scaffold HTTP ${res.status}`);
60
107
  }
61
108
  if (!res.body) throw new Error('scaffold stream missing');
@@ -71,15 +118,13 @@ export async function streamScaffold(body, token, onEvent) {
71
118
  const lines = buf.split('\n');
72
119
  buf = lines.pop() || '';
73
120
  for (const line of lines) {
74
- const t = line.trim();
75
- if (!t) continue;
76
- let evt;
121
+ const text = line.trim();
122
+ if (!text) continue;
77
123
  try {
78
- evt = JSON.parse(t);
124
+ await onEvent(JSON.parse(text));
79
125
  } catch {
80
- continue;
126
+ /* ignore malformed stream lines */
81
127
  }
82
- await onEvent(evt);
83
128
  }
84
129
  }
85
130
  const tail = buf.trim();
@@ -87,7 +132,7 @@ export async function streamScaffold(body, token, onEvent) {
87
132
  try {
88
133
  await onEvent(JSON.parse(tail));
89
134
  } catch {
90
- /* ignore */
135
+ /* ignore malformed trailing line */
91
136
  }
92
137
  }
93
138
  }
@@ -5,7 +5,7 @@
5
5
  import { execFile } from 'child_process';
6
6
  import { promisify } from 'util';
7
7
  import { coreBaseUrl } from './core-client.js';
8
- import { resolveAccountSdkKey } from './account-session.js';
8
+ import { resolveAccountAuth } from './account-session.js';
9
9
 
10
10
  const execFileAsync = promisify(execFile);
11
11
 
@@ -167,11 +167,9 @@ async function detectCloudflare() {
167
167
  };
168
168
  }
169
169
 
170
- async function probeSdkBearer(token) {
170
+ async function probeAccountBearer(token) {
171
171
  const t = String(token || '').trim();
172
- if (!t || !t.startsWith('sdk_')) {
173
- return { valid: false, error: 'not_sdk_bearer' };
174
- }
172
+ if (!t) return { valid: false, error: 'account_auth_required' };
175
173
  try {
176
174
  const res = await fetch(`${coreBaseUrl()}/api/sdk/context`, {
177
175
  headers: { Accept: 'application/json', Authorization: `Bearer ${t}` },
@@ -197,7 +195,7 @@ function envPresent(name) {
197
195
  return v != null && String(v).trim() !== '';
198
196
  }
199
197
 
200
- /** IAM auth for SDK init — only verified sdk_* bearer counts as ready. */
198
+ /** IAM auth for SDK init — accepts a verified account API key or browser session credential. */
201
199
  async function detectIam(explicitToken = '') {
202
200
  const aux = [];
203
201
 
@@ -210,9 +208,9 @@ async function detectIam(explicitToken = '') {
210
208
  });
211
209
  }
212
210
 
213
- const workerKey = envPresent('IAM_API_KEY') || envPresent('AGENTSAM_API_KEY');
211
+ const workerKey = envPresent('IAM_API_KEY');
214
212
  if (workerKey) {
215
- const name = envPresent('IAM_API_KEY') ? 'IAM_API_KEY' : 'AGENTSAM_API_KEY';
213
+ const name = 'IAM_API_KEY';
216
214
  aux.push({
217
215
  var: name,
218
216
  role: 'Worker runtime secret',
@@ -221,22 +219,22 @@ async function detectIam(explicitToken = '') {
221
219
  });
222
220
  }
223
221
 
224
- const sdkToken = resolveAccountSdkKey({ env: process.env, explicit: explicitToken }).value;
225
- if (sdkToken.trim()) {
226
- const probe = await probeSdkBearer(sdkToken);
222
+ const accountAuth = resolveAccountAuth({ env: process.env, explicit: explicitToken });
223
+ if (accountAuth.value) {
224
+ const probe = await probeAccountBearer(accountAuth.value);
227
225
  if (probe.valid) {
228
226
  return {
229
- source: 'sdk-key',
227
+ source: accountAuth.kind || 'account-auth',
230
228
  ready: true,
231
- detail: `AGENTSAM_SDK_KEY · user ${probe.user_id || '?'}`,
229
+ detail: `${accountAuth.kind === 'api_key' ? 'AGENTSAM_API_KEY' : 'browser session'} · user ${probe.user_id || '?'}`,
232
230
  probe,
233
231
  aux,
234
232
  };
235
233
  }
236
234
  return {
237
- source: 'sdk-key',
235
+ source: accountAuth.kind || 'account-auth',
238
236
  ready: false,
239
- detail: `AGENTSAM_SDK_KEY invalid (${probe.error}) → will open browser`,
237
+ detail: `${accountAuth.kind === 'api_key' ? 'AGENTSAM_API_KEY' : 'browser session'} invalid (${probe.error}) → will open browser`,
240
238
  probe,
241
239
  aux,
242
240
  };
@@ -247,7 +245,7 @@ async function detectIam(explicitToken = '') {
247
245
  return {
248
246
  source: 'execos-env',
249
247
  ready: false,
250
- detail: `IAM_PTY_USER_ID set (ExecOS identity — SDK bearer still needed)`,
248
+ detail: `IAM_PTY_USER_ID set (ExecOS identity — account auth still needed)`,
251
249
  aux,
252
250
  };
253
251
  }