@plurnk/plurnk-schemes-http 1.7.0 → 1.9.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/.env.defaults CHANGED
@@ -14,10 +14,6 @@
14
14
  PLURNK_SCHEMES_HTTP_FETCH_TIMEOUT=30000
15
15
 
16
16
  # ── Defaults (floor-set; override to change) ────────────────────────────────
17
- # Tavily Extract extraction depth ("basic" or "advanced").
18
- PLURNK_SCHEMES_HTTP_TAVILY_DEPTH=basic
19
- # Tavily Extract request timeout in ms.
20
- PLURNK_SCHEMES_HTTP_TAVILY_TIMEOUT_MS=30000
21
17
  # Per-URL freshness TTL in ms: a re-read of a URL fetched within this window
22
18
  # serves the stored copy with ZERO round-trips (no conditional GET). This makes
23
19
  # a just-materialized web entry immediately readable like any other entry.
@@ -36,6 +32,9 @@ PLURNK_SCHEMES_HTTP_SSE_MAX_BUFFER_CHARS=1048576
36
32
  PLURNK_SCHEMES_HTTP_ERROR_DETAIL_LIMIT=512
37
33
 
38
34
  # ── Optional modes (absence IS the mode — never floor-set) ──────────────────
39
- # Optional Tavily API key for HTML-to-Markdown extraction. When omitted/unset,
40
- # the installed HTML mimetype projection is used as the local floor.
41
- # TAVILY_API_KEY=
35
+ # The documented third-party materializer showcase: install the
36
+ # @plurnk/plurnk-schemes-http-tavily plugin, then select it and provide its
37
+ # key. Absence (unset) of either keeps the installed HTML projection as the
38
+ # local floor; the plugin documents its own depth/timeout knobs.
39
+ # PLURNK_SCHEMES_HTTP_MATERIALIZER=tavily-extract
40
+ # TAVILY_API_KEY=...
package/README.md CHANGED
@@ -45,9 +45,10 @@ projection ceiling returns `413`.
45
45
  - WebFetcher checks automatic byte targets and redirects; direct HTTP and
46
46
  WebSocket retain their explicit-target authority.
47
47
  - Generic GETs negotiate origin Markdown first. When the origin returns HTML,
48
- configured Tavily produces `body`; otherwise the installed HTML reader is the
49
- local route. Recoverable Tavily failures use that same reader as a `203`
50
- recovery floor. Hard provider failures do not silently change producers.
48
+ a selected materializer plugin ({§http-materializer-plugins}) produces
49
+ `body`; otherwise the installed HTML reader is the local route. Recoverable
50
+ materializer failures use that same reader as a `203` recovery floor. Hard
51
+ provider failures do not silently change producers.
51
52
  - `body`, `header`, and `html` settle independently. The selected channel
52
53
  determines operation success, so raw server source and evidence remain usable
53
54
  when body production fails, and a provider body can survive origin transport
@@ -79,11 +80,11 @@ plurnk start
79
80
  ```
80
81
 
81
82
  The shipped `.env.defaults` is the canonical operator configuration registry.
82
- To enable Tavily Extract for generic public HTML materialization, set
83
- `TAVILY_API_KEY` in `.env`. Depth and client timeout are controlled by
84
- `PLURNK_SCHEMES_HTTP_TAVILY_DEPTH` and
85
- `PLURNK_SCHEMES_HTTP_TAVILY_TIMEOUT_MS`. Without Tavily,
86
- `@plurnk/plurnk-mimetypes-text-html` supplies the local body.
83
+ To select a page-materializer plugin for generic public HTML materialization,
84
+ install it and set `PLURNK_SCHEMES_HTTP_MATERIALIZER=<id>` in `.env` (the
85
+ `@plurnk/plurnk-schemes-http-tavily` showcase plugin supplies `tavily-extract`).
86
+ Without a selection, `@plurnk/plurnk-mimetypes-text-html` supplies the local
87
+ body.
87
88
 
88
89
  ## Verify
89
90
 
package/SPEC.md CHANGED
@@ -1,14 +1,15 @@
1
1
  # plurnk-schemes-http — Specification
2
2
 
3
3
  This package owns the `http(s)://` request/response scheme, the `ws(s)://`
4
- full-duplex scheme, plus automatic entry-acquisition, Tavily Extract, and local
5
- HTML projection foundations. Both handlers implement the DB-free `SchemeCtx` author contract.
4
+ full-duplex scheme, plus automatic entry-acquisition, pluggable page
5
+ materializers, and the local HTML projection foundation. Both handlers implement
6
+ the DB-free `SchemeCtx` author contract.
6
7
 
7
8
  ## §http-manifest §1 HTTP manifest
8
9
 
9
10
  | Field | Value |
10
11
  | --------------- | ----------------------------- |
11
- | Registered name | `http` (`https` routes to it) |
12
+ | Registered name | `https` (plain `http` routes to it — supported, never advertised as a peer endpoint) |
12
13
  | Category | `data` |
13
14
  | Writers | `model`, `client` |
14
15
  | Volatile | `true` |
@@ -26,13 +27,14 @@ HTML projection foundations. Both handlers implement the DB-free `SchemeCtx` aut
26
27
  `wss` through `Ws`.
27
28
 
28
29
  Routing is not identity. `NetworkAddress` {§network-address} retains the exact
29
- addressed protocol and stores
30
- `/<host>[:<non-default-port>]<path>[?<serialized-query>]`. Query ordering,
30
+ addressed protocol and stores authority `<host>[:<non-default-port>]` separately
31
+ from pathname `<path>[?<serialized-query>]`. Query ordering,
31
32
  duplicate names, and an explicit empty `?` remain significant. The fragment is
32
33
  a Plurnk channel selector and never enters network identity or transport.
33
34
  Request metadata affects transport but not identity. URL userinfo is rejected;
34
- neither credentials nor metadata are reconstructed from `raw`. Within storage
35
- pathnames, `%28` and `%29` canonicalize to literal parentheses.
35
+ neither credentials nor metadata are reconstructed from `raw`. Within the path
36
+ component, `%28` and `%29` canonicalize to literal parentheses; the query is
37
+ preserved exactly.
36
38
 
37
39
  ## §op-surface §2 HTTP operation surface
38
40
 
@@ -66,13 +68,13 @@ flowchart TD
66
68
  origin -->|text/markdown| markdown["Use origin Markdown as body<br/>and request an HTML variant"]
67
69
  origin -->|server HTML| source["Retain exact server HTML"]
68
70
  origin -->|transport unavailable<br/>after public admission| noSource["No HTML source"]
69
- source --> eligible{"Generic public request<br/>with Tavily configured?"}
71
+ source --> eligible{"Generic public request<br/>with a materializer selected?"}
70
72
  noSource --> eligible
71
73
  eligible -->|no| local["Installed HTML projection"]
72
- eligible -->|yes| tavily["Tavily Extract Markdown"]
73
- tavily -->|success| providerBody["Tavily body"]
74
- tavily -->|recoverable failure<br/>and source exists| recovery["Local projection; body 203"]
75
- tavily -->|hard failure or<br/>no recovery source| bodyError["Body errored"]
74
+ eligible -->|yes| mat["Materializer Markdown"]
75
+ mat -->|success| providerBody["Materializer body"]
76
+ mat -->|recoverable failure<br/>and source exists| recovery["Local projection; body 203"]
77
+ mat -->|hard failure or<br/>no recovery source| bodyError["Body errored"]
76
78
  local -->|present, including empty| localBody["Local body"]
77
79
  local -->|absent or no source| bodyError
78
80
  markdown --> settle["Write complete canonical channels<br/>and durable producer results"]
@@ -93,7 +95,7 @@ but cold and warm representations pass through the same core projection.
93
95
  | Response | `body` | Auxiliary materialization | Completion |
94
96
  | --------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------- |
95
97
  | Negotiated origin Markdown | Exact origin Markdown | Independently acquired server source in `html` when found | Static representation, then core projection |
96
- | HTML-page production | Tavily Markdown, local Markdown, or independent error | Exact server source in `html` when origin supplied it | Static representation, then core projection |
98
+ | HTML-page production | Materializer Markdown, local Markdown, or independent error | Exact server source in `html` when origin supplied it | Static representation, then core projection |
97
99
  | `text/event-stream` | One `data` value plus newline per `text/plain` chunk | Initial response in `header` | `102`; origin close settles subscription |
98
100
  | No response body | Present empty text | Response and package metadata in `header` | Static representation; body READ is `204` |
99
101
  | Origin HTTP `4xx`/`5xx` | Preserve available origin or independently produced text | Exact response evidence; origin-backed channels errored | Selected channel's durable outcome |
@@ -138,52 +140,51 @@ mutation responses retain incremental streaming.
138
140
  | Configured binary with reader | Apply the bounded byte projection | Derived Unicode plus source type and identity |
139
141
  | Configured binary without reader | Cancel without retaining bytes | `null`; the consumer applies its absence policy |
140
142
  | Binary input exceeds the common bound | Cancel and preserve the typed cause | `WebMaterializationError` caused by `ProjectionInputLimitError` |
141
- | Negotiated origin `text/markdown` | Accept it without Tavily | Exact Markdown in `body`; independently requested source in `html` |
142
- | Eligible HTML with Tavily configured | Use Tavily as the body producer | Tavily Markdown in `body`; origin server source in `html` |
143
- | Ineligible HTML or Tavily not configured | Use the installed HTML projection | Local Markdown in `body`; origin server source in `html` |
144
- | Recoverable Tavily failure with source HTML | Use the installed projection as recovery | Local Markdown in `body` with terminal `203`; source in `html` |
145
- | Hard Tavily failure | Preserve evidence; do not run local recovery | `body` errored; independently successful `html`/`header` survive |
146
- | Tavily success after origin transport loss | Preserve provider body and origin failure | `body` static; `html` errored |
143
+ | Negotiated origin `text/markdown` | Accept it without the materializer | Exact Markdown in `body`; independently requested source in `html` |
144
+ | Eligible HTML with a materializer selected | Use the materializer as the body producer | Materializer Markdown in `body`; origin server source in `html` |
145
+ | Ineligible HTML or no materializer selected | Use the installed HTML projection | Local Markdown in `body`; origin server source in `html` |
146
+ | Recoverable materializer failure with HTML | Use the installed projection as recovery | Local Markdown in `body` with terminal `203`; source in `html` |
147
+ | Hard materializer failure | Preserve evidence; do not run local recovery | `body` errored; independently successful `html`/`header` survive |
148
+ | Materializer success after origin loss | Preserve provider body and origin failure | `body` static; `html` errored |
147
149
  | Projection implementation throws | Stop and preserve the cause | `WebMaterializationError` with stage `projection` |
148
150
 
149
151
  A projection object is present even when its content is `""`; only `null`
150
152
  denotes absence. A materialization exception retains its original `cause`; it
151
153
  never enters the absence channel.
152
154
 
153
- #### §tavily-extract Tavily Extract boundary
154
-
155
- Tavily is eligible only for a credential-free generic request whose target has
156
- been admitted as public. Authored request metadata—including an authored
157
- `Accept` field—makes the request ineligible. The package-generated Markdown
158
- negotiation, browser-compatible `User-Agent`, and conditional cache fields are
159
- transport mechanics, not authored metadata. No target headers or origin
160
- credentials cross the provider boundary.
155
+ #### §http-materializer-plugins Materializer plugins
156
+
157
+ `PLURNK_SCHEMES_HTTP_MATERIALIZER` selects one discovered `http-materializer`
158
+ plugin by id; unset means the installed HTML projection is the only body
159
+ producer. A materializer package declares
160
+ `plurnk: { kind: "http-materializer", materializers: [{ id, module }] }` and
161
+ exports one `HttpMaterializer` per entry under the executor family's
162
+ discovery, trust, and one-flat-id-namespace rules ({§plugin-discovery}). The
163
+ selected materializer is consulted only for a credential-free generic request
164
+ whose target has been admitted as public. Authored request metadata—including
165
+ an authored `Accept` field—makes the request ineligible. The package-generated
166
+ Markdown negotiation, browser-compatible `User-Agent`, and conditional cache
167
+ fields are transport mechanics, not authored metadata. No target headers or
168
+ origin credentials cross the materializer boundary.
161
169
 
162
170
  When no `Accept` is authored, origin acquisition offers
163
171
  `text/markdown, text/html;q=0.9, */*;q=0.1`. An origin `text/markdown`
164
- representation wins and Tavily is skipped; a second origin request with
165
- `Accept: text/html` may populate `html`. An authored `Accept` value is sent
166
- unchanged and makes any returned HTML use the local projection route.
167
-
168
- One provider request is `POST https://api.tavily.com/extract` with bearer
169
- authentication and exactly one URL, configured `basic` or `advanced` depth,
170
- `format: "markdown"`, and `include_usage: true`. A package-owned abort timeout
171
- bounds the call. Success requires Markdown plus `request_id` and
172
- `usage.credits`; those facts remain durable in `header`.
173
-
174
- | Provider outcome | Classification | Body behavior when server HTML exists |
175
- | ---------------------------------------- | -------------- | ------------------------------------------------------ |
176
- | Success with required evidence | Success | Use Tavily Markdown |
177
- | Caller cancellation | Cancelled | Return exact `499`; no recovery |
178
- | Client timeout or transport failure | Recoverable | Local projection, terminal `203` |
179
- | `429`, `5xx`, or `failed_results` | Recoverable | Local projection, terminal `203` |
180
- | `401` or `403` | Hard | `tavily-authentication-failed`; no local projection |
181
- | Other `4xx` | Hard | `tavily-provider-rejected`; no local projection |
182
- | Malformed success or missing evidence | Hard | `tavily-invalid-response`; no local projection |
183
-
184
- Without server HTML, no local recovery input exists. A Tavily success can still
185
- close `body`, but every Tavily failure remains the body failure and `html`
186
- closes errored.
172
+ representation wins and the materializer is skipped; a second origin request
173
+ with `Accept: text/html` may populate `html`. An authored `Accept` value is
174
+ sent unchanged and makes any returned HTML use the local projection route.
175
+
176
+ The materializer's `eligible(url)` returns its identity string when it will
177
+ serve the URL (its credentials and supported targets are its own); `extract`
178
+ returns `success` (its sanitized Markdown body plus its own evidence headers),
179
+ `recoverable` (local projection fallback with the materializer's evidence and
180
+ terminal `203`), or `hard` (an exact materializer-owned Problem; no local
181
+ recovery). A materializer `extract` throw is the materialization failure, never
182
+ a silent fallback. A selected id with no installed provider fails hard rather
183
+ than degrading silently.
184
+
185
+ Without server HTML, no local recovery input exists. A materializer success can
186
+ still close `body`, but every materializer failure remains the body failure and
187
+ `html` closes errored.
187
188
 
188
189
  #### §http-channel-outcomes Channel outcomes
189
190
 
@@ -201,9 +202,19 @@ representation. A missing source variant is an explicit empty `errored`
201
202
  channel, never an absent fact that later cache use can reinterpret as successful
202
203
  empty content. A direct non-success HTTP response likewise remains available as
203
204
  evidence, while each origin-backed channel carries an exact
204
- `http-response-status` producer Problem. A Tavily-produced body and the
205
+ `http-response-status` producer Problem. A materializer-produced body and the
205
206
  acquisition `header` remain independent of an unavailable origin source.
206
207
 
208
+ §http-llms-txt **Origin llms.txt companions.** After a successful generic GET
209
+ materialization, the scheme opportunistically acquires `<origin>/llms.txt`
210
+ once per origin per TTL window and materializes it as its own origin entry
211
+ (`https://host/llms.txt`), surfaceable by FIND. A missing companion, any
212
+ non-2xx, binary content, or a transport failure is quiet: the companion is
213
+ never fabricated, the failed probe is remembered (not retried per READ), and
214
+ the piggyback never fails the READ that carried it. The companion itself
215
+ never recurses. This is lazy by construction — one extra request only when
216
+ an origin is first being read.
217
+
207
218
  ## §http-status §4 HTTP status mapping
208
219
 
209
220
  | Outcome | Operation status |
@@ -214,7 +225,7 @@ acquisition `header` remain independent of an unavailable origin source.
214
225
  | Selected origin-backed channel received HTTP `4xx`/`5xx` | Exact durable `http-response-status` Problem |
215
226
  | Selected HTML-page channel fails | That channel's exact durable producer Problem |
216
227
  | Local HTML projection is absent | `422` (`no-readable-projection`) |
217
- | Recoverable Tavily failure uses local projection | Readable body with durable status `203` |
228
+ | Recoverable materializer failure uses local projection | Readable body with durable status `203` |
218
229
  | Finite textual or projected-binary response | Universal READ result |
219
230
  | Finite empty text response | `204` |
220
231
  | Finite binary response has no readable projection | `415` (`binary-response-unsupported`) |
@@ -235,8 +246,8 @@ acquisition `header` remain independent of an unavailable origin source.
235
246
  An HTTP error status is still a successfully acquired direct response: its
236
247
  content and response evidence are preserved, and origin-backed channels carry
237
248
  that exact status as durable producer evidence. HTML still enters page
238
- production, so a successful Tavily `body` can remain readable while the origin
239
- `html` records the HTTP error. Automatic WebFetcher acquisition instead treats
249
+ production, so a successful materializer `body` can remain readable while the
250
+ origin `html` records the HTTP error. Automatic WebFetcher acquisition instead treats
240
251
  a non-2xx response as unavailable. Handler failures use RFC 9457 Problem Details.
241
252
  Caught direct-acquisition diagnostics are bounded by
242
253
  `PLURNK_SCHEMES_HTTP_ERROR_DETAIL_LIMIT` in model-facing detail while complete
@@ -260,7 +271,7 @@ handler does not sniff or guess unknown bytes.
260
271
  | ----------------- | --------------------------------------------------------------------------------------------------------- |
261
272
  | Platform | Node ≥26 native fetch, streams, abort signals, decoding, DNS, and `WebSocket` |
262
273
  | SSE | `eventsource-parser` for bounded WHATWG event-stream framing |
263
- | Tavily Extract | Optional Tavily Extract API primitive when `TAVILY_API_KEY` is present |
274
+ | Materializer plugins | Discovered `http-materializer` packages selected by `PLURNK_SCHEMES_HTTP_MATERIALIZER` {§http-materializer-plugins} |
264
275
 
265
276
  ### §http-config Operator configuration
266
277
 
@@ -268,9 +279,7 @@ handler does not sniff or guess unknown bytes.
268
279
  | --------------------- | -------------------------------------------------------------------------------------------------------- |
269
280
  | Canonical registry | Shipped `.env.defaults`; the daemon assembles it as a set-if-unset floor {§operator-config-env-defaults} |
270
281
  | Required values | Missing or invalid required values fail at readiness/owning read; code carries no hidden fallback |
271
- | Tavily API Key | Optional `# TAVILY_API_KEY=`; enables Tavily Extract for eligible public HTML |
272
- | Tavily Extract Depth | `PLURNK_SCHEMES_HTTP_TAVILY_DEPTH=basic`; only `basic` or `advanced` |
273
- | Tavily client timeout | Positive `PLURNK_SCHEMES_HTTP_TAVILY_TIMEOUT_MS=30000` |
282
+ | Materializer selection | `PLURNK_SCHEMES_HTTP_MATERIALIZER=` names a discovered materializer id; unset = local projection only |
274
283
 
275
284
  ### §automatic-fetch-check Automatic acquisition URL check
276
285
 
@@ -279,7 +288,7 @@ acquisition, it accepts credential-free HTTP(S) targets only when every
279
288
  resolved address is ordinary globally reachable unicast, and repeats the check
280
289
  before every manually followed redirect. A refused or unresolvable target is
281
290
  the ordinary unavailable `null` result. A transport failure after public
282
- admission may still enter Tavily page production.
291
+ admission may still enter materializer page production.
283
292
 
284
293
  Direct HTTP operations and WebSocket connections do not use this check. Loopback,
285
294
  private, and link-local destinations are therefore valid explicit targets.
@@ -296,12 +305,12 @@ check therefore makes no DNS-rebinding or total-egress claim.
296
305
 
297
306
  | Concern | Contract |
298
307
  | -------------- | ---------------------------------------------------------------------------------------------------------- |
299
- | Direct gate | Explicit targets use native origin transport; only a generic public request grants Tavily authority |
308
+ | Direct gate | Explicit targets use native origin transport; only a generic public request grants materializer authority |
300
309
  | Automatic gate | Target and redirects require public admission; accepted generic HTML follows the same page producer |
301
- | Body owner | Origin Markdown wins; otherwise configured eligible Tavily is structural, with one local projection floor |
310
+ | Body owner | Origin Markdown wins; otherwise the configured eligible materializer is structural, with one local projection floor |
302
311
  | Source owner | `html` is exact server-source HTML; it is never provider-generated or DOM-generated |
303
312
  | Projection | A present projection, including `""`, becomes `body`; `null` alone means absence |
304
- | Cancellation | One caller signal spans origin, auxiliary origin, projection, and Tavily work |
313
+ | Cancellation | One caller signal spans origin, auxiliary origin, projection, and materializer work |
305
314
 
306
315
  ### §host-rewrite Acquisition target rewrite
307
316
 
@@ -317,7 +326,7 @@ are checked under {§automatic-fetch-check} only when `WebFetcher` acquires them
317
326
  Acquired responses append package-owned `x-plurnk-request-method`,
318
327
  `x-plurnk-fetched-at`, and `x-plurnk-cache-variant` fields after origin headers.
319
328
  Page bodies append `x-plurnk-materializer-id`; local projections append
320
- `x-plurnk-projection-id`; Tavily appends status, route, request, usage, timing,
329
+ `x-plurnk-projection-id`; The materializer appends its own evidence headers;
321
330
  and bounded failure evidence. Only package metadata after the acquisition stamp
322
331
  is authoritative.
323
332
  Plurnk stores one representation per canonical URL rather than a variant set:
@@ -388,16 +397,16 @@ different source representation.
388
397
 
389
398
  A stored derived representation requires the current projection identity. A
390
399
  stored page body also requires its current route: negotiated origin Markdown,
391
- local projection while Tavily is unconfigured, metadata-ineligible local
392
- projection, configured Tavily depth/version, or the corresponding recoverable
393
- local-fallback route. Enabling Tavily or changing depth invalidates the affected
394
- route.
400
+ local projection while no materializer is selected, metadata-ineligible local
401
+ projection, the configured materializer id, or the corresponding recoverable
402
+ local-fallback route. Selecting or changing the materializer invalidates the
403
+ affected route.
395
404
 
396
405
  Once a page representation leaves its fresh window, Plurnk performs complete
397
406
  reacquisition without old origin validators. Origin `304` can certify origin
398
- bytes, but it cannot certify a composite that may also contain a fresh Tavily
399
- extraction, a newly negotiated Markdown representation, or an independently
400
- acquired HTML variant. Projection or materializer mismatch likewise invalidates
407
+ bytes, but it cannot certify a composite that may also contain a fresh
408
+ materializer extraction, a newly negotiated Markdown representation, or an
409
+ independently acquired HTML variant. Projection or materializer mismatch likewise invalidates
401
410
  body, TTL, and validators together.
402
411
 
403
412
  POST, PUT, and DELETE responses retain their method marker but cannot satisfy a
@@ -435,7 +444,8 @@ operation is cancelled.
435
444
  Top-level `null` is an automatic-acquisition liveness value rather than a thrown
436
445
  failure. Caller cancellation is not liveness: a pre-aborted caller fails before
437
446
  acquisition and a caller abort wins with its exact reason. Package-owned origin
438
- and Tavily timeouts are ordinary classified acquisition outcomes. WebFetcher
447
+ timeouts are ordinary classified acquisition outcomes; materializer timeouts
448
+ are the materializer's own classified outcomes. WebFetcher
439
449
  owns no entry identity, registry selection, selected-channel policy, or query
440
450
  policy; its consumers supply those boundaries and the projection capability.
441
451
 
@@ -482,8 +492,9 @@ Dispatcher phase order ({§op-mode-phases}) runs EDIT before READ and SEND after
482
492
  READ, so an EDIT requires an owner opened before the turn while SEND can follow
483
493
  the opening READ in the same turn.
484
494
 
485
- The in-instance registry is keyed by workspace, addressed protocol, and
486
- canonical network pathname. The claim remains registered through terminal
495
+ The in-instance registry is keyed by the owning Worker, addressed protocol,
496
+ and canonical network pathname, matching the Worker-owned durable entry so
497
+ independent Workers never share a live handle. The claim remains registered through terminal
487
498
  cleanup, so a new READ cannot overlap an owner's subscription settlement. Every
488
499
  terminal path drains retained owner work, closes the transport when necessary,
489
500
  closes the durable subscription, then releases the claim. A persistence failure
@@ -1 +1 @@
1
- {"version":3,"file":"Http.d.ts","sourceRoot":"","sources":["../src/Http.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACR,SAAS,EAGT,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,gCAAgC,EAChC,+BAA+B,EAC/B,aAAa,EACb,qBAAqB,EACrB,aAAa,EAOhB,MAAM,wBAAwB,CAAC;AAuMhC,MAAM,CAAC,OAAO,OAAO,IAAK,YAAW,aAAa;;IAC9C,MAAM,CAAC,QAAQ,EAAE,cAAc,CA2B7B;IAIF,cAGC;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAE3B;IAKK,qBAAqB,CACvB,OAAO,EAAE,gCAAgC,EACzC,GAAG,EAAE,SAAS,GACf,OAAO,CAAC,+BAA+B,CAAC,CAS1C;IAkNK,SAAS,CAAC,UAAU,EAAE,SAAS,qBAAqB,EAAE,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAqCxG;IAEK,IAAI,CAAC,SAAS,EAAE,qBAAqB,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAEvF;IAIK,IAAI,CAAC,SAAS,EAAE,aAAa,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAS/E;IAQK,IAAI,CAAC,SAAS,EAAE,aAAa,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAoC/E;CAypBJ"}
1
+ {"version":3,"file":"Http.d.ts","sourceRoot":"","sources":["../src/Http.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACR,SAAS,EAIT,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,gCAAgC,EAChC,+BAA+B,EAC/B,aAAa,EACb,qBAAqB,EACrB,aAAa,EAOhB,MAAM,wBAAwB,CAAC;AAuMhC,MAAM,CAAC,OAAO,OAAO,IAAK,YAAW,aAAa;;IAC9C,MAAM,CAAC,QAAQ,EAAE,cAAc,CAiC7B;IAQF,cAGC;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAE3B;IAKK,qBAAqB,CACvB,OAAO,EAAE,gCAAgC,EACzC,GAAG,EAAE,SAAS,GACf,OAAO,CAAC,+BAA+B,CAAC,CAS1C;IAsRK,SAAS,CAAC,UAAU,EAAE,SAAS,qBAAqB,EAAE,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAqCxG;IAEK,IAAI,CAAC,SAAS,EAAE,qBAAqB,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAEvF;IAIK,IAAI,CAAC,SAAS,EAAE,aAAa,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAS/E;IAQK,IAAI,CAAC,SAAS,EAAE,aAAa,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAoC/E;CAgrBJ"}
package/dist/Http.js CHANGED
@@ -6,7 +6,7 @@ import { createParser } from "eventsource-parser";
6
6
  import { MimetypeClassifier, NetworkAddress, ProjectionInputLimitError, Results, } from "@plurnk/plurnk-schemes";
7
7
  import { readFile } from "node:fs/promises";
8
8
  import ErrorDetail from "./ErrorDetail.js";
9
- import WebFetcher, { DEFAULT_WEB_UA, CACHE_VARIANT_HEADER, MATERIALIZER_ID_HEADER, PROJECTION_ID_HEADER, TAVILY_CREDITS_HEADER, TAVILY_REQUEST_ID_HEADER, cacheVariantEvidence, classifyCacheVariant, WebMaterializationError, } from "./WebFetcher.js";
9
+ import WebFetcher, { DEFAULT_WEB_UA, CACHE_VARIANT_HEADER, MATERIALIZER_ID_HEADER, PROJECTION_ID_HEADER, cacheVariantEvidence, classifyCacheVariant, WebMaterializationError, } from "./WebFetcher.js";
10
10
  import { responseMimetype } from "./ContentType.js";
11
11
  import { requireNonNegativeIntegerEnv as requireNumEnv } from "./Config.js";
12
12
  // The channel the response body streams into, and the header metadata channel.
@@ -134,10 +134,15 @@ const httpDate = (value, now = Date.now()) => {
134
134
  };
135
135
  // The package-shipped model teaching is loaded verbatim and fails hard if absent.
136
136
  // The relative path is identical from src/ during development and dist/ after build.
137
- const documentation = await readFile(new URL("../docs/http.md", import.meta.url), "utf-8");
137
+ const documentation = await readFile(new URL("../docs/https.md", import.meta.url), "utf-8");
138
+ const LLMS_TEXT_ATTEMPT_TTL_MS = 3_600_000;
138
139
  class Http {
139
140
  static manifest = {
140
- name: "http",
141
+ // "https" is the registered face — plain http folds into it exactly as
142
+ // ws folds into wss ({§http-manifest}); supported, never advertised as
143
+ // a peer endpoint.
144
+ name: "https",
145
+ authority: "resource",
141
146
  // Channel mimetypes here are SEED DEFAULTS (pre-fetch placeholders).
142
147
  // body is retyped per-call via notifyChunk's mimetype arg — to the real
143
148
  // response Content-Type or the configured readable projection type;
@@ -146,6 +151,8 @@ class Http {
146
151
  channels: { [BODY]: "application/octet-stream", [HEADER]: "text/plain", html: "text/html" },
147
152
  defaultChannel: BODY,
148
153
  category: "data",
154
+ entryOwner: "worker",
155
+ inherit: "snapshot",
149
156
  writableBy: ["model", "client"],
150
157
  volatile: true, // remote content can change between fetches
151
158
  modelVisible: true,
@@ -166,6 +173,10 @@ class Http {
166
173
  };
167
174
  #errorDetailLimit;
168
175
  #webFetcher;
176
+ // {§http-llms-txt} — one opportunistic origin-companion attempt per TTL
177
+ // window; success and failure share the timer so a 404 does not re-probe
178
+ // on every READ.
179
+ #llmsTextAttempts = new Map();
169
180
  constructor() {
170
181
  this.#errorDetailLimit = ErrorDetail.configuredLimit();
171
182
  this.#webFetcher = new WebFetcher();
@@ -264,7 +275,49 @@ class Http {
264
275
  });
265
276
  }
266
277
  const responseHeaders = fetched.responseHeaders ?? [];
267
- if (!_a.#revalidationCorresponds(cachedHeader, new Headers(responseHeaders.map(([name, value]) => [name, value])))) {
278
+ if (_a.#revalidationCorresponds(cachedHeader, new Headers(responseHeaders.map(([name, value]) => [name, value])))) {
279
+ const channels = {
280
+ ...cached.channels,
281
+ [HEADER]: {
282
+ ...cached.channels[HEADER],
283
+ content: _a.#refreshAfter304(cachedHeader, responseHeaders, requestHeaders),
284
+ },
285
+ };
286
+ const written = await ctx.entries.write(pathname, {
287
+ channels,
288
+ });
289
+ if (Results.isErrorStatus(written.status))
290
+ return _a.#passthrough(written);
291
+ return { status: 200 };
292
+ }
293
+ // {§revalidation} — a genuinely mismatched 304 (different opaque
294
+ // tags): the conditional is the problem, so fall back to one
295
+ // unconditional GET and acquire normally instead of surfacing
296
+ // an unrecoverable 502. A second 304 has no way out and is the
297
+ // honest failure.
298
+ try {
299
+ fetched = await this.#webFetcher.fetch(url, {
300
+ signal: ctx.signal,
301
+ headers: requestHeaders,
302
+ guarded: false,
303
+ acceptHttpErrors: true,
304
+ preserveUnavailable: true,
305
+ });
306
+ }
307
+ catch (error) {
308
+ if (ctx.signal?.aborted === true && error === ctx.signal.reason) {
309
+ return _a.#cancelled(url, "GET");
310
+ }
311
+ throw error;
312
+ }
313
+ if (fetched === null) {
314
+ return _a.#bad(404, "http", "not-materialized", `The URL ${url} could not be materialized.`, {
315
+ target: url,
316
+ stage: "acquisition",
317
+ retryable: true,
318
+ });
319
+ }
320
+ if (fetched.status === 304) {
268
321
  return _a.#bad(502, "http", "fetch-failed", `HTTP GET ${url} returned 304 without identifying the stored representation nominated for revalidation.`, {
269
322
  target: url,
270
323
  method: "GET",
@@ -272,19 +325,7 @@ class Http {
272
325
  retryable: true,
273
326
  });
274
327
  }
275
- const channels = {
276
- ...cached.channels,
277
- [HEADER]: {
278
- ...cached.channels[HEADER],
279
- content: _a.#refreshAfter304(cachedHeader, responseHeaders, requestHeaders),
280
- },
281
- };
282
- const written = await ctx.entries.write(pathname, {
283
- channels,
284
- });
285
- if (Results.isErrorStatus(written.status))
286
- return _a.#passthrough(written);
287
- return { status: 200 };
328
+ // Non-304: fall through to ordinary acquisition below.
288
329
  }
289
330
  if (fetched.mimetype === "text/event-stream"
290
331
  && fetched.response?.body !== null
@@ -318,8 +359,46 @@ class Http {
318
359
  });
319
360
  if (Results.isErrorStatus(written.status))
320
361
  return _a.#passthrough(written);
362
+ await this.#piggybackLlmsText(address, ctx);
321
363
  return { status: 200 };
322
364
  }
365
+ // {§http-llms-txt} — after a successful generic GET materialization,
366
+ // acquire <origin>/llms.txt once per TTL window and materialize it as its
367
+ // own https entry. Any failure is quiet: the companion never fails the
368
+ // READ that piggybacked it, and the companion itself never recurses.
369
+ async #piggybackLlmsText(address, ctx) {
370
+ const url = new URL(address.url);
371
+ if (url.pathname === "/llms.txt")
372
+ return;
373
+ const origin = url.origin;
374
+ const last = this.#llmsTextAttempts.get(origin);
375
+ if (last !== undefined && Date.now() - last < LLMS_TEXT_ATTEMPT_TTL_MS)
376
+ return;
377
+ this.#llmsTextAttempts.set(origin, Date.now());
378
+ const llmsUrl = `${origin}/llms.txt`;
379
+ try {
380
+ const fetched = await this.#webFetcher.fetch(llmsUrl, {
381
+ signal: ctx.signal,
382
+ guarded: false,
383
+ acceptHttpErrors: true,
384
+ preserveUnavailable: true,
385
+ });
386
+ if (fetched === null)
387
+ return;
388
+ // A missing companion (404) or any non-2xx is quiet — no entry.
389
+ if ((fetched.status ?? 200) >= 400)
390
+ return;
391
+ const materialized = await WebFetcher.materialize(fetched, ctx.projection, ctx.signal);
392
+ if (materialized === null)
393
+ return;
394
+ await ctx.entries.write("/llms.txt", {
395
+ channels: WebFetcher.materializedChannels(materialized, { url: llmsUrl, method: "GET" }),
396
+ });
397
+ }
398
+ catch {
399
+ // Quiet by contract: 404, guard rejection, network error, binary.
400
+ }
401
+ }
323
402
  async #openEventStream(address, fetched, ctx) {
324
403
  const response = fetched.response;
325
404
  if (response?.body === null || response?.body === undefined) {
@@ -860,11 +939,9 @@ class Http {
860
939
  const isHtml = MimetypeClassifier.isHtml(sourceMimetype);
861
940
  const materializerIdentity = _a.#materializerIdentity(header);
862
941
  if (materializerIdentity !== undefined) {
863
- if (!WebFetcher.materializerCurrent(materializerIdentity))
864
- return false;
865
- if (materializerIdentity.startsWith("tavily-extract:")
866
- || materializerIdentity === "origin-markdown:v1")
867
- return true;
942
+ // {§http-materializer-plugins} — a stored body produced by the current
943
+ // configured materializer (or the built-in producers) is current.
944
+ return WebFetcher.materializerCurrent(materializerIdentity);
868
945
  }
869
946
  if (isHtml && materializerIdentity === undefined)
870
947
  return false;
@@ -886,15 +963,34 @@ class Http {
886
963
  ...retained.map(({ name, value }) => [name, value]),
887
964
  ...updates,
888
965
  ];
889
- const packageEvidence = [
890
- MATERIALIZER_ID_HEADER,
891
- PROJECTION_ID_HEADER,
892
- TAVILY_REQUEST_ID_HEADER,
893
- TAVILY_CREDITS_HEADER,
894
- ].flatMap((field) => {
966
+ // Materializer/provider evidence headers are plugin-owned (unknown names);
967
+ // a 304 does not re-materialize the stored body, so its evidence is
968
+ // retained verbatim beside the rebuilt framework fields. Framework-owned
969
+ // x-plurnk-* fields (cache-variant, materializer/projection ids, method,
970
+ // fetched-at stamp) are rebuilt or omitted, never duplicated stale.
971
+ const rebuilt = [MATERIALIZER_ID_HEADER, PROJECTION_ID_HEADER].flatMap((field) => {
895
972
  const value = _a.#packageHeaderValue(header, field);
896
973
  return value === undefined ? [] : [`${field}: ${value}`];
897
974
  });
975
+ const frameworkOwned = new Set([
976
+ CACHE_VARIANT_HEADER.toLowerCase(),
977
+ MATERIALIZER_ID_HEADER.toLowerCase(),
978
+ PROJECTION_ID_HEADER.toLowerCase(),
979
+ REQUEST_METHOD.toLowerCase(),
980
+ FETCHED_AT.toLowerCase(),
981
+ ]);
982
+ const packageEvidence = [
983
+ ...rebuilt,
984
+ ...lines.slice(1).filter((line) => {
985
+ const colon = line.indexOf(":");
986
+ if (colon <= 0)
987
+ return false;
988
+ const name = line.slice(0, colon).toLowerCase();
989
+ return name.startsWith("x-plurnk-")
990
+ && !frameworkOwned.has(name)
991
+ && !updatedNames.has(name);
992
+ }),
993
+ ];
898
994
  return [
899
995
  lines[0] ?? "HTTP 200 OK",
900
996
  ...retained.map(({ line }) => line),
@@ -916,8 +1012,11 @@ class Http {
916
1012
  : null;
917
1013
  if (responseTag === null || storedTag === null)
918
1014
  return false;
919
- return responseTag.opaque === storedTag.opaque
920
- && (responseTag.weak || !storedTag.weak);
1015
+ // {§revalidation} — If-None-Match correspondence is a weak
1016
+ // comparison (RFC 9110 §8.8.3.2): opaque tags match regardless of
1017
+ // the W/ prefix, so an origin that upgrades a stored weak etag to
1018
+ // a strong etag on the 304 still corresponds.
1019
+ return responseTag.opaque === storedTag.opaque;
921
1020
  }
922
1021
  const responseModifiedValue = responseHeaders.get("last-modified");
923
1022
  const storedModifiedValues = _a.#originValues(fields, "last-modified");
@@ -949,7 +1048,7 @@ class Http {
949
1048
  return out;
950
1049
  }
951
1050
  static #passthrough(result) {
952
- return Results.assert({ ...result, shape: "passthrough" });
1051
+ return Results.assertChannelProducerResult({ ...result, shape: "passthrough" });
953
1052
  }
954
1053
  static #cancelled(url, method) {
955
1054
  return _a.#bad(499, "http", "cancelled", `HTTP ${method} ${url} was cancelled.`, {