@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 +2 -2
- package/dist/index.d.ts +41 -36
- package/dist/index.js +36 -30
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
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.
|
|
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
|
|
1375
|
+
* NAP-RESOURCE is a runtime-mediated byte resolver — read-only, atomic.
|
|
1376
1376
|
*
|
|
1377
|
-
*
|
|
1378
|
-
* policy
|
|
1379
|
-
*
|
|
1380
|
-
*
|
|
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
|
-
*
|
|
1384
|
-
*
|
|
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
|
-
*
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
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
|
-
*
|
|
1434
|
-
*
|
|
1439
|
+
* Runtime-supplied resource resolver. Receives the requested URL and returns
|
|
1440
|
+
* a byte-classified `Response`.
|
|
1435
1441
|
*
|
|
1436
|
-
* The
|
|
1437
|
-
*
|
|
1438
|
-
*
|
|
1439
|
-
*
|
|
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 -
|
|
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.
|
|
1491
|
-
*
|
|
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
|
|
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
|
-
*
|
|
1518
|
-
*
|
|
1519
|
-
*
|
|
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 -
|
|
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) =>
|
|
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"
|
|
2275
|
+
if (typeof options?.fetch !== "function") {
|
|
2276
2276
|
throw new Error(
|
|
2277
|
-
"[RESOURCE-01
|
|
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,
|
|
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
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
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,
|
|
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,
|
|
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
|
|
2537
|
+
description: "NAP-RESOURCE service \u2014 runtime-resolved byte transport (RESOURCE-01..06)"
|
|
2532
2538
|
};
|
|
2533
2539
|
const handler = {
|
|
2534
2540
|
descriptor,
|