@ouraihub/aether-worker 0.0.0-stage → 0.1.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.
@@ -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,40 @@
1
1
  {
2
2
  "name": "@ouraihub/aether-worker",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
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.0",
23
+ "@ouraihub/aether-core": "0.1.0"
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
+ }
6
40
  }
package/src/app.ts ADDED
@@ -0,0 +1,327 @@
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 = { code: 'internal', message: 'Internal request failure', details: { errorId: crypto.randomUUID() } }
51
+ return Response.json(safe, { status: statuses[safe.code] })
52
+ }
53
+ function loopback(url: URL): boolean {
54
+ return url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]'
55
+ }
56
+ async function boundedRequest(request: Request, maximum: number): Promise<Request> {
57
+ if (request.method === 'GET' || request.method === 'HEAD' || request.body === null) return request
58
+ const declared = request.headers.get('content-length')
59
+ if (declared !== null && (!/^\d+$/u.test(declared) || Number(declared) > maximum))
60
+ throw badRequest('body', 'too_large', maximum)
61
+ const reader = request.body.getReader(),
62
+ chunks: Uint8Array[] = []
63
+ let size = 0,
64
+ completed = false
65
+ try {
66
+ while (true) {
67
+ const next = await reader.read()
68
+ if (next.done) {
69
+ completed = true
70
+ break
71
+ }
72
+ size += next.value.byteLength
73
+ if (size > maximum) throw badRequest('body', 'too_large', maximum)
74
+ chunks.push(next.value)
75
+ }
76
+ } finally {
77
+ if (!completed) await reader.cancel().catch(() => undefined)
78
+ reader.releaseLock()
79
+ }
80
+ const bytes = new Uint8Array(size)
81
+ let offset = 0
82
+ for (const chunk of chunks) {
83
+ bytes.set(chunk, offset)
84
+ offset += chunk.byteLength
85
+ }
86
+ return new Request(request, { body: bytes })
87
+ }
88
+ function actorHeader(actor: Actor): string {
89
+ const bytes = new TextEncoder().encode(JSON.stringify(actor))
90
+ let binary = ''
91
+ for (const byte of bytes) binary += String.fromCharCode(byte)
92
+ return btoa(binary)
93
+ }
94
+ function secured(response: Response, origin: string | null, allowed: string): Response {
95
+ const headers = new Headers(response.headers)
96
+ headers.set('X-Content-Type-Options', 'nosniff')
97
+ headers.set('Referrer-Policy', 'no-referrer')
98
+ headers.set(
99
+ 'Cache-Control',
100
+ headers.get('content-type')?.startsWith('text/event-stream') ? 'no-cache, no-transform' : 'no-store',
101
+ )
102
+ if (origin === allowed) {
103
+ headers.set('Access-Control-Allow-Origin', allowed)
104
+ headers.set('Access-Control-Allow-Credentials', 'true')
105
+ headers.append('Vary', 'Origin')
106
+ }
107
+ return new Response(response.body, { status: response.status, statusText: response.statusText, headers })
108
+ }
109
+ /** Product entrypoints compose this handler; this package never supplies product login or business tools. */
110
+ export function createAetherWorkerApp(options: WorkerAppOptions): { fetch(request: Request): Promise<Response> } {
111
+ const publicOrigin = new URL(options.origin)
112
+ if (
113
+ !options.applicationId ||
114
+ publicOrigin.origin !== options.origin ||
115
+ (publicOrigin.protocol !== 'https:' &&
116
+ !(options.allowLoopbackHttp && publicOrigin.protocol === 'http:' && loopback(publicOrigin)))
117
+ )
118
+ throw new KitFailure('needs_config', 'Invalid Worker origin or application')
119
+ const maximum = options.maxBodyBytes ?? 24 * 1024 * 1024
120
+ if (!Number.isSafeInteger(maximum) || maximum <= 0 || maximum > 32 * 1024 * 1024)
121
+ throw badRequest('maxBodyBytes', 'invalid')
122
+ return {
123
+ async fetch(raw: Request): Promise<Response> {
124
+ const now = Date.now(),
125
+ requestId = crypto.randomUUID(),
126
+ url = new URL(raw.url),
127
+ origin = raw.headers.get('origin')
128
+ let response: Response
129
+ try {
130
+ if (url.protocol !== 'https:' && !(options.allowLoopbackHttp && url.protocol === 'http:' && loopback(url)))
131
+ throw new KitFailure('forbidden', 'Secure transport required')
132
+ if (origin !== null && origin !== options.origin) throw new KitFailure('forbidden', 'Origin rejected')
133
+ if (raw.method === 'OPTIONS') {
134
+ if (origin !== options.origin) throw new KitFailure('forbidden', 'Origin rejected')
135
+ return secured(
136
+ new Response(null, {
137
+ status: 204,
138
+ headers: {
139
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
140
+ 'Access-Control-Allow-Headers': 'Content-Type, X-CSRF-Token',
141
+ 'Access-Control-Max-Age': '600',
142
+ },
143
+ }),
144
+ origin,
145
+ options.origin,
146
+ )
147
+ }
148
+ const mutating = !['GET', 'HEAD'].includes(raw.method)
149
+ const publicRoute =
150
+ url.pathname === '/mcp' || url.pathname.startsWith('/oauth/') || url.pathname.startsWith('/.well-known/')
151
+ if (
152
+ mutating &&
153
+ raw.body !== null &&
154
+ !publicRoute &&
155
+ raw.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase() !== 'application/json'
156
+ )
157
+ throw badRequest('contentType', 'json_required')
158
+ const request = await boundedRequest(raw, maximum)
159
+ if (publicRoute) {
160
+ let publicResponse: Response | null = null
161
+ for (const handler of options.publicHandlers ?? []) {
162
+ publicResponse = await handler.handlePublic(request)
163
+ if (publicResponse) break
164
+ }
165
+ if (!publicResponse) throw new KitFailure('not_found', 'Unknown route', { entity: 'route' })
166
+ response = publicResponse
167
+ } else {
168
+ const actor = await options.identity.resolve(request)
169
+ if (!isActor(actor) || actor.applicationId !== options.applicationId)
170
+ throw new KitFailure('unauthorized', 'Authentication required')
171
+ if (
172
+ mutating &&
173
+ actor.kind === 'user' &&
174
+ actor.sessionId !== null &&
175
+ (origin !== options.origin || !(await options.verifyCsrf(request, actor)))
176
+ )
177
+ throw new KitFailure('forbidden', 'Request authorization rejected')
178
+ await options.authorizeRoute(actor, request)
179
+ const context: WorkerContext = { actor: immutableActor(actor), requestId, now, signal: request.signal }
180
+ if (url.pathname === '/api/v1/runs' && request.method === 'POST') {
181
+ const input: unknown = await request.json()
182
+ if (!isContract('RunRequest', input)) throw badRequest('run', 'invalid')
183
+ const runId = crypto.randomUUID(),
184
+ stub = options.runs.get(options.runs.idFromName(`${options.applicationId}:${runId}`))
185
+ const accepted = await stub.fetch('https://aether.internal/accept', {
186
+ method: 'POST',
187
+ headers: { 'Content-Type': 'application/json' },
188
+ body: JSON.stringify({ actor: context.actor, request: input, runId }),
189
+ signal: request.signal,
190
+ })
191
+ const value: unknown = await accepted.json()
192
+ if (accepted.status !== 202) {
193
+ if (isContract('KitError', value)) throw new KitFailure(value.code, value.message, value.details)
194
+ throw new KitFailure('internal', 'Invalid coordinator reply')
195
+ }
196
+ if (!isContract('RunAccepted', value)) throw new KitFailure('internal', 'Invalid coordinator acceptance')
197
+ response = Response.json(
198
+ { accepted: value, events: `/api/v1/runs/${encodeURIComponent(runId)}/events` },
199
+ { status: 202 },
200
+ )
201
+ } else {
202
+ const run = /^\/api\/v1\/runs\/([^/]+)(?:\/(events|cancel|approval))?$/u.exec(url.pathname)
203
+ if (run) {
204
+ const runId = decodeURIComponent(run[1] ?? ''),
205
+ action = run[2] ?? 'snapshot'
206
+ if (
207
+ !/^[A-Za-z0-9_-]{1,128}$/u.test(runId) ||
208
+ (['events', 'snapshot'].includes(action) ? request.method !== 'GET' : request.method !== 'POST')
209
+ )
210
+ throw badRequest('run', 'invalid')
211
+ const stub = options.runs.get(options.runs.idFromName(`${options.applicationId}:${runId}`)),
212
+ headers = new Headers({ 'x-aether-actor': actorHeader(context.actor) })
213
+ if (request.headers.has('Last-Event-ID'))
214
+ headers.set('Last-Event-ID', request.headers.get('Last-Event-ID') ?? '')
215
+ const body = request.method === 'POST' ? await request.text() : undefined
216
+ if (body !== undefined) headers.set('Content-Type', 'application/json')
217
+ response = await stub.fetch(`https://aether.internal/${action}`, {
218
+ method: request.method,
219
+ headers,
220
+ ...(body === undefined ? {} : { body }),
221
+ signal: request.signal,
222
+ })
223
+ if (
224
+ action === 'events' &&
225
+ response.status === 200 &&
226
+ response.body &&
227
+ response.headers.get('content-type')?.startsWith('text/event-stream')
228
+ ) {
229
+ const upstream = response.body.getReader()
230
+ const transport = new TransformStream<Uint8Array, Uint8Array>()
231
+ const writer = transport.writable.getWriter()
232
+ const heartbeat = new TextEncoder().encode(': keep-alive\n\n')
233
+ let finished = false,
234
+ heartbeatPending = false
235
+ let cancellation: Promise<void> | null = null
236
+ const disconnected = (): Promise<void> => {
237
+ if (finished) return Promise.resolve()
238
+ if (cancellation) return cancellation
239
+ clearInterval(timer)
240
+ request.signal.removeEventListener('abort', aborted)
241
+ // RPC response-body cancellation is not a reliable cross-isolate signal.
242
+ // Explicitly notify the same authenticated run without the aborted client signal.
243
+ cancellation = Promise.all([
244
+ stub
245
+ .fetch('https://aether.internal/cancel', {
246
+ method: 'POST',
247
+ headers: { 'x-aether-actor': actorHeader(context.actor), 'Content-Type': 'application/json' },
248
+ body: '{}',
249
+ })
250
+ .then(async (reply) => {
251
+ await reply.body?.cancel()
252
+ }),
253
+ upstream.cancel(),
254
+ writer.abort(),
255
+ ]).then(() => undefined)
256
+ return cancellation
257
+ }
258
+ const aborted = (): void => {
259
+ void disconnected().catch(() => undefined)
260
+ }
261
+ // A comment forces a real transport write even while the engine awaits approval.
262
+ const timer = setInterval(() => {
263
+ if (finished || cancellation || heartbeatPending) return
264
+ heartbeatPending = true
265
+ void writer
266
+ .write(heartbeat)
267
+ .catch(disconnected)
268
+ .finally(() => {
269
+ heartbeatPending = false
270
+ })
271
+ .catch(() => undefined)
272
+ }, 1000)
273
+ request.signal.addEventListener('abort', aborted, { once: true })
274
+ void writer.closed.catch(disconnected).catch(() => undefined)
275
+ const pump = async (): Promise<void> => {
276
+ try {
277
+ while (!cancellation) {
278
+ const next = await upstream.read()
279
+ if (next.done) {
280
+ clearInterval(timer)
281
+ await writer.close()
282
+ finished = true
283
+ break
284
+ }
285
+ await writer.write(next.value)
286
+ }
287
+ } catch {
288
+ await disconnected()
289
+ } finally {
290
+ clearInterval(timer)
291
+ request.signal.removeEventListener('abort', aborted)
292
+ upstream.releaseLock()
293
+ writer.releaseLock()
294
+ }
295
+ }
296
+ void pump().catch(() => undefined)
297
+ if (request.signal.aborted) aborted()
298
+ response = new Response(transport.readable, {
299
+ status: response.status,
300
+ statusText: response.statusText,
301
+ headers: response.headers,
302
+ })
303
+ }
304
+ } else {
305
+ let handled: Response | null = null
306
+ for (const handler of options.handlers ?? []) {
307
+ handled = await handler.handle(request, context)
308
+ if (handled) break
309
+ }
310
+ if (!handled) throw new KitFailure('not_found', 'Unknown route', { entity: 'route' })
311
+ response = handled
312
+ }
313
+ }
314
+ }
315
+ } catch (error) {
316
+ response = failureResponse(error)
317
+ }
318
+ options.logger?.log('request.finished', {
319
+ requestId,
320
+ applicationId: options.applicationId,
321
+ status: response.status,
322
+ durationMs: Date.now() - now,
323
+ })
324
+ return secured(response, origin, options.origin)
325
+ },
326
+ }
327
+ }
@@ -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,49 @@
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' && value.tenantId.length > 0 && value.tenantId.length <= 256)) &&
33
+ 'sessionId' in value &&
34
+ (value.sessionId === null ||
35
+ (typeof value.sessionId === 'string' && value.sessionId.length > 0 && value.sessionId.length <= 256)) &&
36
+ 'scopes' in value &&
37
+ Array.isArray(value.scopes) &&
38
+ value.scopes.length <= 256 &&
39
+ value.scopes.every((scope: unknown) => typeof scope === 'string' && scope.length > 0 && scope.length <= 256)
40
+ )
41
+ }
42
+ export function immutableActor(actor: Actor): Actor {
43
+ if (!isActor(actor)) throw badRequest('actor', 'invalid')
44
+ return Object.freeze({ ...actor, scopes: Object.freeze([...actor.scopes]) })
45
+ }
46
+ export function ownerKey(actor: Actor): string {
47
+ if (!isActor(actor)) throw badRequest('actor', 'invalid')
48
+ return JSON.stringify([actor.applicationId, actor.tenantId, actor.subjectId])
49
+ }