@ixo/oracle-runtime-workers 0.2.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.
- package/README.md +110 -0
- package/package.json +77 -0
- package/src/attachments/classify.ts +114 -0
- package/src/attachments/content-blocks.ts +51 -0
- package/src/attachments/download.ts +235 -0
- package/src/attachments/extract.ts +241 -0
- package/src/attachments/index.ts +84 -0
- package/src/attachments/magic.ts +133 -0
- package/src/attachments/pipeline.ts +409 -0
- package/src/attachments/retention.ts +205 -0
- package/src/attachments/route.ts +41 -0
- package/src/attachments/sandbox-archive.ts +90 -0
- package/src/attachments/sanitize.ts +35 -0
- package/src/attachments/types.ts +86 -0
- package/src/attachments/view.ts +107 -0
- package/src/core/env.ts +200 -0
- package/src/core/index.ts +513 -0
- package/src/core/llm.ts +842 -0
- package/src/core/main-agent-types.ts +123 -0
- package/src/core/main-agent.ts +399 -0
- package/src/core/manifest.ts +365 -0
- package/src/core/meta-tools.ts +232 -0
- package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
- package/src/core/middlewares/capability-gate.ts +76 -0
- package/src/core/middlewares/index.ts +18 -0
- package/src/core/middlewares/page-context.ts +98 -0
- package/src/core/middlewares/safety-guardrail.ts +117 -0
- package/src/core/middlewares/summarization.ts +99 -0
- package/src/core/middlewares/tool-repetition-guard.ts +125 -0
- package/src/core/middlewares/tool-validation.ts +83 -0
- package/src/core/middlewares/work-status.ts +57 -0
- package/src/core/openrouter-pricing.ts +124 -0
- package/src/core/plugin-loader.ts +192 -0
- package/src/core/plugins/index.ts +6 -0
- package/src/core/plugins/skills/index.ts +8 -0
- package/src/core/plugins/skills/skills-tools.ts +356 -0
- package/src/core/plugins/skills/skills.plugin.ts +113 -0
- package/src/core/plugins/weather/index.ts +8 -0
- package/src/core/plugins/weather/weather-client.ts +153 -0
- package/src/core/plugins/weather/weather-middleware.ts +27 -0
- package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
- package/src/core/plugins/weather/weather-tools.ts +103 -0
- package/src/core/plugins/weather/weather.plugin.ts +227 -0
- package/src/core/prompt-composer.ts +469 -0
- package/src/core/registries.ts +642 -0
- package/src/core/runtime-context.ts +619 -0
- package/src/core/state.ts +108 -0
- package/src/core/sub-agent-fallback.ts +187 -0
- package/src/core/subagent-as-tool.ts +322 -0
- package/src/core/template.ts +245 -0
- package/src/core/utils.ts +246 -0
- package/src/core/wrap-plugin-tool.ts +100 -0
- package/src/do/ambient.ts +266 -0
- package/src/do/contracts.ts +562 -0
- package/src/do/debug-timers.ts +120 -0
- package/src/do/fresh-stub.ts +32 -0
- package/src/do/gateway-retry.ts +73 -0
- package/src/do/secrets-adapter.ts +32 -0
- package/src/do/size-alerts.ts +76 -0
- package/src/do/sse-stream.ts +472 -0
- package/src/do/transcript.ts +192 -0
- package/src/do/turn-body.ts +84 -0
- package/src/do/turn-metadata.ts +146 -0
- package/src/do/ucan-service.ts +339 -0
- package/src/do/user-oracle-do.ts +3035 -0
- package/src/gateway-worker.ts +82 -0
- package/src/index.ts +237 -0
- package/src/llm/byo-adapter.ts +59 -0
- package/src/llm/byo-catalog.ts +520 -0
- package/src/llm/byo-client.ts +280 -0
- package/src/llm/byo-routes.ts +255 -0
- package/src/llm/byo-service.ts +836 -0
- package/src/llm/chatgpt-oauth.ts +430 -0
- package/src/llm/provider-error.ts +283 -0
- package/src/matrix/gateway-do.ts +918 -0
- package/src/matrix/ingest.ts +209 -0
- package/src/matrix/replay-format.ts +34 -0
- package/src/matrix/reply-chain.ts +109 -0
- package/src/matrix/room-state-codec.ts +115 -0
- package/src/matrix/user-homeserver.ts +106 -0
- package/src/matrix/work-status.ts +294 -0
- package/src/memory/session-history-indexer.ts +328 -0
- package/src/owner-store/ixo-vfs-store.ts +570 -0
- package/src/owner-store/matrix-media-store.ts +136 -0
- package/src/owner-store/migrating-store.ts +185 -0
- package/src/owner-store/owner-copy-errors.ts +175 -0
- package/src/owner-store/retry.ts +45 -0
- package/src/owner-store/tus-upload.ts +333 -0
- package/src/owner-store/types.ts +0 -0
- package/src/plugin-api/define-plugin.ts +45 -0
- package/src/plugin-api/index.ts +56 -0
- package/src/plugin-api/oracle-plugin.ts +145 -0
- package/src/plugin-api/tool-helper.ts +82 -0
- package/src/plugin-api/types.ts +816 -0
- package/src/plugin-api/ucan-errors.ts +16 -0
- package/src/plugins/agui/agui-agent.ts +115 -0
- package/src/plugins/agui/agui.plugin.ts +148 -0
- package/src/plugins/agui/index.ts +2 -0
- package/src/plugins/attachments/attachments.plugin.ts +54 -0
- package/src/plugins/attachments/index.ts +6 -0
- package/src/plugins/attachments/view-attachment-tool.ts +91 -0
- package/src/plugins/base64.ts +26 -0
- package/src/plugins/composio/composio-tools.ts +365 -0
- package/src/plugins/composio/composio-ucan.ts +43 -0
- package/src/plugins/composio/composio.plugin.ts +214 -0
- package/src/plugins/composio/index.ts +15 -0
- package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
- package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
- package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
- package/src/plugins/domain-indexer/index.ts +3 -0
- package/src/plugins/editor/blocknote-bridge.ts +120 -0
- package/src/plugins/editor/content-session.ts +331 -0
- package/src/plugins/editor/content-tools.ts +686 -0
- package/src/plugins/editor/document-model.ts +0 -0
- package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
- package/src/plugins/editor/editor-agent.ts +34 -0
- package/src/plugins/editor/editor-config.ts +64 -0
- package/src/plugins/editor/editor-mx.ts +128 -0
- package/src/plugins/editor/editor.plugin.ts +143 -0
- package/src/plugins/editor/failures.ts +154 -0
- package/src/plugins/editor/index.ts +31 -0
- package/src/plugins/editor/jsdom-shim.ts +55 -0
- package/src/plugins/editor/prompts.ts +59 -0
- package/src/plugins/editor/prop-policy.ts +147 -0
- package/src/plugins/editor/provider.ts +386 -0
- package/src/plugins/editor/room-membership.ts +95 -0
- package/src/plugins/editor/standalone-editor-tool.ts +200 -0
- package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
- package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
- package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
- package/src/plugins/firecrawl/index.ts +15 -0
- package/src/plugins/flows/action-metadata.ts +3505 -0
- package/src/plugins/flows/actions.ts +190 -0
- package/src/plugins/flows/edit.ts +314 -0
- package/src/plugins/flows/errors.ts +38 -0
- package/src/plugins/flows/explain.ts +43 -0
- package/src/plugins/flows/flow-doc.ts +111 -0
- package/src/plugins/flows/flows.plugin.ts +129 -0
- package/src/plugins/flows/forms.ts +330 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/linkage.ts +147 -0
- package/src/plugins/flows/port-types.ts +53 -0
- package/src/plugins/flows/prompts.ts +65 -0
- package/src/plugins/flows/read.ts +392 -0
- package/src/plugins/flows/references.ts +66 -0
- package/src/plugins/flows/test-support.ts +165 -0
- package/src/plugins/flows/tools/authoring.ts +516 -0
- package/src/plugins/flows/tools/discovery.ts +119 -0
- package/src/plugins/flows/tools/forms.ts +142 -0
- package/src/plugins/flows/tools/inspect.ts +165 -0
- package/src/plugins/flows/tools/linkage.ts +117 -0
- package/src/plugins/flows/tools/settings.ts +184 -0
- package/src/plugins/flows/translator.ts +328 -0
- package/src/plugins/flows/types.ts +210 -0
- package/src/plugins/flows/ydoc-helpers.ts +543 -0
- package/src/plugins/index.ts +70 -0
- package/src/plugins/mcp-call-timeout.ts +38 -0
- package/src/plugins/mcp-tool-adapter.ts +87 -0
- package/src/plugins/memory/index.ts +17 -0
- package/src/plugins/memory/memory-tools.ts +463 -0
- package/src/plugins/memory/memory-ucan.ts +51 -0
- package/src/plugins/memory/memory.plugin.ts +122 -0
- package/src/plugins/memory/types.ts +147 -0
- package/src/plugins/portal/action-log.ts +34 -0
- package/src/plugins/portal/index.ts +2 -0
- package/src/plugins/portal/portal.plugin.ts +203 -0
- package/src/plugins/sandbox/index.ts +34 -0
- package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
- package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
- package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
- package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
- package/src/plugins/tasks/index.ts +17 -0
- package/src/plugins/tasks/manifest.ts +81 -0
- package/src/plugins/tasks/middleware.ts +219 -0
- package/src/plugins/tasks/tasks-tools.ts +414 -0
- package/src/plugins/tasks/tasks.plugin.ts +74 -0
- package/src/plugins/ucan-failure.ts +133 -0
- package/src/plugins/user-preferences/index.ts +22 -0
- package/src/plugins/user-preferences/schema.ts +31 -0
- package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
- package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
- package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
- package/src/plugins/vfs/index.ts +40 -0
- package/src/plugins/vfs/vfs-auth.ts +56 -0
- package/src/plugins/vfs/vfs-client.ts +655 -0
- package/src/plugins/vfs/vfs-content.ts +181 -0
- package/src/plugins/vfs/vfs-errors.ts +163 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
- package/src/plugins/vfs/vfs-tools.ts +635 -0
- package/src/plugins/vfs/vfs.plugin.ts +201 -0
- package/src/realtime/frontend-call-registry.ts +163 -0
- package/src/realtime/realtime-endpoint.ts +619 -0
- package/src/realtime/session-socket-hub.ts +249 -0
- package/src/realtime/socket-io-codec.ts +187 -0
- package/src/secrets/jwe.ts +74 -0
- package/src/secrets/pin-cipher.ts +43 -0
- package/src/secrets/secrets-service.ts +319 -0
- package/src/shell/app.ts +564 -0
- package/src/shell/auth.ts +290 -0
- package/src/sqlite/blob-codec.ts +100 -0
- package/src/sqlite/blob-compactor.ts +146 -0
- package/src/sqlite/cache-config.ts +45 -0
- package/src/sqlite/database.ts +643 -0
- package/src/sqlite/do-vfs.ts +1485 -0
- package/src/sqlite/index.ts +61 -0
- package/src/sqlite/serialization.ts +370 -0
- package/src/sqlite/sessions-store.ts +311 -0
- package/src/sqlite/sqlite-saver.ts +919 -0
- package/src/sqlite/vacuum-policy.ts +77 -0
- package/src/sqlite/wa-sqlite-loader.ts +123 -0
- package/src/sqlite/wasm.d.ts +10 -0
- package/src/tasks/schedule.ts +174 -0
- package/src/tasks/scheduler.ts +753 -0
- package/src/tasks/spec.ts +125 -0
- package/src/tasks/store.ts +351 -0
package/src/shell/app.ts
ADDED
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
/* eslint-disable no-console -- console IS the logger on Workers (Logs/observability). */
|
|
2
|
+
/**
|
|
3
|
+
* The HTTP shell — a Hono app that speaks the same wire protocol as the Node
|
|
4
|
+
* runtime's NestJS modules (`/sessions`, `/messages`, `/delegation`,
|
|
5
|
+
* `/health`, `/models`) so the Portal, the CLI and `@ixo/oracles-client-sdk`
|
|
6
|
+
* work unchanged. Every authenticated route resolves the caller's
|
|
7
|
+
* `UserOracleDO` and forwards to it; streaming turns are proxied byte-for-byte.
|
|
8
|
+
*/
|
|
9
|
+
import { Hono } from 'hono';
|
|
10
|
+
import { cors } from 'hono/cors';
|
|
11
|
+
import type { OracleWorkerEnv, TurnIdentity } from '../do/contracts';
|
|
12
|
+
import { parseOwnerCopyFailure } from '../owner-store/owner-copy-errors';
|
|
13
|
+
import { userObjectName } from '../do/contracts';
|
|
14
|
+
import { ROUTED_USER_HEADER } from '../realtime/realtime-endpoint';
|
|
15
|
+
import {
|
|
16
|
+
decodeRoomStateContent,
|
|
17
|
+
encodeRoomStateContent,
|
|
18
|
+
} from '../matrix/room-state-codec';
|
|
19
|
+
import {
|
|
20
|
+
authenticate,
|
|
21
|
+
isExcluded,
|
|
22
|
+
type AuthResult,
|
|
23
|
+
type RouteExclusion,
|
|
24
|
+
} from './auth';
|
|
25
|
+
|
|
26
|
+
export interface PluginRoute {
|
|
27
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'ALL';
|
|
28
|
+
path: string;
|
|
29
|
+
/**
|
|
30
|
+
* `ctx.auth` carries the validated caller identity for routes that are NOT
|
|
31
|
+
* auth-excluded; it is null on excluded routes. Handlers may ignore it.
|
|
32
|
+
*/
|
|
33
|
+
handler: (
|
|
34
|
+
request: Request,
|
|
35
|
+
env: OracleWorkerEnv,
|
|
36
|
+
ctx: { auth: AuthResult | null },
|
|
37
|
+
) => Response | Promise<Response>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ShellOptions {
|
|
41
|
+
/** Routes contributed by plugins (`getRoutes`) and the host. */
|
|
42
|
+
routes?: PluginRoute[];
|
|
43
|
+
/** Auth exclusions contributed by plugins and the host. */
|
|
44
|
+
authExcludedRoutes?: RouteExclusion[];
|
|
45
|
+
/** `GET /models` payload provider (may fetch live prices). */
|
|
46
|
+
listModels?: (env: OracleWorkerEnv) => unknown | Promise<unknown>;
|
|
47
|
+
/** Version banner for `GET /`. */
|
|
48
|
+
banner?: { name: string; description?: string };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type Variables = { auth: AuthResult };
|
|
52
|
+
|
|
53
|
+
const BUILTIN_EXCLUSIONS: RouteExclusion[] = [
|
|
54
|
+
{ path: '/', method: 'GET' },
|
|
55
|
+
{ path: '/health', method: 'ALL' },
|
|
56
|
+
{ path: '/health/*', method: 'ALL' },
|
|
57
|
+
{ path: '/models', method: 'GET' },
|
|
58
|
+
{ path: '/matrix/status', method: 'GET' },
|
|
59
|
+
{ path: '/matrix/start', method: 'POST' },
|
|
60
|
+
// Operator-only; the /debug/* middleware 404s unless ORACLE_DEBUG_ROUTES=true.
|
|
61
|
+
{ path: '/debug/matrix/restart', method: 'POST' },
|
|
62
|
+
{ path: '/debug/matrix/stop', method: 'POST' },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
/** Express body-parser default in the Node runtime: JSON bodies over 100 KiB are a 413. */
|
|
66
|
+
const MAX_TURN_BODY_BYTES = 100 * 1024;
|
|
67
|
+
|
|
68
|
+
export function createShell(
|
|
69
|
+
opts: ShellOptions = {},
|
|
70
|
+
): Hono<{ Bindings: OracleWorkerEnv; Variables: Variables }> {
|
|
71
|
+
const app = new Hono<{ Bindings: OracleWorkerEnv; Variables: Variables }>();
|
|
72
|
+
const exclusions = [
|
|
73
|
+
...BUILTIN_EXCLUSIONS,
|
|
74
|
+
...(opts.authExcludedRoutes ?? []),
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// --- realtime (socket.io) --------------------------------------------------
|
|
78
|
+
// Registered before every middleware on purpose: a WebSocket upgrade
|
|
79
|
+
// carries no auth headers (browsers cannot set them), so the UCAN material
|
|
80
|
+
// travels in the socket.io CONNECT packet and the user object validates it
|
|
81
|
+
// — the shell only routes on `?userDid`, and the object refuses a token
|
|
82
|
+
// that does not belong to that DID. The 101 response must also reach the
|
|
83
|
+
// client untouched (no CORS header rewriting on an upgraded response).
|
|
84
|
+
app.all('/socket.io/*', async (c) => {
|
|
85
|
+
const userDid = c.req.query('userDid');
|
|
86
|
+
if (!userDid) {
|
|
87
|
+
return c.json(
|
|
88
|
+
{ code: 3, message: 'userDid query parameter is required.' },
|
|
89
|
+
400,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (c.req.header('upgrade')?.toLowerCase() !== 'websocket') {
|
|
93
|
+
return c.json(
|
|
94
|
+
{
|
|
95
|
+
code: 3,
|
|
96
|
+
message:
|
|
97
|
+
'Expected a WebSocket upgrade: this runtime serves socket.io over the websocket transport only (connect with transports: ["websocket"]).',
|
|
98
|
+
},
|
|
99
|
+
426,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const target = new URL(c.req.url);
|
|
103
|
+
target.protocol = 'https:';
|
|
104
|
+
target.host = 'user-oracle';
|
|
105
|
+
const forwarded = new Request(target, c.req.raw);
|
|
106
|
+
forwarded.headers.set(ROUTED_USER_HEADER, userDid);
|
|
107
|
+
return userStub(c.env, userDid).fetch(forwarded);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
app.use('*', async (c, next) => {
|
|
111
|
+
const origin = c.env.CORS_ORIGIN ?? '*';
|
|
112
|
+
return cors({
|
|
113
|
+
origin,
|
|
114
|
+
credentials: origin !== '*',
|
|
115
|
+
allowHeaders: [
|
|
116
|
+
'content-type',
|
|
117
|
+
'authorization',
|
|
118
|
+
'x-auth-type',
|
|
119
|
+
'x-ucan-delegation',
|
|
120
|
+
'x-did',
|
|
121
|
+
'x-timezone',
|
|
122
|
+
'x-request-id',
|
|
123
|
+
'x-matrix-access-token',
|
|
124
|
+
'accept',
|
|
125
|
+
],
|
|
126
|
+
exposeHeaders: ['x-request-id'],
|
|
127
|
+
})(c, next);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// --- public ----------------------------------------------------------------
|
|
131
|
+
app.get('/', (c) =>
|
|
132
|
+
c.json({
|
|
133
|
+
status: 'ok',
|
|
134
|
+
message: `${opts.banner?.name ?? c.env.ORACLE_NAME ?? 'QiForge oracle'} is running on Cloudflare Workers`,
|
|
135
|
+
...(opts.banner?.description && { description: opts.banner.description }),
|
|
136
|
+
timestamp: new Date().toISOString(),
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
app.get('/health', (c) =>
|
|
140
|
+
c.json({ status: 'ok', timestamp: new Date().toISOString() }),
|
|
141
|
+
);
|
|
142
|
+
app.get('/health/matrix', async (c) => {
|
|
143
|
+
const status = await gateway(c.env).status();
|
|
144
|
+
return c.json(status, status.running ? 200 : 503);
|
|
145
|
+
});
|
|
146
|
+
app.get('/matrix/status', async (c) => c.json(await gateway(c.env).status()));
|
|
147
|
+
app.post('/matrix/start', async (c) =>
|
|
148
|
+
c.json(await gateway(c.env).ensureStarted()),
|
|
149
|
+
);
|
|
150
|
+
app.get('/models', async (c) =>
|
|
151
|
+
c.json(
|
|
152
|
+
(await opts.listModels?.(c.env)) ?? {
|
|
153
|
+
models: [],
|
|
154
|
+
// Node's `ModelListing` shape (`{ models, default }`); the client SDK
|
|
155
|
+
// reads `default`.
|
|
156
|
+
default: c.env.DEFAULT_MODEL ?? null,
|
|
157
|
+
},
|
|
158
|
+
),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// --- auth ------------------------------------------------------------------
|
|
162
|
+
app.use('*', async (c, next) => {
|
|
163
|
+
if (isExcluded(c.req.method, c.req.path, exclusions)) return next();
|
|
164
|
+
const outcome = await authenticate(c.req.raw.headers, {
|
|
165
|
+
oracleDid: c.env.ORACLE_DID,
|
|
166
|
+
blocksyncUri: c.env.BLOCKSYNC_GRAPHQL_URL,
|
|
167
|
+
maxTtlSeconds: c.env.UCAN_AUTH_MAX_TTL_SECONDS
|
|
168
|
+
? Number(c.env.UCAN_AUTH_MAX_TTL_SECONDS)
|
|
169
|
+
: undefined,
|
|
170
|
+
});
|
|
171
|
+
if (!outcome.ok)
|
|
172
|
+
return c.json(
|
|
173
|
+
{ statusCode: outcome.status, message: outcome.error },
|
|
174
|
+
outcome.status as 401,
|
|
175
|
+
);
|
|
176
|
+
c.set('auth', outcome.auth);
|
|
177
|
+
return next();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// --- rate limit (per authenticated user DID) -------------------------------
|
|
181
|
+
// Cloudflare's native limiter; the limit/period live on the binding in
|
|
182
|
+
// wrangler.jsonc. Keyed by DID rather than IP so co-located users (one
|
|
183
|
+
// NAT/proxy) never share a bucket and a single leaked token can't burn
|
|
184
|
+
// unbounded LLM spend. Auth-excluded routes (health/models/matrix status)
|
|
185
|
+
// ran `next()` above and never reach here. Absent binding = no limiting
|
|
186
|
+
// (local harness / tests).
|
|
187
|
+
app.use('*', async (c, next) => {
|
|
188
|
+
const limiter = c.env.RATE_LIMIT;
|
|
189
|
+
if (!limiter) return next();
|
|
190
|
+
const auth = c.get('auth') as AuthResult | undefined;
|
|
191
|
+
if (!auth) return next();
|
|
192
|
+
const { success } = await limiter.limit({ key: auth.userDid });
|
|
193
|
+
if (!success)
|
|
194
|
+
return c.json(
|
|
195
|
+
{
|
|
196
|
+
statusCode: 429,
|
|
197
|
+
message:
|
|
198
|
+
'Rate limit exceeded — too many requests. Please slow down and try again shortly.',
|
|
199
|
+
},
|
|
200
|
+
429,
|
|
201
|
+
);
|
|
202
|
+
return next();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// --- sessions ----------------------------------------------------------------
|
|
206
|
+
app.post('/sessions', async (c) => {
|
|
207
|
+
const identity = identityOf(c.get('auth'), c.req.raw.headers);
|
|
208
|
+
const session = await userStub(c.env, identity.userDid).createSession(
|
|
209
|
+
identity,
|
|
210
|
+
);
|
|
211
|
+
return c.json(session, 201);
|
|
212
|
+
});
|
|
213
|
+
app.get('/sessions', async (c) => {
|
|
214
|
+
const identity = identityOf(c.get('auth'), c.req.raw.headers);
|
|
215
|
+
const limit = c.req.query('limit')
|
|
216
|
+
? Number(c.req.query('limit'))
|
|
217
|
+
: undefined;
|
|
218
|
+
const offset = c.req.query('offset')
|
|
219
|
+
? Number(c.req.query('offset'))
|
|
220
|
+
: undefined;
|
|
221
|
+
const { sessions, total } = await userStub(
|
|
222
|
+
c.env,
|
|
223
|
+
identity.userDid,
|
|
224
|
+
).listSessions(identity, { limit, offset });
|
|
225
|
+
// Node's `ListChatSessionsResponseDto`: `{ sessions, total }`.
|
|
226
|
+
return c.json({ sessions, total });
|
|
227
|
+
});
|
|
228
|
+
app.delete('/sessions/:sessionId', async (c) => {
|
|
229
|
+
const identity = identityOf(c.get('auth'), c.req.raw.headers);
|
|
230
|
+
const ok = await userStub(c.env, identity.userDid).deleteSession(
|
|
231
|
+
identity,
|
|
232
|
+
c.req.param('sessionId'),
|
|
233
|
+
);
|
|
234
|
+
return ok
|
|
235
|
+
? c.json({ message: 'Session deleted successfully' })
|
|
236
|
+
: c.json({ message: 'Session not found' }, 404);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// --- messages ----------------------------------------------------------------
|
|
240
|
+
app.post('/messages/abort', async (c) => {
|
|
241
|
+
const identity = identityOf(c.get('auth'), c.req.raw.headers);
|
|
242
|
+
const body = (await c.req.json().catch(() => ({}))) as {
|
|
243
|
+
sessionId?: string;
|
|
244
|
+
};
|
|
245
|
+
if (!body.sessionId)
|
|
246
|
+
return c.json({ message: 'sessionId is required' }, 400);
|
|
247
|
+
const success = await userStub(c.env, identity.userDid).abortTurn(
|
|
248
|
+
body.sessionId,
|
|
249
|
+
);
|
|
250
|
+
return c.json({ success });
|
|
251
|
+
});
|
|
252
|
+
app.get('/messages/:sessionId', async (c) => {
|
|
253
|
+
const identity = identityOf(c.get('auth'), c.req.raw.headers);
|
|
254
|
+
const messages = await userStub(c.env, identity.userDid).listMessages(
|
|
255
|
+
identity,
|
|
256
|
+
c.req.param('sessionId'),
|
|
257
|
+
);
|
|
258
|
+
return new Response(`{"messages":${messages}}`, {
|
|
259
|
+
headers: { 'content-type': 'application/json' },
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
app.post('/messages/:sessionId', async (c) => {
|
|
263
|
+
const identity = identityOf(c.get('auth'), c.req.raw.headers);
|
|
264
|
+
const sessionId = c.req.param('sessionId');
|
|
265
|
+
const body = await c.req.text();
|
|
266
|
+
// Node (express body-parser default) refuses JSON bodies over 100 KiB
|
|
267
|
+
// with a 413; attachments are references, so real turns never get near it.
|
|
268
|
+
if (new TextEncoder().encode(body).byteLength > MAX_TURN_BODY_BYTES) {
|
|
269
|
+
return c.json(
|
|
270
|
+
{ statusCode: 413, message: 'request entity too large' },
|
|
271
|
+
413,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
// Node accepts a client-supplied `requestId` (correlates the SSE events
|
|
275
|
+
// with the client's own bookkeeping); fall back to a fresh UUID.
|
|
276
|
+
const requestId = clientRequestId(body) ?? crypto.randomUUID();
|
|
277
|
+
// The user object owns the turn; it returns either an SSE stream or JSON.
|
|
278
|
+
// We pass the raw body through so attachments/tools/agActions survive.
|
|
279
|
+
const res = await userStub(c.env, identity.userDid).fetch(
|
|
280
|
+
`https://user-oracle/turn/${encodeURIComponent(sessionId)}`,
|
|
281
|
+
{
|
|
282
|
+
method: 'POST',
|
|
283
|
+
headers: {
|
|
284
|
+
'content-type': 'application/json',
|
|
285
|
+
'x-request-id': requestId,
|
|
286
|
+
'x-identity': JSON.stringify(identity),
|
|
287
|
+
},
|
|
288
|
+
body,
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
const headers = new Headers(res.headers);
|
|
292
|
+
headers.set('x-request-id', requestId);
|
|
293
|
+
headers.set('access-control-expose-headers', 'x-request-id');
|
|
294
|
+
return new Response(res.body, { status: res.status, headers });
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// --- delegation (room-state persisted, for header-less Matrix turns) -----------
|
|
298
|
+
app.post('/delegation', async (c) => {
|
|
299
|
+
const auth = c.get('auth');
|
|
300
|
+
const body = (await c.req.json().catch(() => ({}))) as {
|
|
301
|
+
raw?: string;
|
|
302
|
+
expiration?: number;
|
|
303
|
+
};
|
|
304
|
+
const raw = body.raw ?? auth.delegation;
|
|
305
|
+
if (!raw) return c.json({ message: 'raw delegation is required' }, 400);
|
|
306
|
+
const room = await gateway(c.env).resolveUserRoom(auth.userDid);
|
|
307
|
+
if (!room) return c.json({ message: 'No oracle room for this user' }, 404);
|
|
308
|
+
const expiration = body.expiration ?? auth.delegationExpiration;
|
|
309
|
+
// The Node runtime's `DelegationStore` record, in its compressed
|
|
310
|
+
// room-state envelope, so a user can move between runtimes without
|
|
311
|
+
// re-authorising (`updatedAt` is required by Node's schema).
|
|
312
|
+
await gateway(c.env).sendStateEvent(
|
|
313
|
+
room.roomId,
|
|
314
|
+
'ixo.room.state',
|
|
315
|
+
JSON.stringify(
|
|
316
|
+
await encodeRoomStateContent({
|
|
317
|
+
raw,
|
|
318
|
+
issuer: auth.userDid,
|
|
319
|
+
audience: c.env.ORACLE_DID,
|
|
320
|
+
...(expiration && { expiration }),
|
|
321
|
+
updatedAt: new Date().toISOString(),
|
|
322
|
+
}),
|
|
323
|
+
),
|
|
324
|
+
'ucan_delegation',
|
|
325
|
+
);
|
|
326
|
+
// The user's object caches the delegation for header-less (Matrix)
|
|
327
|
+
// turns; hand it the new one so it takes effect immediately rather
|
|
328
|
+
// than when the cached copy expires (Node's cacheDelegation semantics).
|
|
329
|
+
await userStub(c.env, auth.userDid)
|
|
330
|
+
.setDelegation(auth.userDid, raw, expiration)
|
|
331
|
+
.catch((err: unknown) => {
|
|
332
|
+
console.warn(
|
|
333
|
+
`[shell] could not push the deposited delegation to ${auth.userDid}'s object: ${err instanceof Error ? err.message : String(err)}`,
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
return c.json({
|
|
337
|
+
ok: true,
|
|
338
|
+
expiration: body.expiration ?? auth.delegationExpiration,
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
app.get('/delegation', async (c) => {
|
|
342
|
+
const auth = c.get('auth');
|
|
343
|
+
const room = await gateway(c.env).resolveUserRoom(auth.userDid);
|
|
344
|
+
if (!room) return c.json({ authorized: false });
|
|
345
|
+
const raw = await gateway(c.env).getRoomStateEvent(
|
|
346
|
+
room.roomId,
|
|
347
|
+
'ixo.room.state',
|
|
348
|
+
'ucan_delegation',
|
|
349
|
+
);
|
|
350
|
+
const state = raw
|
|
351
|
+
? ((await decodeRoomStateContent(JSON.parse(raw) as unknown)) as {
|
|
352
|
+
raw?: string;
|
|
353
|
+
expiration?: number;
|
|
354
|
+
} | null)
|
|
355
|
+
: null;
|
|
356
|
+
const exp =
|
|
357
|
+
typeof state?.expiration === 'number' ? state.expiration : undefined;
|
|
358
|
+
const authorized =
|
|
359
|
+
Boolean(state?.raw) &&
|
|
360
|
+
(exp === undefined || exp > Math.floor(Date.now() / 1000));
|
|
361
|
+
return c.json({
|
|
362
|
+
authorized,
|
|
363
|
+
...(exp !== undefined && { expiration: exp }),
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
app.delete('/delegation', async (c) => {
|
|
367
|
+
const auth = c.get('auth');
|
|
368
|
+
const room = await gateway(c.env).resolveUserRoom(auth.userDid);
|
|
369
|
+
if (room)
|
|
370
|
+
await gateway(c.env).sendStateEvent(
|
|
371
|
+
room.roomId,
|
|
372
|
+
'ixo.room.state',
|
|
373
|
+
JSON.stringify(await encodeRoomStateContent({})),
|
|
374
|
+
'ucan_delegation',
|
|
375
|
+
);
|
|
376
|
+
// Node's revokeDelegationForUser clears the cache so downstream minting
|
|
377
|
+
// stops NOW, not at the cache TTL. Same here: drop the object's copy.
|
|
378
|
+
await userStub(c.env, auth.userDid)
|
|
379
|
+
.clearDelegation(auth.userDid)
|
|
380
|
+
.catch((err: unknown) => {
|
|
381
|
+
console.warn(
|
|
382
|
+
`[shell] could not clear ${auth.userDid}'s cached delegation: ${err instanceof Error ? err.message : String(err)}`,
|
|
383
|
+
);
|
|
384
|
+
});
|
|
385
|
+
return c.json({ ok: true });
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// --- byo-llm (bring-your-own-credential LLMs) --------------------------------
|
|
389
|
+
// The user's object owns the credentials (secrets service, refresh state),
|
|
390
|
+
// so every route is proxied there — same pattern as streaming turns. The
|
|
391
|
+
// shell only short-circuits the disabled case so stray requests never boot
|
|
392
|
+
// a Durable Object.
|
|
393
|
+
app.all('/byo-llm/*', async (c) => {
|
|
394
|
+
if (c.env.BYO_LLM_ENABLED !== 'true') {
|
|
395
|
+
return c.req.method === 'GET' && c.req.path === '/byo-llm/status'
|
|
396
|
+
? c.json({ enabled: false, providers: [] })
|
|
397
|
+
: c.json(
|
|
398
|
+
{
|
|
399
|
+
statusCode: 404,
|
|
400
|
+
message:
|
|
401
|
+
'Bring-your-own-credential LLMs are not enabled on this oracle',
|
|
402
|
+
},
|
|
403
|
+
404,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
const identity = identityOf(c.get('auth'), c.req.raw.headers);
|
|
407
|
+
const url = new URL(c.req.raw.url);
|
|
408
|
+
const method = c.req.method.toUpperCase();
|
|
409
|
+
const init: RequestInit = {
|
|
410
|
+
method,
|
|
411
|
+
headers: {
|
|
412
|
+
'content-type': 'application/json',
|
|
413
|
+
'x-identity': JSON.stringify(identity),
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
if (method !== 'GET' && method !== 'HEAD') init.body = await c.req.text();
|
|
417
|
+
const res = await userStub(c.env, identity.userDid).fetch(
|
|
418
|
+
`https://user-oracle${url.pathname}${url.search}`,
|
|
419
|
+
init,
|
|
420
|
+
);
|
|
421
|
+
return new Response(res.body, { status: res.status, headers: res.headers });
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// --- debug / operator routes (enabled with ORACLE_DEBUG_ROUTES=true) -------------
|
|
425
|
+
app.use('/debug/*', async (c, next) => {
|
|
426
|
+
if (c.env.ORACLE_DEBUG_ROUTES !== 'true')
|
|
427
|
+
return c.json({ statusCode: 404, message: 'Not found' }, 404);
|
|
428
|
+
return next();
|
|
429
|
+
});
|
|
430
|
+
app.get('/debug/storage', async (c) =>
|
|
431
|
+
c.json(await userStub(c.env, c.get('auth').userDid).storageStatus()),
|
|
432
|
+
);
|
|
433
|
+
app.post('/debug/storage/flush', async (c) =>
|
|
434
|
+
c.json(await userStub(c.env, c.get('auth').userDid).flushToOwnerStore()),
|
|
435
|
+
);
|
|
436
|
+
app.post('/debug/storage/reset', async (c) =>
|
|
437
|
+
c.json(await userStub(c.env, c.get('auth').userDid).resetWorkingCopy()),
|
|
438
|
+
);
|
|
439
|
+
app.get('/debug/memory-schema', async (c) => {
|
|
440
|
+
const userDid = c.get('auth').userDid;
|
|
441
|
+
return c.json(await userStub(c.env, userDid).debugMemorySchema(userDid));
|
|
442
|
+
});
|
|
443
|
+
app.get('/debug/sessions/:sessionId', async (c) => {
|
|
444
|
+
const userDid = c.get('auth').userDid;
|
|
445
|
+
const row = await userStub(c.env, userDid).debugSession(
|
|
446
|
+
userDid,
|
|
447
|
+
c.req.param('sessionId'),
|
|
448
|
+
);
|
|
449
|
+
return row
|
|
450
|
+
? c.json(row)
|
|
451
|
+
: c.json({ statusCode: 404, message: 'Session not found' }, 404);
|
|
452
|
+
});
|
|
453
|
+
app.get('/debug/tasks', async (c) =>
|
|
454
|
+
c.json(await userStub(c.env, c.get('auth').userDid).tasksStatus()),
|
|
455
|
+
);
|
|
456
|
+
app.get('/debug/realtime', async (c) =>
|
|
457
|
+
c.json(await userStub(c.env, c.get('auth').userDid).realtimeStatus()),
|
|
458
|
+
);
|
|
459
|
+
app.get('/debug/delegation', async (c) =>
|
|
460
|
+
c.json(
|
|
461
|
+
await userStub(c.env, c.get('auth').userDid).delegationStatus(
|
|
462
|
+
c.get('auth').userDid,
|
|
463
|
+
),
|
|
464
|
+
),
|
|
465
|
+
);
|
|
466
|
+
app.post('/debug/matrix/rotate-device', async (c) =>
|
|
467
|
+
c.json(await gateway(c.env).rotateDevice('operator')),
|
|
468
|
+
);
|
|
469
|
+
app.get('/debug/matrix/outbox', async (c) =>
|
|
470
|
+
c.json({ rows: await gateway(c.env).listOutbox() }),
|
|
471
|
+
);
|
|
472
|
+
app.post('/debug/matrix/restart', async (c) =>
|
|
473
|
+
c.json(await gateway(c.env).restart()),
|
|
474
|
+
);
|
|
475
|
+
app.post('/debug/matrix/stop', async (c) => {
|
|
476
|
+
await gateway(c.env).stop();
|
|
477
|
+
return c.json({ stopped: true });
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// --- plugin / host routes ------------------------------------------------------
|
|
481
|
+
for (const route of opts.routes ?? []) {
|
|
482
|
+
const path = route.path.startsWith('/') ? route.path : `/${route.path}`;
|
|
483
|
+
const handler = async (c: {
|
|
484
|
+
req: { raw: Request };
|
|
485
|
+
env: OracleWorkerEnv;
|
|
486
|
+
get: (key: 'auth') => AuthResult | undefined;
|
|
487
|
+
}) => route.handler(c.req.raw, c.env, { auth: c.get('auth') ?? null });
|
|
488
|
+
if (route.method === 'ALL') app.all(path, handler);
|
|
489
|
+
else app.on(route.method, path, handler);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
app.notFound((c) =>
|
|
493
|
+
c.json(
|
|
494
|
+
{ statusCode: 404, message: `Cannot ${c.req.method} ${c.req.path}` },
|
|
495
|
+
404,
|
|
496
|
+
),
|
|
497
|
+
);
|
|
498
|
+
app.onError((err, c) => {
|
|
499
|
+
console.error(`[shell] ${c.req.method} ${c.req.path} failed:`, err);
|
|
500
|
+
// A cold boot that could not read the user's owner copy changed nothing.
|
|
501
|
+
// Say precisely why: 503 + retryable for a transient store/VFS failure
|
|
502
|
+
// (the object already retried with backoff), 403 for a missing
|
|
503
|
+
// `ixo:filesystem` delegation or rejected credentials, where retrying
|
|
504
|
+
// cannot help and the client must act.
|
|
505
|
+
// The user object throws these as an RPC envelope (a plain Error on
|
|
506
|
+
// this side); `parseOwnerCopyFailure` reads the class, the envelope or
|
|
507
|
+
// the raw store errors alike.
|
|
508
|
+
const failure = parseOwnerCopyFailure(err);
|
|
509
|
+
if (failure) {
|
|
510
|
+
return c.json(
|
|
511
|
+
{
|
|
512
|
+
statusCode: failure.httpStatus,
|
|
513
|
+
code: failure.code,
|
|
514
|
+
message: failure.message,
|
|
515
|
+
retryable: failure.retryable,
|
|
516
|
+
},
|
|
517
|
+
failure.httpStatus,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
return c.json(
|
|
521
|
+
{ statusCode: 500, message: err.message || 'Internal error' },
|
|
522
|
+
500,
|
|
523
|
+
);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
return app;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const REQUEST_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
530
|
+
|
|
531
|
+
/** The body's `requestId` when it is a sane opaque token; null otherwise. */
|
|
532
|
+
function clientRequestId(rawBody: string): string | null {
|
|
533
|
+
try {
|
|
534
|
+
const parsed: unknown = JSON.parse(rawBody);
|
|
535
|
+
if (parsed && typeof parsed === 'object') {
|
|
536
|
+
const id = (parsed as { requestId?: unknown }).requestId;
|
|
537
|
+
if (typeof id === 'string' && REQUEST_ID_RE.test(id)) return id;
|
|
538
|
+
}
|
|
539
|
+
} catch {
|
|
540
|
+
// not JSON — the user object reports the malformed body
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function identityOf(auth: AuthResult, headers: Headers): TurnIdentity {
|
|
546
|
+
return {
|
|
547
|
+
userDid: auth.userDid,
|
|
548
|
+
ucanDelegation: auth.delegation,
|
|
549
|
+
ucanDelegationExpiration: auth.delegationExpiration,
|
|
550
|
+
timezone: headers.get('x-timezone') ?? undefined,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export function userStub(env: OracleWorkerEnv, userDid: string) {
|
|
555
|
+
const id = env.USER_ORACLE.idFromName(
|
|
556
|
+
userObjectName(userDid, env.ORACLE_DID),
|
|
557
|
+
);
|
|
558
|
+
return env.USER_ORACLE.get(id);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export function gateway(env: OracleWorkerEnv) {
|
|
562
|
+
const id = env.MATRIX_GATEWAY.idFromName(env.ORACLE_DID);
|
|
563
|
+
return env.MATRIX_GATEWAY.get(id);
|
|
564
|
+
}
|