@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
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-- Apply explicitly with the deployment's D1 migration command, never at request startup.
|
|
2
|
+
CREATE TABLE aether_schema (
|
|
3
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
4
|
+
version INTEGER NOT NULL CHECK (version >= 1)
|
|
5
|
+
);
|
|
6
|
+
INSERT INTO aether_schema (singleton, version) VALUES (1, 1);
|
|
7
|
+
CREATE TABLE aether_documents (
|
|
8
|
+
application_id TEXT NOT NULL,
|
|
9
|
+
tenant_key TEXT NOT NULL,
|
|
10
|
+
subject_id TEXT NOT NULL,
|
|
11
|
+
owner_key TEXT NOT NULL,
|
|
12
|
+
collection TEXT NOT NULL,
|
|
13
|
+
key TEXT NOT NULL,
|
|
14
|
+
revision INTEGER NOT NULL CHECK (revision >= 1),
|
|
15
|
+
value_json TEXT,
|
|
16
|
+
blob_key TEXT,
|
|
17
|
+
deleted INTEGER NOT NULL DEFAULT 0 CHECK (deleted IN (0, 1)),
|
|
18
|
+
updated_at INTEGER NOT NULL,
|
|
19
|
+
PRIMARY KEY (application_id, tenant_key, subject_id, collection, key),
|
|
20
|
+
CHECK ((deleted = 0 AND ((value_json IS NOT NULL AND json_valid(value_json) AND blob_key IS NULL) OR (value_json IS NULL AND blob_key IS NOT NULL))) OR (deleted = 1 AND value_json IS NULL AND blob_key IS NULL))
|
|
21
|
+
) WITHOUT ROWID;
|
|
22
|
+
CREATE INDEX aether_documents_list ON aether_documents (application_id, tenant_key, subject_id, collection, deleted, key);
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouraihub/aether-worker",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"license": "LGPL-3.0-only",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/ouraihub/Aether.git",
|
|
8
|
+
"directory": "packages/worker"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./src/index.ts",
|
|
13
|
+
"./providers": "./src/providers/index.ts",
|
|
14
|
+
"./search": "./src/search/index.ts",
|
|
15
|
+
"./images": "./src/images/index.ts",
|
|
16
|
+
"./skills": "./src/skills/index.ts",
|
|
17
|
+
"./mcp": "./src/mcp/index.ts",
|
|
18
|
+
"./conversations": "./src/conversations/index.ts",
|
|
19
|
+
"./*": "./src/*.ts"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@ouraihub/aether-contracts": "0.1.1",
|
|
23
|
+
"@ouraihub/aether-core": "0.1.1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {},
|
|
26
|
+
"files": [
|
|
27
|
+
"src",
|
|
28
|
+
"!src/**/*.test.ts",
|
|
29
|
+
"!src/**/*.test.svelte",
|
|
30
|
+
"!src/**/__tests__",
|
|
31
|
+
"migrations"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"test": "tsx --test \"src/**/*.test.ts\"",
|
|
39
|
+
"scenarios": "tsx scenarios/run.ts"
|
|
40
|
+
}
|
|
6
41
|
}
|
package/src/app.ts
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { isContract } from '@ouraihub/aether-contracts'
|
|
2
|
+
import type { KitError, KitErrorCode } from '@ouraihub/aether-contracts'
|
|
3
|
+
import { badRequest, KitFailure, safeError } from '@ouraihub/aether-core/error'
|
|
4
|
+
import type { DurableObjectNamespace } from './cloudflare.ts'
|
|
5
|
+
import { immutableActor, isActor } from './context.ts'
|
|
6
|
+
import type { Actor, WorkerContext } from './context.ts'
|
|
7
|
+
import type { IdentityPort } from './identity.ts'
|
|
8
|
+
import type { Logger } from './logger.ts'
|
|
9
|
+
|
|
10
|
+
export interface WorkerHandler {
|
|
11
|
+
handle(request: Request, context: WorkerContext): Promise<Response | null>
|
|
12
|
+
}
|
|
13
|
+
export interface PublicWorkerHandler {
|
|
14
|
+
handlePublic(request: Request): Promise<Response | null>
|
|
15
|
+
}
|
|
16
|
+
export interface WorkerAppOptions {
|
|
17
|
+
readonly applicationId: string
|
|
18
|
+
readonly origin: string
|
|
19
|
+
readonly identity: IdentityPort
|
|
20
|
+
readonly runs: DurableObjectNamespace
|
|
21
|
+
readonly handlers?: readonly WorkerHandler[]
|
|
22
|
+
readonly publicHandlers?: readonly PublicWorkerHandler[]
|
|
23
|
+
/** Authentication does not imply resource authorization or admission quota. */
|
|
24
|
+
readonly authorizeRoute: (actor: Actor, request: Request) => Promise<void>
|
|
25
|
+
readonly verifyCsrf: (request: Request, actor: Actor) => Promise<boolean>
|
|
26
|
+
readonly logger?: Logger
|
|
27
|
+
readonly maxBodyBytes?: number
|
|
28
|
+
readonly allowLoopbackHttp?: boolean
|
|
29
|
+
}
|
|
30
|
+
const statuses: Record<KitErrorCode, number> = {
|
|
31
|
+
bad_request: 400,
|
|
32
|
+
not_found: 404,
|
|
33
|
+
conflict: 409,
|
|
34
|
+
unsupported: 422,
|
|
35
|
+
cancelled: 409,
|
|
36
|
+
unauthorized: 401,
|
|
37
|
+
forbidden: 403,
|
|
38
|
+
rate_limited: 429,
|
|
39
|
+
upstream: 502,
|
|
40
|
+
timeout: 504,
|
|
41
|
+
network: 502,
|
|
42
|
+
needs_config: 422,
|
|
43
|
+
budget_exceeded: 413,
|
|
44
|
+
incompatible_db: 503,
|
|
45
|
+
internal: 500,
|
|
46
|
+
}
|
|
47
|
+
function failureResponse(error: unknown): Response {
|
|
48
|
+
let safe: KitError = safeError(error)
|
|
49
|
+
if (safe.code === 'internal')
|
|
50
|
+
safe = {
|
|
51
|
+
code: 'internal',
|
|
52
|
+
message: 'Internal request failure',
|
|
53
|
+
details: { errorId: crypto.randomUUID() },
|
|
54
|
+
}
|
|
55
|
+
return Response.json(safe, { status: statuses[safe.code] })
|
|
56
|
+
}
|
|
57
|
+
function loopback(url: URL): boolean {
|
|
58
|
+
return url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]'
|
|
59
|
+
}
|
|
60
|
+
async function boundedRequest(request: Request, maximum: number): Promise<Request> {
|
|
61
|
+
if (request.method === 'GET' || request.method === 'HEAD' || request.body === null) return request
|
|
62
|
+
const declared = request.headers.get('content-length')
|
|
63
|
+
if (declared !== null && (!/^\d+$/u.test(declared) || Number(declared) > maximum))
|
|
64
|
+
throw badRequest('body', 'too_large', maximum)
|
|
65
|
+
const reader = request.body.getReader(),
|
|
66
|
+
chunks: Uint8Array[] = []
|
|
67
|
+
let size = 0,
|
|
68
|
+
completed = false
|
|
69
|
+
try {
|
|
70
|
+
while (true) {
|
|
71
|
+
const next = await reader.read()
|
|
72
|
+
if (next.done) {
|
|
73
|
+
completed = true
|
|
74
|
+
break
|
|
75
|
+
}
|
|
76
|
+
size += next.value.byteLength
|
|
77
|
+
if (size > maximum) throw badRequest('body', 'too_large', maximum)
|
|
78
|
+
chunks.push(next.value)
|
|
79
|
+
}
|
|
80
|
+
} finally {
|
|
81
|
+
if (!completed) await reader.cancel().catch(() => undefined)
|
|
82
|
+
reader.releaseLock()
|
|
83
|
+
}
|
|
84
|
+
const bytes = new Uint8Array(size)
|
|
85
|
+
let offset = 0
|
|
86
|
+
for (const chunk of chunks) {
|
|
87
|
+
bytes.set(chunk, offset)
|
|
88
|
+
offset += chunk.byteLength
|
|
89
|
+
}
|
|
90
|
+
return new Request(request, { body: bytes })
|
|
91
|
+
}
|
|
92
|
+
function actorHeader(actor: Actor): string {
|
|
93
|
+
const bytes = new TextEncoder().encode(JSON.stringify(actor))
|
|
94
|
+
let binary = ''
|
|
95
|
+
for (const byte of bytes) binary += String.fromCharCode(byte)
|
|
96
|
+
return btoa(binary)
|
|
97
|
+
}
|
|
98
|
+
function secured(response: Response, origin: string | null, allowed: string): Response {
|
|
99
|
+
const headers = new Headers(response.headers)
|
|
100
|
+
headers.set('X-Content-Type-Options', 'nosniff')
|
|
101
|
+
headers.set('Referrer-Policy', 'no-referrer')
|
|
102
|
+
headers.set(
|
|
103
|
+
'Cache-Control',
|
|
104
|
+
headers.get('content-type')?.startsWith('text/event-stream')
|
|
105
|
+
? 'no-cache, no-transform'
|
|
106
|
+
: 'no-store',
|
|
107
|
+
)
|
|
108
|
+
if (origin === allowed) {
|
|
109
|
+
headers.set('Access-Control-Allow-Origin', allowed)
|
|
110
|
+
headers.set('Access-Control-Allow-Credentials', 'true')
|
|
111
|
+
headers.append('Vary', 'Origin')
|
|
112
|
+
}
|
|
113
|
+
return new Response(response.body, {
|
|
114
|
+
status: response.status,
|
|
115
|
+
statusText: response.statusText,
|
|
116
|
+
headers,
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
/** Product entrypoints compose this handler; this package never supplies product login or business tools. */
|
|
120
|
+
export function createAetherWorkerApp(options: WorkerAppOptions): {
|
|
121
|
+
fetch(request: Request): Promise<Response>
|
|
122
|
+
} {
|
|
123
|
+
const publicOrigin = new URL(options.origin)
|
|
124
|
+
if (
|
|
125
|
+
!options.applicationId ||
|
|
126
|
+
publicOrigin.origin !== options.origin ||
|
|
127
|
+
(publicOrigin.protocol !== 'https:' &&
|
|
128
|
+
!(options.allowLoopbackHttp && publicOrigin.protocol === 'http:' && loopback(publicOrigin)))
|
|
129
|
+
)
|
|
130
|
+
throw new KitFailure('needs_config', 'Invalid Worker origin or application')
|
|
131
|
+
const maximum = options.maxBodyBytes ?? 24 * 1024 * 1024
|
|
132
|
+
if (!Number.isSafeInteger(maximum) || maximum <= 0 || maximum > 32 * 1024 * 1024)
|
|
133
|
+
throw badRequest('maxBodyBytes', 'invalid')
|
|
134
|
+
return {
|
|
135
|
+
async fetch(raw: Request): Promise<Response> {
|
|
136
|
+
const now = Date.now(),
|
|
137
|
+
requestId = crypto.randomUUID(),
|
|
138
|
+
url = new URL(raw.url),
|
|
139
|
+
origin = raw.headers.get('origin')
|
|
140
|
+
let response: Response
|
|
141
|
+
try {
|
|
142
|
+
if (
|
|
143
|
+
url.protocol !== 'https:' &&
|
|
144
|
+
!(options.allowLoopbackHttp && url.protocol === 'http:' && loopback(url))
|
|
145
|
+
)
|
|
146
|
+
throw new KitFailure('forbidden', 'Secure transport required')
|
|
147
|
+
if (origin !== null && origin !== options.origin)
|
|
148
|
+
throw new KitFailure('forbidden', 'Origin rejected')
|
|
149
|
+
if (raw.method === 'OPTIONS') {
|
|
150
|
+
if (origin !== options.origin) throw new KitFailure('forbidden', 'Origin rejected')
|
|
151
|
+
return secured(
|
|
152
|
+
new Response(null, {
|
|
153
|
+
status: 204,
|
|
154
|
+
headers: {
|
|
155
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
156
|
+
'Access-Control-Allow-Headers': 'Content-Type, X-CSRF-Token',
|
|
157
|
+
'Access-Control-Max-Age': '600',
|
|
158
|
+
},
|
|
159
|
+
}),
|
|
160
|
+
origin,
|
|
161
|
+
options.origin,
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
const mutating = !['GET', 'HEAD'].includes(raw.method)
|
|
165
|
+
const publicRoute =
|
|
166
|
+
url.pathname === '/mcp' ||
|
|
167
|
+
url.pathname.startsWith('/oauth/') ||
|
|
168
|
+
url.pathname.startsWith('/.well-known/')
|
|
169
|
+
if (
|
|
170
|
+
mutating &&
|
|
171
|
+
raw.body !== null &&
|
|
172
|
+
!publicRoute &&
|
|
173
|
+
raw.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase() !==
|
|
174
|
+
'application/json'
|
|
175
|
+
)
|
|
176
|
+
throw badRequest('contentType', 'json_required')
|
|
177
|
+
const request = await boundedRequest(raw, maximum)
|
|
178
|
+
if (publicRoute) {
|
|
179
|
+
let publicResponse: Response | null = null
|
|
180
|
+
for (const handler of options.publicHandlers ?? []) {
|
|
181
|
+
publicResponse = await handler.handlePublic(request)
|
|
182
|
+
if (publicResponse) break
|
|
183
|
+
}
|
|
184
|
+
if (!publicResponse)
|
|
185
|
+
throw new KitFailure('not_found', 'Unknown route', { entity: 'route' })
|
|
186
|
+
response = publicResponse
|
|
187
|
+
} else {
|
|
188
|
+
const actor = await options.identity.resolve(request)
|
|
189
|
+
if (!isActor(actor) || actor.applicationId !== options.applicationId)
|
|
190
|
+
throw new KitFailure('unauthorized', 'Authentication required')
|
|
191
|
+
if (
|
|
192
|
+
mutating &&
|
|
193
|
+
actor.kind === 'user' &&
|
|
194
|
+
actor.sessionId !== null &&
|
|
195
|
+
(origin !== options.origin || !(await options.verifyCsrf(request, actor)))
|
|
196
|
+
)
|
|
197
|
+
throw new KitFailure('forbidden', 'Request authorization rejected')
|
|
198
|
+
await options.authorizeRoute(actor, request)
|
|
199
|
+
const context: WorkerContext = {
|
|
200
|
+
actor: immutableActor(actor),
|
|
201
|
+
requestId,
|
|
202
|
+
now,
|
|
203
|
+
signal: request.signal,
|
|
204
|
+
}
|
|
205
|
+
if (url.pathname === '/api/v1/runs' && request.method === 'POST') {
|
|
206
|
+
const input: unknown = await request.json()
|
|
207
|
+
if (!isContract('RunRequest', input)) throw badRequest('run', 'invalid')
|
|
208
|
+
const runId = crypto.randomUUID(),
|
|
209
|
+
stub = options.runs.get(options.runs.idFromName(`${options.applicationId}:${runId}`))
|
|
210
|
+
const accepted = await stub.fetch('https://aether.internal/accept', {
|
|
211
|
+
method: 'POST',
|
|
212
|
+
headers: { 'Content-Type': 'application/json' },
|
|
213
|
+
body: JSON.stringify({ actor: context.actor, request: input, runId }),
|
|
214
|
+
signal: request.signal,
|
|
215
|
+
})
|
|
216
|
+
const value: unknown = await accepted.json()
|
|
217
|
+
if (accepted.status !== 202) {
|
|
218
|
+
if (isContract('KitError', value))
|
|
219
|
+
throw new KitFailure(value.code, value.message, value.details)
|
|
220
|
+
throw new KitFailure('internal', 'Invalid coordinator reply')
|
|
221
|
+
}
|
|
222
|
+
if (!isContract('RunAccepted', value))
|
|
223
|
+
throw new KitFailure('internal', 'Invalid coordinator acceptance')
|
|
224
|
+
response = Response.json(
|
|
225
|
+
{ accepted: value, events: `/api/v1/runs/${encodeURIComponent(runId)}/events` },
|
|
226
|
+
{ status: 202 },
|
|
227
|
+
)
|
|
228
|
+
} else {
|
|
229
|
+
const run = /^\/api\/v1\/runs\/([^/]+)(?:\/(events|cancel|approval))?$/u.exec(
|
|
230
|
+
url.pathname,
|
|
231
|
+
)
|
|
232
|
+
if (run) {
|
|
233
|
+
const runId = decodeURIComponent(run[1] ?? ''),
|
|
234
|
+
action = run[2] ?? 'snapshot'
|
|
235
|
+
if (
|
|
236
|
+
!/^[A-Za-z0-9_-]{1,128}$/u.test(runId) ||
|
|
237
|
+
(['events', 'snapshot'].includes(action)
|
|
238
|
+
? request.method !== 'GET'
|
|
239
|
+
: request.method !== 'POST')
|
|
240
|
+
)
|
|
241
|
+
throw badRequest('run', 'invalid')
|
|
242
|
+
const stub = options.runs.get(
|
|
243
|
+
options.runs.idFromName(`${options.applicationId}:${runId}`),
|
|
244
|
+
),
|
|
245
|
+
headers = new Headers({ 'x-aether-actor': actorHeader(context.actor) })
|
|
246
|
+
if (request.headers.has('Last-Event-ID'))
|
|
247
|
+
headers.set('Last-Event-ID', request.headers.get('Last-Event-ID') ?? '')
|
|
248
|
+
const body = request.method === 'POST' ? await request.text() : undefined
|
|
249
|
+
if (body !== undefined) headers.set('Content-Type', 'application/json')
|
|
250
|
+
response = await stub.fetch(`https://aether.internal/${action}`, {
|
|
251
|
+
method: request.method,
|
|
252
|
+
headers,
|
|
253
|
+
...(body === undefined ? {} : { body }),
|
|
254
|
+
signal: request.signal,
|
|
255
|
+
})
|
|
256
|
+
if (
|
|
257
|
+
action === 'events' &&
|
|
258
|
+
response.status === 200 &&
|
|
259
|
+
response.body &&
|
|
260
|
+
response.headers.get('content-type')?.startsWith('text/event-stream')
|
|
261
|
+
) {
|
|
262
|
+
const upstream = response.body.getReader()
|
|
263
|
+
const transport = new TransformStream<Uint8Array, Uint8Array>()
|
|
264
|
+
const writer = transport.writable.getWriter()
|
|
265
|
+
const heartbeat = new TextEncoder().encode(': keep-alive\n\n')
|
|
266
|
+
let finished = false,
|
|
267
|
+
heartbeatPending = false
|
|
268
|
+
let cancellation: Promise<void> | null = null
|
|
269
|
+
const disconnected = (): Promise<void> => {
|
|
270
|
+
if (finished) return Promise.resolve()
|
|
271
|
+
if (cancellation) return cancellation
|
|
272
|
+
clearInterval(timer)
|
|
273
|
+
request.signal.removeEventListener('abort', aborted)
|
|
274
|
+
// RPC response-body cancellation is not a reliable cross-isolate signal.
|
|
275
|
+
// Explicitly notify the same authenticated run without the aborted client signal.
|
|
276
|
+
cancellation = Promise.all([
|
|
277
|
+
stub
|
|
278
|
+
.fetch('https://aether.internal/cancel', {
|
|
279
|
+
method: 'POST',
|
|
280
|
+
headers: {
|
|
281
|
+
'x-aether-actor': actorHeader(context.actor),
|
|
282
|
+
'Content-Type': 'application/json',
|
|
283
|
+
},
|
|
284
|
+
body: '{}',
|
|
285
|
+
})
|
|
286
|
+
.then(async (reply) => {
|
|
287
|
+
await reply.body?.cancel()
|
|
288
|
+
}),
|
|
289
|
+
upstream.cancel(),
|
|
290
|
+
writer.abort(),
|
|
291
|
+
]).then(() => undefined)
|
|
292
|
+
return cancellation
|
|
293
|
+
}
|
|
294
|
+
const aborted = (): void => {
|
|
295
|
+
void disconnected().catch(() => undefined)
|
|
296
|
+
}
|
|
297
|
+
// A comment forces a real transport write even while the engine awaits approval.
|
|
298
|
+
const timer = setInterval(() => {
|
|
299
|
+
if (finished || cancellation || heartbeatPending) return
|
|
300
|
+
heartbeatPending = true
|
|
301
|
+
void writer
|
|
302
|
+
.write(heartbeat)
|
|
303
|
+
.catch(disconnected)
|
|
304
|
+
.finally(() => {
|
|
305
|
+
heartbeatPending = false
|
|
306
|
+
})
|
|
307
|
+
.catch(() => undefined)
|
|
308
|
+
}, 1000)
|
|
309
|
+
request.signal.addEventListener('abort', aborted, { once: true })
|
|
310
|
+
void writer.closed.catch(disconnected).catch(() => undefined)
|
|
311
|
+
const pump = async (): Promise<void> => {
|
|
312
|
+
try {
|
|
313
|
+
while (!cancellation) {
|
|
314
|
+
const next = await upstream.read()
|
|
315
|
+
if (next.done) {
|
|
316
|
+
clearInterval(timer)
|
|
317
|
+
await writer.close()
|
|
318
|
+
finished = true
|
|
319
|
+
break
|
|
320
|
+
}
|
|
321
|
+
await writer.write(next.value)
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
await disconnected()
|
|
325
|
+
} finally {
|
|
326
|
+
clearInterval(timer)
|
|
327
|
+
request.signal.removeEventListener('abort', aborted)
|
|
328
|
+
upstream.releaseLock()
|
|
329
|
+
writer.releaseLock()
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
void pump().catch(() => undefined)
|
|
333
|
+
if (request.signal.aborted) aborted()
|
|
334
|
+
response = new Response(transport.readable, {
|
|
335
|
+
status: response.status,
|
|
336
|
+
statusText: response.statusText,
|
|
337
|
+
headers: response.headers,
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
let handled: Response | null = null
|
|
342
|
+
for (const handler of options.handlers ?? []) {
|
|
343
|
+
handled = await handler.handle(request, context)
|
|
344
|
+
if (handled) break
|
|
345
|
+
}
|
|
346
|
+
if (!handled) throw new KitFailure('not_found', 'Unknown route', { entity: 'route' })
|
|
347
|
+
response = handled
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
} catch (error) {
|
|
352
|
+
response = failureResponse(error)
|
|
353
|
+
}
|
|
354
|
+
options.logger?.log('request.finished', {
|
|
355
|
+
requestId,
|
|
356
|
+
applicationId: options.applicationId,
|
|
357
|
+
status: response.status,
|
|
358
|
+
durationMs: Date.now() - now,
|
|
359
|
+
})
|
|
360
|
+
return secured(response, origin, options.origin)
|
|
361
|
+
},
|
|
362
|
+
}
|
|
363
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Structural subsets: the adapter needs no Cloudflare ambient globals. */
|
|
2
|
+
export interface D1Statement {
|
|
3
|
+
bind(...values: (string | number | null)[]): D1Statement
|
|
4
|
+
first(): Promise<unknown>
|
|
5
|
+
all(): Promise<{ readonly results?: readonly unknown[]; readonly success: boolean }>
|
|
6
|
+
}
|
|
7
|
+
export interface D1Database {
|
|
8
|
+
prepare(sql: string): D1Statement
|
|
9
|
+
}
|
|
10
|
+
export interface R2Object {
|
|
11
|
+
readonly size: number
|
|
12
|
+
readonly httpMetadata?: { readonly contentType?: string }
|
|
13
|
+
arrayBuffer(): Promise<ArrayBuffer>
|
|
14
|
+
}
|
|
15
|
+
export interface R2Bucket {
|
|
16
|
+
put(
|
|
17
|
+
key: string,
|
|
18
|
+
value: ArrayBuffer | Uint8Array,
|
|
19
|
+
options?: {
|
|
20
|
+
readonly httpMetadata?: { readonly contentType?: string }
|
|
21
|
+
readonly onlyIf?: { readonly etagDoesNotMatch: string }
|
|
22
|
+
},
|
|
23
|
+
): Promise<unknown>
|
|
24
|
+
get(key: string): Promise<R2Object | null>
|
|
25
|
+
delete(key: string): Promise<void>
|
|
26
|
+
}
|
|
27
|
+
export interface DurableObjectStorage {
|
|
28
|
+
get<T = unknown>(key: string): Promise<T | undefined>
|
|
29
|
+
put<T>(key: string, value: T): Promise<void>
|
|
30
|
+
delete(key: string): Promise<boolean>
|
|
31
|
+
setAlarm(scheduledTime: number | Date): Promise<void>
|
|
32
|
+
getAlarm(): Promise<number | null>
|
|
33
|
+
deleteAlarm(): Promise<void>
|
|
34
|
+
}
|
|
35
|
+
export interface DurableObjectState {
|
|
36
|
+
readonly storage: DurableObjectStorage
|
|
37
|
+
blockConcurrencyWhile<T>(callback: () => Promise<T>): Promise<T>
|
|
38
|
+
waitUntil(promise: Promise<unknown>): void
|
|
39
|
+
}
|
|
40
|
+
export interface DurableObjectStub {
|
|
41
|
+
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>
|
|
42
|
+
}
|
|
43
|
+
export interface DurableObjectNamespace {
|
|
44
|
+
idFromName(name: string): unknown
|
|
45
|
+
get(id: unknown): DurableObjectStub
|
|
46
|
+
}
|
|
47
|
+
export interface FetchBinding {
|
|
48
|
+
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>
|
|
49
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { badRequest } from '@ouraihub/aether-core'
|
|
2
|
+
|
|
3
|
+
export interface Actor {
|
|
4
|
+
readonly subjectId: string
|
|
5
|
+
readonly kind: 'user' | 'service' | 'mcp_client'
|
|
6
|
+
readonly applicationId: string
|
|
7
|
+
readonly tenantId: string | null
|
|
8
|
+
readonly sessionId: string | null
|
|
9
|
+
readonly scopes: readonly string[]
|
|
10
|
+
}
|
|
11
|
+
export interface WorkerContext {
|
|
12
|
+
readonly actor: Actor
|
|
13
|
+
readonly signal: AbortSignal
|
|
14
|
+
readonly requestId: string
|
|
15
|
+
readonly now: number
|
|
16
|
+
}
|
|
17
|
+
export function isActor(value: unknown): value is Actor {
|
|
18
|
+
if (typeof value !== 'object' || value === null) return false
|
|
19
|
+
return (
|
|
20
|
+
'subjectId' in value &&
|
|
21
|
+
typeof value.subjectId === 'string' &&
|
|
22
|
+
value.subjectId.length > 0 &&
|
|
23
|
+
value.subjectId.length <= 256 &&
|
|
24
|
+
'applicationId' in value &&
|
|
25
|
+
typeof value.applicationId === 'string' &&
|
|
26
|
+
value.applicationId.length > 0 &&
|
|
27
|
+
value.applicationId.length <= 256 &&
|
|
28
|
+
'kind' in value &&
|
|
29
|
+
(value.kind === 'user' || value.kind === 'service' || value.kind === 'mcp_client') &&
|
|
30
|
+
'tenantId' in value &&
|
|
31
|
+
(value.tenantId === null ||
|
|
32
|
+
(typeof value.tenantId === 'string' &&
|
|
33
|
+
value.tenantId.length > 0 &&
|
|
34
|
+
value.tenantId.length <= 256)) &&
|
|
35
|
+
'sessionId' in value &&
|
|
36
|
+
(value.sessionId === null ||
|
|
37
|
+
(typeof value.sessionId === 'string' &&
|
|
38
|
+
value.sessionId.length > 0 &&
|
|
39
|
+
value.sessionId.length <= 256)) &&
|
|
40
|
+
'scopes' in value &&
|
|
41
|
+
Array.isArray(value.scopes) &&
|
|
42
|
+
value.scopes.length <= 256 &&
|
|
43
|
+
value.scopes.every(
|
|
44
|
+
(scope: unknown) => typeof scope === 'string' && scope.length > 0 && scope.length <= 256,
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
export function immutableActor(actor: Actor): Actor {
|
|
49
|
+
if (!isActor(actor)) throw badRequest('actor', 'invalid')
|
|
50
|
+
return Object.freeze({ ...actor, scopes: Object.freeze([...actor.scopes]) })
|
|
51
|
+
}
|
|
52
|
+
export function ownerKey(actor: Actor): string {
|
|
53
|
+
if (!isActor(actor)) throw badRequest('actor', 'invalid')
|
|
54
|
+
return JSON.stringify([actor.applicationId, actor.tenantId, actor.subjectId])
|
|
55
|
+
}
|