@ouraihub/aether-worker 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/mcp/index.ts CHANGED
@@ -26,7 +26,11 @@ function rpc(id: unknown, result: unknown, error = false): Response {
26
26
  ...(error ? { error: result } : { result }),
27
27
  }
28
28
  if (encoder.encode(JSON.stringify(message)).byteLength > MAX_RESULT_BYTES)
29
- return json({ jsonrpc: '2.0', id: message.id, error: { code: -32603, message: 'Result budget exceeded' } })
29
+ return json({
30
+ jsonrpc: '2.0',
31
+ id: message.id,
32
+ error: { code: -32603, message: 'Result budget exceeded' },
33
+ })
30
34
  return json(message)
31
35
  }
32
36
  function rpcError(id: unknown, code: number, message: string): Response {
@@ -79,7 +83,8 @@ export class McpService {
79
83
  !url.pathname.startsWith('/.well-known/oauth-')
80
84
  )
81
85
  return null
82
- if (url.origin !== this.options.origin || url.protocol !== 'https:') return json({ error: 'invalid_request' }, 400)
86
+ if (url.origin !== this.options.origin || url.protocol !== 'https:')
87
+ return json({ error: 'invalid_request' }, 400)
83
88
  if (url.pathname === '/mcp') return this.mcp(request)
84
89
  return this.oauth.handle(request)
85
90
  }
@@ -99,12 +104,18 @@ export class McpService {
99
104
  !value.enabledScopes.every((scope: unknown) => typeof scope === 'string')
100
105
  )
101
106
  throw badRequest('enabledScopes', 'invalid')
102
- const requested = value.enabledScopes.length ? this.auth.requested(value.enabledScopes, context.actor) : []
107
+ const requested = value.enabledScopes.length
108
+ ? this.auth.requested(value.enabledScopes, context.actor)
109
+ : []
103
110
  if (!(await this.options.authorizeConsent(context.actor, requested, request))) forbidden()
104
- return json({ ...(await this.auth.saveSettings(context.actor, value)), scopes: this.options.scopes })
111
+ return json({
112
+ ...(await this.auth.saveSettings(context.actor, value)),
113
+ scopes: this.options.scopes,
114
+ })
105
115
  }
106
116
  }
107
- if (path === '/api/v1/mcp/grants' && request.method === 'GET') return json(await this.auth.grants(context.actor))
117
+ if (path === '/api/v1/mcp/grants' && request.method === 'GET')
118
+ return json(await this.auth.grants(context.actor))
108
119
  if (path === '/api/v1/mcp/api-keys') {
109
120
  if (request.method === 'GET') return json(await this.auth.keys(context.actor))
110
121
  if (request.method === 'POST')
@@ -114,26 +125,43 @@ export class McpService {
114
125
  if (revoke && request.method === 'POST') {
115
126
  const id = revoke[2]
116
127
  if (!id) throw badRequest('id', 'invalid')
117
- await this.auth.revoke(context.actor, revoke[1] === 'grants' ? 'grant' : 'key', decodeURIComponent(id))
128
+ await this.auth.revoke(
129
+ context.actor,
130
+ revoke[1] === 'grants' ? 'grant' : 'key',
131
+ decodeURIComponent(id),
132
+ )
118
133
  return json({ ok: true })
119
134
  }
120
135
  return json({ code: 'not_found', message: 'Route not found' }, 404)
121
136
  }
122
137
  private async mcp(request: Request): Promise<Response> {
123
138
  const origin = request.headers.get('Origin')
124
- if (origin !== null && ![this.options.origin, ...(this.options.mcpOrigins ?? [])].includes(origin))
139
+ if (
140
+ origin !== null &&
141
+ ![this.options.origin, ...(this.options.mcpOrigins ?? [])].includes(origin)
142
+ )
125
143
  return json({ error: 'forbidden' }, 403)
126
- if (request.method !== 'POST') return new Response(null, { status: 405, headers: { Allow: 'POST' } })
144
+ if (request.method !== 'POST')
145
+ return new Response(null, { status: 405, headers: { Allow: 'POST' } })
146
+ const requiresAccess = this.options.scopes.some((scope) => scope.id === 'mcp:access')
147
+ const challenge =
148
+ `Bearer resource_metadata="${this.options.origin}/.well-known/oauth-protected-resource/mcp"` +
149
+ (requiresAccess ? ', scope="mcp:access"' : '')
127
150
  const bearer = /^Bearer[\t ]+([^\s,]+)$/iu.exec(request.headers.get('Authorization') ?? '')?.[1]
128
151
  const authentication = bearer ? await this.auth.authenticate(bearer) : null
129
152
  if (!authentication)
130
153
  return json({ error: 'unauthorized' }, 401, {
131
- 'WWW-Authenticate': `Bearer resource_metadata="${this.options.origin}/.well-known/oauth-protected-resource"`,
154
+ 'WWW-Authenticate': challenge,
155
+ })
156
+ if (requiresAccess && !authentication.scopes.includes('mcp:access'))
157
+ return json({ error: 'insufficient_scope' }, 403, {
158
+ 'WWW-Authenticate': `${challenge}, error="insufficient_scope"`,
132
159
  })
133
- if (this.options.scopes.some((scope) => scope.id === 'mcp:access') && !authentication.scopes.includes('mcp:access'))
134
- return json({ error: 'forbidden' }, 403)
135
160
  const maxBytes =
136
- this.options.tools.reduce((max, tool) => Math.max(max, tool.maxRequestBytes ?? 65536), 65536) + 8192
161
+ this.options.tools.reduce(
162
+ (max, tool) => Math.max(max, tool.maxRequestBytes ?? 65536),
163
+ 65536,
164
+ ) + 8192
137
165
  let message: unknown
138
166
  try {
139
167
  message = JSON.parse(await body(request, maxBytes))
@@ -146,20 +174,32 @@ export class McpService {
146
174
  !object(message) ||
147
175
  message.jsonrpc !== '2.0' ||
148
176
  typeof message.method !== 'string' ||
149
- ('id' in message && typeof message.id !== 'string' && typeof message.id !== 'number' && message.id !== null)
177
+ ('id' in message &&
178
+ typeof message.id !== 'string' &&
179
+ typeof message.id !== 'number' &&
180
+ message.id !== null)
150
181
  )
151
182
  return rpcError(null, -32600, 'Invalid request')
152
183
  if (!('id' in message)) {
153
- if (message.method === 'notifications/initialized' || message.method === 'notifications/cancelled')
184
+ if (
185
+ message.method === 'notifications/initialized' ||
186
+ message.method === 'notifications/cancelled'
187
+ )
154
188
  return new Response(null, { status: 202 })
155
189
  return rpcError(null, -32600, 'Invalid request')
156
190
  }
157
191
  const id = message.id
158
192
  if (message.method === 'initialize') {
159
- if (!object(message.params) || !['2024-11-05', '2025-03-26'].includes(String(message.params.protocolVersion)))
160
- return rpcError(id, -32602, 'Unsupported protocol version')
193
+ if (
194
+ !object(message.params) ||
195
+ typeof message.params.protocolVersion !== 'string' ||
196
+ !message.params.protocolVersion.trim()
197
+ )
198
+ return rpcError(id, -32602, 'Invalid protocol version')
161
199
  return rpc(id, {
162
- protocolVersion: message.params.protocolVersion,
200
+ protocolVersion: ['2024-11-05', '2025-03-26'].includes(message.params.protocolVersion)
201
+ ? message.params.protocolVersion
202
+ : '2025-03-26',
163
203
  capabilities: { tools: {} },
164
204
  serverInfo: { name: 'Aether MCP', version: '1.0.0' },
165
205
  })
@@ -208,18 +248,31 @@ export class McpService {
208
248
  if (signal.aborted || !(await this.options.authorizeTool(auth.actor, tool, args, signal)))
209
249
  return rpcError(id, -32001, 'Forbidden')
210
250
  if (signal.aborted) return rpcError(id, -32603, 'Cancelled')
211
- const result = await tool.handler(args, { actor: auth.actor, tokenId: auth.tokenId, scopes: auth.scopes, signal })
251
+ const result = await tool.handler(args, {
252
+ actor: auth.actor,
253
+ tokenId: auth.tokenId,
254
+ scopes: auth.scopes,
255
+ signal,
256
+ })
212
257
  if (result === undefined) throw new KitFailure('internal', 'Invalid tool result')
213
258
  const payload = object(result)
214
- ? { content: [{ type: 'text', text: JSON.stringify(result) }], structuredContent: result, isError: false }
259
+ ? {
260
+ content: [{ type: 'text', text: JSON.stringify(result) }],
261
+ structuredContent: result,
262
+ isError: false,
263
+ }
215
264
  : {
216
- content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result) }],
265
+ content: [
266
+ { type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result) },
267
+ ],
217
268
  isError: false,
218
269
  }
219
270
  const serialized = JSON.stringify({ jsonrpc: '2.0', id, result: payload })
220
271
  if (encoder.encode(serialized).byteLength > MAX_RESULT_BYTES)
221
272
  return rpcError(id, -32603, 'Result budget exceeded')
222
- return new Response(serialized, { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' } })
273
+ return new Response(serialized, {
274
+ headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' },
275
+ })
223
276
  } catch (error) {
224
277
  const safe = safeError(error)
225
278
  const code =
package/src/mcp/oauth.ts CHANGED
@@ -53,7 +53,10 @@ function redirectAllowed(value: string): boolean {
53
53
  }
54
54
  }
55
55
  function redirect(target: string): Response {
56
- return new Response(null, { status: 302, headers: { Location: target, 'Cache-Control': 'no-store' } })
56
+ return new Response(null, {
57
+ status: 302,
58
+ headers: { Location: target, 'Cache-Control': 'no-store' },
59
+ })
57
60
  }
58
61
 
59
62
  export class McpOAuth {
@@ -67,7 +70,11 @@ export class McpOAuth {
67
70
  const url = new URL(request.url)
68
71
  const origin = this.options.origin
69
72
  const resource = `${origin}/mcp`
70
- if (url.pathname === '/.well-known/oauth-protected-resource' && request.method === 'GET')
73
+ if (
74
+ (url.pathname === '/.well-known/oauth-protected-resource' ||
75
+ url.pathname === '/.well-known/oauth-protected-resource/mcp') &&
76
+ request.method === 'GET'
77
+ )
71
78
  return json({
72
79
  resource,
73
80
  authorization_servers: [origin],
@@ -86,15 +93,22 @@ export class McpOAuth {
86
93
  token_endpoint_auth_methods_supported: ['none'],
87
94
  scopes_supported: this.options.scopes.map((scope) => scope.id),
88
95
  })
89
- if (url.pathname === '/oauth/register' && request.method === 'POST') return this.register(request)
90
- if (url.pathname === '/oauth/authorize' && (request.method === 'GET' || request.method === 'POST'))
96
+ if (url.pathname === '/oauth/register' && request.method === 'POST')
97
+ return this.register(request)
98
+ if (
99
+ url.pathname === '/oauth/authorize' &&
100
+ (request.method === 'GET' || request.method === 'POST')
101
+ )
91
102
  return this.authorize(request)
92
103
  if (url.pathname === '/oauth/token' && request.method === 'POST') {
93
104
  try {
94
105
  return await this.token(request)
95
106
  } catch (error) {
96
107
  if (!(error instanceof KitFailure)) throw error
97
- return json({ error: error.code === 'bad_request' ? 'invalid_request' : 'invalid_grant' }, 400)
108
+ return json(
109
+ { error: error.code === 'bad_request' ? 'invalid_request' : 'invalid_grant' },
110
+ 400,
111
+ )
98
112
  }
99
113
  }
100
114
  if (url.pathname.startsWith('/oauth/') || url.pathname.startsWith('/.well-known/oauth-'))
@@ -110,7 +124,10 @@ export class McpOAuth {
110
124
  input.redirect_uris.some((uri) => uri.length > 2048 || !redirectAllowed(uri))
111
125
  )
112
126
  throw badRequest('redirect_uris', 'invalid')
113
- if (input.token_endpoint_auth_method !== undefined && input.token_endpoint_auth_method !== 'none')
127
+ if (
128
+ input.token_endpoint_auth_method !== undefined &&
129
+ input.token_endpoint_auth_method !== 'none'
130
+ )
114
131
  throw badRequest('token_endpoint_auth_method', 'unsupported')
115
132
  if (
116
133
  input.grant_types !== undefined &&
@@ -123,7 +140,10 @@ export class McpOAuth {
123
140
  (!strings(input.response_types) || input.response_types.some((type) => type !== 'code'))
124
141
  )
125
142
  throw badRequest('response_types', 'unsupported')
126
- if (input.client_name !== undefined && (typeof input.client_name !== 'string' || input.client_name.length > 128))
143
+ if (
144
+ input.client_name !== undefined &&
145
+ (typeof input.client_name !== 'string' || input.client_name.length > 128)
146
+ )
127
147
  throw badRequest('client_name', 'invalid')
128
148
  const record: OAuthClient = {
129
149
  id: crypto.randomUUID(),
@@ -157,18 +177,31 @@ export class McpOAuth {
157
177
  'resource',
158
178
  ])
159
179
  if (query.getAll(key).length > 1) throw badRequest(key, 'duplicate')
160
- const record = await this.options.store.get(this.auth.system, 'mcp-clients', query.get('client_id') ?? '')
180
+ const record = await this.options.store.get(
181
+ this.auth.system,
182
+ 'mcp-clients',
183
+ query.get('client_id') ?? '',
184
+ )
161
185
  if (!record || !client(record.value)) throw badRequest('client_id', 'invalid')
162
186
  const redirectUri = query.get('redirect_uri') ?? ''
163
- if (!record.value.redirectUris.includes(redirectUri)) throw badRequest('redirect_uri', 'mismatch')
187
+ if (!record.value.redirectUris.includes(redirectUri))
188
+ throw badRequest('redirect_uri', 'mismatch')
164
189
  if (query.get('response_type') !== 'code' || query.get('code_challenge_method') !== 'S256')
165
190
  throw badRequest('response_type', 'unsupported')
166
191
  const challenge = query.get('code_challenge') ?? ''
167
192
  if (!/^[A-Za-z0-9_-]{43}$/u.test(challenge)) throw badRequest('code_challenge', 'invalid')
168
193
  const resource = `${this.options.origin}/mcp`
169
- if (query.has('resource') && query.get('resource') !== resource) throw badRequest('resource', 'invalid')
194
+ if (query.has('resource') && query.get('resource') !== resource)
195
+ throw badRequest('resource', 'invalid')
170
196
  const scopes = this.auth.requested((query.get('scope') ?? '').split(/\s+/u).filter(Boolean))
171
- return { client: record.value, redirectUri, scopes, challenge, state: query.get('state') ?? '', resource }
197
+ return {
198
+ client: record.value,
199
+ redirectUri,
200
+ scopes,
201
+ challenge,
202
+ state: query.get('state') ?? '',
203
+ resource,
204
+ }
172
205
  }
173
206
  private async authorize(request: Request): Promise<Response> {
174
207
  const parsed = await this.parse(request)
@@ -178,7 +211,8 @@ export class McpOAuth {
178
211
  this.auth.actor(actor)
179
212
  this.auth.requested(parsed.scopes, actor)
180
213
  const settings = await this.auth.settings(actor)
181
- if (!settings.enabled || parsed.scopes.some((scope) => !settings.enabledScopes.includes(scope))) forbidden()
214
+ if (!settings.enabled || parsed.scopes.some((scope) => !settings.enabledScopes.includes(scope)))
215
+ forbidden()
182
216
  const requestHash = await hash(new URL(request.url).searchParams.toString())
183
217
  if (request.method === 'GET') {
184
218
  const csrf = random()
@@ -196,7 +230,8 @@ export class McpOAuth {
196
230
  headers: {
197
231
  'Content-Type': 'text/html; charset=utf-8',
198
232
  'Cache-Control': 'no-store',
199
- 'Content-Security-Policy': "default-src 'none'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'",
233
+ 'Content-Security-Policy':
234
+ "default-src 'none'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'",
200
235
  'Referrer-Policy': 'no-referrer',
201
236
  'X-Content-Type-Options': 'nosniff',
202
237
  },
@@ -222,11 +257,19 @@ export class McpOAuth {
222
257
  })
223
258
  const target = new URL(parsed.redirectUri)
224
259
  if (parsed.state) target.searchParams.set('state', parsed.state)
225
- if (form.get('decision') !== 'approve' || !(await this.options.authorizeConsent(actor, parsed.scopes, request))) {
260
+ if (
261
+ form.get('decision') !== 'approve' ||
262
+ !(await this.options.authorizeConsent(actor, parsed.scopes, request))
263
+ ) {
226
264
  target.searchParams.set('error', 'access_denied')
227
265
  return redirect(target.toString())
228
266
  }
229
- const grant = await this.auth.createGrant(actor, parsed.client.id, parsed.redirectUri, parsed.scopes)
267
+ const grant = await this.auth.createGrant(
268
+ actor,
269
+ parsed.client.id,
270
+ parsed.redirectUri,
271
+ parsed.scopes,
272
+ )
230
273
  const code = random()
231
274
  await this.options.store.compareAndSet(this.auth.system, 'mcp-codes', await hash(code), null, {
232
275
  owner: actor,
@@ -242,10 +285,15 @@ export class McpOAuth {
242
285
  return redirect(target.toString())
243
286
  }
244
287
  private async form(request: Request): Promise<URLSearchParams> {
245
- if (!(request.headers.get('Content-Type') ?? '').toLowerCase().startsWith('application/x-www-form-urlencoded'))
288
+ if (
289
+ !(request.headers.get('Content-Type') ?? '')
290
+ .toLowerCase()
291
+ .startsWith('application/x-www-form-urlencoded')
292
+ )
246
293
  throw badRequest('body', 'form_required')
247
294
  const form = new URLSearchParams(await body(request, 16384))
248
- for (const key of new Set(form.keys())) if (form.getAll(key).length !== 1) throw badRequest(key, 'duplicate')
295
+ for (const key of new Set(form.keys()))
296
+ if (form.getAll(key).length !== 1) throw badRequest(key, 'duplicate')
249
297
  return form
250
298
  }
251
299
  private async token(request: Request): Promise<Response> {
@@ -255,8 +303,13 @@ export class McpOAuth {
255
303
  throw badRequest('resource', 'invalid')
256
304
  if (form.get('grant_type') === 'refresh_token')
257
305
  return json(await this.auth.refresh(form.get('refresh_token') ?? '', clientId))
258
- if (form.get('grant_type') !== 'authorization_code') throw badRequest('grant_type', 'unsupported')
259
- const document = await this.options.store.get(this.auth.system, 'mcp-codes', await hash(form.get('code') ?? ''))
306
+ if (form.get('grant_type') !== 'authorization_code')
307
+ throw badRequest('grant_type', 'unsupported')
308
+ const document = await this.options.store.get(
309
+ this.auth.system,
310
+ 'mcp-codes',
311
+ await hash(form.get('code') ?? ''),
312
+ )
260
313
  if (!document || !object(document.value)) throw new KitFailure('unauthorized', 'Invalid grant')
261
314
  const value = document.value
262
315
  const verifier = form.get('code_verifier') ?? ''
@@ -275,10 +328,16 @@ export class McpOAuth {
275
328
  !equal(await hash(verifier), value.challenge)
276
329
  )
277
330
  throw new KitFailure('unauthorized', 'Invalid grant')
278
- await this.options.store.compareAndSet(this.auth.system, 'mcp-codes', document.key, document.revision, {
279
- ...value,
280
- used: true,
281
- })
331
+ await this.options.store.compareAndSet(
332
+ this.auth.system,
333
+ 'mcp-codes',
334
+ document.key,
335
+ document.revision,
336
+ {
337
+ ...value,
338
+ used: true,
339
+ },
340
+ )
282
341
  return json(await this.auth.issueTokens(value.owner, value.grantId, undefined, clientId))
283
342
  }
284
343
  }
package/src/mcp/types.ts CHANGED
@@ -18,7 +18,10 @@ export interface ExposedMcpTool {
18
18
  readonly idempotent: boolean
19
19
  readonly maxRequestBytes?: number
20
20
  readonly validate: (args: Readonly<Record<string, unknown>>) => boolean
21
- readonly handler: (args: Readonly<Record<string, unknown>>, context: McpCallContext) => Promise<unknown>
21
+ readonly handler: (
22
+ args: Readonly<Record<string, unknown>>,
23
+ context: McpCallContext,
24
+ ) => Promise<unknown>
22
25
  }
23
26
  export interface McpCallContext {
24
27
  readonly actor: Actor
@@ -36,7 +39,11 @@ export interface McpServiceOptions {
36
39
  readonly tools: readonly ExposedMcpTool[]
37
40
  readonly resolveIdentity: (request: Request) => Promise<Actor | null>
38
41
  /** Must check the actor may grant every requested scope, not just that login exists. */
39
- readonly authorizeConsent: (actor: Actor, scopes: readonly string[], request: Request) => Promise<boolean>
42
+ readonly authorizeConsent: (
43
+ actor: Actor,
44
+ scopes: readonly string[],
45
+ request: Request,
46
+ ) => Promise<boolean>
40
47
  /** Must check resource/business access immediately before external tool dispatch. */
41
48
  readonly authorizeTool: (
42
49
  actor: Actor,
package/src/outbound.ts CHANGED
@@ -48,7 +48,12 @@ function privateLiteral(host: string): boolean {
48
48
  )
49
49
  }
50
50
  if (!/^\d+(?:\.\d+){3}$/u.test(host))
51
- return host === 'localhost' || host.endsWith('.localhost') || host.endsWith('.local') || !host.includes('.')
51
+ return (
52
+ host === 'localhost' ||
53
+ host.endsWith('.localhost') ||
54
+ host.endsWith('.local') ||
55
+ !host.includes('.')
56
+ )
52
57
  const parts = host.split('.').map(Number)
53
58
  const a = parts[0],
54
59
  b = parts[1],
@@ -81,11 +86,13 @@ export function createWorkerOutbound(options: WorkerOutboundOptions): OutboundHt
81
86
  } catch {
82
87
  throw badRequest('allowedOrigins', 'invalid')
83
88
  }
84
- if (url.origin !== origin || url.username || url.password) throw badRequest('allowedOrigins', 'invalid')
89
+ if (url.origin !== origin || url.username || url.password)
90
+ throw badRequest('allowedOrigins', 'invalid')
85
91
  return origin
86
92
  }),
87
93
  )
88
- if (origins.size === 0) throw new KitFailure('needs_config', 'Outbound origins must be explicitly configured')
94
+ if (origins.size === 0)
95
+ throw new KitFailure('needs_config', 'Outbound origins must be explicitly configured')
89
96
  const privateOrigins = new Set(options.privateGateway?.allowedOrigins ?? [])
90
97
  for (const origin of privateOrigins) {
91
98
  let target: URL
@@ -126,7 +133,9 @@ export function createWorkerOutbound(options: WorkerOutboundOptions): OutboundHt
126
133
  !local &&
127
134
  (!gateway ||
128
135
  gateway.security !==
129
- (privateTarget ? 'explicit-private-targets-at-connect-time' : 'public-addresses-at-connect-time'))
136
+ (privateTarget
137
+ ? 'explicit-private-targets-at-connect-time'
138
+ : 'public-addresses-at-connect-time'))
130
139
  )
131
140
  throw new KitFailure('needs_config', 'Trusted connect-time outbound gateway is required')
132
141
  const abort = new AbortController()
@@ -169,7 +178,8 @@ export function createWorkerOutbound(options: WorkerOutboundOptions): OutboundHt
169
178
  init.signal?.addEventListener('abort', callerAbort, { once: true })
170
179
  abort.signal.addEventListener('abort', onAbort, { once: true })
171
180
  if (init.signal?.aborted) callerAbort()
172
- if (abort.signal.aborted) throw failure ?? new KitFailure('cancelled', 'Outbound request cancelled')
181
+ if (abort.signal.aborted)
182
+ throw failure ?? new KitFailure('cancelled', 'Outbound request cancelled')
173
183
  try {
174
184
  const headers = new Headers(init.headers)
175
185
  headers.set('User-Agent', 'Aether-Worker/1')
@@ -184,7 +194,8 @@ export function createWorkerOutbound(options: WorkerOutboundOptions): OutboundHt
184
194
  const pending = local
185
195
  ? (options.localFetch ?? fetch)(target.href, request)
186
196
  : gateway?.binding.fetch(target.href, request)
187
- if (!pending) throw new KitFailure('needs_config', 'Trusted connect-time outbound gateway is required')
197
+ if (!pending)
198
+ throw new KitFailure('needs_config', 'Trusted connect-time outbound gateway is required')
188
199
  void pending.then(
189
200
  (response) => {
190
201
  if (abort.signal.aborted) void response.body?.cancel().catch(() => {})
@@ -192,7 +203,8 @@ export function createWorkerOutbound(options: WorkerOutboundOptions): OutboundHt
192
203
  () => {},
193
204
  )
194
205
  const aborted = Promise.withResolvers<never>()
195
- const rejectAbort = () => aborted.reject(failure ?? new KitFailure('cancelled', 'Outbound request cancelled'))
206
+ const rejectAbort = () =>
207
+ aborted.reject(failure ?? new KitFailure('cancelled', 'Outbound request cancelled'))
196
208
  abort.signal.addEventListener('abort', rejectAbort, { once: true })
197
209
  let response: Response
198
210
  try {
@@ -209,7 +221,10 @@ export function createWorkerOutbound(options: WorkerOutboundOptions): OutboundHt
209
221
  throw badRequest('url', 'redirect_forbidden')
210
222
  }
211
223
  const length = response.headers.get('content-length')
212
- if (length !== null && (!/^\d+$/u.test(length) || Number(length) > limits.maxResponseBytes)) {
224
+ if (
225
+ length !== null &&
226
+ (!/^\d+$/u.test(length) || Number(length) > limits.maxResponseBytes)
227
+ ) {
213
228
  void response.body?.cancel().catch(() => {})
214
229
  throw badRequest('response', 'too_large', limits.maxResponseBytes)
215
230
  }
@@ -0,0 +1,63 @@
1
+ import { isContract, type Provider } from '@ouraihub/aether-contracts'
2
+ import { KitFailure } from '@ouraihub/aether-core/error'
3
+ import { inferReasoning } from '@ouraihub/aether-core/providers/infer'
4
+ import { isSealedCredential, type SealedCredential } from '../credentials.js'
5
+
6
+ export interface Secret {
7
+ endpointId: string
8
+ keyId: string
9
+ sealed: SealedCredential
10
+ }
11
+
12
+ export interface Catalog {
13
+ providers: Provider[]
14
+ secrets: Secret[]
15
+ inferred: string[]
16
+ }
17
+
18
+ function record(value: unknown): value is Record<string, unknown> {
19
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
20
+ }
21
+
22
+ export function parseCatalog(value: unknown): Catalog {
23
+ if (
24
+ !record(value) ||
25
+ !Array.isArray(value.providers) ||
26
+ !Array.isArray(value.secrets) ||
27
+ !Array.isArray(value.inferred)
28
+ )
29
+ throw new KitFailure('incompatible_db', 'Invalid provider catalog')
30
+ const providers: Provider[] = []
31
+ const secrets: Secret[] = []
32
+ const inferred: string[] = []
33
+ for (const item of value.providers) {
34
+ if (!isContract('Provider', item))
35
+ throw new KitFailure('incompatible_db', 'Invalid provider document')
36
+ providers.push(item)
37
+ }
38
+ for (const item of value.secrets) {
39
+ if (
40
+ !record(item) ||
41
+ typeof item.endpointId !== 'string' ||
42
+ typeof item.keyId !== 'string' ||
43
+ !isSealedCredential(item.sealed)
44
+ )
45
+ throw new KitFailure('incompatible_db', 'Invalid provider credential')
46
+ secrets.push({ endpointId: item.endpointId, keyId: item.keyId, sealed: item.sealed })
47
+ }
48
+ for (const item of value.inferred) {
49
+ if (typeof item !== 'string')
50
+ throw new KitFailure('incompatible_db', 'Invalid reasoning declaration')
51
+ inferred.push(item)
52
+ }
53
+ const automatic = new Set(inferred)
54
+ for (const provider of providers)
55
+ for (const endpoint of provider.endpoints)
56
+ for (const model of endpoint.models)
57
+ if (automatic.has(JSON.stringify([endpoint.id, model.id])))
58
+ model.reasoning =
59
+ model.modelType === 'chat'
60
+ ? inferReasoning(model.id, endpoint.protocol, endpoint.baseUrl, model.maxOutput)
61
+ : null
62
+ return { providers, secrets, inferred }
63
+ }
@@ -0,0 +1,88 @@
1
+ import {
2
+ isContract,
3
+ type ModelFilter,
4
+ type ProviderSave,
5
+ type EndpointKeySave,
6
+ type ProbeTarget,
7
+ } from '@ouraihub/aether-contracts'
8
+ import { KitFailure, badRequest } from '@ouraihub/aether-core/error'
9
+ import type { WorkerContext } from '../context.js'
10
+ import type { ProviderService } from './index.js'
11
+
12
+ function record(value: unknown): value is Record<string, unknown> {
13
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
14
+ }
15
+
16
+ export async function handleProviderRequest(
17
+ service: ProviderService,
18
+ request: Request,
19
+ context: WorkerContext,
20
+ authorizeReveal: (request: Request, context: WorkerContext) => Promise<void>,
21
+ ): Promise<Response | null> {
22
+ const url = new URL(request.url)
23
+ const path = url.pathname
24
+ const root = '/api/v1/providers'
25
+ if (path !== root && !path.startsWith(`${root}/`)) return null
26
+ const actor = context.actor
27
+ const json = (value: unknown): Response => Response.json(value)
28
+ if (path === root && request.method === 'GET') return json(await service.list(actor))
29
+ if (path === root && request.method === 'POST') {
30
+ const value: unknown = await request.json()
31
+ if (!isContract('ProviderSave', value)) throw badRequest('provider', 'invalid')
32
+ return json(await service.save(actor, value as ProviderSave))
33
+ }
34
+ if (path === `${root}/model-options` && request.method === 'GET') {
35
+ const value: unknown = {
36
+ ...(url.searchParams.has('modelType')
37
+ ? { modelType: url.searchParams.get('modelType') }
38
+ : {}),
39
+ ...(url.searchParams.has('supportsTools')
40
+ ? { supportsTools: url.searchParams.get('supportsTools') === 'true' }
41
+ : {}),
42
+ }
43
+ if (
44
+ url.searchParams.has('supportsTools') &&
45
+ !['true', 'false'].includes(url.searchParams.get('supportsTools') ?? '')
46
+ )
47
+ throw badRequest('supportsTools', 'invalid')
48
+ if (!isContract('ModelFilter', value)) throw badRequest('filter', 'invalid')
49
+ return json(await service.modelOptions(actor, value as ModelFilter))
50
+ }
51
+ if (path === `${root}/probe` && request.method === 'POST') {
52
+ const value: unknown = await request.json()
53
+ if (!isContract('ProbeTarget', value)) throw badRequest('target', 'invalid')
54
+ return json(await service.probe(actor, value as ProbeTarget, context.signal))
55
+ }
56
+ if (path === `${root}/keys` && request.method === 'POST') {
57
+ const value: unknown = await request.json()
58
+ if (!isContract('EndpointKeySave', value)) throw badRequest('key', 'invalid')
59
+ return json(await service.saveKey(actor, value as EndpointKeySave))
60
+ }
61
+ const reveal = path.match(/^\/api\/v1\/providers\/keys\/([^/]+)\/reveal$/)
62
+ if (reveal && request.method === 'POST') {
63
+ await authorizeReveal(request, context)
64
+ return json(await service.revealKey(actor, decodeURIComponent(reveal[1] ?? '')))
65
+ }
66
+ const key = path.match(/^\/api\/v1\/providers\/([^/]+)\/keys\/([^/]+)$/)
67
+ if (key && request.method === 'DELETE')
68
+ return json(
69
+ await service.removeKey(
70
+ actor,
71
+ decodeURIComponent(key[1] ?? ''),
72
+ decodeURIComponent(key[2] ?? ''),
73
+ ),
74
+ )
75
+ const enabled = path.match(/^\/api\/v1\/providers\/([^/]+)\/enabled$/)
76
+ if (enabled && request.method === 'POST') {
77
+ const value: unknown = await request.json()
78
+ if (!record(value) || Object.keys(value).length !== 1 || typeof value.enabled !== 'boolean')
79
+ throw badRequest('enabled', 'invalid')
80
+ return json(
81
+ await service.setEnabled(actor, decodeURIComponent(enabled[1] ?? ''), value.enabled),
82
+ )
83
+ }
84
+ const provider = path.match(/^\/api\/v1\/providers\/([^/]+)$/)
85
+ if (provider && request.method === 'DELETE')
86
+ return json(await service.remove(actor, decodeURIComponent(provider[1] ?? '')))
87
+ throw new KitFailure('not_found', 'Unknown route', { entity: 'route' })
88
+ }