@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.
Files changed (215) hide show
  1. package/README.md +110 -0
  2. package/package.json +77 -0
  3. package/src/attachments/classify.ts +114 -0
  4. package/src/attachments/content-blocks.ts +51 -0
  5. package/src/attachments/download.ts +235 -0
  6. package/src/attachments/extract.ts +241 -0
  7. package/src/attachments/index.ts +84 -0
  8. package/src/attachments/magic.ts +133 -0
  9. package/src/attachments/pipeline.ts +409 -0
  10. package/src/attachments/retention.ts +205 -0
  11. package/src/attachments/route.ts +41 -0
  12. package/src/attachments/sandbox-archive.ts +90 -0
  13. package/src/attachments/sanitize.ts +35 -0
  14. package/src/attachments/types.ts +86 -0
  15. package/src/attachments/view.ts +107 -0
  16. package/src/core/env.ts +200 -0
  17. package/src/core/index.ts +513 -0
  18. package/src/core/llm.ts +842 -0
  19. package/src/core/main-agent-types.ts +123 -0
  20. package/src/core/main-agent.ts +399 -0
  21. package/src/core/manifest.ts +365 -0
  22. package/src/core/meta-tools.ts +232 -0
  23. package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
  24. package/src/core/middlewares/capability-gate.ts +76 -0
  25. package/src/core/middlewares/index.ts +18 -0
  26. package/src/core/middlewares/page-context.ts +98 -0
  27. package/src/core/middlewares/safety-guardrail.ts +117 -0
  28. package/src/core/middlewares/summarization.ts +99 -0
  29. package/src/core/middlewares/tool-repetition-guard.ts +125 -0
  30. package/src/core/middlewares/tool-validation.ts +83 -0
  31. package/src/core/middlewares/work-status.ts +57 -0
  32. package/src/core/openrouter-pricing.ts +124 -0
  33. package/src/core/plugin-loader.ts +192 -0
  34. package/src/core/plugins/index.ts +6 -0
  35. package/src/core/plugins/skills/index.ts +8 -0
  36. package/src/core/plugins/skills/skills-tools.ts +356 -0
  37. package/src/core/plugins/skills/skills.plugin.ts +113 -0
  38. package/src/core/plugins/weather/index.ts +8 -0
  39. package/src/core/plugins/weather/weather-client.ts +153 -0
  40. package/src/core/plugins/weather/weather-middleware.ts +27 -0
  41. package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
  42. package/src/core/plugins/weather/weather-tools.ts +103 -0
  43. package/src/core/plugins/weather/weather.plugin.ts +227 -0
  44. package/src/core/prompt-composer.ts +469 -0
  45. package/src/core/registries.ts +642 -0
  46. package/src/core/runtime-context.ts +619 -0
  47. package/src/core/state.ts +108 -0
  48. package/src/core/sub-agent-fallback.ts +187 -0
  49. package/src/core/subagent-as-tool.ts +322 -0
  50. package/src/core/template.ts +245 -0
  51. package/src/core/utils.ts +246 -0
  52. package/src/core/wrap-plugin-tool.ts +100 -0
  53. package/src/do/ambient.ts +266 -0
  54. package/src/do/contracts.ts +562 -0
  55. package/src/do/debug-timers.ts +120 -0
  56. package/src/do/fresh-stub.ts +32 -0
  57. package/src/do/gateway-retry.ts +73 -0
  58. package/src/do/secrets-adapter.ts +32 -0
  59. package/src/do/size-alerts.ts +76 -0
  60. package/src/do/sse-stream.ts +472 -0
  61. package/src/do/transcript.ts +192 -0
  62. package/src/do/turn-body.ts +84 -0
  63. package/src/do/turn-metadata.ts +146 -0
  64. package/src/do/ucan-service.ts +339 -0
  65. package/src/do/user-oracle-do.ts +3035 -0
  66. package/src/gateway-worker.ts +82 -0
  67. package/src/index.ts +237 -0
  68. package/src/llm/byo-adapter.ts +59 -0
  69. package/src/llm/byo-catalog.ts +520 -0
  70. package/src/llm/byo-client.ts +280 -0
  71. package/src/llm/byo-routes.ts +255 -0
  72. package/src/llm/byo-service.ts +836 -0
  73. package/src/llm/chatgpt-oauth.ts +430 -0
  74. package/src/llm/provider-error.ts +283 -0
  75. package/src/matrix/gateway-do.ts +918 -0
  76. package/src/matrix/ingest.ts +209 -0
  77. package/src/matrix/replay-format.ts +34 -0
  78. package/src/matrix/reply-chain.ts +109 -0
  79. package/src/matrix/room-state-codec.ts +115 -0
  80. package/src/matrix/user-homeserver.ts +106 -0
  81. package/src/matrix/work-status.ts +294 -0
  82. package/src/memory/session-history-indexer.ts +328 -0
  83. package/src/owner-store/ixo-vfs-store.ts +570 -0
  84. package/src/owner-store/matrix-media-store.ts +136 -0
  85. package/src/owner-store/migrating-store.ts +185 -0
  86. package/src/owner-store/owner-copy-errors.ts +175 -0
  87. package/src/owner-store/retry.ts +45 -0
  88. package/src/owner-store/tus-upload.ts +333 -0
  89. package/src/owner-store/types.ts +0 -0
  90. package/src/plugin-api/define-plugin.ts +45 -0
  91. package/src/plugin-api/index.ts +56 -0
  92. package/src/plugin-api/oracle-plugin.ts +145 -0
  93. package/src/plugin-api/tool-helper.ts +82 -0
  94. package/src/plugin-api/types.ts +816 -0
  95. package/src/plugin-api/ucan-errors.ts +16 -0
  96. package/src/plugins/agui/agui-agent.ts +115 -0
  97. package/src/plugins/agui/agui.plugin.ts +148 -0
  98. package/src/plugins/agui/index.ts +2 -0
  99. package/src/plugins/attachments/attachments.plugin.ts +54 -0
  100. package/src/plugins/attachments/index.ts +6 -0
  101. package/src/plugins/attachments/view-attachment-tool.ts +91 -0
  102. package/src/plugins/base64.ts +26 -0
  103. package/src/plugins/composio/composio-tools.ts +365 -0
  104. package/src/plugins/composio/composio-ucan.ts +43 -0
  105. package/src/plugins/composio/composio.plugin.ts +214 -0
  106. package/src/plugins/composio/index.ts +15 -0
  107. package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
  108. package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
  109. package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
  110. package/src/plugins/domain-indexer/index.ts +3 -0
  111. package/src/plugins/editor/blocknote-bridge.ts +120 -0
  112. package/src/plugins/editor/content-session.ts +331 -0
  113. package/src/plugins/editor/content-tools.ts +686 -0
  114. package/src/plugins/editor/document-model.ts +0 -0
  115. package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
  116. package/src/plugins/editor/editor-agent.ts +34 -0
  117. package/src/plugins/editor/editor-config.ts +64 -0
  118. package/src/plugins/editor/editor-mx.ts +128 -0
  119. package/src/plugins/editor/editor.plugin.ts +143 -0
  120. package/src/plugins/editor/failures.ts +154 -0
  121. package/src/plugins/editor/index.ts +31 -0
  122. package/src/plugins/editor/jsdom-shim.ts +55 -0
  123. package/src/plugins/editor/prompts.ts +59 -0
  124. package/src/plugins/editor/prop-policy.ts +147 -0
  125. package/src/plugins/editor/provider.ts +386 -0
  126. package/src/plugins/editor/room-membership.ts +95 -0
  127. package/src/plugins/editor/standalone-editor-tool.ts +200 -0
  128. package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
  129. package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
  130. package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
  131. package/src/plugins/firecrawl/index.ts +15 -0
  132. package/src/plugins/flows/action-metadata.ts +3505 -0
  133. package/src/plugins/flows/actions.ts +190 -0
  134. package/src/plugins/flows/edit.ts +314 -0
  135. package/src/plugins/flows/errors.ts +38 -0
  136. package/src/plugins/flows/explain.ts +43 -0
  137. package/src/plugins/flows/flow-doc.ts +111 -0
  138. package/src/plugins/flows/flows.plugin.ts +129 -0
  139. package/src/plugins/flows/forms.ts +330 -0
  140. package/src/plugins/flows/index.ts +2 -0
  141. package/src/plugins/flows/linkage.ts +147 -0
  142. package/src/plugins/flows/port-types.ts +53 -0
  143. package/src/plugins/flows/prompts.ts +65 -0
  144. package/src/plugins/flows/read.ts +392 -0
  145. package/src/plugins/flows/references.ts +66 -0
  146. package/src/plugins/flows/test-support.ts +165 -0
  147. package/src/plugins/flows/tools/authoring.ts +516 -0
  148. package/src/plugins/flows/tools/discovery.ts +119 -0
  149. package/src/plugins/flows/tools/forms.ts +142 -0
  150. package/src/plugins/flows/tools/inspect.ts +165 -0
  151. package/src/plugins/flows/tools/linkage.ts +117 -0
  152. package/src/plugins/flows/tools/settings.ts +184 -0
  153. package/src/plugins/flows/translator.ts +328 -0
  154. package/src/plugins/flows/types.ts +210 -0
  155. package/src/plugins/flows/ydoc-helpers.ts +543 -0
  156. package/src/plugins/index.ts +70 -0
  157. package/src/plugins/mcp-call-timeout.ts +38 -0
  158. package/src/plugins/mcp-tool-adapter.ts +87 -0
  159. package/src/plugins/memory/index.ts +17 -0
  160. package/src/plugins/memory/memory-tools.ts +463 -0
  161. package/src/plugins/memory/memory-ucan.ts +51 -0
  162. package/src/plugins/memory/memory.plugin.ts +122 -0
  163. package/src/plugins/memory/types.ts +147 -0
  164. package/src/plugins/portal/action-log.ts +34 -0
  165. package/src/plugins/portal/index.ts +2 -0
  166. package/src/plugins/portal/portal.plugin.ts +203 -0
  167. package/src/plugins/sandbox/index.ts +34 -0
  168. package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
  169. package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
  170. package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
  171. package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
  172. package/src/plugins/tasks/index.ts +17 -0
  173. package/src/plugins/tasks/manifest.ts +81 -0
  174. package/src/plugins/tasks/middleware.ts +219 -0
  175. package/src/plugins/tasks/tasks-tools.ts +414 -0
  176. package/src/plugins/tasks/tasks.plugin.ts +74 -0
  177. package/src/plugins/ucan-failure.ts +133 -0
  178. package/src/plugins/user-preferences/index.ts +22 -0
  179. package/src/plugins/user-preferences/schema.ts +31 -0
  180. package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
  181. package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
  182. package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
  183. package/src/plugins/vfs/index.ts +40 -0
  184. package/src/plugins/vfs/vfs-auth.ts +56 -0
  185. package/src/plugins/vfs/vfs-client.ts +655 -0
  186. package/src/plugins/vfs/vfs-content.ts +181 -0
  187. package/src/plugins/vfs/vfs-errors.ts +163 -0
  188. package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
  189. package/src/plugins/vfs/vfs-tools.ts +635 -0
  190. package/src/plugins/vfs/vfs.plugin.ts +201 -0
  191. package/src/realtime/frontend-call-registry.ts +163 -0
  192. package/src/realtime/realtime-endpoint.ts +619 -0
  193. package/src/realtime/session-socket-hub.ts +249 -0
  194. package/src/realtime/socket-io-codec.ts +187 -0
  195. package/src/secrets/jwe.ts +74 -0
  196. package/src/secrets/pin-cipher.ts +43 -0
  197. package/src/secrets/secrets-service.ts +319 -0
  198. package/src/shell/app.ts +564 -0
  199. package/src/shell/auth.ts +290 -0
  200. package/src/sqlite/blob-codec.ts +100 -0
  201. package/src/sqlite/blob-compactor.ts +146 -0
  202. package/src/sqlite/cache-config.ts +45 -0
  203. package/src/sqlite/database.ts +643 -0
  204. package/src/sqlite/do-vfs.ts +1485 -0
  205. package/src/sqlite/index.ts +61 -0
  206. package/src/sqlite/serialization.ts +370 -0
  207. package/src/sqlite/sessions-store.ts +311 -0
  208. package/src/sqlite/sqlite-saver.ts +919 -0
  209. package/src/sqlite/vacuum-policy.ts +77 -0
  210. package/src/sqlite/wa-sqlite-loader.ts +123 -0
  211. package/src/sqlite/wasm.d.ts +10 -0
  212. package/src/tasks/schedule.ts +174 -0
  213. package/src/tasks/scheduler.ts +753 -0
  214. package/src/tasks/spec.ts +125 -0
  215. package/src/tasks/store.ts +351 -0
@@ -0,0 +1,339 @@
1
+ /**
2
+ * Oracle-side UCAN helpers — a port of the Node runtime's `UcanService`
3
+ * minting paths (`createInvocationFromDelegation`, `mintSelfSignedInvocation`,
4
+ * `resolveServiceDid`, `getServiceDelegation`) with no NestJS and no
5
+ * process-wide singletons. One instance per user object; caches are
6
+ * per-instance and short-lived.
7
+ *
8
+ * The oracle signs with an Ed25519 mnemonic (`ORACLE_SIGNING_MNEMONIC`, the
9
+ * same material the Node runtime loads from its Matrix account room). Without
10
+ * it every mint returns `{ error }` and `hasSigningKey()` is false — plugins
11
+ * gate their mint-capable tools on that, exactly as on Node.
12
+ */
13
+ import {
14
+ createInvocation,
15
+ parseDelegation,
16
+ serializeInvocation,
17
+ signerFromMnemonic,
18
+ type SupportedDID,
19
+ } from '@ixo/ucan';
20
+ import type { RuntimeContext, UcanDelegation } from '../plugin-api/types';
21
+
22
+ type MintResult = { invocation: string } | { error: string };
23
+ type ServiceDelegationResult =
24
+ | { token: string; with: string }
25
+ | { error: 'no-delegation' | 'store-error'; detail?: string };
26
+
27
+ export interface UcanServiceOptions {
28
+ oracleDid: string;
29
+ signingMnemonic?: string;
30
+ logger?: { warn(msg: string): void; debug?(msg: string): void };
31
+ }
32
+
33
+ function toSupportedDid(did: string): SupportedDID {
34
+ if (did.startsWith('did:ixo:') || did.startsWith('did:key:'))
35
+ return did as SupportedDID;
36
+ throw new Error(`Unsupported oracle DID method: ${did}`);
37
+ }
38
+
39
+ function abilityCovers(granted: string, required: string): boolean {
40
+ if (granted === '*' || granted === required) return true;
41
+ if (granted.endsWith('/*')) {
42
+ const ns = granted.slice(0, -2);
43
+ return required === ns || required.startsWith(`${ns}/`);
44
+ }
45
+ return false;
46
+ }
47
+
48
+ /** Upper bound on a delegated invocation's lifetime (mirrors the Node runtime). */
49
+ const MAX_INVOCATION_TTL_SECONDS = 60 * 60;
50
+
51
+ export class WorkersUcanService {
52
+ private readonly serviceDidCache = new Map<
53
+ string,
54
+ { did: string; expiresAt: number }
55
+ >();
56
+ private readonly storeDelegationCache = new Map<
57
+ string,
58
+ { value: { token: string; with: string }; expiresAt: number }
59
+ >();
60
+
61
+ constructor(private readonly opts: UcanServiceOptions) {}
62
+
63
+ hasSigningKey(): boolean {
64
+ return Boolean(this.opts.signingMnemonic);
65
+ }
66
+
67
+ async resolveServiceDid(serviceUrl: string): Promise<string | null> {
68
+ try {
69
+ const origin = new URL(serviceUrl).origin;
70
+ const cached = this.serviceDidCache.get(origin);
71
+ if (cached && cached.expiresAt > Date.now()) return cached.did;
72
+ const res = await fetch(`${origin}/.well-known/did.json`);
73
+ if (!res.ok) {
74
+ this.opts.logger?.warn(`[UCAN] did.json ${origin}: HTTP ${res.status}`);
75
+ return null;
76
+ }
77
+ const doc: { id?: string } = await res.json<{ id?: string }>();
78
+ if (!doc.id) return null;
79
+ this.serviceDidCache.set(origin, {
80
+ did: doc.id,
81
+ expiresAt: Date.now() + 60 * 60 * 1000,
82
+ });
83
+ return doc.id;
84
+ } catch (error) {
85
+ this.opts.logger?.warn(
86
+ `[UCAN] resolveServiceDid ${serviceUrl}: ${error instanceof Error ? error.message : String(error)}`,
87
+ );
88
+ return null;
89
+ }
90
+ }
91
+
92
+ async createInvocationFromDelegation(
93
+ delegationCar: string,
94
+ serviceUrlOrDid: string,
95
+ capability: { can: string; with: string; nb?: Record<string, unknown> },
96
+ options: { maxTtlSeconds?: number } = {},
97
+ ): Promise<MintResult> {
98
+ if (!this.opts.signingMnemonic)
99
+ return {
100
+ error:
101
+ 'Oracle has no signing key configured — set ORACLE_SIGNING_MNEMONIC',
102
+ };
103
+ if (!delegationCar)
104
+ return { error: 'delegationCar (base64 CAR) is required' };
105
+ // Plugins that already resolved the service DID (memory, composio) pass
106
+ // the DID itself — mint straight to it, as the Node runtime's
107
+ // `mintInvocationForServiceDid` does. A DID is not a fetchable URL:
108
+ // `new URL('did:web:x').origin` is "null", so routing it through
109
+ // `resolveServiceDid` can never succeed.
110
+ const serviceDid = serviceUrlOrDid.startsWith('did:')
111
+ ? serviceUrlOrDid
112
+ : await this.resolveServiceDid(serviceUrlOrDid);
113
+ if (!serviceDid)
114
+ return {
115
+ error: `Could not resolve worker DID via ${serviceUrlOrDid}/.well-known/did.json`,
116
+ };
117
+ try {
118
+ const { signer } = await signerFromMnemonic(
119
+ this.opts.signingMnemonic,
120
+ toSupportedDid(this.opts.oracleDid),
121
+ );
122
+ let delegation: Awaited<ReturnType<typeof parseDelegation>>;
123
+ try {
124
+ delegation = await parseDelegation(delegationCar);
125
+ } catch (err) {
126
+ return {
127
+ error: `Could not parse delegation CAR: ${err instanceof Error ? err.message : String(err)}`,
128
+ };
129
+ }
130
+ const audience = delegation.audience?.did?.();
131
+ if (audience && audience !== this.opts.oracleDid) {
132
+ return {
133
+ error: `Delegation audience (${audience}) does not match this oracle DID (${this.opts.oracleDid})`,
134
+ };
135
+ }
136
+ const nowSeconds = Math.floor(Date.now() / 1000);
137
+ // Same lifetime rule as the Node runtime (`MAX_INVOCATION_TTL_SECONDS`):
138
+ // an invocation lives up to an hour, capped by the delegation. An MCP
139
+ // session minted at turn start keeps re-using its bearer for the whole
140
+ // turn, so a 60 s token expired under long tool calls.
141
+ let expiration =
142
+ nowSeconds + (options.maxTtlSeconds ?? MAX_INVOCATION_TTL_SECONDS);
143
+ const delegationExp =
144
+ typeof delegation.expiration === 'number' &&
145
+ isFinite(delegation.expiration)
146
+ ? delegation.expiration
147
+ : null;
148
+ if (delegationExp !== null) {
149
+ if (delegationExp <= nowSeconds)
150
+ return {
151
+ error: `Delegation expired at ${new Date(delegationExp * 1000).toISOString()}`,
152
+ };
153
+ expiration = Math.min(expiration, delegationExp);
154
+ }
155
+ const invocation = await createInvocation({
156
+ issuer: signer,
157
+ audience: serviceDid,
158
+ capability: {
159
+ can: capability.can as `${string}/${string}`,
160
+ with: capability.with as `${string}:${string}`,
161
+ // Caveats (e.g. the VFS `nb.hidden` reveal set) pass through
162
+ // verbatim; services intersect them across the proof chain.
163
+ ...(capability.nb !== undefined ? { nb: capability.nb } : {}),
164
+ },
165
+ proofs: [delegation],
166
+ expiration,
167
+ facts: [{ nonce: crypto.randomUUID() }],
168
+ });
169
+ return { invocation: await serializeInvocation(invocation) };
170
+ } catch (error) {
171
+ return {
172
+ error: `Failed to mint invocation: ${error instanceof Error ? error.message : String(error)}`,
173
+ };
174
+ }
175
+ }
176
+
177
+ async mintSelfSignedInvocation(
178
+ serviceUrl: string,
179
+ capability: { can: string; with: string },
180
+ options: { maxTtlSeconds?: number } = {},
181
+ ): Promise<MintResult> {
182
+ if (!this.opts.signingMnemonic)
183
+ return {
184
+ error:
185
+ 'Oracle has no signing key configured — set ORACLE_SIGNING_MNEMONIC',
186
+ };
187
+ const serviceDid = await this.resolveServiceDid(serviceUrl);
188
+ if (!serviceDid)
189
+ return {
190
+ error: `Could not resolve worker DID via ${serviceUrl}/.well-known/did.json`,
191
+ };
192
+ try {
193
+ const { signer } = await signerFromMnemonic(
194
+ this.opts.signingMnemonic,
195
+ toSupportedDid(this.opts.oracleDid),
196
+ );
197
+ const nowSeconds = Math.floor(Date.now() / 1000);
198
+ const invocation = await createInvocation({
199
+ issuer: signer,
200
+ audience: serviceDid,
201
+ capability: {
202
+ can: capability.can as `${string}/${string}`,
203
+ with: capability.with as `${string}:${string}`,
204
+ },
205
+ proofs: [],
206
+ expiration: nowSeconds + (options.maxTtlSeconds ?? 120),
207
+ facts: [{ nonce: crypto.randomUUID() }],
208
+ });
209
+ return { invocation: await serializeInvocation(invocation) };
210
+ } catch (error) {
211
+ return {
212
+ error: `Failed to mint invocation: ${error instanceof Error ? error.message : String(error)}`,
213
+ };
214
+ }
215
+ }
216
+
217
+ async getServiceDelegation(
218
+ userDid: string,
219
+ opts: { storeUrl: string; resource: string; requiredAbility: string },
220
+ ): Promise<ServiceDelegationResult> {
221
+ const cacheKey = `${userDid}:${opts.resource}:${opts.requiredAbility}`;
222
+ const cached = this.storeDelegationCache.get(cacheKey);
223
+ if (cached && cached.expiresAt > Date.now()) return cached.value;
224
+
225
+ const inv = await this.mintSelfSignedInvocation(
226
+ opts.storeUrl,
227
+ { can: 'store/get', with: 'ixo:ucan-store' },
228
+ { maxTtlSeconds: 120 },
229
+ );
230
+ if ('error' in inv) return { error: 'store-error', detail: inv.error };
231
+
232
+ interface StoreDelegation {
233
+ token: string;
234
+ capabilities: Array<{ can: string; with: string }>;
235
+ expiresAt: number | null;
236
+ lifecycleState: string;
237
+ }
238
+ let res: Response;
239
+ try {
240
+ res = await fetch(
241
+ `${opts.storeUrl}/api/delegations?rootIssuer=${encodeURIComponent(userDid)}`,
242
+ {
243
+ headers: {
244
+ authorization: `Bearer ${inv.invocation}`,
245
+ 'x-auth-type': 'ucan',
246
+ },
247
+ },
248
+ );
249
+ } catch (error) {
250
+ return {
251
+ error: 'store-error',
252
+ detail: error instanceof Error ? error.message : String(error),
253
+ };
254
+ }
255
+ if (!res.ok)
256
+ return res.status === 404
257
+ ? { error: 'no-delegation' }
258
+ : { error: 'store-error', detail: `store ${res.status}` };
259
+ let body: { delegations?: StoreDelegation[] };
260
+ try {
261
+ body = await res.json();
262
+ } catch (error) {
263
+ return {
264
+ error: 'store-error',
265
+ detail: error instanceof Error ? error.message : String(error),
266
+ };
267
+ }
268
+ const nowSeconds = Math.floor(Date.now() / 1000);
269
+ for (const row of body.delegations ?? []) {
270
+ if (row.lifecycleState !== 'active') continue;
271
+ if (row.expiresAt != null && row.expiresAt <= nowSeconds) continue;
272
+ const match = (row.capabilities ?? []).find(
273
+ (c) =>
274
+ (c.with === opts.resource || c.with.startsWith(opts.resource)) &&
275
+ abilityCovers(c.can, opts.requiredAbility),
276
+ );
277
+ if (!match) continue;
278
+ const value = { token: row.token, with: match.with };
279
+ const ttl =
280
+ row.expiresAt != null ? Math.min(row.expiresAt - nowSeconds, 600) : 600;
281
+ if (ttl > 0)
282
+ this.storeDelegationCache.set(cacheKey, {
283
+ value,
284
+ expiresAt: Date.now() + ttl * 1000,
285
+ });
286
+ return value;
287
+ }
288
+ return { error: 'no-delegation' };
289
+ }
290
+
291
+ /**
292
+ * Build the `ctx.ucan` surface for one turn. `delegation` is the user's
293
+ * delegation for this oracle (from the request header or the room state) —
294
+ * `mintInvocation` proves through it; capability checks read it.
295
+ */
296
+ forTurn(
297
+ userDid: string,
298
+ delegation: UcanDelegation | undefined,
299
+ ): RuntimeContext['ucan'] {
300
+ const caps = delegation?.capabilities ?? [];
301
+ const has = (resource: string, action: string) =>
302
+ caps.some(
303
+ (c) =>
304
+ (c.resource === resource || c.resource.startsWith(resource)) &&
305
+ abilityCovers(c.action, action),
306
+ );
307
+ return {
308
+ hasCapability: has,
309
+ requireCapability: (resource, action) => {
310
+ if (!has(resource, action))
311
+ throw new Error(`Missing UCAN capability ${action} on ${resource}`);
312
+ },
313
+ hasSigningKey: () => this.hasSigningKey(),
314
+ resolveServiceDid: (url) => this.resolveServiceDid(url),
315
+ mintInvocation: async (target, opts) => {
316
+ if (!delegation?.raw)
317
+ throw new Error(
318
+ `No delegation from ${userDid} available to mint an invocation from`,
319
+ );
320
+ const [can] = [opts?.can ?? '*'];
321
+ const minted = await this.createInvocationFromDelegation(
322
+ delegation.raw,
323
+ target.did,
324
+ {
325
+ can,
326
+ with: target.capability,
327
+ },
328
+ );
329
+ if ('error' in minted) throw new Error(minted.error);
330
+ return minted.invocation;
331
+ },
332
+ createInvocationFromDelegation: (car, url, cap, o) =>
333
+ this.createInvocationFromDelegation(car, url, cap, o),
334
+ mintSelfSignedInvocation: (url, cap, o) =>
335
+ this.mintSelfSignedInvocation(url, cap, o),
336
+ getServiceDelegation: (did, o) => this.getServiceDelegation(did, o),
337
+ };
338
+ }
339
+ }