@ixo/oracle-runtime-workers 0.17.0 → 0.17.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ixo/oracle-runtime-workers",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "description": "QiForge oracle runtime for Cloudflare Workers — Durable Object per user (SQLite in WASM over DO storage), Matrix E2EE gateway Durable Object, Hono HTTP/SSE shell.",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -29,6 +29,8 @@ import { briefBody, record } from './document';
29
29
  import { boundFindings } from './findings';
30
30
  import { decode, INDEX_MAX_BYTES, verifyBytes } from './integrity';
31
31
  import { LruMap } from './lru';
32
+ import type { Logger } from '../../plugin-api/types';
33
+ import { NOOP_LOGGER } from '../utils';
32
34
  import type {
33
35
  DocumentRequest,
34
36
  DomainAnchor,
@@ -189,8 +191,11 @@ export class DomainContextResolver {
189
191
  weigh: parsedWeight,
190
192
  });
191
193
 
192
- constructor(opts: { now?: () => number } = {}) {
194
+ private readonly logger: Logger;
195
+
196
+ constructor(opts: { now?: () => number; logger?: Logger } = {}) {
193
197
  this.now = opts.now ?? Date.now;
198
+ this.logger = opts.logger ?? NOOP_LOGGER;
194
199
  }
195
200
 
196
201
  /**
@@ -374,6 +379,13 @@ export class DomainContextResolver {
374
379
 
375
380
  private failed(snapshot: DomainSnapshot, error: unknown): DomainSnapshot {
376
381
  const code = errorCode(error, 'resolution-failed');
382
+ // A failure outside the finding vocabulary is a runtime fault (a thrown
383
+ // TypeError, a provider's message), not a property of the domain: the
384
+ // provenance only carries the fallback code, so the cause goes to the log.
385
+ if (errorCode(error, '') === '')
386
+ this.logger.warn(
387
+ `[domain-context] resolution of ${snapshot.did} failed: ${error instanceof Error ? error.message : String(error)}`,
388
+ );
377
389
  const failed: DomainSnapshot = {
378
390
  ...snapshot,
379
391
  status: INVALID_PATTERN.test(code) ? 'invalid' : 'unavailable',
@@ -2,8 +2,10 @@
2
2
  * The network edge of domain context: the IID anchor lookup on Blocksync and
3
3
  * document reads. Reads go only to https origins on an allowlist (configured
4
4
  * origins, the VFS, the Matrix homeserver, the IPFS gateway), never follow
5
- * redirects, never carry URL credentials and never buffer more than the
6
- * request's byte bound. Private documents are read through the user's VFS
5
+ * redirects (a 3xx answer is refused: workerd has no `redirect: 'error'`, so
6
+ * every request is made with `redirect: 'manual'` and the status checked),
7
+ * never carry URL credentials and never buffer more than the request's byte
8
+ * bound. Private documents are read through the user's VFS
7
9
  * UCAN bearer or the host's `readPrivateDocument` adapter.
8
10
  */
9
11
  import { CID } from 'multiformats/cid';
@@ -21,6 +23,32 @@ import type {
21
23
  DomainTransport,
22
24
  } from './types';
23
25
 
26
+ /** What `createDomainTransport` needs beyond the runtime context; tests inject a fetch. */
27
+ export interface DomainTransportDeps {
28
+ /** Default: the global `fetch`, looked up per call. */
29
+ fetch?: typeof fetch;
30
+ }
31
+
32
+ /**
33
+ * A request that never follows a redirect. `redirect: 'error'` does not
34
+ * exist on workerd (it throws a TypeError before any network call), so the
35
+ * request is made with `redirect: 'manual'`, which hands back the 3xx
36
+ * answer itself, and any 3xx is refused with `code`, its body cancelled.
37
+ */
38
+ export async function fetchWithoutRedirects(
39
+ fetcher: typeof fetch,
40
+ input: string | URL,
41
+ init: RequestInit,
42
+ code: string,
43
+ ): Promise<Response> {
44
+ const response = await fetcher(input, { ...init, redirect: 'manual' });
45
+ if (response.status >= 300 && response.status < 400) {
46
+ await response.body?.cancel().catch(() => undefined);
47
+ throw new Error(code);
48
+ }
49
+ return response;
50
+ }
51
+
24
52
  /** Bound of one document read when the caller's signal does not end it sooner. */
25
53
  export const DOCUMENT_READ_TIMEOUT_MS = 10_000;
26
54
 
@@ -136,7 +164,12 @@ function positiveNumber(value: unknown, fallback: number): number {
136
164
  export function createDomainTransport(
137
165
  options: DomainContextOptions,
138
166
  ctx: RuntimeContext,
167
+ deps: DomainTransportDeps = {},
139
168
  ): DomainTransport {
169
+ // Never keep the bare global: workerd rejects `fetch` invoked with a
170
+ // foreign `this`, and a test may stub the global after this is built.
171
+ const fetcher: typeof fetch =
172
+ deps.fetch ?? ((input, init) => globalThis.fetch(input, init));
140
173
  const blocksync =
141
174
  typeof ctx.config.BLOCKSYNC_GRAPHQL_URL === 'string'
142
175
  ? ctx.config.BLOCKSYNC_GRAPHQL_URL
@@ -222,13 +255,17 @@ export function createDomainTransport(
222
255
 
223
256
  async resolve(did, signal) {
224
257
  if (!blocksync) throw new Error('iid-unavailable');
225
- const response = await fetch(blocksync, {
226
- method: 'POST',
227
- redirect: 'error',
228
- signal,
229
- headers: { 'content-type': 'application/json' },
230
- body: JSON.stringify({ query: IID_QUERY, variables: { id: did } }),
231
- });
258
+ const response = await fetchWithoutRedirects(
259
+ fetcher,
260
+ blocksync,
261
+ {
262
+ method: 'POST',
263
+ signal,
264
+ headers: { 'content-type': 'application/json' },
265
+ body: JSON.stringify({ query: IID_QUERY, variables: { id: did } }),
266
+ },
267
+ 'iid-unavailable',
268
+ );
232
269
  const body = iidResponse.safeParse(
233
270
  JSON.parse(
234
271
  new TextDecoder().decode(
@@ -332,7 +369,12 @@ export function createDomainTransport(
332
369
  headers.Authorization = `Bearer ${credentials.accessToken}`;
333
370
  }
334
371
  return boundedBytes(
335
- await fetch(url, { headers, redirect: 'error', signal }),
372
+ await fetchWithoutRedirects(
373
+ fetcher,
374
+ url,
375
+ { headers, signal },
376
+ 'document-redirect-refused',
377
+ ),
336
378
  request.maxBytes,
337
379
  );
338
380
  },
@@ -762,7 +762,9 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
762
762
  * Domain-context anchors and documents, cached for every session of this
763
763
  * object; `refresh_domain_context` invalidates entries here.
764
764
  */
765
- private readonly domainResolver = new DomainContextResolver();
765
+ private readonly domainResolver = new DomainContextResolver({
766
+ logger: console,
767
+ });
766
768
  /** Per-turn domain-context provenance (`domain_context_runs`). */
767
769
  private domainContextStore: DomainContextStore | null = null;
768
770
  private runs: RunCoordinator | null = null;