@ouraihub/aether-worker 0.0.0-stage → 0.1.1
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/LICENSE +789 -0
- package/migrations/0001.sql +22 -0
- package/package.json +38 -3
- package/src/app.ts +363 -0
- package/src/cloudflare.ts +49 -0
- package/src/context.ts +55 -0
- package/src/conversations/action.ts +257 -0
- package/src/conversations/index.ts +480 -0
- package/src/conversations/model.ts +427 -0
- package/src/conversations/routes.ts +101 -0
- package/src/coordinator-helpers.ts +131 -0
- package/src/coordinator.ts +488 -0
- package/src/credentials.ts +171 -0
- package/src/files.ts +117 -0
- package/src/host.ts +25 -0
- package/src/identity.ts +227 -0
- package/src/images/index.ts +73 -0
- package/src/index.ts +30 -0
- package/src/logger.ts +98 -0
- package/src/mcp/auth.ts +398 -0
- package/src/mcp/client.ts +491 -0
- package/src/mcp/common.ts +124 -0
- package/src/mcp/index.ts +292 -0
- package/src/mcp/oauth.ts +343 -0
- package/src/mcp/types.ts +81 -0
- package/src/outbound.ts +320 -0
- package/src/providers/catalog.ts +63 -0
- package/src/providers/http.ts +88 -0
- package/src/providers/index.ts +445 -0
- package/src/providers/model-options.ts +28 -0
- package/src/runtime-input.ts +44 -0
- package/src/runtime-tools.ts +298 -0
- package/src/runtime.ts +476 -0
- package/src/search/index.ts +497 -0
- package/src/skills/index.ts +1 -0
- package/src/skills/persistence.ts +153 -0
- package/src/skills/service.ts +250 -0
- package/src/storage.ts +293 -0
- package/src/testing/fakes.ts +97 -0
- package/README.md +0 -3
package/src/mcp/auth.ts
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { KitFailure, badRequest } from '@ouraihub/aether-core/error'
|
|
2
|
+
import type { Actor } from '../context.js'
|
|
3
|
+
import type { StoredDocument } from '../storage.js'
|
|
4
|
+
import type { ApiKeyRecord, GrantRecord, McpServiceOptions, McpSettings } from './types.js'
|
|
5
|
+
import {
|
|
6
|
+
equal,
|
|
7
|
+
forbidden,
|
|
8
|
+
hash,
|
|
9
|
+
notFound,
|
|
10
|
+
object,
|
|
11
|
+
random,
|
|
12
|
+
strings,
|
|
13
|
+
text,
|
|
14
|
+
unique,
|
|
15
|
+
validActor,
|
|
16
|
+
} from './common.js'
|
|
17
|
+
|
|
18
|
+
interface KeyState {
|
|
19
|
+
record: ApiKeyRecord
|
|
20
|
+
hash: string
|
|
21
|
+
}
|
|
22
|
+
interface GrantState {
|
|
23
|
+
record: GrantRecord
|
|
24
|
+
refreshHash: string
|
|
25
|
+
refreshExpiresAt: number
|
|
26
|
+
generation: number
|
|
27
|
+
}
|
|
28
|
+
interface TokenIndex {
|
|
29
|
+
owner: Actor
|
|
30
|
+
id: string
|
|
31
|
+
kind: 'key' | 'access'
|
|
32
|
+
expiresAt: number | null
|
|
33
|
+
}
|
|
34
|
+
export interface Authentication {
|
|
35
|
+
actor: Actor
|
|
36
|
+
tokenId: string
|
|
37
|
+
scopes: readonly string[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function keyRecord(value: unknown): value is ApiKeyRecord {
|
|
41
|
+
return (
|
|
42
|
+
object(value) &&
|
|
43
|
+
typeof value.id === 'string' &&
|
|
44
|
+
typeof value.name === 'string' &&
|
|
45
|
+
strings(value.scopes) &&
|
|
46
|
+
typeof value.prefix === 'string' &&
|
|
47
|
+
typeof value.createdAt === 'number' &&
|
|
48
|
+
nullableTime(value.expiresAt) &&
|
|
49
|
+
nullableTime(value.revokedAt)
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
function grantRecord(value: unknown): value is GrantRecord {
|
|
53
|
+
return (
|
|
54
|
+
object(value) &&
|
|
55
|
+
typeof value.id === 'string' &&
|
|
56
|
+
typeof value.applicationId === 'string' &&
|
|
57
|
+
(value.tenantId === null || typeof value.tenantId === 'string') &&
|
|
58
|
+
typeof value.subjectId === 'string' &&
|
|
59
|
+
typeof value.clientId === 'string' &&
|
|
60
|
+
typeof value.redirectUri === 'string' &&
|
|
61
|
+
strings(value.scopes) &&
|
|
62
|
+
typeof value.createdAt === 'number' &&
|
|
63
|
+
nullableTime(value.expiresAt) &&
|
|
64
|
+
nullableTime(value.revokedAt)
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
function nullableTime(value: unknown): value is number | null {
|
|
68
|
+
return value === null || (typeof value === 'number' && Number.isFinite(value))
|
|
69
|
+
}
|
|
70
|
+
function readKey(document: StoredDocument): KeyState {
|
|
71
|
+
const value = document.value
|
|
72
|
+
if (!object(value) || !keyRecord(value.record) || typeof value.hash !== 'string')
|
|
73
|
+
throw new KitFailure('internal', 'Invalid key storage')
|
|
74
|
+
return { record: value.record, hash: value.hash }
|
|
75
|
+
}
|
|
76
|
+
function readGrant(document: StoredDocument): GrantState {
|
|
77
|
+
const value = document.value
|
|
78
|
+
if (
|
|
79
|
+
!object(value) ||
|
|
80
|
+
!grantRecord(value.record) ||
|
|
81
|
+
typeof value.refreshHash !== 'string' ||
|
|
82
|
+
typeof value.refreshExpiresAt !== 'number' ||
|
|
83
|
+
typeof value.generation !== 'number'
|
|
84
|
+
)
|
|
85
|
+
throw new KitFailure('internal', 'Invalid grant storage')
|
|
86
|
+
return {
|
|
87
|
+
record: value.record,
|
|
88
|
+
refreshHash: value.refreshHash,
|
|
89
|
+
refreshExpiresAt: value.refreshExpiresAt,
|
|
90
|
+
generation: value.generation,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class McpAuth {
|
|
95
|
+
readonly system: Actor
|
|
96
|
+
readonly options: McpServiceOptions
|
|
97
|
+
constructor(options: McpServiceOptions) {
|
|
98
|
+
this.options = options
|
|
99
|
+
this.system = {
|
|
100
|
+
subjectId: 'aether:mcp:registry',
|
|
101
|
+
applicationId: options.applicationId,
|
|
102
|
+
tenantId: null,
|
|
103
|
+
kind: 'service',
|
|
104
|
+
sessionId: null,
|
|
105
|
+
scopes: [],
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
now(): number {
|
|
109
|
+
return this.options.now?.() ?? Date.now()
|
|
110
|
+
}
|
|
111
|
+
actor(actor: Actor): void {
|
|
112
|
+
if (
|
|
113
|
+
!validActor(actor, this.options.applicationId) ||
|
|
114
|
+
actor.kind !== 'user' ||
|
|
115
|
+
actor.subjectId === this.system.subjectId
|
|
116
|
+
)
|
|
117
|
+
forbidden()
|
|
118
|
+
}
|
|
119
|
+
requested(value: unknown, actor?: Actor): string[] {
|
|
120
|
+
if (!strings(value) || value.length === 0 || value.length > 100)
|
|
121
|
+
throw badRequest('scopes', 'invalid')
|
|
122
|
+
const scopes = unique(value)
|
|
123
|
+
if (
|
|
124
|
+
scopes.some(
|
|
125
|
+
(scope) =>
|
|
126
|
+
!this.options.scopes.some((known) => known.id === scope) ||
|
|
127
|
+
(actor && !actor.scopes.includes(scope)),
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
forbidden()
|
|
131
|
+
return scopes
|
|
132
|
+
}
|
|
133
|
+
async settings(owner: Actor): Promise<McpSettings> {
|
|
134
|
+
const doc = await this.options.store.get(owner, 'mcp-settings', 'settings')
|
|
135
|
+
if (!doc) return { enabled: false, enabledScopes: [] }
|
|
136
|
+
if (
|
|
137
|
+
!object(doc.value) ||
|
|
138
|
+
typeof doc.value.enabled !== 'boolean' ||
|
|
139
|
+
!strings(doc.value.enabledScopes)
|
|
140
|
+
)
|
|
141
|
+
throw new KitFailure('internal', 'Invalid MCP settings')
|
|
142
|
+
return { enabled: doc.value.enabled, enabledScopes: doc.value.enabledScopes }
|
|
143
|
+
}
|
|
144
|
+
async saveSettings(actor: Actor, value: Record<string, unknown>): Promise<McpSettings> {
|
|
145
|
+
this.actor(actor)
|
|
146
|
+
if (typeof value.enabled !== 'boolean' || !strings(value.enabledScopes))
|
|
147
|
+
throw badRequest('settings', 'invalid')
|
|
148
|
+
const enabledScopes = value.enabledScopes.length
|
|
149
|
+
? this.requested(value.enabledScopes, actor)
|
|
150
|
+
: []
|
|
151
|
+
const current = await this.options.store.get(actor, 'mcp-settings', 'settings')
|
|
152
|
+
const settings = { enabled: value.enabled, enabledScopes }
|
|
153
|
+
await this.options.store.compareAndSet(
|
|
154
|
+
actor,
|
|
155
|
+
'mcp-settings',
|
|
156
|
+
'settings',
|
|
157
|
+
current?.revision ?? null,
|
|
158
|
+
settings,
|
|
159
|
+
)
|
|
160
|
+
return settings
|
|
161
|
+
}
|
|
162
|
+
async keys(actor: Actor): Promise<ApiKeyRecord[]> {
|
|
163
|
+
this.actor(actor)
|
|
164
|
+
const result: ApiKeyRecord[] = []
|
|
165
|
+
let after: string | undefined
|
|
166
|
+
while (true) {
|
|
167
|
+
const page = await this.options.store.list(actor, 'mcp-keys', {
|
|
168
|
+
...(after === undefined ? {} : { after }),
|
|
169
|
+
limit: 200,
|
|
170
|
+
})
|
|
171
|
+
result.push(...page.map((doc) => readKey(doc).record))
|
|
172
|
+
if (page.length < 200) return result
|
|
173
|
+
const next = page[page.length - 1]?.key
|
|
174
|
+
if (!next || (after !== undefined && next <= after))
|
|
175
|
+
throw new KitFailure('internal', 'Invalid key pagination')
|
|
176
|
+
after = next
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async grants(actor: Actor): Promise<GrantRecord[]> {
|
|
180
|
+
this.actor(actor)
|
|
181
|
+
const result: GrantRecord[] = []
|
|
182
|
+
let after: string | undefined
|
|
183
|
+
while (true) {
|
|
184
|
+
const page = await this.options.store.list(actor, 'mcp-grants', {
|
|
185
|
+
...(after === undefined ? {} : { after }),
|
|
186
|
+
limit: 200,
|
|
187
|
+
})
|
|
188
|
+
result.push(...page.map((doc) => readGrant(doc).record))
|
|
189
|
+
if (page.length < 200) return result
|
|
190
|
+
const next = page[page.length - 1]?.key
|
|
191
|
+
if (!next || (after !== undefined && next <= after))
|
|
192
|
+
throw new KitFailure('internal', 'Invalid grant pagination')
|
|
193
|
+
after = next
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async issueKey(
|
|
197
|
+
actor: Actor,
|
|
198
|
+
input: Record<string, unknown>,
|
|
199
|
+
request: Request,
|
|
200
|
+
): Promise<{ token: string; record: ApiKeyRecord }> {
|
|
201
|
+
this.actor(actor)
|
|
202
|
+
const name = text(input.name, 'name', 128).trim()
|
|
203
|
+
const scopes = this.requested(input.scopes, actor)
|
|
204
|
+
if (!(await this.options.authorizeConsent(actor, scopes, request))) forbidden()
|
|
205
|
+
const now = this.now()
|
|
206
|
+
const expiresAt = input.expiresAt === undefined ? now + 90 * 86400000 : input.expiresAt
|
|
207
|
+
if (
|
|
208
|
+
expiresAt !== null &&
|
|
209
|
+
(typeof expiresAt !== 'number' || !Number.isFinite(expiresAt) || expiresAt <= now)
|
|
210
|
+
)
|
|
211
|
+
throw badRequest('expiresAt', 'invalid')
|
|
212
|
+
const id = crypto.randomUUID()
|
|
213
|
+
const token = `aether_key_${random()}`
|
|
214
|
+
const digest = await hash(token)
|
|
215
|
+
const record: ApiKeyRecord = {
|
|
216
|
+
id,
|
|
217
|
+
name,
|
|
218
|
+
scopes,
|
|
219
|
+
prefix: `${token.slice(0, 16)}…`,
|
|
220
|
+
createdAt: now,
|
|
221
|
+
expiresAt,
|
|
222
|
+
revokedAt: null,
|
|
223
|
+
}
|
|
224
|
+
await this.options.store.compareAndSet(actor, 'mcp-keys', id, null, { record, hash: digest })
|
|
225
|
+
await this.index(digest, { owner: actor, id, kind: 'key', expiresAt })
|
|
226
|
+
return { token, record }
|
|
227
|
+
}
|
|
228
|
+
async revoke(actor: Actor, kind: 'key' | 'grant', id: string): Promise<void> {
|
|
229
|
+
this.actor(actor)
|
|
230
|
+
const collection = kind === 'key' ? 'mcp-keys' : 'mcp-grants'
|
|
231
|
+
const document = await this.options.store.get(actor, collection, id)
|
|
232
|
+
if (!document) notFound()
|
|
233
|
+
const state = kind === 'key' ? readKey(document) : readGrant(document)
|
|
234
|
+
if (state.record.revokedAt !== null) return
|
|
235
|
+
await this.options.store.compareAndSet(actor, collection, id, document.revision, {
|
|
236
|
+
...state,
|
|
237
|
+
record: { ...state.record, revokedAt: this.now() },
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
private async index(digest: string, value: TokenIndex): Promise<void> {
|
|
241
|
+
await this.options.store.compareAndSet(this.system, 'mcp-token-index', digest, null, {
|
|
242
|
+
...value,
|
|
243
|
+
audience: `${this.options.origin}/mcp`,
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
async authenticate(token: string): Promise<Authentication | null> {
|
|
247
|
+
if (token.length > 512) return null
|
|
248
|
+
const digest = await hash(token)
|
|
249
|
+
const doc = await this.options.store.get(this.system, 'mcp-token-index', digest)
|
|
250
|
+
if (!doc || !object(doc.value)) return null
|
|
251
|
+
const value = doc.value
|
|
252
|
+
if (
|
|
253
|
+
value.audience !== `${this.options.origin}/mcp` ||
|
|
254
|
+
!validActor(value.owner, this.options.applicationId) ||
|
|
255
|
+
value.owner.kind !== 'user' ||
|
|
256
|
+
typeof value.id !== 'string' ||
|
|
257
|
+
!nullableTime(value.expiresAt) ||
|
|
258
|
+
(value.expiresAt !== null && value.expiresAt <= this.now())
|
|
259
|
+
)
|
|
260
|
+
return null
|
|
261
|
+
const owner = value.owner
|
|
262
|
+
let scopes: readonly string[]
|
|
263
|
+
if (value.kind === 'key') {
|
|
264
|
+
const key = await this.options.store.get(owner, 'mcp-keys', value.id)
|
|
265
|
+
if (!key) return null
|
|
266
|
+
const state = readKey(key)
|
|
267
|
+
if (
|
|
268
|
+
state.record.revokedAt !== null ||
|
|
269
|
+
(state.record.expiresAt !== null && state.record.expiresAt <= this.now()) ||
|
|
270
|
+
!equal(state.hash, digest)
|
|
271
|
+
)
|
|
272
|
+
return null
|
|
273
|
+
scopes = state.record.scopes
|
|
274
|
+
} else if (value.kind === 'access') {
|
|
275
|
+
const grant = await this.options.store.get(owner, 'mcp-grants', value.id)
|
|
276
|
+
if (!grant) return null
|
|
277
|
+
const state = readGrant(grant)
|
|
278
|
+
if (!this.activeGrant(state, owner)) return null
|
|
279
|
+
scopes = state.record.scopes
|
|
280
|
+
} else return null
|
|
281
|
+
const settings = await this.settings(owner)
|
|
282
|
+
if (!settings.enabled) return null
|
|
283
|
+
const effective = scopes.filter(
|
|
284
|
+
(scope) =>
|
|
285
|
+
settings.enabledScopes.includes(scope) &&
|
|
286
|
+
this.options.scopes.some((known) => known.id === scope),
|
|
287
|
+
)
|
|
288
|
+
return {
|
|
289
|
+
actor: { ...owner, kind: 'mcp_client', sessionId: null, scopes: effective },
|
|
290
|
+
tokenId: value.id,
|
|
291
|
+
scopes: effective,
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
private activeGrant(state: GrantState, owner: Actor): boolean {
|
|
295
|
+
return (
|
|
296
|
+
state.record.applicationId === owner.applicationId &&
|
|
297
|
+
state.record.tenantId === owner.tenantId &&
|
|
298
|
+
state.record.subjectId === owner.subjectId &&
|
|
299
|
+
state.record.revokedAt === null &&
|
|
300
|
+
(state.record.expiresAt === null || state.record.expiresAt > this.now())
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
async createGrant(
|
|
304
|
+
owner: Actor,
|
|
305
|
+
clientId: string,
|
|
306
|
+
redirectUri: string,
|
|
307
|
+
scopes: readonly string[],
|
|
308
|
+
): Promise<GrantRecord> {
|
|
309
|
+
const now = this.now()
|
|
310
|
+
const record: GrantRecord = {
|
|
311
|
+
id: crypto.randomUUID(),
|
|
312
|
+
applicationId: owner.applicationId,
|
|
313
|
+
tenantId: owner.tenantId,
|
|
314
|
+
subjectId: owner.subjectId,
|
|
315
|
+
clientId,
|
|
316
|
+
redirectUri,
|
|
317
|
+
scopes,
|
|
318
|
+
createdAt: now,
|
|
319
|
+
expiresAt: now + 30 * 86400000,
|
|
320
|
+
revokedAt: null,
|
|
321
|
+
}
|
|
322
|
+
await this.options.store.compareAndSet(owner, 'mcp-grants', record.id, null, {
|
|
323
|
+
record,
|
|
324
|
+
refreshHash: '',
|
|
325
|
+
refreshExpiresAt: 0,
|
|
326
|
+
generation: 0,
|
|
327
|
+
})
|
|
328
|
+
return record
|
|
329
|
+
}
|
|
330
|
+
async issueTokens(
|
|
331
|
+
owner: Actor,
|
|
332
|
+
grantId: string,
|
|
333
|
+
refresh?: string,
|
|
334
|
+
clientId?: string,
|
|
335
|
+
): Promise<Record<string, unknown>> {
|
|
336
|
+
const document = await this.options.store.get(owner, 'mcp-grants', grantId)
|
|
337
|
+
if (!document) throw new KitFailure('unauthorized', 'Invalid grant')
|
|
338
|
+
const state = readGrant(document)
|
|
339
|
+
if (
|
|
340
|
+
!this.activeGrant(state, owner) ||
|
|
341
|
+
(clientId !== undefined && state.record.clientId !== clientId)
|
|
342
|
+
)
|
|
343
|
+
throw new KitFailure('unauthorized', 'Invalid grant')
|
|
344
|
+
if (
|
|
345
|
+
refresh !== undefined &&
|
|
346
|
+
(state.refreshExpiresAt <= this.now() || !equal(state.refreshHash, await hash(refresh)))
|
|
347
|
+
)
|
|
348
|
+
throw new KitFailure('unauthorized', 'Invalid grant')
|
|
349
|
+
const accessToken = `aether_access_${random()}`
|
|
350
|
+
const refreshToken = `aether_refresh_${random()}`
|
|
351
|
+
const now = this.now()
|
|
352
|
+
const updated: GrantState = {
|
|
353
|
+
...state,
|
|
354
|
+
refreshHash: await hash(refreshToken),
|
|
355
|
+
refreshExpiresAt: Math.min(now + 30 * 86400000, state.record.expiresAt ?? Infinity),
|
|
356
|
+
generation: state.generation + 1,
|
|
357
|
+
}
|
|
358
|
+
await this.options.store.compareAndSet(owner, 'mcp-grants', grantId, document.revision, updated)
|
|
359
|
+
await this.index(await hash(accessToken), {
|
|
360
|
+
owner,
|
|
361
|
+
id: grantId,
|
|
362
|
+
kind: 'access',
|
|
363
|
+
expiresAt: now + 3600000,
|
|
364
|
+
})
|
|
365
|
+
await this.options.store.compareAndSet(
|
|
366
|
+
this.system,
|
|
367
|
+
'mcp-refresh-index',
|
|
368
|
+
await hash(refreshToken),
|
|
369
|
+
null,
|
|
370
|
+
{
|
|
371
|
+
owner,
|
|
372
|
+
grantId,
|
|
373
|
+
clientId: state.record.clientId,
|
|
374
|
+
audience: `${this.options.origin}/mcp`,
|
|
375
|
+
},
|
|
376
|
+
)
|
|
377
|
+
return {
|
|
378
|
+
access_token: accessToken,
|
|
379
|
+
token_type: 'Bearer',
|
|
380
|
+
expires_in: 3600,
|
|
381
|
+
refresh_token: refreshToken,
|
|
382
|
+
scope: state.record.scopes.join(' '),
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async refresh(token: string, clientId: string): Promise<Record<string, unknown>> {
|
|
386
|
+
const doc = await this.options.store.get(this.system, 'mcp-refresh-index', await hash(token))
|
|
387
|
+
if (
|
|
388
|
+
!doc ||
|
|
389
|
+
!object(doc.value) ||
|
|
390
|
+
doc.value.audience !== `${this.options.origin}/mcp` ||
|
|
391
|
+
!validActor(doc.value.owner, this.options.applicationId) ||
|
|
392
|
+
typeof doc.value.grantId !== 'string' ||
|
|
393
|
+
doc.value.clientId !== clientId
|
|
394
|
+
)
|
|
395
|
+
throw new KitFailure('unauthorized', 'Invalid grant')
|
|
396
|
+
return this.issueTokens(doc.value.owner, doc.value.grantId, token, clientId)
|
|
397
|
+
}
|
|
398
|
+
}
|