UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

154 lines • 6.51 kB
import { OAuthError } from './errors.js'; import { readJson } from './http.js'; import { parseRetryAfter } from './util.js'; export const GRANT_JWT_BEARER = 'urn:ietf:params:oauth:grant-type:jwt-bearer'; export const GRANT_DEVICE_CODE = 'urn:ietf:params:oauth:grant-type:device_code'; export const CLIENT_ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'; /** * OAuth calls against the Company's authorization server (section 4): every request carries * a `private_key_jwt` client assertion (4.1), and token requests carry a DPoP proof unless a * Bearer token (MCP) is wanted (4.3). */ export class TokenClient { identity; metadata; http; opts; constructor(identity, metadata, http, opts = {}) { this.identity = identity; this.metadata = metadata; this.http = http; this.opts = opts; } async clientAuth() { const aud = this.opts.clientAssertionAudience === 'issuer' ? this.metadata.issuer : this.metadata.token_endpoint; return { client_id: this.identity.clientId, client_assertion_type: CLIENT_ASSERTION_TYPE, client_assertion: await this.identity.clientAssertion(aud), }; } /** POSTs a form; the builder runs per attempt so retries get fresh assertions. */ async post(endpoint, build, dpop) { const res = await this.http.request(endpoint, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json', }, dpop, body: async () => { const params = { ...(await build()), ...(await this.clientAuth()) }; const form = new URLSearchParams(); for (const [k, v] of Object.entries(params)) if (v !== undefined && v !== '') form.set(k, v); return form.toString(); }, }); return { status: res.status, body: await readJson(res), headers: res.headers }; } toError(r) { const code = typeof r.body?.error === 'string' ? r.body.error : `http_${r.status}`; return new OAuthError(code, { status: r.status, description: typeof r.body?.error_description === 'string' ? r.body.error_description : undefined, retryAfter: parseRetryAfter(r.headers.get('retry-after')), details: r.body, }); } /** Any token-endpoint grant. Validates the response shape of 4.2/4.4. */ async token(params, dpop) { const r = await this.post(this.metadata.token_endpoint, params, dpop); if (r.status !== 200 || !r.body) throw this.toError(r); const t = r.body; if (typeof t.access_token !== 'string' || !t.access_token) { throw new OAuthError('invalid_response', { description: 'no access_token in token response', }); } if (typeof t.expires_in !== 'number' || !(t.expires_in > 0)) { throw new OAuthError('invalid_response', { description: 'expires_in must be > 0 (4.2)' }); } if (typeof t.session_id !== 'string' || !t.session_id) { throw new OAuthError('invalid_response', { description: 'no session_id in token response' }); } const expected = dpop ? 'dpop' : 'bearer'; if (String(t.token_type).toLowerCase() !== expected) { // RFC 9449 section 5: a client that sent a proof must not accept a downgraded token. throw new OAuthError('invalid_response', { description: `token_type is ${t.token_type}, expected ${dpop ? 'DPoP' : 'Bearer'}`, }); } t.signed_in = t.signed_in === true; t.scope = typeof t.scope === 'string' ? t.scope : ''; return t; } /** JWT bearer grant: starts a Session, or renews a signed-out one with `sessionId` (4.2). */ jwtBearer(input) { return this.token(async () => ({ grant_type: GRANT_JWT_BEARER, assertion: await this.identity.sessionAssertion(input.userId, this.metadata.token_endpoint), session_id: input.sessionId, scope: input.scope, resource: input.resource, }), input.dpop); } /** Account Token grant (4.8): signs a Session in, or starts a new signed-in Session. */ refresh(input) { return this.token(async () => ({ grant_type: 'refresh_token', refresh_token: input.refreshToken, session_id: input.sessionId, scope: input.scope, resource: input.resource, }), input.dpop); } /** Direct Sign-In code exchange (4.5 step 4). */ authorizationCode(input) { return this.token(async () => ({ grant_type: 'authorization_code', code: input.code, redirect_uri: input.redirectUri, code_verifier: input.codeVerifier, session_id: input.sessionId, }), input.dpop); } /** Device Sign-In step 1 (4.6). */ async deviceAuthorization(scope) { const endpoint = this.metadata.device_authorization_endpoint; if (!endpoint) throw new OAuthError('unsupported', { description: 'no device_authorization_endpoint' }); const r = await this.post(endpoint, async () => ({ scope })); if (r.status !== 200 || !r.body) throw this.toError(r); const d = r.body; if (!d.device_code || !d.user_code || !d.verification_uri) { throw new OAuthError('invalid_response', { description: 'incomplete device authorization response', }); } return d; } /** Device Sign-In polling (4.6 step 3). Throws `authorization_pending` / `slow_down` etc. */ deviceToken(input) { return this.token(async () => ({ grant_type: GRANT_DEVICE_CODE, device_code: input.deviceCode, session_id: input.sessionId, }), input.dpop); } /** RFC 7009 revocation of an Account Token (4.9). */ async revoke(token, hint = 'refresh_token') { const r = await this.post(this.metadata.revocation_endpoint, async () => ({ token, token_type_hint: hint, })); if (r.status !== 200) throw this.toError(r); } } //# sourceMappingURL=token_client.js.map