@plurnk/plurnk-schemes-http 1.7.0 → 1.8.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 +6 -7
- package/README.md +9 -8
- package/SPEC.md +75 -66
- package/dist/Http.d.ts.map +1 -1
- package/dist/Http.js +121 -28
- package/dist/Http.js.map +1 -1
- package/dist/Materializer.d.ts +49 -0
- package/dist/Materializer.d.ts.map +1 -0
- package/dist/Materializer.js +112 -0
- package/dist/Materializer.js.map +1 -0
- package/dist/WebFetcher.d.ts +4 -11
- package/dist/WebFetcher.d.ts.map +1 -1
- package/dist/WebFetcher.js +70 -88
- package/dist/WebFetcher.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/docs/http.md +14 -10
- package/docs/wss.md +4 -0
- package/package.json +7 -2
- package/dist/TavilyExtract.d.ts +0 -35
- package/dist/TavilyExtract.d.ts.map +0 -1
- package/dist/TavilyExtract.js +0 -204
- package/dist/TavilyExtract.js.map +0 -1
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
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
83
|
-
|
|
84
|
-
`
|
|
85
|
-
`
|
|
86
|
-
|
|
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,8 +1,9 @@
|
|
|
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,
|
|
5
|
-
HTML projection
|
|
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
|
|
|
@@ -66,13 +67,13 @@ flowchart TD
|
|
|
66
67
|
origin -->|text/markdown| markdown["Use origin Markdown as body<br/>and request an HTML variant"]
|
|
67
68
|
origin -->|server HTML| source["Retain exact server HTML"]
|
|
68
69
|
origin -->|transport unavailable<br/>after public admission| noSource["No HTML source"]
|
|
69
|
-
source --> eligible{"Generic public request<br/>with
|
|
70
|
+
source --> eligible{"Generic public request<br/>with a materializer selected?"}
|
|
70
71
|
noSource --> eligible
|
|
71
72
|
eligible -->|no| local["Installed HTML projection"]
|
|
72
|
-
eligible -->|yes|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
eligible -->|yes| mat["Materializer Markdown"]
|
|
74
|
+
mat -->|success| providerBody["Materializer body"]
|
|
75
|
+
mat -->|recoverable failure<br/>and source exists| recovery["Local projection; body 203"]
|
|
76
|
+
mat -->|hard failure or<br/>no recovery source| bodyError["Body errored"]
|
|
76
77
|
local -->|present, including empty| localBody["Local body"]
|
|
77
78
|
local -->|absent or no source| bodyError
|
|
78
79
|
markdown --> settle["Write complete canonical channels<br/>and durable producer results"]
|
|
@@ -93,7 +94,7 @@ but cold and warm representations pass through the same core projection.
|
|
|
93
94
|
| Response | `body` | Auxiliary materialization | Completion |
|
|
94
95
|
| --------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------- |
|
|
95
96
|
| Negotiated origin Markdown | Exact origin Markdown | Independently acquired server source in `html` when found | Static representation, then core projection |
|
|
96
|
-
| HTML-page production |
|
|
97
|
+
| 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
98
|
| `text/event-stream` | One `data` value plus newline per `text/plain` chunk | Initial response in `header` | `102`; origin close settles subscription |
|
|
98
99
|
| No response body | Present empty text | Response and package metadata in `header` | Static representation; body READ is `204` |
|
|
99
100
|
| 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 +139,51 @@ mutation responses retain incremental streaming.
|
|
|
138
139
|
| Configured binary with reader | Apply the bounded byte projection | Derived Unicode plus source type and identity |
|
|
139
140
|
| Configured binary without reader | Cancel without retaining bytes | `null`; the consumer applies its absence policy |
|
|
140
141
|
| Binary input exceeds the common bound | Cancel and preserve the typed cause | `WebMaterializationError` caused by `ProjectionInputLimitError` |
|
|
141
|
-
| Negotiated origin `text/markdown` | Accept it without
|
|
142
|
-
| Eligible HTML with
|
|
143
|
-
| Ineligible HTML or
|
|
144
|
-
| Recoverable
|
|
145
|
-
| Hard
|
|
146
|
-
|
|
|
142
|
+
| Negotiated origin `text/markdown` | Accept it without the materializer | Exact Markdown in `body`; independently requested source in `html` |
|
|
143
|
+
| Eligible HTML with a materializer selected | Use the materializer as the body producer | Materializer Markdown in `body`; origin server source in `html` |
|
|
144
|
+
| Ineligible HTML or no materializer selected | Use the installed HTML projection | Local Markdown in `body`; origin server source in `html` |
|
|
145
|
+
| Recoverable materializer failure with HTML | Use the installed projection as recovery | Local Markdown in `body` with terminal `203`; source in `html` |
|
|
146
|
+
| Hard materializer failure | Preserve evidence; do not run local recovery | `body` errored; independently successful `html`/`header` survive |
|
|
147
|
+
| Materializer success after origin loss | Preserve provider body and origin failure | `body` static; `html` errored |
|
|
147
148
|
| Projection implementation throws | Stop and preserve the cause | `WebMaterializationError` with stage `projection` |
|
|
148
149
|
|
|
149
150
|
A projection object is present even when its content is `""`; only `null`
|
|
150
151
|
denotes absence. A materialization exception retains its original `cause`; it
|
|
151
152
|
never enters the absence channel.
|
|
152
153
|
|
|
153
|
-
#### §
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
154
|
+
#### §http-materializer-plugins Materializer plugins
|
|
155
|
+
|
|
156
|
+
`PLURNK_SCHEMES_HTTP_MATERIALIZER` selects one discovered `http-materializer`
|
|
157
|
+
plugin by id; unset means the installed HTML projection is the only body
|
|
158
|
+
producer. A materializer package declares
|
|
159
|
+
`plurnk: { kind: "http-materializer", materializers: [{ id, module }] }` and
|
|
160
|
+
exports one `HttpMaterializer` per entry under the executor family's
|
|
161
|
+
discovery, trust, and one-flat-id-namespace rules ({§plugin-discovery}). The
|
|
162
|
+
selected materializer is consulted only for a credential-free generic request
|
|
163
|
+
whose target has been admitted as public. Authored request metadata—including
|
|
164
|
+
an authored `Accept` field—makes the request ineligible. The package-generated
|
|
165
|
+
Markdown negotiation, browser-compatible `User-Agent`, and conditional cache
|
|
166
|
+
fields are transport mechanics, not authored metadata. No target headers or
|
|
167
|
+
origin credentials cross the materializer boundary.
|
|
161
168
|
|
|
162
169
|
When no `Accept` is authored, origin acquisition offers
|
|
163
170
|
`text/markdown, text/html;q=0.9, */*;q=0.1`. An origin `text/markdown`
|
|
164
|
-
representation wins and
|
|
165
|
-
`Accept: text/html` may populate `html`. An authored `Accept` value is
|
|
166
|
-
unchanged and makes any returned HTML use the local projection route.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
`
|
|
171
|
-
|
|
172
|
-
`
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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.
|
|
171
|
+
representation wins and the materializer is skipped; a second origin request
|
|
172
|
+
with `Accept: text/html` may populate `html`. An authored `Accept` value is
|
|
173
|
+
sent unchanged and makes any returned HTML use the local projection route.
|
|
174
|
+
|
|
175
|
+
The materializer's `eligible(url)` returns its identity string when it will
|
|
176
|
+
serve the URL (its credentials and supported targets are its own); `extract`
|
|
177
|
+
returns `success` (its sanitized Markdown body plus its own evidence headers),
|
|
178
|
+
`recoverable` (local projection fallback with the materializer's evidence and
|
|
179
|
+
terminal `203`), or `hard` (an exact materializer-owned Problem; no local
|
|
180
|
+
recovery). A materializer `extract` throw is the materialization failure, never
|
|
181
|
+
a silent fallback. A selected id with no installed provider fails hard rather
|
|
182
|
+
than degrading silently.
|
|
183
|
+
|
|
184
|
+
Without server HTML, no local recovery input exists. A materializer success can
|
|
185
|
+
still close `body`, but every materializer failure remains the body failure and
|
|
186
|
+
`html` closes errored.
|
|
187
187
|
|
|
188
188
|
#### §http-channel-outcomes Channel outcomes
|
|
189
189
|
|
|
@@ -201,9 +201,19 @@ representation. A missing source variant is an explicit empty `errored`
|
|
|
201
201
|
channel, never an absent fact that later cache use can reinterpret as successful
|
|
202
202
|
empty content. A direct non-success HTTP response likewise remains available as
|
|
203
203
|
evidence, while each origin-backed channel carries an exact
|
|
204
|
-
`http-response-status` producer Problem. A
|
|
204
|
+
`http-response-status` producer Problem. A materializer-produced body and the
|
|
205
205
|
acquisition `header` remain independent of an unavailable origin source.
|
|
206
206
|
|
|
207
|
+
§http-llms-txt **Origin llms.txt companions.** After a successful generic GET
|
|
208
|
+
materialization, the scheme opportunistically acquires `<origin>/llms.txt`
|
|
209
|
+
once per origin per TTL window and materializes it as its own origin entry
|
|
210
|
+
(`https://host/llms.txt`), surfaceable by FIND. A missing companion, any
|
|
211
|
+
non-2xx, binary content, or a transport failure is quiet: the companion is
|
|
212
|
+
never fabricated, the failed probe is remembered (not retried per READ), and
|
|
213
|
+
the piggyback never fails the READ that carried it. The companion itself
|
|
214
|
+
never recurses. This is lazy by construction — one extra request only when
|
|
215
|
+
an origin is first being read.
|
|
216
|
+
|
|
207
217
|
## §http-status §4 HTTP status mapping
|
|
208
218
|
|
|
209
219
|
| Outcome | Operation status |
|
|
@@ -214,7 +224,7 @@ acquisition `header` remain independent of an unavailable origin source.
|
|
|
214
224
|
| Selected origin-backed channel received HTTP `4xx`/`5xx` | Exact durable `http-response-status` Problem |
|
|
215
225
|
| Selected HTML-page channel fails | That channel's exact durable producer Problem |
|
|
216
226
|
| Local HTML projection is absent | `422` (`no-readable-projection`) |
|
|
217
|
-
| Recoverable
|
|
227
|
+
| Recoverable materializer failure uses local projection | Readable body with durable status `203` |
|
|
218
228
|
| Finite textual or projected-binary response | Universal READ result |
|
|
219
229
|
| Finite empty text response | `204` |
|
|
220
230
|
| Finite binary response has no readable projection | `415` (`binary-response-unsupported`) |
|
|
@@ -235,8 +245,8 @@ acquisition `header` remain independent of an unavailable origin source.
|
|
|
235
245
|
An HTTP error status is still a successfully acquired direct response: its
|
|
236
246
|
content and response evidence are preserved, and origin-backed channels carry
|
|
237
247
|
that exact status as durable producer evidence. HTML still enters page
|
|
238
|
-
production, so a successful
|
|
239
|
-
`html` records the HTTP error. Automatic WebFetcher acquisition instead treats
|
|
248
|
+
production, so a successful materializer `body` can remain readable while the
|
|
249
|
+
origin `html` records the HTTP error. Automatic WebFetcher acquisition instead treats
|
|
240
250
|
a non-2xx response as unavailable. Handler failures use RFC 9457 Problem Details.
|
|
241
251
|
Caught direct-acquisition diagnostics are bounded by
|
|
242
252
|
`PLURNK_SCHEMES_HTTP_ERROR_DETAIL_LIMIT` in model-facing detail while complete
|
|
@@ -260,7 +270,7 @@ handler does not sniff or guess unknown bytes.
|
|
|
260
270
|
| ----------------- | --------------------------------------------------------------------------------------------------------- |
|
|
261
271
|
| Platform | Node ≥26 native fetch, streams, abort signals, decoding, DNS, and `WebSocket` |
|
|
262
272
|
| SSE | `eventsource-parser` for bounded WHATWG event-stream framing |
|
|
263
|
-
|
|
|
273
|
+
| Materializer plugins | Discovered `http-materializer` packages selected by `PLURNK_SCHEMES_HTTP_MATERIALIZER` {§http-materializer-plugins} |
|
|
264
274
|
|
|
265
275
|
### §http-config Operator configuration
|
|
266
276
|
|
|
@@ -268,9 +278,7 @@ handler does not sniff or guess unknown bytes.
|
|
|
268
278
|
| --------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
269
279
|
| Canonical registry | Shipped `.env.defaults`; the daemon assembles it as a set-if-unset floor {§operator-config-env-defaults} |
|
|
270
280
|
| Required values | Missing or invalid required values fail at readiness/owning read; code carries no hidden fallback |
|
|
271
|
-
|
|
|
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` |
|
|
281
|
+
| Materializer selection | `PLURNK_SCHEMES_HTTP_MATERIALIZER=` names a discovered materializer id; unset = local projection only |
|
|
274
282
|
|
|
275
283
|
### §automatic-fetch-check Automatic acquisition URL check
|
|
276
284
|
|
|
@@ -279,7 +287,7 @@ acquisition, it accepts credential-free HTTP(S) targets only when every
|
|
|
279
287
|
resolved address is ordinary globally reachable unicast, and repeats the check
|
|
280
288
|
before every manually followed redirect. A refused or unresolvable target is
|
|
281
289
|
the ordinary unavailable `null` result. A transport failure after public
|
|
282
|
-
admission may still enter
|
|
290
|
+
admission may still enter materializer page production.
|
|
283
291
|
|
|
284
292
|
Direct HTTP operations and WebSocket connections do not use this check. Loopback,
|
|
285
293
|
private, and link-local destinations are therefore valid explicit targets.
|
|
@@ -296,12 +304,12 @@ check therefore makes no DNS-rebinding or total-egress claim.
|
|
|
296
304
|
|
|
297
305
|
| Concern | Contract |
|
|
298
306
|
| -------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
299
|
-
| Direct gate | Explicit targets use native origin transport; only a generic public request grants
|
|
307
|
+
| Direct gate | Explicit targets use native origin transport; only a generic public request grants materializer authority |
|
|
300
308
|
| 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
|
|
309
|
+
| Body owner | Origin Markdown wins; otherwise the configured eligible materializer is structural, with one local projection floor |
|
|
302
310
|
| Source owner | `html` is exact server-source HTML; it is never provider-generated or DOM-generated |
|
|
303
311
|
| Projection | A present projection, including `""`, becomes `body`; `null` alone means absence |
|
|
304
|
-
| Cancellation | One caller signal spans origin, auxiliary origin, projection, and
|
|
312
|
+
| Cancellation | One caller signal spans origin, auxiliary origin, projection, and materializer work |
|
|
305
313
|
|
|
306
314
|
### §host-rewrite Acquisition target rewrite
|
|
307
315
|
|
|
@@ -317,7 +325,7 @@ are checked under {§automatic-fetch-check} only when `WebFetcher` acquires them
|
|
|
317
325
|
Acquired responses append package-owned `x-plurnk-request-method`,
|
|
318
326
|
`x-plurnk-fetched-at`, and `x-plurnk-cache-variant` fields after origin headers.
|
|
319
327
|
Page bodies append `x-plurnk-materializer-id`; local projections append
|
|
320
|
-
`x-plurnk-projection-id`;
|
|
328
|
+
`x-plurnk-projection-id`; The materializer appends its own evidence headers;
|
|
321
329
|
and bounded failure evidence. Only package metadata after the acquisition stamp
|
|
322
330
|
is authoritative.
|
|
323
331
|
Plurnk stores one representation per canonical URL rather than a variant set:
|
|
@@ -388,16 +396,16 @@ different source representation.
|
|
|
388
396
|
|
|
389
397
|
A stored derived representation requires the current projection identity. A
|
|
390
398
|
stored page body also requires its current route: negotiated origin Markdown,
|
|
391
|
-
local projection while
|
|
392
|
-
projection, configured
|
|
393
|
-
local-fallback route.
|
|
394
|
-
route.
|
|
399
|
+
local projection while no materializer is selected, metadata-ineligible local
|
|
400
|
+
projection, the configured materializer id, or the corresponding recoverable
|
|
401
|
+
local-fallback route. Selecting or changing the materializer invalidates the
|
|
402
|
+
affected route.
|
|
395
403
|
|
|
396
404
|
Once a page representation leaves its fresh window, Plurnk performs complete
|
|
397
405
|
reacquisition without old origin validators. Origin `304` can certify origin
|
|
398
|
-
bytes, but it cannot certify a composite that may also contain a fresh
|
|
399
|
-
extraction, a newly negotiated Markdown representation, or an
|
|
400
|
-
acquired HTML variant. Projection or materializer mismatch likewise invalidates
|
|
406
|
+
bytes, but it cannot certify a composite that may also contain a fresh
|
|
407
|
+
materializer extraction, a newly negotiated Markdown representation, or an
|
|
408
|
+
independently acquired HTML variant. Projection or materializer mismatch likewise invalidates
|
|
401
409
|
body, TTL, and validators together.
|
|
402
410
|
|
|
403
411
|
POST, PUT, and DELETE responses retain their method marker but cannot satisfy a
|
|
@@ -435,7 +443,8 @@ operation is cancelled.
|
|
|
435
443
|
Top-level `null` is an automatic-acquisition liveness value rather than a thrown
|
|
436
444
|
failure. Caller cancellation is not liveness: a pre-aborted caller fails before
|
|
437
445
|
acquisition and a caller abort wins with its exact reason. Package-owned origin
|
|
438
|
-
|
|
446
|
+
timeouts are ordinary classified acquisition outcomes; materializer timeouts
|
|
447
|
+
are the materializer's own classified outcomes. WebFetcher
|
|
439
448
|
owns no entry identity, registry selection, selected-channel policy, or query
|
|
440
449
|
policy; its consumers supply those boundaries and the projection capability.
|
|
441
450
|
|
package/dist/Http.d.ts.map
CHANGED
|
@@ -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;
|
|
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;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;CA8qBJ"}
|
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,
|
|
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.
|
|
@@ -135,6 +135,7 @@ const httpDate = (value, now = Date.now()) => {
|
|
|
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
137
|
const documentation = await readFile(new URL("../docs/http.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
141
|
name: "http",
|
|
@@ -166,6 +167,10 @@ class Http {
|
|
|
166
167
|
};
|
|
167
168
|
#errorDetailLimit;
|
|
168
169
|
#webFetcher;
|
|
170
|
+
// {§http-llms-txt} — one opportunistic origin-companion attempt per TTL
|
|
171
|
+
// window; success and failure share the timer so a 404 does not re-probe
|
|
172
|
+
// on every READ.
|
|
173
|
+
#llmsTextAttempts = new Map();
|
|
169
174
|
constructor() {
|
|
170
175
|
this.#errorDetailLimit = ErrorDetail.configuredLimit();
|
|
171
176
|
this.#webFetcher = new WebFetcher();
|
|
@@ -264,7 +269,49 @@ class Http {
|
|
|
264
269
|
});
|
|
265
270
|
}
|
|
266
271
|
const responseHeaders = fetched.responseHeaders ?? [];
|
|
267
|
-
if (
|
|
272
|
+
if (_a.#revalidationCorresponds(cachedHeader, new Headers(responseHeaders.map(([name, value]) => [name, value])))) {
|
|
273
|
+
const channels = {
|
|
274
|
+
...cached.channels,
|
|
275
|
+
[HEADER]: {
|
|
276
|
+
...cached.channels[HEADER],
|
|
277
|
+
content: _a.#refreshAfter304(cachedHeader, responseHeaders, requestHeaders),
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
const written = await ctx.entries.write(pathname, {
|
|
281
|
+
channels,
|
|
282
|
+
});
|
|
283
|
+
if (Results.isErrorStatus(written.status))
|
|
284
|
+
return _a.#passthrough(written);
|
|
285
|
+
return { status: 200 };
|
|
286
|
+
}
|
|
287
|
+
// {§revalidation} — a genuinely mismatched 304 (different opaque
|
|
288
|
+
// tags): the conditional is the problem, so fall back to one
|
|
289
|
+
// unconditional GET and acquire normally instead of surfacing
|
|
290
|
+
// an unrecoverable 502. A second 304 has no way out and is the
|
|
291
|
+
// honest failure.
|
|
292
|
+
try {
|
|
293
|
+
fetched = await this.#webFetcher.fetch(url, {
|
|
294
|
+
signal: ctx.signal,
|
|
295
|
+
headers: requestHeaders,
|
|
296
|
+
guarded: false,
|
|
297
|
+
acceptHttpErrors: true,
|
|
298
|
+
preserveUnavailable: true,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
if (ctx.signal?.aborted === true && error === ctx.signal.reason) {
|
|
303
|
+
return _a.#cancelled(url, "GET");
|
|
304
|
+
}
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
if (fetched === null) {
|
|
308
|
+
return _a.#bad(404, "http", "not-materialized", `The URL ${url} could not be materialized.`, {
|
|
309
|
+
target: url,
|
|
310
|
+
stage: "acquisition",
|
|
311
|
+
retryable: true,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (fetched.status === 304) {
|
|
268
315
|
return _a.#bad(502, "http", "fetch-failed", `HTTP GET ${url} returned 304 without identifying the stored representation nominated for revalidation.`, {
|
|
269
316
|
target: url,
|
|
270
317
|
method: "GET",
|
|
@@ -272,19 +319,7 @@ class Http {
|
|
|
272
319
|
retryable: true,
|
|
273
320
|
});
|
|
274
321
|
}
|
|
275
|
-
|
|
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 };
|
|
322
|
+
// Non-304: fall through to ordinary acquisition below.
|
|
288
323
|
}
|
|
289
324
|
if (fetched.mimetype === "text/event-stream"
|
|
290
325
|
&& fetched.response?.body !== null
|
|
@@ -318,8 +353,46 @@ class Http {
|
|
|
318
353
|
});
|
|
319
354
|
if (Results.isErrorStatus(written.status))
|
|
320
355
|
return _a.#passthrough(written);
|
|
356
|
+
await this.#piggybackLlmsText(address, ctx);
|
|
321
357
|
return { status: 200 };
|
|
322
358
|
}
|
|
359
|
+
// {§http-llms-txt} — after a successful generic GET materialization,
|
|
360
|
+
// acquire <origin>/llms.txt once per TTL window and materialize it as its
|
|
361
|
+
// own https entry. Any failure is quiet: the companion never fails the
|
|
362
|
+
// READ that piggybacked it, and the companion itself never recurses.
|
|
363
|
+
async #piggybackLlmsText(address, ctx) {
|
|
364
|
+
const url = new URL(address.url);
|
|
365
|
+
if (url.pathname === "/llms.txt")
|
|
366
|
+
return;
|
|
367
|
+
const origin = url.origin;
|
|
368
|
+
const last = this.#llmsTextAttempts.get(origin);
|
|
369
|
+
if (last !== undefined && Date.now() - last < LLMS_TEXT_ATTEMPT_TTL_MS)
|
|
370
|
+
return;
|
|
371
|
+
this.#llmsTextAttempts.set(origin, Date.now());
|
|
372
|
+
const llmsUrl = `${origin}/llms.txt`;
|
|
373
|
+
try {
|
|
374
|
+
const fetched = await this.#webFetcher.fetch(llmsUrl, {
|
|
375
|
+
signal: ctx.signal,
|
|
376
|
+
guarded: false,
|
|
377
|
+
acceptHttpErrors: true,
|
|
378
|
+
preserveUnavailable: true,
|
|
379
|
+
});
|
|
380
|
+
if (fetched === null)
|
|
381
|
+
return;
|
|
382
|
+
// A missing companion (404) or any non-2xx is quiet — no entry.
|
|
383
|
+
if ((fetched.status ?? 200) >= 400)
|
|
384
|
+
return;
|
|
385
|
+
const materialized = await WebFetcher.materialize(fetched, ctx.projection, ctx.signal);
|
|
386
|
+
if (materialized === null)
|
|
387
|
+
return;
|
|
388
|
+
await ctx.entries.write(`/${url.host}/llms.txt`, {
|
|
389
|
+
channels: WebFetcher.materializedChannels(materialized, { url: llmsUrl, method: "GET" }),
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
// Quiet by contract: 404, guard rejection, network error, binary.
|
|
394
|
+
}
|
|
395
|
+
}
|
|
323
396
|
async #openEventStream(address, fetched, ctx) {
|
|
324
397
|
const response = fetched.response;
|
|
325
398
|
if (response?.body === null || response?.body === undefined) {
|
|
@@ -860,11 +933,9 @@ class Http {
|
|
|
860
933
|
const isHtml = MimetypeClassifier.isHtml(sourceMimetype);
|
|
861
934
|
const materializerIdentity = _a.#materializerIdentity(header);
|
|
862
935
|
if (materializerIdentity !== undefined) {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|| materializerIdentity === "origin-markdown:v1")
|
|
867
|
-
return true;
|
|
936
|
+
// {§http-materializer-plugins} — a stored body produced by the current
|
|
937
|
+
// configured materializer (or the built-in producers) is current.
|
|
938
|
+
return WebFetcher.materializerCurrent(materializerIdentity);
|
|
868
939
|
}
|
|
869
940
|
if (isHtml && materializerIdentity === undefined)
|
|
870
941
|
return false;
|
|
@@ -886,15 +957,34 @@ class Http {
|
|
|
886
957
|
...retained.map(({ name, value }) => [name, value]),
|
|
887
958
|
...updates,
|
|
888
959
|
];
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
].flatMap((field) => {
|
|
960
|
+
// Materializer/provider evidence headers are plugin-owned (unknown names);
|
|
961
|
+
// a 304 does not re-materialize the stored body, so its evidence is
|
|
962
|
+
// retained verbatim beside the rebuilt framework fields. Framework-owned
|
|
963
|
+
// x-plurnk-* fields (cache-variant, materializer/projection ids, method,
|
|
964
|
+
// fetched-at stamp) are rebuilt or omitted, never duplicated stale.
|
|
965
|
+
const rebuilt = [MATERIALIZER_ID_HEADER, PROJECTION_ID_HEADER].flatMap((field) => {
|
|
895
966
|
const value = _a.#packageHeaderValue(header, field);
|
|
896
967
|
return value === undefined ? [] : [`${field}: ${value}`];
|
|
897
968
|
});
|
|
969
|
+
const frameworkOwned = new Set([
|
|
970
|
+
CACHE_VARIANT_HEADER.toLowerCase(),
|
|
971
|
+
MATERIALIZER_ID_HEADER.toLowerCase(),
|
|
972
|
+
PROJECTION_ID_HEADER.toLowerCase(),
|
|
973
|
+
REQUEST_METHOD.toLowerCase(),
|
|
974
|
+
FETCHED_AT.toLowerCase(),
|
|
975
|
+
]);
|
|
976
|
+
const packageEvidence = [
|
|
977
|
+
...rebuilt,
|
|
978
|
+
...lines.slice(1).filter((line) => {
|
|
979
|
+
const colon = line.indexOf(":");
|
|
980
|
+
if (colon <= 0)
|
|
981
|
+
return false;
|
|
982
|
+
const name = line.slice(0, colon).toLowerCase();
|
|
983
|
+
return name.startsWith("x-plurnk-")
|
|
984
|
+
&& !frameworkOwned.has(name)
|
|
985
|
+
&& !updatedNames.has(name);
|
|
986
|
+
}),
|
|
987
|
+
];
|
|
898
988
|
return [
|
|
899
989
|
lines[0] ?? "HTTP 200 OK",
|
|
900
990
|
...retained.map(({ line }) => line),
|
|
@@ -916,8 +1006,11 @@ class Http {
|
|
|
916
1006
|
: null;
|
|
917
1007
|
if (responseTag === null || storedTag === null)
|
|
918
1008
|
return false;
|
|
919
|
-
|
|
920
|
-
|
|
1009
|
+
// {§revalidation} — If-None-Match correspondence is a weak
|
|
1010
|
+
// comparison (RFC 9110 §8.8.3.2): opaque tags match regardless of
|
|
1011
|
+
// the W/ prefix, so an origin that upgrades a stored weak etag to
|
|
1012
|
+
// a strong etag on the 304 still corresponds.
|
|
1013
|
+
return responseTag.opaque === storedTag.opaque;
|
|
921
1014
|
}
|
|
922
1015
|
const responseModifiedValue = responseHeaders.get("last-modified");
|
|
923
1016
|
const storedModifiedValues = _a.#originValues(fields, "last-modified");
|