@inneranimalmedia/agentsam-sdk 2.4.1 → 2.6.0
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/AGENTSAM.md +55 -0
- package/README.md +17 -12
- package/bin/agentsam +2 -0
- package/docs/AGENTSAM_ASTRA_OPENAI_INTEGRATION.md +1363 -0
- package/docs/AGENTSAM_WORKBENCH.md +30 -0
- package/docs/AUTH_IDENTITY_CONTRACT.md +52 -0
- package/docs/CAPABILITIES.md +5 -3
- package/docs/CLI_SHELL.md +167 -80
- package/docs/CMS_STUDIO.md +101 -0
- package/docs/CONTEXT.md +170 -0
- package/docs/LOCAL_OLLAMA.md +58 -0
- package/docs/MERKLE.md +19 -19
- package/docs/PORTABLE_CONTEXT.md +4 -3
- package/docs/PROJECT_CONFIG.md +72 -0
- package/docs/RELEASES.md +35 -2
- package/docs/REPOSITORY_INTELLIGENCE.md +1 -1
- package/docs/REPOSITORY_KNOWLEDGE.md +114 -0
- package/docs/SDK_WORKER.md +86 -0
- package/docs/SECURITY.md +59 -22
- package/docs/client-cms-editor.md +15 -0
- package/docs/local-studio/WORKMODE_DONOR_NOTES.md +485 -0
- package/package.json +35 -10
- package/packages/connectors/cloudflare/package.json +10 -0
- package/packages/connectors/cloudflare/src/index.js +127 -0
- package/packages/connectors/cloudflare/src/owner.js +76 -0
- package/packages/connectors/cloudflare/src/routes.js +223 -0
- package/packages/connectors/cloudflare/src/vault.js +80 -0
- package/packages/connectors/cloudflare/tests/connector.test.mjs +44 -0
- package/packages/identity/package.json +10 -1
- package/packages/identity/src/contracts/auth-config.js +109 -0
- package/packages/identity/src/index.js +1 -0
- package/packages/identity/src/oauth/README.md +2 -2
- package/packages/identity/src/oauth/credentials.js +11 -4
- package/packages/identity/src/oauth/iam-platform.js +3 -3
- package/packages/identity/src/providers/iam/index.js +7 -7
- package/packages/identity/src/providers/iam/oauth.js +6 -4
- package/packages/identity/src/providers/iam/profile.js +5 -5
- package/packages/identity/tests/auth-config.test.mjs +61 -0
- package/packages/identity/tests/oauth-credentials.test.mjs +13 -2
- package/protocol/FILEMETA_V1.md +95 -0
- package/protocol/INSPECT_VIEWS_V1.md +40 -0
- package/protocol/MERKLE_PERSISTENCE_V1.md +50 -0
- package/protocol/MERKLE_V1.md +3 -1
- package/protocol/README.md +1 -0
- package/protocol/capabilities/cloudflare-cpu-audit-input.schema.json +19 -0
- package/protocol/capabilities/cloudflare-cpu-profile-input.schema.json +13 -0
- package/protocol/capabilities/cloudflare-wrangler-native-input.schema.json +19 -0
- package/protocol/capabilities/manifest.json +79 -3
- package/protocol/capabilities/repository-snapshot.schema.json +1 -0
- package/protocol/context/context-budget.schema.json +36 -0
- package/protocol/context/context-item.schema.json +19 -0
- package/protocol/context/resolved-context-pack.schema.json +47 -0
- package/protocol/context/result-policy.schema.json +17 -0
- package/protocol/knowledge/context-pack.schema.json +33 -0
- package/protocol/knowledge/retrieval-query.schema.json +68 -13
- package/protocol/models/README.md +373 -0
- package/protocol/models/model-inventory-v2.schema.json +212 -0
- package/python/README.md +15 -7
- package/python/agentsam_sdk/cli.py +0 -21
- package/python/agentsam_sdk/tui/README.md +17 -12
- package/python/agentsam_sdk/tui/bootstrap.py +2 -2
- package/python/agentsam_sdk/tui/demo.py +25 -10
- package/python/agentsam_sdk/tui/onboarding.py +208 -0
- package/python/tests/test_tui_cli.py +7 -6
- package/skills/README.md +21 -0
- package/skills/agentsam-app-fundamentals/SKILL.md +165 -0
- package/skills/agentsam-app-fundamentals/references/graphs-contracts-ast-merkle.md +89 -0
- package/skills/agentsam-app-fundamentals/references/trust-credentials-and-destinations.md +99 -0
- package/skills/agentsam-cloudflare-workers/SKILL.md +53 -0
- package/skills/agentsam-cloudflare-workers/references/cpu-profiling.md +16 -0
- package/skills/agentsam-cloudflare-workers/references/errors-and-observability.md +29 -0
- package/skills/agentsam-cloudflare-workers/references/wrangler-native-map.md +28 -0
- package/skills/agentsam-jr-dev/SKILL.md +232 -0
- package/skills/agentsam-jr-dev/references/real-application-logic.md +156 -0
- package/skills/agentsam-jr-dev/references/web-application-fundamentals.md +240 -0
- package/skills/agentsam-progression-guard/SKILL.md +197 -0
- package/skills/agentsam-progression-guard/references/checkpoint-chain.md +111 -0
- package/skills/agentsam-progression-guard/references/hooks-operational-io.md +96 -0
- package/skills/catalog.json +71 -0
- package/src/agent/capability-adapter.js +25 -13
- package/src/agent/index.js +1 -0
- package/src/agent/responses-runner.js +325 -0
- package/src/capabilities/index.js +7 -0
- package/src/capabilities/repository-snapshot-view.js +238 -0
- package/src/capabilities/repository-snapshot.js +28 -14
- package/src/cli.js +198 -76
- package/src/cloudflare/cpu-profile.js +115 -0
- package/src/cloudflare/index.js +14 -0
- package/src/cloudflare/wrangler.js +132 -0
- package/src/commands/account-auth.js +47 -0
- package/src/commands/cloudflare.js +58 -0
- package/src/commands/connections.js +93 -0
- package/src/commands/context-economics.js +114 -0
- package/src/commands/context.js +14 -2
- package/src/commands/db.js +4 -7
- package/src/commands/deploy.js +54 -32
- package/src/commands/eval.js +63 -0
- package/src/commands/interactive.js +18 -0
- package/src/commands/knowledge.js +27 -4
- package/src/commands/merkle-persist.js +118 -0
- package/src/commands/merkle.js +32 -17
- package/src/commands/models.js +152 -0
- package/src/commands/ollama.js +259 -0
- package/src/commands/preferences.js +144 -0
- package/src/commands/product.js +86 -16
- package/src/commands/resume.js +67 -0
- package/src/commands/security.js +8 -6
- package/src/commands/shell.js +405 -100
- package/src/commands/skills.js +66 -0
- package/src/commands/start-local.js +1 -1
- package/src/commands/tunnel.js +5 -4
- package/src/commands/whoami.js +86 -0
- package/src/context/budget.js +120 -0
- package/src/context/compact.js +28 -0
- package/src/context/index.js +7 -0
- package/src/context/rehydrate.js +35 -0
- package/src/context/resolve.js +116 -0
- package/src/context/result-policy.js +66 -0
- package/src/errors/diagnostic.js +160 -0
- package/src/errors/index.js +9 -0
- package/src/eval/context.js +191 -0
- package/src/eval/index.js +1 -0
- package/src/index.js +70 -0
- package/src/indexing/execution-boundary.js +144 -0
- package/src/indexing/index.js +8 -0
- package/src/indexing/provider.js +41 -0
- package/src/knowledge/config.js +2 -2
- package/src/knowledge/context-pack.js +12 -1
- package/src/knowledge/contracts.js +9 -4
- package/src/knowledge/engine.js +7 -3
- package/src/knowledge/service/server.js +1 -1
- package/src/lib/account-session.js +98 -0
- package/src/lib/agent-instructions.js +73 -0
- package/src/lib/auth.js +4 -0
- package/src/lib/bridge-client.js +7 -5
- package/src/lib/cli-preferences.js +78 -0
- package/src/lib/core-client.js +8 -8
- package/src/lib/deploy/git-guard.js +69 -0
- package/src/lib/deploy/health.js +57 -0
- package/src/lib/deploy/local-studio.js +283 -0
- package/src/lib/deploy/secret-scan.js +65 -0
- package/src/lib/deploy-receipt/index.js +5 -2
- package/src/lib/detect-context.js +6 -5
- package/src/lib/execution-approvals.js +59 -0
- package/src/lib/identity-scaffold.js +1 -1
- package/src/lib/local-scaffold.js +35 -33
- package/src/lib/local-sessions.js +127 -0
- package/src/lib/local-status.js +9 -17
- package/src/lib/merkle/cloudflare-persistence.js +321 -0
- package/src/lib/merkle/filemeta.js +43 -0
- package/src/lib/merkle/git-ignore.js +24 -0
- package/src/lib/merkle/hash.js +1 -0
- package/src/lib/merkle/index.js +22 -0
- package/src/lib/merkle/persistence.js +72 -0
- package/src/lib/merkle/semantic.js +359 -0
- package/src/lib/merkle/snapshot.js +9 -3
- package/src/lib/merkle/tree.js +11 -6
- package/src/lib/project-config.js +227 -0
- package/src/lib/project-rules.js +68 -0
- package/src/lib/provider-credentials.js +83 -0
- package/src/lib/save-sdk-token.js +1 -1
- package/src/lib/scaffold/templates/worker-api/index.js +101 -20
- package/src/lib/scaffold/wizards/worker-api.js +27 -11
- package/src/lib/slash-commands.js +22 -15
- package/src/lib/tools.js +11 -5
- package/src/models/catalog.js +135 -0
- package/src/models/index.js +7 -0
- package/src/providers/index.js +5 -0
- package/src/providers/openai-responses.js +275 -0
- package/src/security/index.js +1 -0
- package/src/security/inventory.js +4 -1
- package/src/security/process.js +35 -9
- package/src/security/render.js +27 -5
- package/src/security/scan.js +24 -9
- package/src/security/trust-boundary.js +24 -0
- package/src/skills/index.js +64 -0
- package/src/telemetry/contracts.js +203 -0
- package/src/telemetry/events.js +48 -0
- package/src/telemetry/index.js +8 -0
- package/src/tools/hydrate.js +35 -0
- package/src/tools/index.js +2 -0
- package/src/tools/search.js +70 -0
- package/src/ui/ansi.js +1 -1
- package/src/ui/boot.js +54 -0
- package/src/ui/merkle/render.js +1 -0
- package/src/ui/runtime-activity.js +192 -0
- package/src/ui/theme.js +19 -18
- package/test/account-session.test.mjs +36 -0
- package/test/app-building-skills.test.mjs +61 -0
- package/test/apps-scaffold-contract.test.mjs +56 -0
- package/test/capabilities.test.mjs +53 -4
- package/test/cli-preferences.test.mjs +46 -0
- package/test/cloudflare-connector.test.mjs +96 -0
- package/test/cloudflare-runtime.test.mjs +75 -0
- package/test/context.test.mjs +144 -0
- package/test/deploy-health-scan.test.mjs +67 -0
- package/test/error-diagnostics.test.mjs +59 -0
- package/test/eval-context.test.mjs +37 -0
- package/test/execution-approvals.test.mjs +27 -0
- package/test/indexing-provider.test.mjs +29 -0
- package/test/jr-dev-skill.test.mjs +26 -0
- package/test/knowledge-context-pack.test.mjs +19 -0
- package/test/knowledge.test.mjs +1 -1
- package/test/local-sessions.test.mjs +42 -0
- package/test/local-studio-deploy.test.mjs +83 -0
- package/test/merkle-persistence.test.mjs +91 -0
- package/test/merkle.test.mjs +77 -3
- package/test/model-catalog.test.mjs +43 -0
- package/test/models.test.mjs +51 -0
- package/test/npm10-lock.test.mjs +29 -0
- package/test/ollama.test.mjs +94 -0
- package/test/openai-responses.test.mjs +95 -0
- package/test/project-config.test.mjs +81 -0
- package/test/project-rules.test.mjs +44 -0
- package/test/provider-credentials.test.mjs +52 -0
- package/test/rehydrate.test.mjs +25 -0
- package/test/release-hygiene.test.mjs +34 -0
- package/test/repository-snapshot-view.test.mjs +112 -0
- package/test/responses-runner.test.mjs +148 -0
- package/test/runtime-activity.test.mjs +98 -0
- package/test/sdk-worker-contract.test.mjs +68 -0
- package/test/security.test.mjs +46 -0
- package/test/shell.test.mjs +55 -21
- package/test/skills.test.mjs +22 -0
- package/test/smoke.mjs +5 -2
- package/test/telemetry.test.mjs +79 -0
- package/test/theme-portability.test.mjs +14 -0
- package/test/tools-search.test.mjs +40 -0
- package/test/whoami-resume.test.mjs +56 -0
- package/examples/agentsam-tui-ansi.mjs +0 -149
- package/src/commands/tui.js +0 -120
- package/src/ui/splash-xterm.js +0 -290
- package/src/ui/splash.js +0 -426
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare account connector (not an identity provider).
|
|
3
|
+
* Answers: what Cloudflare account has this authenticated AgentSam user authorized?
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const CLOUDFLARE_OAUTH_AUTHORIZE_URL = 'https://dash.cloudflare.com/oauth2/auth';
|
|
7
|
+
export const CLOUDFLARE_OAUTH_TOKEN_URL = 'https://dash.cloudflare.com/oauth2/token';
|
|
8
|
+
export const CLOUDFLARE_CALLBACK_PATH = '/api/connections/cloudflare/callback';
|
|
9
|
+
export const CLOUDFLARE_FIXTURE_CLIENT_ID = 'sillynotreal';
|
|
10
|
+
export const CLOUDFLARE_FIXTURE_CLIENT_SECRET = 'sillynotreal-secret';
|
|
11
|
+
|
|
12
|
+
export const CLOUDFLARE_OAUTH_REVOKE_URL = 'https://dash.cloudflare.com/oauth2/revoke';
|
|
13
|
+
|
|
14
|
+
/** Smallest useful scopes mapped to Cloudflare API token permission names. */
|
|
15
|
+
export const CLOUDFLARE_CAPABILITY_SCOPES = Object.freeze({
|
|
16
|
+
workers_deploy: {
|
|
17
|
+
scopes: ['workers-scripts.write'],
|
|
18
|
+
why: 'Deploy Workers for the connected account (Workers Scripts Edit).',
|
|
19
|
+
},
|
|
20
|
+
d1_inspect: {
|
|
21
|
+
scopes: ['d1.read'],
|
|
22
|
+
why: 'Inspect D1 databases bound to the deployable.',
|
|
23
|
+
},
|
|
24
|
+
r2_inspect: {
|
|
25
|
+
scopes: ['workers-r2-storage.read'],
|
|
26
|
+
why: 'Inspect R2 buckets bound to the deployable.',
|
|
27
|
+
},
|
|
28
|
+
worker_logs: {
|
|
29
|
+
scopes: ['workers-scripts.read'],
|
|
30
|
+
why: 'Read Worker script metadata/logs for postdeploy health.',
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export function requestedCloudflareScopes() {
|
|
35
|
+
const set = new Set();
|
|
36
|
+
for (const cap of Object.values(CLOUDFLARE_CAPABILITY_SCOPES)) {
|
|
37
|
+
for (const scope of cap.scopes) set.add(scope);
|
|
38
|
+
}
|
|
39
|
+
return [...set];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function clean(value) {
|
|
43
|
+
return value == null ? '' : String(value).trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isFixtureCloudflareCredential(value) {
|
|
47
|
+
const v = clean(value);
|
|
48
|
+
return v === CLOUDFLARE_FIXTURE_CLIENT_ID || v === CLOUDFLARE_FIXTURE_CLIENT_SECRET;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveCloudflareOAuthClient(env = {}) {
|
|
52
|
+
const clientId = clean(env.CLOUDFLARE_OAUTH_CLIENT_ID);
|
|
53
|
+
const clientSecret = clean(env.CLOUDFLARE_OAUTH_CLIENT_SECRET);
|
|
54
|
+
const present = Boolean(clientId && clientSecret);
|
|
55
|
+
const fixture = isFixtureCloudflareCredential(clientId) || isFixtureCloudflareCredential(clientSecret);
|
|
56
|
+
if (!present) {
|
|
57
|
+
return {
|
|
58
|
+
configured: false,
|
|
59
|
+
productionReady: false,
|
|
60
|
+
fixture: false,
|
|
61
|
+
status: 'not_configured',
|
|
62
|
+
clientIdConfigured: Boolean(clientId),
|
|
63
|
+
secretConfigured: Boolean(clientSecret),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
configured: true,
|
|
68
|
+
productionReady: !fixture,
|
|
69
|
+
fixture,
|
|
70
|
+
status: fixture ? 'fixture' : 'ready',
|
|
71
|
+
clientIdConfigured: true,
|
|
72
|
+
secretConfigured: true,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function cloudflareConnectionSafeStatus(env = {}, connection = null, ownerId = '') {
|
|
77
|
+
const client = resolveCloudflareOAuthClient(env);
|
|
78
|
+
const record = connection && connection.ownerId === ownerId ? connection : null;
|
|
79
|
+
return {
|
|
80
|
+
provider: 'cloudflare',
|
|
81
|
+
status: record ? 'connected' : client.status === 'ready' ? 'ready' : client.status,
|
|
82
|
+
configured: client.configured && client.productionReady,
|
|
83
|
+
fixture: client.fixture,
|
|
84
|
+
clientId: client.clientIdConfigured ? 'configured' : 'missing',
|
|
85
|
+
secret: client.secretConfigured ? 'configured' : 'missing',
|
|
86
|
+
callbackPath: CLOUDFLARE_CALLBACK_PATH,
|
|
87
|
+
connection: record
|
|
88
|
+
? {
|
|
89
|
+
connection_id: record.connectionId,
|
|
90
|
+
owner: record.ownerId,
|
|
91
|
+
cloudflare_account_id: record.cloudflareAccountId || null,
|
|
92
|
+
scopes: record.scopes || [],
|
|
93
|
+
status: record.status,
|
|
94
|
+
created_at: record.createdAt,
|
|
95
|
+
updated_at: record.updatedAt,
|
|
96
|
+
expires_at: record.expiresAt || null,
|
|
97
|
+
}
|
|
98
|
+
: null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function assertConnectionOwner(connection, ownerId) {
|
|
103
|
+
if (!ownerId) {
|
|
104
|
+
const err = new Error('unauthenticated');
|
|
105
|
+
err.code = 'unauthenticated';
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
if (!connection || connection.ownerId !== ownerId) {
|
|
109
|
+
const err = new Error('cloudflare_connection_forbidden');
|
|
110
|
+
err.code = 'cloudflare_connection_forbidden';
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
return connection;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function buildAuthorizeUrl({ clientId, redirectUri, state, codeChallenge, scopes }) {
|
|
117
|
+
const params = new URLSearchParams({
|
|
118
|
+
response_type: 'code',
|
|
119
|
+
client_id: clientId,
|
|
120
|
+
redirect_uri: redirectUri,
|
|
121
|
+
state,
|
|
122
|
+
code_challenge: codeChallenge,
|
|
123
|
+
code_challenge_method: 'S256',
|
|
124
|
+
scope: (scopes || requestedCloudflareScopes()).join(' '),
|
|
125
|
+
});
|
|
126
|
+
return `${CLOUDFLARE_OAUTH_AUTHORIZE_URL}?${params}`;
|
|
127
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare connection ownership is derived from authenticated AgentSam session.
|
|
3
|
+
* Browser-submitted account_id / user_id / owner_id / X-User-Id are never authority.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const UNTRUSTED = new Set(['account_id', 'user_id', 'owner_id', 'workspace_id']);
|
|
7
|
+
|
|
8
|
+
export function extractSessionToken(request) {
|
|
9
|
+
const auth = request.headers.get('authorization') || '';
|
|
10
|
+
if (/^bearer\s+/i.test(auth)) {
|
|
11
|
+
const token = auth.replace(/^bearer\s+/i, '').trim();
|
|
12
|
+
if (token && !token.startsWith('cf_') && token !== 'sillynotreal-secret') return token;
|
|
13
|
+
}
|
|
14
|
+
const cookie = request.headers.get('cookie') || '';
|
|
15
|
+
const match = cookie.match(/(?:^|;\s*)agentsam_session=([^;]+)/);
|
|
16
|
+
return match ? decodeURIComponent(match[1]).trim() : '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function rejectUntrustedOwnerHints(request, url, body = {}) {
|
|
20
|
+
const headerUser = (request.headers.get('x-user-id') || '').trim();
|
|
21
|
+
for (const key of UNTRUSTED) {
|
|
22
|
+
if (url.searchParams.get(key)) {
|
|
23
|
+
const err = new Error('untrusted_owner_hint');
|
|
24
|
+
err.code = 'untrusted_owner_hint';
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
if (body && body[key]) {
|
|
28
|
+
const err = new Error('untrusted_owner_hint');
|
|
29
|
+
err.code = 'untrusted_owner_hint';
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// X-User-Id is vault v1 compatibility only and is never connector authority.
|
|
34
|
+
return { ignoredXUserId: Boolean(headerUser) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function resolveAuthenticatedOwner(request, env, url, body) {
|
|
38
|
+
rejectUntrustedOwnerHints(request, url, body);
|
|
39
|
+
const sessionToken = extractSessionToken(request);
|
|
40
|
+
if (!sessionToken) {
|
|
41
|
+
const err = new Error('unauthenticated');
|
|
42
|
+
err.code = 'unauthenticated';
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
if (env?.sessions instanceof Map) {
|
|
46
|
+
const owner = env.sessions.get(sessionToken);
|
|
47
|
+
if (!owner) {
|
|
48
|
+
const err = new Error('unauthenticated');
|
|
49
|
+
err.code = 'unauthenticated';
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
return String(owner);
|
|
53
|
+
}
|
|
54
|
+
if (env?.DB?.prepare) {
|
|
55
|
+
const tables = [
|
|
56
|
+
['agentsam_sessions', 'session_token', 'user_id'],
|
|
57
|
+
['sessions', 'id', 'user_id'],
|
|
58
|
+
['identity_sessions', 'session_token', 'user_id'],
|
|
59
|
+
];
|
|
60
|
+
for (const [table, tokenCol, userCol] of tables) {
|
|
61
|
+
try {
|
|
62
|
+
const row = await env.DB.prepare(
|
|
63
|
+
`SELECT ${userCol} AS user_id FROM ${table} WHERE ${tokenCol} = ? LIMIT 1`,
|
|
64
|
+
)
|
|
65
|
+
.bind(sessionToken)
|
|
66
|
+
.first();
|
|
67
|
+
if (row?.user_id) return String(row.user_id);
|
|
68
|
+
} catch {
|
|
69
|
+
// table may not exist yet
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const err = new Error('unauthenticated');
|
|
74
|
+
err.code = 'unauthenticated';
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CLOUDFLARE_CALLBACK_PATH,
|
|
3
|
+
CLOUDFLARE_OAUTH_TOKEN_URL,
|
|
4
|
+
CLOUDFLARE_OAUTH_REVOKE_URL,
|
|
5
|
+
buildAuthorizeUrl,
|
|
6
|
+
cloudflareConnectionSafeStatus,
|
|
7
|
+
resolveCloudflareOAuthClient,
|
|
8
|
+
requestedCloudflareScopes,
|
|
9
|
+
assertConnectionOwner,
|
|
10
|
+
} from './index.js';
|
|
11
|
+
import { resolveAuthenticatedOwner } from './owner.js';
|
|
12
|
+
import { sealToken, vaultConfigured } from './vault.js';
|
|
13
|
+
|
|
14
|
+
function json(data, status = 200) {
|
|
15
|
+
return new Response(JSON.stringify(data), {
|
|
16
|
+
status,
|
|
17
|
+
headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function redirect(location) {
|
|
22
|
+
return new Response(null, { status: 302, headers: { location } });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isCloudflareConnectionPath(pathname) {
|
|
26
|
+
return pathname === '/api/connections/cloudflare'
|
|
27
|
+
|| pathname === '/api/connections/cloudflare/start'
|
|
28
|
+
|| pathname === CLOUDFLARE_CALLBACK_PATH
|
|
29
|
+
|| pathname === '/api/connections/cloudflare/disconnect';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function ensureTables(env) {
|
|
33
|
+
if (!env?.DB?.prepare) return;
|
|
34
|
+
await env.DB.prepare(`CREATE TABLE IF NOT EXISTS agentsam_cloudflare_connections (
|
|
35
|
+
connection_id TEXT PRIMARY KEY,
|
|
36
|
+
owner_id TEXT NOT NULL,
|
|
37
|
+
cloudflare_account_id TEXT,
|
|
38
|
+
scopes TEXT,
|
|
39
|
+
status TEXT,
|
|
40
|
+
access_token_encrypted TEXT,
|
|
41
|
+
refresh_token_encrypted TEXT,
|
|
42
|
+
created_at INTEGER,
|
|
43
|
+
updated_at INTEGER,
|
|
44
|
+
expires_at INTEGER
|
|
45
|
+
)`).run();
|
|
46
|
+
await env.DB.prepare(`CREATE TABLE IF NOT EXISTS agentsam_cloudflare_oauth_state (
|
|
47
|
+
state TEXT PRIMARY KEY,
|
|
48
|
+
owner_id TEXT NOT NULL,
|
|
49
|
+
code_verifier TEXT NOT NULL,
|
|
50
|
+
created_at INTEGER
|
|
51
|
+
)`).run();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function handleCloudflareConnectionRequest(request, env) {
|
|
55
|
+
const url = new URL(request.url);
|
|
56
|
+
const client = resolveCloudflareOAuthClient(env);
|
|
57
|
+
if (request.method === 'OPTIONS') {
|
|
58
|
+
return new Response(null, { status: 204 });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let body = {};
|
|
62
|
+
if (request.method === 'POST') {
|
|
63
|
+
body = await request.json().catch(() => ({}));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const isCallback = url.pathname === CLOUDFLARE_CALLBACK_PATH && request.method === 'GET';
|
|
67
|
+
let ownerId = '';
|
|
68
|
+
try {
|
|
69
|
+
ownerId = await resolveAuthenticatedOwner(request, env, url, body);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
if (!isCallback || err.code === 'untrusted_owner_hint') {
|
|
72
|
+
const code = err.code || 'unauthenticated';
|
|
73
|
+
return json({ ok: false, error: code }, code === 'untrusted_owner_hint' ? 400 : 401);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!client.productionReady) {
|
|
78
|
+
if (url.pathname === '/api/connections/cloudflare' && request.method === 'GET') {
|
|
79
|
+
return json({
|
|
80
|
+
ok: true,
|
|
81
|
+
...cloudflareConnectionSafeStatus(env, null, ownerId),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (url.pathname === '/api/connections/cloudflare/start') {
|
|
85
|
+
return json({
|
|
86
|
+
ok: false,
|
|
87
|
+
error: 'not_configured',
|
|
88
|
+
message: 'real Cloudflare OAuth client still required',
|
|
89
|
+
fixture: client.fixture,
|
|
90
|
+
}, 503);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await ensureTables(env);
|
|
95
|
+
|
|
96
|
+
if (url.pathname === '/api/connections/cloudflare' && request.method === 'GET') {
|
|
97
|
+
let connection = null;
|
|
98
|
+
if (env.DB) {
|
|
99
|
+
const row = await env.DB.prepare(
|
|
100
|
+
`SELECT connection_id, owner_id, cloudflare_account_id, scopes, status, created_at, updated_at, expires_at
|
|
101
|
+
FROM agentsam_cloudflare_connections WHERE owner_id = ? LIMIT 1`,
|
|
102
|
+
).bind(ownerId).first();
|
|
103
|
+
if (row) {
|
|
104
|
+
connection = {
|
|
105
|
+
connectionId: row.connection_id,
|
|
106
|
+
ownerId: row.owner_id,
|
|
107
|
+
cloudflareAccountId: row.cloudflare_account_id,
|
|
108
|
+
scopes: row.scopes ? String(row.scopes).split(' ') : [],
|
|
109
|
+
status: row.status,
|
|
110
|
+
createdAt: row.created_at,
|
|
111
|
+
updatedAt: row.updated_at,
|
|
112
|
+
expiresAt: row.expires_at,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return json({ ok: true, ...cloudflareConnectionSafeStatus(env, connection, ownerId) });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (url.pathname === '/api/connections/cloudflare/start' && request.method === 'GET') {
|
|
120
|
+
const state = crypto.randomUUID().replace(/-/g, '');
|
|
121
|
+
const verifierBytes = crypto.getRandomValues(new Uint8Array(32));
|
|
122
|
+
const verifier = btoa(String.fromCharCode(...verifierBytes)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
123
|
+
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
|
|
124
|
+
const challenge = btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
125
|
+
if (env.DB) {
|
|
126
|
+
await env.DB.prepare(
|
|
127
|
+
`INSERT INTO agentsam_cloudflare_oauth_state (state, owner_id, code_verifier, created_at) VALUES (?, ?, ?, unixepoch())`,
|
|
128
|
+
).bind(state, ownerId, verifier).run();
|
|
129
|
+
} else if (env.oauthState instanceof Map) {
|
|
130
|
+
env.oauthState.set(state, { ownerId, verifier });
|
|
131
|
+
}
|
|
132
|
+
const redirectUri = `${url.origin}${CLOUDFLARE_CALLBACK_PATH}`;
|
|
133
|
+
const authorize = buildAuthorizeUrl({
|
|
134
|
+
clientId: String(env.CLOUDFLARE_OAUTH_CLIENT_ID),
|
|
135
|
+
redirectUri,
|
|
136
|
+
state,
|
|
137
|
+
codeChallenge: challenge,
|
|
138
|
+
});
|
|
139
|
+
return json({ ok: true, authorize_url: authorize, callback: CLOUDFLARE_CALLBACK_PATH });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (url.pathname === CLOUDFLARE_CALLBACK_PATH && request.method === 'GET') {
|
|
143
|
+
const code = url.searchParams.get('code') || '';
|
|
144
|
+
const state = url.searchParams.get('state') || '';
|
|
145
|
+
let stored = null;
|
|
146
|
+
if (env.DB) {
|
|
147
|
+
stored = await env.DB.prepare(
|
|
148
|
+
`SELECT owner_id, code_verifier FROM agentsam_cloudflare_oauth_state WHERE state = ?`,
|
|
149
|
+
).bind(state).first();
|
|
150
|
+
} else if (env.oauthState instanceof Map) {
|
|
151
|
+
stored = env.oauthState.get(state);
|
|
152
|
+
if (stored) stored = { owner_id: stored.ownerId, code_verifier: stored.verifier };
|
|
153
|
+
}
|
|
154
|
+
if (!stored) {
|
|
155
|
+
return json({ ok: false, error: 'cloudflare_connection_forbidden' }, 403);
|
|
156
|
+
}
|
|
157
|
+
if (!ownerId) ownerId = stored.owner_id;
|
|
158
|
+
if (stored.owner_id !== ownerId) {
|
|
159
|
+
return json({ ok: false, error: 'cloudflare_connection_forbidden' }, 403);
|
|
160
|
+
}
|
|
161
|
+
const tokenRes = await fetch(CLOUDFLARE_OAUTH_TOKEN_URL, {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
164
|
+
body: new URLSearchParams({
|
|
165
|
+
grant_type: 'authorization_code',
|
|
166
|
+
code,
|
|
167
|
+
redirect_uri: `${url.origin}${CLOUDFLARE_CALLBACK_PATH}`,
|
|
168
|
+
client_id: String(env.CLOUDFLARE_OAUTH_CLIENT_ID),
|
|
169
|
+
client_secret: String(env.CLOUDFLARE_OAUTH_CLIENT_SECRET),
|
|
170
|
+
code_verifier: stored.code_verifier,
|
|
171
|
+
}),
|
|
172
|
+
});
|
|
173
|
+
if (!tokenRes.ok) {
|
|
174
|
+
return json({ ok: false, error: 'token_exchange_failed' }, 502);
|
|
175
|
+
}
|
|
176
|
+
const tokens = await tokenRes.json();
|
|
177
|
+
const connectionId = `cfconn_${crypto.randomUUID().replace(/-/g, '').slice(0, 16)}`;
|
|
178
|
+
if (tokens.access_token && !vaultConfigured(env)) {
|
|
179
|
+
return json({ ok: false, error: 'vault_unavailable' }, 503);
|
|
180
|
+
}
|
|
181
|
+
const aad = `cloudflare-connection:${ownerId}`;
|
|
182
|
+
let accessEnc = null;
|
|
183
|
+
let refreshEnc = null;
|
|
184
|
+
try {
|
|
185
|
+
accessEnc = await sealToken(env, tokens.access_token, aad);
|
|
186
|
+
refreshEnc = await sealToken(env, tokens.refresh_token, aad);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
return json({ ok: false, error: err.code || 'vault_unavailable' }, 503);
|
|
189
|
+
}
|
|
190
|
+
if (env.DB) {
|
|
191
|
+
await env.DB.prepare(
|
|
192
|
+
`INSERT INTO agentsam_cloudflare_connections (
|
|
193
|
+
connection_id, owner_id, cloudflare_account_id, scopes, status,
|
|
194
|
+
access_token_encrypted, refresh_token_encrypted, created_at, updated_at, expires_at
|
|
195
|
+
) VALUES (?, ?, ?, ?, 'connected', ?, ?, unixepoch(), unixepoch(), ?)`,
|
|
196
|
+
).bind(
|
|
197
|
+
connectionId,
|
|
198
|
+
ownerId,
|
|
199
|
+
tokens.account_id || null,
|
|
200
|
+
(tokens.scope || requestedCloudflareScopes().join(' ')),
|
|
201
|
+
accessEnc,
|
|
202
|
+
refreshEnc,
|
|
203
|
+
tokens.expires_in ? Math.floor(Date.now() / 1000) + Number(tokens.expires_in) : null,
|
|
204
|
+
).run();
|
|
205
|
+
}
|
|
206
|
+
return redirect(`/agentsam?connection=cloudflare`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (url.pathname === '/api/connections/cloudflare/disconnect' && request.method === 'POST') {
|
|
210
|
+
if (env.DB) {
|
|
211
|
+
const row = await env.DB.prepare(
|
|
212
|
+
`SELECT connection_id, owner_id FROM agentsam_cloudflare_connections WHERE owner_id = ?`,
|
|
213
|
+
).bind(ownerId).first();
|
|
214
|
+
if (row) {
|
|
215
|
+
assertConnectionOwner({ ownerId: row.owner_id, connectionId: row.connection_id }, ownerId);
|
|
216
|
+
await env.DB.prepare(`DELETE FROM agentsam_cloudflare_connections WHERE owner_id = ?`).bind(ownerId).run();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return json({ ok: true, status: 'not_configured' });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return json({ ok: false, error: 'not_found' }, 404);
|
|
223
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-256-GCM token vault matching apps/local-studio/backend/worker encryptSecret.
|
|
3
|
+
* Connector tokens are server-only. Never return plaintext on status routes.
|
|
4
|
+
*/
|
|
5
|
+
const enc = new TextEncoder();
|
|
6
|
+
const dec = new TextDecoder();
|
|
7
|
+
|
|
8
|
+
function b64ToBytes(value) {
|
|
9
|
+
const bin = atob(value);
|
|
10
|
+
const out = new Uint8Array(bin.length);
|
|
11
|
+
for (let i = 0; i < bin.length; i += 1) out[i] = bin.charCodeAt(i);
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function bytesToB64(bytes) {
|
|
16
|
+
let bin = '';
|
|
17
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
18
|
+
return btoa(bin);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function vaultConfigured(env = {}) {
|
|
22
|
+
return Boolean(env.VAULT_MASTER_KEY || env.VAULT_KEY);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function importVaultKey(env) {
|
|
26
|
+
const raw = env.VAULT_MASTER_KEY || env.VAULT_KEY;
|
|
27
|
+
if (!raw) throw new Error('VAULT_MASTER_KEY missing');
|
|
28
|
+
let keyBytes;
|
|
29
|
+
try {
|
|
30
|
+
keyBytes = b64ToBytes(raw);
|
|
31
|
+
} catch {
|
|
32
|
+
keyBytes = enc.encode(raw);
|
|
33
|
+
}
|
|
34
|
+
if (keyBytes.byteLength === 32) {
|
|
35
|
+
// ok
|
|
36
|
+
} else if (keyBytes.byteLength > 32) {
|
|
37
|
+
keyBytes = keyBytes.slice(0, 32);
|
|
38
|
+
} else {
|
|
39
|
+
const hash = await crypto.subtle.digest('SHA-256', keyBytes);
|
|
40
|
+
keyBytes = new Uint8Array(hash);
|
|
41
|
+
}
|
|
42
|
+
return crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function encryptSecret(env, plaintext, aad) {
|
|
46
|
+
const key = await importVaultKey(env);
|
|
47
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
48
|
+
const cipher = await crypto.subtle.encrypt(
|
|
49
|
+
{ name: 'AES-GCM', iv, additionalData: enc.encode(aad || '') },
|
|
50
|
+
key,
|
|
51
|
+
enc.encode(plaintext),
|
|
52
|
+
);
|
|
53
|
+
const packed = new Uint8Array(iv.byteLength + cipher.byteLength);
|
|
54
|
+
packed.set(iv, 0);
|
|
55
|
+
packed.set(new Uint8Array(cipher), iv.byteLength);
|
|
56
|
+
return bytesToB64(packed);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function decryptSecret(env, packedB64, aad) {
|
|
60
|
+
const key = await importVaultKey(env);
|
|
61
|
+
const packed = b64ToBytes(packedB64);
|
|
62
|
+
const iv = packed.slice(0, 12);
|
|
63
|
+
const data = packed.slice(12);
|
|
64
|
+
const plain = await crypto.subtle.decrypt(
|
|
65
|
+
{ name: 'AES-GCM', iv, additionalData: enc.encode(aad || '') },
|
|
66
|
+
key,
|
|
67
|
+
data,
|
|
68
|
+
);
|
|
69
|
+
return dec.decode(plain);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function sealToken(env, plaintext, aad) {
|
|
73
|
+
if (!plaintext) return null;
|
|
74
|
+
if (!vaultConfigured(env)) {
|
|
75
|
+
const err = new Error('vault_unavailable');
|
|
76
|
+
err.code = 'vault_unavailable';
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
return encryptSecret(env, plaintext, aad);
|
|
80
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import {
|
|
4
|
+
assertConnectionOwner,
|
|
5
|
+
cloudflareConnectionSafeStatus,
|
|
6
|
+
resolveCloudflareOAuthClient,
|
|
7
|
+
} from '../src/index.js';
|
|
8
|
+
|
|
9
|
+
describe('cloudflare connector', () => {
|
|
10
|
+
it('reports not_configured when credentials are absent', () => {
|
|
11
|
+
const client = resolveCloudflareOAuthClient({});
|
|
12
|
+
assert.equal(client.status, 'not_configured');
|
|
13
|
+
assert.equal(client.configured, false);
|
|
14
|
+
assert.equal(client.productionReady, false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('loads fake fixtures without treating them as production authority', () => {
|
|
18
|
+
const client = resolveCloudflareOAuthClient({
|
|
19
|
+
CLOUDFLARE_OAUTH_CLIENT_ID: 'sillynotreal',
|
|
20
|
+
CLOUDFLARE_OAUTH_CLIENT_SECRET: 'sillynotreal-secret',
|
|
21
|
+
});
|
|
22
|
+
assert.equal(client.configured, true);
|
|
23
|
+
assert.equal(client.fixture, true);
|
|
24
|
+
assert.equal(client.productionReady, false);
|
|
25
|
+
assert.equal(client.status, 'fixture');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('never includes secrets in safe status output', () => {
|
|
29
|
+
const status = cloudflareConnectionSafeStatus({
|
|
30
|
+
CLOUDFLARE_OAUTH_CLIENT_ID: 'sillynotreal',
|
|
31
|
+
CLOUDFLARE_OAUTH_CLIENT_SECRET: 'sillynotreal-secret',
|
|
32
|
+
});
|
|
33
|
+
const blob = JSON.stringify(status);
|
|
34
|
+
assert.equal(blob.includes('sillynotreal-secret'), false);
|
|
35
|
+
assert.equal(status.secret, 'configured');
|
|
36
|
+
assert.equal(status.configured, false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('denies cross-account connection access', () => {
|
|
40
|
+
const record = { ownerId: 'sam', connectionId: 'c1' };
|
|
41
|
+
assert.throws(() => assertConnectionOwner(record, 'connor'), /cloudflare_connection_forbidden/);
|
|
42
|
+
assert.equal(assertConnectionOwner(record, 'sam').connectionId, 'c1');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inneranimalmedia/agentsam-sdk-identity",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Identity module for @inneranimalmedia/agentsam-sdk (workspace — publish via root SDK)",
|
|
7
|
+
"agentsam": {
|
|
8
|
+
"system": "identity",
|
|
9
|
+
"kind": "library",
|
|
10
|
+
"tags": [
|
|
11
|
+
"authentication",
|
|
12
|
+
"account-scoped"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
7
15
|
"main": "./src/index.js",
|
|
8
16
|
"exports": {
|
|
9
17
|
".": "./src/index.js",
|
|
@@ -18,6 +26,7 @@
|
|
|
18
26
|
"./contracts/session": "./src/contracts/session.js",
|
|
19
27
|
"./contracts/provider": "./src/contracts/provider.js",
|
|
20
28
|
"./contracts/external-identity": "./src/contracts/external-identity.js",
|
|
29
|
+
"./contracts/auth-config": "./src/contracts/auth-config.js",
|
|
21
30
|
"./oauth/callback": "./src/oauth/callback.js",
|
|
22
31
|
"./frontend/auth-portal/*": "./src/frontend/auth-portal/*"
|
|
23
32
|
},
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical AgentSam identity/auth environment contract.
|
|
3
|
+
*
|
|
4
|
+
* The SDK owns these public names. Host applications consume this contract and
|
|
5
|
+
* may provide storage/verification adapters without inventing competing aliases.
|
|
6
|
+
*
|
|
7
|
+
* Canonical variables:
|
|
8
|
+
* IAM_OAUTH_ISSUER public authorization-server authority (issuer)
|
|
9
|
+
* IAM_CLIENT_ID OAuth client id
|
|
10
|
+
* IAM_CLIENT_SECRET OAuth client secret
|
|
11
|
+
* IAM_ORIGIN deprecated alias of IAM_OAUTH_ISSUER (migration window)
|
|
12
|
+
* AGENTSAM_SDK_KEY account/delegated sdk_* bearer
|
|
13
|
+
* AGENTSAM_BRIDGE_KEY machine/integration credential
|
|
14
|
+
*
|
|
15
|
+
* Migration-only fallbacks:
|
|
16
|
+
* IAM_ORIGIN -> IAM_OAUTH_ISSUER
|
|
17
|
+
* AGENTSAM_SDK_TOKEN -> AGENTSAM_SDK_KEY
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_IAM_OAUTH_ISSUER = 'https://inneranimalmedia.com';
|
|
21
|
+
/** @deprecated Use DEFAULT_IAM_OAUTH_ISSUER. */
|
|
22
|
+
export const DEFAULT_IAM_ORIGIN = DEFAULT_IAM_OAUTH_ISSUER;
|
|
23
|
+
|
|
24
|
+
export const AGENTSAM_AUTH_ENV = Object.freeze({
|
|
25
|
+
iamIssuer: 'IAM_OAUTH_ISSUER',
|
|
26
|
+
iamOrigin: 'IAM_ORIGIN',
|
|
27
|
+
iamClientId: 'IAM_CLIENT_ID',
|
|
28
|
+
iamClientSecret: 'IAM_CLIENT_SECRET',
|
|
29
|
+
sdkKey: 'AGENTSAM_SDK_KEY',
|
|
30
|
+
bridgeKey: 'AGENTSAM_BRIDGE_KEY',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const AGENTSAM_AUTH_LEGACY_ENV = Object.freeze({
|
|
34
|
+
iamOrigin: 'IAM_ORIGIN',
|
|
35
|
+
sdkKey: 'AGENTSAM_SDK_TOKEN',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const AGENTSAM_AUTH_CONTRACT = Object.freeze({
|
|
39
|
+
version: 1,
|
|
40
|
+
authority: 'agentsam-sdk',
|
|
41
|
+
iam: Object.freeze({
|
|
42
|
+
issuer: AGENTSAM_AUTH_ENV.iamIssuer,
|
|
43
|
+
origin: AGENTSAM_AUTH_ENV.iamOrigin,
|
|
44
|
+
clientId: AGENTSAM_AUTH_ENV.iamClientId,
|
|
45
|
+
clientSecret: AGENTSAM_AUTH_ENV.iamClientSecret,
|
|
46
|
+
}),
|
|
47
|
+
sdk: Object.freeze({
|
|
48
|
+
env: AGENTSAM_AUTH_ENV.sdkKey,
|
|
49
|
+
tokenPrefix: 'sdk_',
|
|
50
|
+
authorizationScheme: 'Bearer',
|
|
51
|
+
subject: 'account/delegated',
|
|
52
|
+
durableStore: 'agentsam_sdk_tokens',
|
|
53
|
+
}),
|
|
54
|
+
bridge: Object.freeze({
|
|
55
|
+
env: AGENTSAM_AUTH_ENV.bridgeKey,
|
|
56
|
+
subject: 'machine/integration',
|
|
57
|
+
sdkTokenType: 'integration',
|
|
58
|
+
userAuth: false,
|
|
59
|
+
}),
|
|
60
|
+
compatibility: Object.freeze({
|
|
61
|
+
iamOriginFallback: AGENTSAM_AUTH_LEGACY_ENV.iamOrigin,
|
|
62
|
+
iamIssuerCanonical: AGENTSAM_AUTH_ENV.iamIssuer,
|
|
63
|
+
sdkKeyFallback: AGENTSAM_AUTH_LEGACY_ENV.sdkKey,
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
function clean(value) {
|
|
68
|
+
return value == null ? '' : String(value).trim();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeOrigin(value) {
|
|
72
|
+
return clean(value).replace(/\/+$/, '');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Resolve canonical IAM issuer. IAM_OAUTH_ISSUER wins; IAM_ORIGIN is compatibility only. */
|
|
76
|
+
export function resolveIamIssuer(env = {}, explicit = '') {
|
|
77
|
+
return normalizeOrigin(
|
|
78
|
+
explicit || env?.IAM_OAUTH_ISSUER || env?.IAM_ORIGIN || DEFAULT_IAM_OAUTH_ISSUER,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** @deprecated Use resolveIamIssuer. Same resolution order as issuer. */
|
|
83
|
+
export function resolveIamOrigin(env = {}, explicit = '') {
|
|
84
|
+
return resolveIamIssuer(env, explicit);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Resolve account/delegated SDK bearer; canonical name wins over legacy. */
|
|
88
|
+
export function resolveSdkKey(env = {}, explicit = '') {
|
|
89
|
+
return clean(explicit || env?.AGENTSAM_SDK_KEY || env?.AGENTSAM_SDK_TOKEN);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Resolve machine/integration bridge credential. No user-SDK alias is accepted. */
|
|
93
|
+
export function resolveBridgeKey(env = {}, explicit = '') {
|
|
94
|
+
return clean(explicit || env?.AGENTSAM_BRIDGE_KEY);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function isSdkKey(value) {
|
|
98
|
+
return clean(value).startsWith(AGENTSAM_AUTH_CONTRACT.sdk.tokenPrefix);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Build the public bearer header for an SDK key. */
|
|
102
|
+
export function buildSdkAuthorizationHeaders(key, headers = {}) {
|
|
103
|
+
const resolved = clean(key);
|
|
104
|
+
if (!isSdkKey(resolved)) throw new Error('AGENTSAM_SDK_KEY_required');
|
|
105
|
+
return {
|
|
106
|
+
...headers,
|
|
107
|
+
Authorization: `Bearer ${resolved}`,
|
|
108
|
+
};
|
|
109
|
+
}
|