@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 +6 -7
- package/README.md +9 -8
- package/SPEC.md +84 -73
- package/dist/Http.d.ts.map +1 -1
- package/dist/Http.js +130 -31
- 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/Ws.d.ts.map +1 -1
- package/dist/Ws.js +12 -7
- package/dist/Ws.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 → https.md} +25 -18
- package/docs/wss.md +5 -1
- package/package.json +8 -3
- 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,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,
|
|
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
|
|
|
9
10
|
| Field | Value |
|
|
10
11
|
| --------------- | ----------------------------- |
|
|
11
|
-
| Registered name | `
|
|
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
|
-
|
|
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
|
|
35
|
-
|
|
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
|
|
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|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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 |
|
|
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
|
|
142
|
-
| Eligible HTML with
|
|
143
|
-
| Ineligible HTML or
|
|
144
|
-
| Recoverable
|
|
145
|
-
| Hard
|
|
146
|
-
|
|
|
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
|
-
#### §
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
|
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
|
-
|
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`;
|
|
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
|
|
392
|
-
projection, configured
|
|
393
|
-
local-fallback route.
|
|
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
|
|
399
|
-
extraction, a newly negotiated Markdown representation, or an
|
|
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
|
-
|
|
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
|
|
486
|
-
canonical network pathname
|
|
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
|
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,
|
|
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,
|
|
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/
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
864
|
-
|
|
865
|
-
|
|
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
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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
|
-
|
|
920
|
-
|
|
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.
|
|
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.`, {
|