@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/docs/AUTH_IDENTITY_CONTRACT.md +25 -27
- package/docs/SDK_WORKER.md +4 -3
- package/package.json +1 -1
- package/packages/identity/package.json +2 -2
- package/packages/identity/src/contracts/auth-config.js +23 -40
- package/packages/identity/tests/auth-config.test.mjs +14 -23
- package/src/cli.js +13 -23
- package/src/commands/account-auth.js +1 -1
- package/src/commands/deploy.js +2 -2
- package/src/commands/providers.js +308 -0
- package/src/commands/shell.js +4 -0
- package/src/commands/tunnel.js +8 -8
- package/src/commands/whoami.js +42 -21
- package/src/errors/diagnostic.js +1 -0
- package/src/lib/account-session.js +94 -25
- package/src/lib/auth.js +314 -51
- package/src/lib/core-client.js +75 -30
- package/src/lib/detect-context.js +14 -16
- package/src/lib/provider-credentials.js +171 -55
- package/src/lib/slash-commands.js +1 -0
- package/src/models/discovery.js +35 -0
- package/src/ui/cli/help.js +3 -1
- package/test/account-session.test.mjs +48 -11
- package/test/apps-scaffold-contract.test.mjs +43 -0
- package/test/sdk-worker-contract.test.mjs +5 -1
- package/test/smoke.mjs +2 -1
- package/test/whoami-resume.test.mjs +6 -5
- package/src/lib/prompt-byok.js +0 -57
- package/src/lib/save-sdk-token.js +0 -19
package/src/lib/auth.js
CHANGED
|
@@ -1,71 +1,334 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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 {
|
|
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 {
|
|
12
|
+
import {
|
|
13
|
+
isBrowserSessionExpired,
|
|
14
|
+
readAccountSession,
|
|
15
|
+
resolveAccountApiKey,
|
|
16
|
+
saveAccountSession,
|
|
17
|
+
} from './account-session.js';
|
|
9
18
|
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
*
|
|
135
|
+
* Canonical SDK account authority resolution.
|
|
136
|
+
* explicit aak_* -> AGENTSAM_API_KEY -> stored browser OAuth -> OAuth refresh.
|
|
16
137
|
*/
|
|
17
|
-
export async function
|
|
18
|
-
const
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
}
|
package/src/lib/core-client.js
CHANGED
|
@@ -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
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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,
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
const
|
|
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,
|
|
48
|
-
const
|
|
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
|
|
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
|
|
75
|
-
if (!
|
|
76
|
-
let evt;
|
|
121
|
+
const text = line.trim();
|
|
122
|
+
if (!text) continue;
|
|
77
123
|
try {
|
|
78
|
-
|
|
124
|
+
await onEvent(JSON.parse(text));
|
|
79
125
|
} catch {
|
|
80
|
-
|
|
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 {
|
|
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
|
|
170
|
+
async function probeAccountBearer(token) {
|
|
171
171
|
const t = String(token || '').trim();
|
|
172
|
-
if (!t
|
|
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 —
|
|
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')
|
|
211
|
+
const workerKey = envPresent('IAM_API_KEY');
|
|
214
212
|
if (workerKey) {
|
|
215
|
-
const name =
|
|
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
|
|
225
|
-
if (
|
|
226
|
-
const probe = await
|
|
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: '
|
|
227
|
+
source: accountAuth.kind || 'account-auth',
|
|
230
228
|
ready: true,
|
|
231
|
-
detail:
|
|
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: '
|
|
235
|
+
source: accountAuth.kind || 'account-auth',
|
|
238
236
|
ready: false,
|
|
239
|
-
detail:
|
|
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 —
|
|
248
|
+
detail: `IAM_PTY_USER_ID set (ExecOS identity — account auth still needed)`,
|
|
251
249
|
aux,
|
|
252
250
|
};
|
|
253
251
|
}
|