@kehto/services 0.20.0 → 0.20.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/README.md CHANGED
@@ -4,7 +4,7 @@ Reference service handlers for the napplet protocol — identity, relay pool,
4
4
  cache, keys, media, notify, theme, link, common, lists, serial, BLE, WebRTC,
5
5
  and DM.
6
6
 
7
- > **Alpha status:** Kehto is an early runtime implementation for a draft NIP-5D
7
+ > **Alpha status:** Kehto is an early runtime toolkit for a draft NIP-5D
8
8
  > protocol. NAP contracts and service envelopes are not final; treat these
9
9
  > handlers as reference implementations for the current draft.
10
10
 
@@ -44,7 +44,7 @@ Current draft posture:
44
44
  - `createNotifyService` handles direct `notify.*` envelopes. It is not an INC
45
45
  topic handler.
46
46
  - `createOutboxService` supports `outbox.getEvent` from draft NAP-OUTBOX. Single-event lookups run through shell-owned relay routing and only return events whose ID matches the request. The draft wire contract keeps `outbox.query` one-shot and `outbox.subscribe` streaming; Kehto's concrete `createRelayPoolOutboxRouter` additionally exposes host-side `queryStream()` so verified query events can arrive before asynchronous NIP-65 discovery completes.
47
- - `createResourceService` implements the draft NAP-RESOURCE request lifecycle and current result shapes. It fail-closes when no scheme is configured, checks per-identity origin grants, enforces disclosed size/bulk caps, scopes cancellation to the requesting window, drops cancelled terminal envelopes, and never forwards response headers. The host policy fetch must return byte-classified output and owns scheme-specific SSRF, redirect, integrity, and SVG handling.
47
+ - `createResourceService` implements the draft NAP-RESOURCE request lifecycle and current result shapes without choosing a runtime's network policy. By default it delegates every syntactically valid URL to the injected `fetch` resolver; `resource.info.schemes` is advisory and never authorizes or blocks a request. Runtimes that want origin grants can supply `isOriginGranted`, `getConnectGrants`, and `resolveIdentity` together, while permissive runtimes can omit all three. The service enforces supplied size/bulk caps, scopes cancellation to the requesting window, drops cancelled terminal envelopes, never forwards response headers, and preserves explicit `ResourceServiceError` codes from the resolver. Browser fetch failures, including responses hidden by CORS, use the canonical `network-error`; a resolver that can read the bytes returns them normally.
48
48
  - `createUploadService` supports `upload.info` from draft NAP-UPLOAD. Hosts may expose configured rails, return URL forms, maximum bytes, and MIME type policy without requiring napplets to start an upload.
49
49
  - `createConfigService` implements draft NAP-CONFIG's shell-writer boundary: recursive bounded-subset schema validation, per-window schema/subscription state, deterministic defaults, invalid/orphan removal, version rollback rejection, scoped host persistence hooks, and settings-UI commits. Reads before schema registration fail closed with `no-schema`.
50
50
  - `createDmService` keeps NAP-DM request correlation, per-window subscriptions, and packaged message shapes in runtime-owned code. Adapters cover concrete transports: verified NIP-17 gift wraps and relay history via `nostr-tools`, structural NDR runtimes with relay hooks, and Cordn/ContextVM coordinator clients.
package/dist/index.d.ts CHANGED
@@ -1372,16 +1372,18 @@ declare function createConfigService(options: ConfigServiceOptions): ConfigServi
1372
1372
  * resource.bytesMany.result, resource.bytesMany.error
1373
1373
  *
1374
1374
  * ──────────────────────── SCOPE BOUNDARY (RESOURCE-01) ────────────────────────
1375
- * NAP-RESOURCE is an **authenticated fetch proxy** — read-only, atomic.
1375
+ * NAP-RESOURCE is a runtime-mediated byte resolver — read-only, atomic.
1376
1376
  *
1377
- * The host fetch boundary performs scheme-specific I/O and MUST return only a
1378
- * policy-checked, byte-classified response. This service independently enforces
1379
- * identity, scheme disclosure, origin grants, bulk limits, response-size caps,
1380
- * cancellation, and the current wire shape. It never forwards upstream headers.
1377
+ * Kehto owns neutral wire lifecycle, cancellation, result projection, and
1378
+ * policy tooling. The runtime supplies the resolver and decides whether to use
1379
+ * the origin-grant hooks, which schemes to resolve, and which limits to expose.
1380
+ * Advisory `resource.info` scheme disclosure never authorizes or blocks a byte
1381
+ * request.
1381
1382
  * ──────────────────────────────────────────────────────────────────────────────
1382
1383
  *
1383
- * Host integration: provide `fetch`, `isOriginGranted`, `getConnectGrants`,
1384
- * and `resolveIdentity`. ALL FOUR are required from day one (H-03 prevention).
1384
+ * Runtime integration requires only `fetch`. A runtime MAY also install the
1385
+ * optional origin-policy adapter (`isOriginGranted`, `getConnectGrants`, and
1386
+ * `resolveIdentity` together).
1385
1387
  *
1386
1388
  * @example
1387
1389
  * ```ts
@@ -1389,11 +1391,18 @@ declare function createConfigService(options: ConfigServiceOptions): ConfigServi
1389
1391
  *
1390
1392
  * const resourceSvc = createResourceService({
1391
1393
  * fetch: (url, init) => globalThis.fetch(url, init),
1394
+ * });
1395
+ * runtime.registerService('resource', resourceSvc);
1396
+ * ```
1397
+ *
1398
+ * @example
1399
+ * ```ts
1400
+ * const resourceSvc = createResourceService({
1401
+ * fetch: (url, init) => runtimeResources.resolve(url, init),
1392
1402
  * isOriginGranted: (origin, grants) => grants.includes(origin),
1393
1403
  * getConnectGrants: (dTag, hash) => myOriginGrantStore.getOrigins(dTag, hash),
1394
1404
  * resolveIdentity: (windowId) => sessionRegistry.getEntryByWindowId(windowId) ?? null,
1395
1405
  * });
1396
- * runtime.registerService('resource', resourceSvc);
1397
1406
  * ```
1398
1407
  */
1399
1408
 
@@ -1421,25 +1430,22 @@ type ResourceInfoProvider = ResourceInfo | ((context: ResourceInfoContext) => Re
1421
1430
  /**
1422
1431
  * Options for `createResourceService` (options-as-bridge per v1.6 Decision 18).
1423
1432
  *
1424
- * ALL FOUR fields are required. The factory throws at construction if any is
1425
- * missing — H-03 prevention: the grants source (`getConnectGrants`) MUST be
1426
- * wired from day one so there is no window where resource requests bypass the
1427
- * grant check.
1428
- *
1429
- * @see PITFALLS.md:228 (H-03) — grants-source coupling must be present at construction
1433
+ * `fetch` is the only required field. Kehto does not select a runtime's scheme
1434
+ * or origin policy. The optional origin-policy fields form one adapter and must
1435
+ * therefore be supplied together when a runtime chooses to use it.
1430
1436
  */
1431
1437
  interface ResourceServiceOptions {
1432
1438
  /**
1433
- * Host-supplied policy fetch implementation. Receives the URL, a partial init
1434
- * (method, headers, signal), and returns a sanitized `Response`.
1439
+ * Runtime-supplied resource resolver. Receives the requested URL and returns
1440
+ * a byte-classified `Response`.
1435
1441
  *
1436
- * The returned `content-type` MUST be derived from inspected output bytes,
1437
- * never an upstream header. The host boundary also owns redirect-by-redirect
1438
- * private-address checks, SVG rasterization, and scheme-specific integrity.
1439
- * Throw `ResourceServiceError` to preserve a protocol error classification.
1442
+ * The runtime owns scheme dispatch and every NAP-RESOURCE policy decision.
1443
+ * The returned `content-type` must describe the runtime-classified bytes, not
1444
+ * an untrusted upstream header. Throw `ResourceServiceError` to preserve a
1445
+ * canonical protocol error classification.
1440
1446
  *
1441
1447
  * @param url - The URL from the resource.bytes request
1442
- * @param init - Method, headers (from napplet), and an AbortSignal
1448
+ * @param init - Read-only method and request-scoped AbortSignal
1443
1449
  */
1444
1450
  fetch(url: string, init: {
1445
1451
  method?: string;
@@ -1449,6 +1455,8 @@ interface ResourceServiceOptions {
1449
1455
  /**
1450
1456
  * Returns true if `origin` is present in `grants` (the list returned by
1451
1457
  * `getConnectGrants` for the napplet's dTag + aggregateHash).
1458
+ * Optional: when omitted with `getConnectGrants`, Kehto delegates directly
1459
+ * to the runtime resolver without an origin-grant decision.
1452
1460
  *
1453
1461
  * The reference implementation is simply `grants.includes(origin)`. Host apps
1454
1462
  * may provide normalized-origin comparison if needed.
@@ -1456,21 +1464,19 @@ interface ResourceServiceOptions {
1456
1464
  * @param origin - Parsed origin of the requested URL (scheme + host + port)
1457
1465
  * @param grants - Readonly list from getConnectGrants for this napplet identity
1458
1466
  */
1459
- isOriginGranted(origin: string, grants: readonly string[]): boolean;
1467
+ isOriginGranted?(origin: string, grants: readonly string[]): boolean;
1460
1468
  /**
1461
1469
  * Returns the list of allowed fetch origins for the given napplet identity.
1462
1470
  * Called on every `resource.bytes` request — must be synchronous and fast.
1471
+ * Optional and paired with `isOriginGranted` plus `resolveIdentity`.
1463
1472
  *
1464
1473
  * Host-supplied grant source (e.g. a static per-dTag allowlist map, or any
1465
1474
  * other host-controlled policy). Returns an empty array to deny all origins.
1466
1475
  *
1467
- * H-03 prevention: REQUIRED from day one — factory throws on construction
1468
- * if omitted.
1469
- *
1470
1476
  * @param dTag - The napplet's d-tag (from session registry)
1471
1477
  * @param aggregateHash - The napplet's aggregate hash (from session registry)
1472
1478
  */
1473
- getConnectGrants(dTag: string, aggregateHash: string): readonly string[];
1479
+ getConnectGrants?(dTag: string, aggregateHash: string): readonly string[];
1474
1480
  /**
1475
1481
  * Resolve a windowId to the napplet's identity (dTag + aggregateHash).
1476
1482
  * Returns null if the window is not in the session registry.
@@ -1479,7 +1485,7 @@ interface ResourceServiceOptions {
1479
1485
  *
1480
1486
  * @param windowId - The iframe window identifier
1481
1487
  */
1482
- resolveIdentity(windowId: string): {
1488
+ resolveIdentity?(windowId: string): {
1483
1489
  dTag: string;
1484
1490
  aggregateHash: string;
1485
1491
  } | null;
@@ -1487,8 +1493,8 @@ interface ResourceServiceOptions {
1487
1493
  * Advisory NAP-RESOURCE introspection exposed through `resource.info`.
1488
1494
  *
1489
1495
  * Provide a static snapshot or a resolver when the shell wants to disclose
1490
- * configured schemes and coarse limits. Omit to expose the reference
1491
- * service's fail-closed default (no enabled schemes or numeric limits).
1496
+ * configured schemes and coarse limits. Scheme disclosure is advisory and is
1497
+ * never used as fetch authorization. Omit to disclose no schemes or limits.
1492
1498
  */
1493
1499
  resourceInfo?: ResourceInfoProvider;
1494
1500
  }
@@ -1498,7 +1504,7 @@ interface ResourceServiceOptions {
1498
1504
  */
1499
1505
  type ResourceService = ServiceHandler;
1500
1506
  type ResourceErrorCode = 'invalid-request' | 'not-found' | 'blocked-by-policy' | 'timeout' | 'too-large' | 'unsupported-scheme' | 'decode-failed' | 'network-error' | 'quota-exceeded';
1501
- /** A host policy failure with a stable NAP-RESOURCE error classification. */
1507
+ /** A runtime resolver failure with a stable NAP-RESOURCE error classification. */
1502
1508
  declare class ResourceServiceError extends Error {
1503
1509
  readonly code: ResourceErrorCode;
1504
1510
  /**
@@ -1514,15 +1520,14 @@ declare class ResourceServiceError extends Error {
1514
1520
  * `resource.bytes`, `resource.bytesMany`, `resource.cancel`, and their
1515
1521
  * result/error envelopes.
1516
1522
  *
1517
- * On-construction guard (H-03 prevention): all four options are validated at
1518
- * factory call time. If any is missing, the factory throws immediately with a
1519
- * message containing `[RESOURCE-01 / H-03]` so misconfigured shell apps fail
1520
- * loudly at startup rather than silently at first dispatch.
1523
+ * The runtime resolver is validated at construction. Runtime implementers may
1524
+ * additionally install the origin-grant adapter; Kehto provides and enforces
1525
+ * the hook contract without selecting the grants or requiring that policy.
1521
1526
  *
1522
1527
  * Returns a `ServiceHandler` (no `publishValues`-style surface — resource has
1523
1528
  * no shell-initiated push beyond the response/error path).
1524
1529
  *
1525
- * @param options - REQUIRED: fetch, isOriginGranted, getConnectGrants, resolveIdentity
1530
+ * @param options - Runtime resolver plus optional runtime-owned policy helpers
1526
1531
  * @returns ServiceHandler to register via `runtime.registerService('resource', handler)`
1527
1532
  *
1528
1533
  * @example
@@ -1530,7 +1535,7 @@ declare class ResourceServiceError extends Error {
1530
1535
  * import { createResourceService } from '@kehto/services';
1531
1536
  *
1532
1537
  * const svc = createResourceService({
1533
- * fetch: (url, init) => globalThis.fetch(url, init),
1538
+ * fetch: (url, init) => runtimeResources.resolve(url, init),
1534
1539
  * isOriginGranted: (origin, grants) => grants.includes(origin),
1535
1540
  * getConnectGrants: (dTag, hash) => myOriginGrantStore.getOrigins(dTag, hash),
1536
1541
  * resolveIdentity: (windowId) => sessionRegistry.getEntryByWindowId(windowId) ?? null,
package/dist/index.js CHANGED
@@ -2272,11 +2272,20 @@ function requestKey(windowId, requestId) {
2272
2272
  return `${windowId}\0${requestId}`;
2273
2273
  }
2274
2274
  function assertResourceOptions(options) {
2275
- if (typeof options?.fetch !== "function" || typeof options?.isOriginGranted !== "function" || typeof options?.getConnectGrants !== "function" || typeof options?.resolveIdentity !== "function") {
2275
+ if (typeof options?.fetch !== "function") {
2276
2276
  throw new Error(
2277
- "[RESOURCE-01 / H-03] createResourceService requires {fetch, isOriginGranted, getConnectGrants, resolveIdentity} \u2014 all four options are required from day one. The grants source (getConnectGrants) MUST be wired at construction time to prevent unguarded fetch proxying."
2277
+ "[RESOURCE-01] createResourceService requires a runtime-provided fetch resolver."
2278
2278
  );
2279
2279
  }
2280
+ const hasOriginPolicy = options.isOriginGranted !== void 0 || options.getConnectGrants !== void 0;
2281
+ if (hasOriginPolicy && (typeof options.isOriginGranted !== "function" || typeof options.getConnectGrants !== "function" || typeof options.resolveIdentity !== "function")) {
2282
+ throw new Error(
2283
+ "[RESOURCE-01] optional origin policy requires {isOriginGranted, getConnectGrants, resolveIdentity} together."
2284
+ );
2285
+ }
2286
+ if (options.resolveIdentity !== void 0 && typeof options.resolveIdentity !== "function") {
2287
+ throw new Error("[RESOURCE-01] resolveIdentity must be a function when provided.");
2288
+ }
2280
2289
  }
2281
2290
  function trackRequest(state, requestId, windowId, controller) {
2282
2291
  const key = requestKey(windowId, requestId);
@@ -2332,7 +2341,7 @@ function normalizeResourceInfo(info) {
2332
2341
  }
2333
2342
  async function resolveResourceInfo(options, windowId) {
2334
2343
  const configured = options.resourceInfo ?? DEFAULT_RESOURCE_INFO;
2335
- const info = typeof configured === "function" ? await configured({ windowId, identity: options.resolveIdentity(windowId) }) : configured;
2344
+ const info = typeof configured === "function" ? await configured({ windowId, identity: options.resolveIdentity?.(windowId) ?? null }) : configured;
2336
2345
  return normalizeResourceInfo(info);
2337
2346
  }
2338
2347
  async function handleInfo(options, windowId, requestId, send) {
@@ -2366,22 +2375,29 @@ function classifyResourceFailure(error) {
2366
2375
  message: error instanceof Error ? error.message : String(error)
2367
2376
  };
2368
2377
  }
2369
- async function fetchResourceItem(options, identity, info, url, signal) {
2378
+ async function fetchResourceItem(options, windowId, info, url, signal) {
2370
2379
  const parsedUrl = parseResourceUrl(url);
2371
2380
  if (!parsedUrl) return resourceInvalidRequest(url, `invalid URL: ${url}`);
2372
- const scheme = parsedUrl.protocol.slice(0, -1).toLowerCase();
2373
- if (!info.schemes.some((item) => item.enabled && item.scheme.toLowerCase() === scheme)) {
2374
- return { ok: false, url, error: "unsupported-scheme", message: `scheme ${scheme} is not enabled` };
2375
- }
2376
- const origin = parsedUrl.origin;
2377
- const grants = options.getConnectGrants(identity.dTag, identity.aggregateHash);
2378
- if (!options.isOriginGranted(origin, grants)) {
2379
- return {
2380
- ok: false,
2381
- url,
2382
- error: "blocked-by-policy",
2383
- message: `origin ${origin} not granted`
2384
- };
2381
+ if (options.isOriginGranted && options.getConnectGrants) {
2382
+ const identity = options.resolveIdentity?.(windowId) ?? null;
2383
+ if (!identity) {
2384
+ return {
2385
+ ok: false,
2386
+ url,
2387
+ error: "blocked-by-policy",
2388
+ message: "napplet identity not resolvable for runtime origin policy"
2389
+ };
2390
+ }
2391
+ const origin = parsedUrl.origin;
2392
+ const grants = options.getConnectGrants(identity.dTag, identity.aggregateHash);
2393
+ if (!options.isOriginGranted(origin, grants)) {
2394
+ return {
2395
+ ok: false,
2396
+ url,
2397
+ error: "blocked-by-policy",
2398
+ message: `origin ${origin} not granted by runtime policy`
2399
+ };
2400
+ }
2385
2401
  }
2386
2402
  try {
2387
2403
  const response = await options.fetch(url, {
@@ -2411,16 +2427,11 @@ async function fetchResourceItem(options, identity, info, url, signal) {
2411
2427
  }
2412
2428
  async function handleBytes(options, state, windowId, msg, send) {
2413
2429
  const { requestId, url } = msg;
2414
- const identity = options.resolveIdentity(windowId);
2415
- if (!identity) {
2416
- sendResourceError(send, requestId, "napplet identity not resolvable", "blocked-by-policy");
2417
- return;
2418
- }
2419
2430
  const controller = new AbortController();
2420
2431
  trackRequest(state, requestId, windowId, controller);
2421
2432
  try {
2422
2433
  const info = await resolveResourceInfo(options, windowId);
2423
- const item = await fetchResourceItem(options, identity, info, url, controller.signal);
2434
+ const item = await fetchResourceItem(options, windowId, info, url, controller.signal);
2424
2435
  if (controller.signal.aborted) return;
2425
2436
  if (!item.ok) {
2426
2437
  sendResourceError(send, requestId, item.message, item.error);
@@ -2447,11 +2458,6 @@ async function handleBytesMany(options, state, windowId, msg, send) {
2447
2458
  sendBytesManyError(send, requestId, "resource.bytesMany requires a non-empty urls array", "invalid-request");
2448
2459
  return;
2449
2460
  }
2450
- const identity = options.resolveIdentity(windowId);
2451
- if (!identity) {
2452
- sendBytesManyError(send, requestId, "napplet identity not resolvable", "blocked-by-policy");
2453
- return;
2454
- }
2455
2461
  let info;
2456
2462
  try {
2457
2463
  info = await resolveResourceInfo(options, windowId);
@@ -2469,7 +2475,7 @@ async function handleBytesMany(options, state, windowId, msg, send) {
2469
2475
  try {
2470
2476
  const items = [];
2471
2477
  for (const url of urls) {
2472
- const item = await fetchResourceItem(options, identity, info, url, controller.signal);
2478
+ const item = await fetchResourceItem(options, windowId, info, url, controller.signal);
2473
2479
  if (controller.signal.aborted) return;
2474
2480
  if (item.ok) {
2475
2481
  items.push({
@@ -2528,7 +2534,7 @@ function createResourceService(options) {
2528
2534
  const descriptor = {
2529
2535
  name: "resource",
2530
2536
  version: RESOURCE_SERVICE_VERSION,
2531
- description: "NAP-RESOURCE reference service \u2014 shell-proxied authenticated fetch (RESOURCE-01..06)"
2537
+ description: "NAP-RESOURCE service \u2014 runtime-resolved byte transport (RESOURCE-01..06)"
2532
2538
  };
2533
2539
  const handler = {
2534
2540
  descriptor,