@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
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cancel,
|
|
3
|
+
confirm,
|
|
4
|
+
intro,
|
|
5
|
+
isCancel,
|
|
6
|
+
multiselect,
|
|
7
|
+
outro,
|
|
8
|
+
password,
|
|
9
|
+
spinner,
|
|
10
|
+
text,
|
|
11
|
+
} from '@clack/prompts';
|
|
12
|
+
import { getJson } from '../lib/core-client.js';
|
|
13
|
+
import {
|
|
14
|
+
PROVIDER_CREDENTIALS,
|
|
15
|
+
describeProviderCredential,
|
|
16
|
+
listProviderCredentialStatus,
|
|
17
|
+
normalizeProviderId,
|
|
18
|
+
providerCredentialSpec,
|
|
19
|
+
removeProviderCredential,
|
|
20
|
+
resolveProviderCredential,
|
|
21
|
+
setProviderCredential,
|
|
22
|
+
} from '../lib/provider-credentials.js';
|
|
23
|
+
import { discoverProviderModels } from '../models/discovery.js';
|
|
24
|
+
|
|
25
|
+
const PROVIDER_ORDER = Object.freeze([
|
|
26
|
+
'openai',
|
|
27
|
+
'anthropic',
|
|
28
|
+
'gemini',
|
|
29
|
+
'cursor',
|
|
30
|
+
'xai',
|
|
31
|
+
'cloudflare',
|
|
32
|
+
'inneranimalmedia',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
function clean(value) { return value == null ? '' : String(value).trim(); }
|
|
36
|
+
function writeLine(write, value = '') { write(`${value}\n`); }
|
|
37
|
+
|
|
38
|
+
export function providerChoices(options = {}) {
|
|
39
|
+
const statuses = new Map(listProviderCredentialStatus(options).map((row) => [row.provider, row]));
|
|
40
|
+
return PROVIDER_ORDER.map((provider) => {
|
|
41
|
+
const spec = providerCredentialSpec(provider);
|
|
42
|
+
const status = statuses.get(provider);
|
|
43
|
+
return {
|
|
44
|
+
value: provider,
|
|
45
|
+
label: spec?.label || provider,
|
|
46
|
+
hint: status?.configured
|
|
47
|
+
? `configured · ${status.source || 'runtime'}`
|
|
48
|
+
: status?.error ? `blocked · ${status.error}` : spec?.env || '',
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function verifyInnerAnimalMedia(credential, options = {}) {
|
|
54
|
+
if (!credential?.configured || !credential?.value) {
|
|
55
|
+
return { attempted: false, ok: false, error: credential?.error || 'credential unavailable', models: [] };
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const loader = options.iamContextLoader || ((token) => getJson('/api/sdk/context', token));
|
|
59
|
+
const context = await loader(credential.value);
|
|
60
|
+
return {
|
|
61
|
+
attempted: true,
|
|
62
|
+
ok: true,
|
|
63
|
+
error: null,
|
|
64
|
+
models: [],
|
|
65
|
+
identity: {
|
|
66
|
+
user_id: context?.user_id || null,
|
|
67
|
+
account_id: context?.account_id || null,
|
|
68
|
+
email: context?.email || context?.user?.email || null,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return { attempted: true, ok: false, error: error?.message || String(error), models: [] };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function verifyProviderCredential(provider, options = {}) {
|
|
77
|
+
const id = normalizeProviderId(provider);
|
|
78
|
+
const credential = resolveProviderCredential(id, options);
|
|
79
|
+
if (id === 'inneranimalmedia') return verifyInnerAnimalMedia(credential, options);
|
|
80
|
+
if (!credential.configured) {
|
|
81
|
+
return { attempted: false, ok: false, error: credential.error || 'credential unavailable', models: [] };
|
|
82
|
+
}
|
|
83
|
+
return discoverProviderModels(id, credential, { fetchImpl: options.fetchImpl || fetch });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function collectProviderStatus(options = {}) {
|
|
87
|
+
const rows = listProviderCredentialStatus(options);
|
|
88
|
+
if (options.verify !== true) {
|
|
89
|
+
return rows.map((row) => ({ ...row, verification: null }));
|
|
90
|
+
}
|
|
91
|
+
const verified = await Promise.all(rows.map(async (row) => {
|
|
92
|
+
if (!row.configured) return { ...row, verification: null };
|
|
93
|
+
const verification = await verifyProviderCredential(row.provider, options);
|
|
94
|
+
return {
|
|
95
|
+
...row,
|
|
96
|
+
verification: {
|
|
97
|
+
attempted: verification.attempted === true,
|
|
98
|
+
ok: verification.ok === true,
|
|
99
|
+
error: verification.error || null,
|
|
100
|
+
model_count: Array.isArray(verification.models) ? verification.models.length : 0,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}));
|
|
104
|
+
return verified;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function renderProviderStatus(rows = []) {
|
|
108
|
+
const lines = ['', ' Agent Sam · providers', ''];
|
|
109
|
+
for (const row of rows) {
|
|
110
|
+
const state = row.configured ? 'configured' : row.error ? `blocked (${row.error})` : 'not configured';
|
|
111
|
+
let verification = '';
|
|
112
|
+
if (row.verification) {
|
|
113
|
+
verification = row.verification.ok
|
|
114
|
+
? ` · verified${row.verification.model_count ? ` · ${row.verification.model_count} models` : ''}`
|
|
115
|
+
: ` · verify failed: ${row.verification.error || 'unknown'}`;
|
|
116
|
+
}
|
|
117
|
+
lines.push(` ${String(row.label || row.provider).padEnd(18)} ${state}${verification}`);
|
|
118
|
+
}
|
|
119
|
+
lines.push('');
|
|
120
|
+
lines.push(' Profiles: ~/.agentsam/env.d/<provider>.env · mode 0600');
|
|
121
|
+
lines.push(' Load one or many: source ~/.agentsam/load-agent-env.sh openai gemini cursor');
|
|
122
|
+
lines.push('');
|
|
123
|
+
return lines.join('\n');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function promptProviderCredential(provider, options = {}) {
|
|
127
|
+
const spec = providerCredentialSpec(provider);
|
|
128
|
+
if (!spec) throw new Error(`unsupported_provider:${provider}`);
|
|
129
|
+
const promptPassword = options.passwordImpl || password;
|
|
130
|
+
const promptText = options.textImpl || text;
|
|
131
|
+
const secret = await promptPassword({
|
|
132
|
+
message: `${spec.label} · ${spec.env}`,
|
|
133
|
+
mask: '•',
|
|
134
|
+
validate(value) {
|
|
135
|
+
const trimmed = clean(value);
|
|
136
|
+
if (!trimmed) return 'Credential is required';
|
|
137
|
+
if (spec.tokenPrefix && !trimmed.startsWith(spec.tokenPrefix)) return `Expected ${spec.tokenPrefix}…`;
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
if (isCancel(secret)) return null;
|
|
141
|
+
|
|
142
|
+
let accountId = '';
|
|
143
|
+
if (provider === 'cloudflare') {
|
|
144
|
+
const answer = await promptText({
|
|
145
|
+
message: 'Cloudflare account ID',
|
|
146
|
+
placeholder: '32-character account ID',
|
|
147
|
+
validate(value) {
|
|
148
|
+
const id = clean(value);
|
|
149
|
+
if (!id) return 'Cloudflare account ID is required';
|
|
150
|
+
if (!/^[a-f0-9]{32}$/i.test(id)) return 'Expected a 32-character hexadecimal account ID';
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
if (isCancel(answer)) return null;
|
|
154
|
+
accountId = clean(answer);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
setProviderCredential(provider, String(secret), { ...options, accountId });
|
|
158
|
+
return verifyProviderCredential(provider, options);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function runInteractiveProviders(options = {}) {
|
|
162
|
+
const promptMultiselect = options.multiselectImpl || multiselect;
|
|
163
|
+
const selected = await promptMultiselect({
|
|
164
|
+
message: 'Providers to configure',
|
|
165
|
+
required: false,
|
|
166
|
+
options: providerChoices(options),
|
|
167
|
+
});
|
|
168
|
+
if (isCancel(selected)) {
|
|
169
|
+
cancel('Provider setup cancelled.');
|
|
170
|
+
return collectProviderStatus(options);
|
|
171
|
+
}
|
|
172
|
+
if (!selected.length) return collectProviderStatus(options);
|
|
173
|
+
|
|
174
|
+
intro('Agent Sam provider setup');
|
|
175
|
+
const spin = options.spinnerImpl ? options.spinnerImpl() : spinner();
|
|
176
|
+
for (const provider of selected) {
|
|
177
|
+
const result = await promptProviderCredential(provider, options);
|
|
178
|
+
if (!result) continue;
|
|
179
|
+
spin.start(`Verifying ${providerCredentialSpec(provider)?.label || provider}`);
|
|
180
|
+
if (result.ok) {
|
|
181
|
+
const suffix = result.models?.length ? ` · ${result.models.length} models` : '';
|
|
182
|
+
spin.stop(`Verified${suffix}`);
|
|
183
|
+
} else {
|
|
184
|
+
spin.stop(`Saved · verification failed: ${result.error || 'unknown'}`, 1);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
outro('Provider setup complete');
|
|
188
|
+
return collectProviderStatus(options);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function parseArgs(argv = []) {
|
|
192
|
+
const out = { command: 'interactive', provider: '', json: false, verify: false, yes: false, fromEnv: '' };
|
|
193
|
+
const args = [...argv];
|
|
194
|
+
if (args[0] && !args[0].startsWith('-')) out.command = args.shift();
|
|
195
|
+
if (['add', 'set', 'remove', 'verify', 'status'].includes(out.command) && args[0] && !args[0].startsWith('-')) {
|
|
196
|
+
out.provider = normalizeProviderId(args.shift());
|
|
197
|
+
}
|
|
198
|
+
while (args.length) {
|
|
199
|
+
const arg = args.shift();
|
|
200
|
+
if (arg === '--json') out.json = true;
|
|
201
|
+
else if (arg === '--verify') out.verify = true;
|
|
202
|
+
else if (arg === '--yes' || arg === '-y') out.yes = true;
|
|
203
|
+
else if (arg === '--from-env') out.fromEnv = clean(args.shift());
|
|
204
|
+
else if (arg === '--help' || arg === '-h') out.command = 'help';
|
|
205
|
+
else throw new Error(`unknown providers option: ${arg}`);
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function runProviders(argv = [], options = {}) {
|
|
211
|
+
const write = options.write || ((value) => process.stdout.write(value));
|
|
212
|
+
const parsed = parseArgs(argv);
|
|
213
|
+
|
|
214
|
+
if (parsed.command === 'help') {
|
|
215
|
+
write([
|
|
216
|
+
'agentsam providers',
|
|
217
|
+
'agentsam providers status [provider] [--verify] [--json]',
|
|
218
|
+
'agentsam providers add <provider> [--from-env NAME]',
|
|
219
|
+
'agentsam providers verify [provider] [--json]',
|
|
220
|
+
'agentsam providers remove <provider> [--yes]',
|
|
221
|
+
'',
|
|
222
|
+
`Providers: ${PROVIDER_ORDER.join(', ')}`,
|
|
223
|
+
'',
|
|
224
|
+
].join('\n'));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (parsed.command === 'interactive') {
|
|
229
|
+
if (options.interactive === false || !(options.interactive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY))) {
|
|
230
|
+
const rows = await collectProviderStatus({ ...options, verify: false });
|
|
231
|
+
write(renderProviderStatus(rows));
|
|
232
|
+
return rows;
|
|
233
|
+
}
|
|
234
|
+
const rows = await runInteractiveProviders(options);
|
|
235
|
+
write(renderProviderStatus(rows));
|
|
236
|
+
return rows;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (parsed.provider && !providerCredentialSpec(parsed.provider)) throw new Error(`unsupported_provider:${parsed.provider}`);
|
|
240
|
+
|
|
241
|
+
if (parsed.command === 'status') {
|
|
242
|
+
let rows = await collectProviderStatus({ ...options, verify: parsed.verify });
|
|
243
|
+
if (parsed.provider) rows = rows.filter((row) => row.provider === parsed.provider);
|
|
244
|
+
if (parsed.json) writeLine(write, JSON.stringify(rows, null, 2));
|
|
245
|
+
else write(renderProviderStatus(rows));
|
|
246
|
+
return rows;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (parsed.command === 'verify') {
|
|
250
|
+
const providers = parsed.provider
|
|
251
|
+
? [parsed.provider]
|
|
252
|
+
: Object.keys(PROVIDER_CREDENTIALS).filter((provider) => describeProviderCredential(provider, options).configured);
|
|
253
|
+
const results = [];
|
|
254
|
+
for (const provider of providers) {
|
|
255
|
+
const result = await verifyProviderCredential(provider, options);
|
|
256
|
+
results.push({
|
|
257
|
+
provider,
|
|
258
|
+
ok: result.ok === true,
|
|
259
|
+
error: result.error || null,
|
|
260
|
+
model_count: Array.isArray(result.models) ? result.models.length : 0,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
if (parsed.json) writeLine(write, JSON.stringify(results, null, 2));
|
|
264
|
+
else {
|
|
265
|
+
writeLine(write, '');
|
|
266
|
+
for (const row of results) {
|
|
267
|
+
writeLine(write, ` ${row.provider.padEnd(18)} ${row.ok ? `verified${row.model_count ? ` · ${row.model_count} models` : ''}` : `failed · ${row.error}`}`);
|
|
268
|
+
}
|
|
269
|
+
writeLine(write, '');
|
|
270
|
+
}
|
|
271
|
+
return results;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (parsed.command === 'add' || parsed.command === 'set') {
|
|
275
|
+
if (!parsed.provider) throw new Error('providers add requires a provider');
|
|
276
|
+
if (parsed.fromEnv) {
|
|
277
|
+
const value = clean((options.env || process.env)[parsed.fromEnv]);
|
|
278
|
+
if (!value) throw new Error(`environment credential missing: ${parsed.fromEnv}`);
|
|
279
|
+
setProviderCredential(parsed.provider, value, options);
|
|
280
|
+
return verifyProviderCredential(parsed.provider, options);
|
|
281
|
+
}
|
|
282
|
+
if (options.interactive === false || !(options.interactive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY))) {
|
|
283
|
+
throw new Error('providers add requires an interactive terminal or --from-env NAME');
|
|
284
|
+
}
|
|
285
|
+
const result = await promptProviderCredential(parsed.provider, options);
|
|
286
|
+
if (!result) return null;
|
|
287
|
+
writeLine(write, result.ok ? ` ${parsed.provider} verified` : ` ${parsed.provider} saved · verification failed: ${result.error}`);
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (parsed.command === 'remove') {
|
|
292
|
+
if (!parsed.provider) throw new Error('providers remove requires a provider');
|
|
293
|
+
let approved = parsed.yes;
|
|
294
|
+
if (!approved) {
|
|
295
|
+
if (options.interactive === false || !(options.interactive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY))) {
|
|
296
|
+
throw new Error('providers remove requires --yes in non-interactive mode');
|
|
297
|
+
}
|
|
298
|
+
const answer = await (options.confirmImpl || confirm)({ message: `Remove ${providerCredentialSpec(parsed.provider)?.label} credential profile from this machine?` });
|
|
299
|
+
approved = !isCancel(answer) && answer === true;
|
|
300
|
+
}
|
|
301
|
+
if (!approved) return { provider: parsed.provider, removed: false };
|
|
302
|
+
const result = removeProviderCredential(parsed.provider, options);
|
|
303
|
+
writeLine(write, result.removed ? ` removed ${parsed.provider}` : ` ${parsed.provider} was not configured`);
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
throw new Error(`unknown providers command: ${parsed.command}`);
|
|
308
|
+
}
|
package/src/commands/shell.js
CHANGED
|
@@ -10,6 +10,7 @@ import { runDb } from './db.js';
|
|
|
10
10
|
import { runDeploy } from './deploy.js';
|
|
11
11
|
import { runStatus } from './status.js';
|
|
12
12
|
import { runModels } from './models.js';
|
|
13
|
+
import { runProviders } from './providers.js';
|
|
13
14
|
import { configureCliPreferences } from './preferences.js';
|
|
14
15
|
import { runCloudflare } from './cloudflare.js';
|
|
15
16
|
import { probeOllamaModel, resolveOllamaConfig } from './ollama.js';
|
|
@@ -593,6 +594,9 @@ export async function dispatchShellLine(line, state = {}) {
|
|
|
593
594
|
case '/models':
|
|
594
595
|
await runModels(args, { cwd: state.cwd, write, home: state.home });
|
|
595
596
|
break;
|
|
597
|
+
case '/providers':
|
|
598
|
+
await runProviders(args, { write, home: state.home, interactive: state.interactive });
|
|
599
|
+
break;
|
|
596
600
|
case '/login':
|
|
597
601
|
await runLogin(args, { write, home: state.home });
|
|
598
602
|
break;
|
package/src/commands/tunnel.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { spawn, spawnSync } from 'node:child_process';
|
|
8
8
|
import { authenticateViaBrowser } from '../lib/auth.js';
|
|
9
9
|
import { postJson } from '../lib/core-client.js';
|
|
10
|
-
import {
|
|
10
|
+
import { resolveAccountAuth } from '../lib/account-session.js';
|
|
11
11
|
|
|
12
12
|
const DEFAULT_PORT = 3099;
|
|
13
13
|
|
|
@@ -33,7 +33,7 @@ function parseArgs(argv) {
|
|
|
33
33
|
else if (a === '--platform') opts.platform = argv[++i] || opts.platform;
|
|
34
34
|
else if (a === '--shell') opts.shell = argv[++i] || opts.shell;
|
|
35
35
|
else if (a === '--token' && argv[i + 1]) {
|
|
36
|
-
process.env.
|
|
36
|
+
process.env.AGENTSAM_API_KEY = argv[++i];
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
return opts;
|
|
@@ -51,13 +51,13 @@ function ensureCloudflared() {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
async function resolveToken() {
|
|
54
|
-
const existing =
|
|
55
|
-
if (existing.
|
|
54
|
+
const existing = resolveAccountAuth({ env: process.env });
|
|
55
|
+
if (existing.value) return existing.value;
|
|
56
|
+
if (existing.error) throw new Error(existing.error);
|
|
56
57
|
const session = await authenticateViaBrowser();
|
|
57
|
-
const
|
|
58
|
-
if (!
|
|
59
|
-
|
|
60
|
-
return tok;
|
|
58
|
+
const token = String(session?.access_token || '').trim();
|
|
59
|
+
if (!token) throw new Error('IAM auth did not return a browser session credential');
|
|
60
|
+
return token;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
async function assertLocalPty(port) {
|
package/src/commands/whoami.js
CHANGED
|
@@ -1,49 +1,68 @@
|
|
|
1
1
|
import { getJson } from '../lib/core-client.js';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
describeAccountSession,
|
|
4
|
+
resolveAccountApiKey,
|
|
5
|
+
resolveAccountAuth,
|
|
6
|
+
} from '../lib/account-session.js';
|
|
3
7
|
import { listProviderCredentialStatus } from '../lib/provider-credentials.js';
|
|
4
8
|
|
|
5
9
|
function writeLine(write, value = '') { write(`${value}\n`); }
|
|
6
10
|
|
|
7
11
|
export async function collectWhoami(options = {}) {
|
|
8
12
|
const env = options.env || process.env;
|
|
9
|
-
const
|
|
10
|
-
const
|
|
13
|
+
const apiKey = resolveAccountApiKey({ env, explicit: options.token || '', home: options.home });
|
|
14
|
+
const browserSession = describeAccountSession({ env, home: options.home });
|
|
15
|
+
const active = resolveAccountAuth({ env, explicit: options.token || '', home: options.home });
|
|
11
16
|
const credentials = listProviderCredentialStatus({ env, home: options.home });
|
|
12
|
-
|
|
13
|
-
|
|
17
|
+
|
|
18
|
+
const base = {
|
|
19
|
+
schema_version: 2,
|
|
14
20
|
authenticated: false,
|
|
15
21
|
authority: 'iam',
|
|
16
22
|
identity: null,
|
|
17
|
-
|
|
23
|
+
active_auth: {
|
|
24
|
+
configured: Boolean(active.value || active.error),
|
|
25
|
+
kind: active.kind || null,
|
|
26
|
+
source: active.source || null,
|
|
27
|
+
valid: null,
|
|
28
|
+
error: active.error || null,
|
|
29
|
+
},
|
|
30
|
+
api_key: {
|
|
31
|
+
configured: Boolean(apiKey.value || apiKey.error),
|
|
32
|
+
source: apiKey.source || null,
|
|
33
|
+
valid: apiKey.error ? false : null,
|
|
34
|
+
error: apiKey.error || null,
|
|
35
|
+
},
|
|
36
|
+
browser_session: browserSession,
|
|
18
37
|
provider_credentials: credentials,
|
|
19
38
|
};
|
|
39
|
+
|
|
40
|
+
if (!active.value) return base;
|
|
41
|
+
|
|
20
42
|
try {
|
|
21
|
-
const loader = options.contextLoader || ((
|
|
22
|
-
const context = await loader(
|
|
43
|
+
const loader = options.contextLoader || ((token) => getJson('/api/sdk/context', token));
|
|
44
|
+
const context = await loader(active.value);
|
|
23
45
|
return {
|
|
24
|
-
|
|
46
|
+
...base,
|
|
25
47
|
authenticated: true,
|
|
26
|
-
authority: 'iam',
|
|
27
48
|
identity: {
|
|
28
49
|
user_id: context?.user_id || null,
|
|
29
50
|
account_id: context?.account_id || null,
|
|
30
51
|
email: context?.email || context?.user?.email || null,
|
|
31
52
|
},
|
|
32
|
-
|
|
33
|
-
|
|
53
|
+
active_auth: { ...base.active_auth, valid: true, error: null },
|
|
54
|
+
api_key: active.kind === 'api_key' ? { ...base.api_key, valid: true, error: null } : base.api_key,
|
|
34
55
|
cloudflare_connected: context?.cloudflare?.ok === true,
|
|
35
56
|
byok: context?.byok && typeof context.byok === 'object'
|
|
36
57
|
? Object.fromEntries(Object.entries(context.byok).map(([key, value]) => [key, { configured: value?.configured === true }]))
|
|
37
58
|
: {},
|
|
38
59
|
};
|
|
39
60
|
} catch (error) {
|
|
61
|
+
const message = error?.message || String(error);
|
|
40
62
|
return {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
identity: null,
|
|
45
|
-
sdk_credential: { configured: true, source: sdk.source, valid: false, error: error?.message || String(error) },
|
|
46
|
-
provider_credentials: credentials,
|
|
63
|
+
...base,
|
|
64
|
+
active_auth: { ...base.active_auth, valid: false, error: message },
|
|
65
|
+
api_key: active.kind === 'api_key' ? { ...base.api_key, valid: false, error: message } : base.api_key,
|
|
47
66
|
};
|
|
48
67
|
}
|
|
49
68
|
}
|
|
@@ -56,17 +75,19 @@ export function renderWhoami(status) {
|
|
|
56
75
|
if (status.identity?.email) lines.push(` email ${status.identity.email}`);
|
|
57
76
|
if (status.identity?.account_id) lines.push(` account ${status.identity.account_id}`);
|
|
58
77
|
if (status.identity?.user_id) lines.push(` user ${status.identity.user_id}`);
|
|
78
|
+
lines.push(` active auth ${status.active_auth?.kind || 'unknown'} · ${status.active_auth?.source || 'runtime'}`);
|
|
59
79
|
} else {
|
|
60
80
|
lines.push(' authenticated no');
|
|
61
|
-
lines.push(`
|
|
62
|
-
|
|
81
|
+
lines.push(` API key ${status.api_key?.configured ? status.api_key?.valid === false ? 'invalid' : 'configured' : 'not configured'}`);
|
|
82
|
+
lines.push(` browser login ${status.browser_session?.configured ? 'stored' : 'not configured'}`);
|
|
83
|
+
if (status.active_auth?.error) lines.push(` error ${status.active_auth.error}`);
|
|
63
84
|
}
|
|
64
85
|
lines.push('');
|
|
65
86
|
lines.push(' Provider credentials');
|
|
66
87
|
for (const row of status.provider_credentials || []) {
|
|
67
88
|
const state = row.configured ? 'available' : row.error ? `blocked (${row.error})` : 'not configured';
|
|
68
89
|
const source = row.source ? ` · ${row.source}` : '';
|
|
69
|
-
lines.push(` ${String(row.provider).padEnd(
|
|
90
|
+
lines.push(` ${String(row.provider).padEnd(16)} ${state}${source}`);
|
|
70
91
|
}
|
|
71
92
|
lines.push('');
|
|
72
93
|
lines.push(' Secret values are never printed by whoami.');
|
package/src/errors/diagnostic.js
CHANGED
|
@@ -3,6 +3,7 @@ const SECRET_KEY = /(?:authorization|api[-_]?key|access[-_]?token|refresh[-_]?to
|
|
|
3
3
|
const SECRET_VALUE_PATTERNS = [
|
|
4
4
|
/\bBearer\s+[A-Za-z0-9._~+\/-]+=*/gi,
|
|
5
5
|
/\bsk-[A-Za-z0-9_-]{12,}\b/g,
|
|
6
|
+
/\baak_[A-Za-z0-9_-]{8,}\b/g,
|
|
6
7
|
/\bsdk_[A-Za-z0-9_-]{8,}\b/g,
|
|
7
8
|
/\bgh[pousr]_[A-Za-z0-9]{20,}\b/g,
|
|
8
9
|
];
|
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { isApiKey, resolveApiKey } from '../../packages/identity/src/contracts/auth-config.js';
|
|
5
5
|
|
|
6
|
-
export const ACCOUNT_SESSION_SCHEMA = 'agentsam-account-session-
|
|
6
|
+
export const ACCOUNT_SESSION_SCHEMA = 'agentsam-account-session-v3';
|
|
7
|
+
const LEGACY_ACCOUNT_SESSION_SCHEMA = 'agentsam-account-session-v2';
|
|
7
8
|
|
|
8
9
|
function clean(value) { return value == null ? '' : String(value).trim(); }
|
|
9
10
|
function homeDirectory(options = {}) {
|
|
10
11
|
return path.resolve(clean(options.home) || clean(options.env?.HOME) || clean(options.env?.USERPROFILE) || os.homedir());
|
|
11
12
|
}
|
|
13
|
+
function isoOrNull(value) {
|
|
14
|
+
const raw = clean(value);
|
|
15
|
+
if (!raw) return null;
|
|
16
|
+
const ms = Date.parse(raw);
|
|
17
|
+
return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
|
|
18
|
+
}
|
|
19
|
+
function expiresAtFrom(session = {}, nowMs = Date.now()) {
|
|
20
|
+
const explicit = isoOrNull(session.expires_at);
|
|
21
|
+
if (explicit) return explicit;
|
|
22
|
+
const expiresIn = Number(session.expires_in);
|
|
23
|
+
if (!Number.isFinite(expiresIn) || expiresIn <= 0) return null;
|
|
24
|
+
return new Date(nowMs + (expiresIn * 1000)).toISOString();
|
|
25
|
+
}
|
|
12
26
|
|
|
13
27
|
export function accountSessionPath(options = {}) {
|
|
14
28
|
return path.join(homeDirectory(options), '.agentsam', 'auth', 'session.json');
|
|
@@ -22,17 +36,24 @@ export function readAccountSession(options = {}) {
|
|
|
22
36
|
if (!stat.isFile()) return null;
|
|
23
37
|
if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0) return null;
|
|
24
38
|
const parsed = JSON.parse(fs.readFileSync(filename, 'utf8'));
|
|
25
|
-
if (parsed?.schema_version
|
|
26
|
-
const
|
|
27
|
-
|
|
39
|
+
if (![ACCOUNT_SESSION_SCHEMA, LEGACY_ACCOUNT_SESSION_SCHEMA].includes(parsed?.schema_version)) return null;
|
|
40
|
+
const accessToken = clean(parsed.access_token);
|
|
41
|
+
const refreshToken = clean(parsed.refresh_token);
|
|
42
|
+
if (!accessToken || isApiKey(accessToken) || (refreshToken && isApiKey(refreshToken))) return null;
|
|
28
43
|
return {
|
|
29
44
|
schema_version: ACCOUNT_SESSION_SCHEMA,
|
|
30
|
-
|
|
45
|
+
access_token: accessToken,
|
|
46
|
+
refresh_token: refreshToken || null,
|
|
47
|
+
token_type: clean(parsed.token_type) || 'Bearer',
|
|
48
|
+
scope: clean(parsed.scope) || null,
|
|
49
|
+
expires_at: isoOrNull(parsed.expires_at),
|
|
50
|
+
client_id: clean(parsed.client_id) || null,
|
|
31
51
|
user_id: clean(parsed.user_id) || null,
|
|
32
52
|
account_id: clean(parsed.account_id) || null,
|
|
33
53
|
email: clean(parsed.email) || null,
|
|
34
|
-
created_at:
|
|
35
|
-
updated_at:
|
|
54
|
+
created_at: isoOrNull(parsed.created_at),
|
|
55
|
+
updated_at: isoOrNull(parsed.updated_at),
|
|
56
|
+
migrated_from: parsed.schema_version === LEGACY_ACCOUNT_SESSION_SCHEMA ? LEGACY_ACCOUNT_SESSION_SCHEMA : null,
|
|
36
57
|
};
|
|
37
58
|
} catch {
|
|
38
59
|
return null;
|
|
@@ -40,25 +61,38 @@ export function readAccountSession(options = {}) {
|
|
|
40
61
|
}
|
|
41
62
|
|
|
42
63
|
export function saveAccountSession(session = {}, options = {}) {
|
|
43
|
-
const
|
|
44
|
-
|
|
64
|
+
const accessToken = clean(session.access_token);
|
|
65
|
+
const suppliedRefresh = clean(session.refresh_token);
|
|
66
|
+
if (!accessToken || isApiKey(accessToken) || (suppliedRefresh && isApiKey(suppliedRefresh))) {
|
|
67
|
+
throw new Error('account_browser_oauth_session_required');
|
|
68
|
+
}
|
|
69
|
+
|
|
45
70
|
const filename = accountSessionPath(options);
|
|
46
71
|
const dir = path.dirname(filename);
|
|
47
72
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
48
73
|
if (process.platform !== 'win32') {
|
|
49
74
|
try { fs.chmodSync(dir, 0o700); } catch { /* best effort */ }
|
|
50
75
|
}
|
|
76
|
+
|
|
51
77
|
const previous = readAccountSession(options);
|
|
52
|
-
const
|
|
78
|
+
const nowMs = Number.isFinite(Number(options.nowMs)) ? Number(options.nowMs) : Date.now();
|
|
79
|
+
const now = new Date(nowMs).toISOString();
|
|
80
|
+
const preserveRefreshToken = options.preserveRefreshToken !== false;
|
|
53
81
|
const value = {
|
|
54
82
|
schema_version: ACCOUNT_SESSION_SCHEMA,
|
|
55
|
-
|
|
83
|
+
access_token: accessToken,
|
|
84
|
+
refresh_token: suppliedRefresh || (preserveRefreshToken ? previous?.refresh_token : null) || null,
|
|
85
|
+
token_type: clean(session.token_type) || previous?.token_type || 'Bearer',
|
|
86
|
+
scope: clean(session.scope) || previous?.scope || null,
|
|
87
|
+
expires_at: expiresAtFrom(session, nowMs),
|
|
88
|
+
client_id: clean(session.client_id) || previous?.client_id || null,
|
|
56
89
|
user_id: clean(session.user_id) || previous?.user_id || null,
|
|
57
90
|
account_id: clean(session.account_id) || previous?.account_id || null,
|
|
58
91
|
email: clean(session.email) || previous?.email || null,
|
|
59
92
|
created_at: previous?.created_at || now,
|
|
60
93
|
updated_at: now,
|
|
61
94
|
};
|
|
95
|
+
|
|
62
96
|
const temp = `${filename}.${process.pid}.tmp`;
|
|
63
97
|
fs.writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
64
98
|
if (process.platform !== 'win32') {
|
|
@@ -75,24 +109,59 @@ export function clearAccountSession(options = {}) {
|
|
|
75
109
|
return true;
|
|
76
110
|
}
|
|
77
111
|
|
|
78
|
-
export function
|
|
112
|
+
export function isBrowserSessionExpired(session, options = {}) {
|
|
113
|
+
const expiresAt = Date.parse(clean(session?.expires_at));
|
|
114
|
+
if (!Number.isFinite(expiresAt)) return false;
|
|
115
|
+
const nowMs = Number.isFinite(Number(options.nowMs)) ? Number(options.nowMs) : Date.now();
|
|
116
|
+
const skewMs = Number.isFinite(Number(options.skewMs)) ? Number(options.skewMs) : 60_000;
|
|
117
|
+
return expiresAt <= (nowMs + Math.max(0, skewMs));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function resolveAccountApiKey(options = {}) {
|
|
79
121
|
const env = options.env || process.env;
|
|
80
122
|
const explicit = clean(options.explicit);
|
|
81
|
-
const
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
return
|
|
85
|
-
|
|
86
|
-
|
|
123
|
+
const value = resolveApiKey(env, explicit);
|
|
124
|
+
if (!value) return { value: '', source: null, kind: null };
|
|
125
|
+
if (!isApiKey(value)) return { value: '', source: explicit ? 'explicit' : 'environment', kind: 'api_key', error: 'invalid_api_key_prefix' };
|
|
126
|
+
return { value, source: explicit ? 'explicit' : 'environment', kind: 'api_key' };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function resolveBrowserSessionCredential(options = {}) {
|
|
130
|
+
const session = readAccountSession(options);
|
|
131
|
+
if (!session?.access_token) return { value: '', source: null, kind: null, session: null, expired: false };
|
|
132
|
+
const expired = isBrowserSessionExpired(session, options);
|
|
133
|
+
return {
|
|
134
|
+
value: expired ? '' : session.access_token,
|
|
135
|
+
source: 'agentsam_browser_oauth',
|
|
136
|
+
kind: 'browser_oauth',
|
|
137
|
+
session,
|
|
138
|
+
expired,
|
|
139
|
+
error: expired ? 'browser_oauth_session_expired' : null,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Synchronous authority snapshot kept for low-level compatibility.
|
|
145
|
+
* Runtime SDK calls should use resolveAccountAuthority() from auth.js so expired
|
|
146
|
+
* OAuth sessions can refresh before a request is sent.
|
|
147
|
+
*/
|
|
148
|
+
export function resolveAccountAuth(options = {}) {
|
|
149
|
+
const apiKey = resolveAccountApiKey(options);
|
|
150
|
+
if (apiKey.value || apiKey.error) return apiKey;
|
|
151
|
+
return resolveBrowserSessionCredential(options);
|
|
87
152
|
}
|
|
88
153
|
|
|
89
154
|
export function describeAccountSession(options = {}) {
|
|
90
|
-
const
|
|
155
|
+
const session = readAccountSession(options);
|
|
91
156
|
return {
|
|
92
|
-
configured: Boolean(
|
|
93
|
-
source:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
157
|
+
configured: Boolean(session?.access_token),
|
|
158
|
+
source: session?.access_token ? 'agentsam_browser_oauth' : null,
|
|
159
|
+
kind: session?.access_token ? 'browser_oauth' : null,
|
|
160
|
+
refreshable: Boolean(session?.refresh_token),
|
|
161
|
+
expires_at: session?.expires_at || null,
|
|
162
|
+
expired: session ? isBrowserSessionExpired(session, options) : false,
|
|
163
|
+
user_id: session?.user_id || null,
|
|
164
|
+
account_id: session?.account_id || null,
|
|
165
|
+
email: session?.email || null,
|
|
97
166
|
};
|
|
98
167
|
}
|