@melra/http-runtime 0.3.0-alpha.11 → 0.3.0-alpha.13
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/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -25
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -119
- package/dist/index.test.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -11,10 +11,11 @@ export interface HttpRuntimeOptions extends NetworkPolicy {
|
|
|
11
11
|
* One governed HTTP call.
|
|
12
12
|
*
|
|
13
13
|
* The interesting part is which socket opens. `assertSafeDestination` resolves
|
|
14
|
-
* the name and checks every answer, then this connects
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* the name and checks every answer, then this connects through a `lookup` that
|
|
15
|
+
* answers only with those checked addresses, never by resolving the name again
|
|
16
|
+
* — the same pinning `PinningProxy` does for the browser, and for the same
|
|
17
|
+
* reason: a resolver that answers public once and private the second time
|
|
18
|
+
* would otherwise make the check meaningless. TLS still verifies the name.
|
|
18
19
|
*/
|
|
19
20
|
export declare class HttpRuntime {
|
|
20
21
|
private readonly options;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,aAAa,EACnB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,kBAAmB,SAAQ,aAAa;IACvD;;;OAGG;IACH,WAAW,CAAC,EAAE,gBAAgB,CAAC;CAChC;AAED
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,aAAa,EACnB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,kBAAmB,SAAQ,aAAa;IACvD;;;OAGG;IACH,WAAW,CAAC,EAAE,gBAAgB,CAAC;CAChC;AAED;;;;;;;;;GASG;AACH,qBAAa,WAAW;IACV,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,kBAAkB;IAElD,OAAO,CACX,SAAS,EAAE,aAAa,EACxB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CA4JpC"}
|
package/dist/index.js
CHANGED
|
@@ -8,10 +8,11 @@ import { assertSafeDestination, classifyOperation, } from "@melra/policy-core";
|
|
|
8
8
|
* One governed HTTP call.
|
|
9
9
|
*
|
|
10
10
|
* The interesting part is which socket opens. `assertSafeDestination` resolves
|
|
11
|
-
* the name and checks every answer, then this connects
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* the name and checks every answer, then this connects through a `lookup` that
|
|
12
|
+
* answers only with those checked addresses, never by resolving the name again
|
|
13
|
+
* — the same pinning `PinningProxy` does for the browser, and for the same
|
|
14
|
+
* reason: a resolver that answers public once and private the second time
|
|
15
|
+
* would otherwise make the check meaningless. TLS still verifies the name.
|
|
15
16
|
*/
|
|
16
17
|
export class HttpRuntime {
|
|
17
18
|
options;
|
|
@@ -19,24 +20,28 @@ export class HttpRuntime {
|
|
|
19
20
|
this.options = options;
|
|
20
21
|
}
|
|
21
22
|
async execute(operation, signal) {
|
|
22
|
-
const { url,
|
|
23
|
+
const { url, host, lookup } = await assertSafeDestination(operation.url, this.options);
|
|
23
24
|
// Checked here and not only in the router: the destination check above is
|
|
24
25
|
// an await, and a cancel that lands inside that window would otherwise be
|
|
25
26
|
// lost and the request would go out anyway.
|
|
26
|
-
|
|
27
|
+
const cancelled = () => signal?.aborted === true;
|
|
28
|
+
if (cancelled())
|
|
27
29
|
throw new Error("task_cancelled");
|
|
28
30
|
const secure = url.protocol === "https:";
|
|
29
31
|
const send = secure ? httpsRequest : httpRequest;
|
|
30
|
-
// Brokered against the
|
|
31
|
-
//
|
|
32
|
-
// credential scoped
|
|
33
|
-
// will actually talk to.
|
|
32
|
+
// Brokered against the host that was *checked*: the name, or an IPv6
|
|
33
|
+
// literal without the brackets `URL.hostname` keeps, so `[::1]` and a
|
|
34
|
+
// credential scoped to `::1` are the same host.
|
|
34
35
|
const { capability, target } = classifyOperation(operation);
|
|
35
36
|
const brokered = await this.options.credentials?.headersFor({
|
|
36
|
-
host
|
|
37
|
+
host,
|
|
37
38
|
capability,
|
|
38
39
|
target,
|
|
39
40
|
});
|
|
41
|
+
// Again: reading a credential from a file is an await too, and the abort
|
|
42
|
+
// listener below only hears an abort that comes after it is added.
|
|
43
|
+
if (cancelled())
|
|
44
|
+
throw new Error("task_cancelled");
|
|
40
45
|
const headers = {
|
|
41
46
|
// Lowercased before the merge, or `Authorization: guess` from the caller
|
|
42
47
|
// and `authorization: <secret>` from the broker are two distinct keys and
|
|
@@ -48,8 +53,7 @@ export class HttpRuntime {
|
|
|
48
53
|
// Last, so a caller cannot name the same header and have its own value
|
|
49
54
|
// ride out under a credential's name.
|
|
50
55
|
...brokered?.headers,
|
|
51
|
-
//
|
|
52
|
-
// the far end even though the socket was opened to a literal address.
|
|
56
|
+
// The URL's authority exactly as the caller wrote it.
|
|
53
57
|
host: url.host,
|
|
54
58
|
...(operation.idempotencyKey === undefined
|
|
55
59
|
? {}
|
|
@@ -58,17 +62,22 @@ export class HttpRuntime {
|
|
|
58
62
|
if (operation.content !== undefined) {
|
|
59
63
|
headers["content-length"] = String(Buffer.byteLength(operation.content));
|
|
60
64
|
}
|
|
65
|
+
let deadline;
|
|
61
66
|
return await new Promise((resolve, reject) => {
|
|
62
67
|
const call = send({
|
|
63
|
-
host
|
|
68
|
+
host,
|
|
69
|
+
lookup,
|
|
64
70
|
port: url.port === "" ? (secure ? 443 : 80) : Number(url.port),
|
|
65
71
|
method: operation.method,
|
|
66
72
|
path: `${url.pathname}${url.search}`,
|
|
67
73
|
headers,
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
// A fresh connection per effect, never one from Node's keep-alive
|
|
75
|
+
// pool. A pooled socket the server has already closed takes the whole
|
|
76
|
+
// request and then resets, and below that reads as "the far end may
|
|
77
|
+
// have acted" — a request that never reached anyone parked for
|
|
78
|
+
// reconciliation. On a connection of its own, a reset after the write
|
|
79
|
+
// really is an unknown outcome.
|
|
80
|
+
agent: false,
|
|
72
81
|
}, (response) => {
|
|
73
82
|
const chunks = [];
|
|
74
83
|
let bytes = 0;
|
|
@@ -85,6 +94,14 @@ export class HttpRuntime {
|
|
|
85
94
|
chunks.push(chunk);
|
|
86
95
|
});
|
|
87
96
|
const finish = () => {
|
|
97
|
+
// The headers arrived, so the request did too: a body cut off
|
|
98
|
+
// before its end is an answer that stopped, not a call that never
|
|
99
|
+
// happened, and only `outcomeUnknown` says so. Stopping at the
|
|
100
|
+
// size cap is the one deliberate exception.
|
|
101
|
+
if (!response.complete && !truncated) {
|
|
102
|
+
reject(outcomeUnknown("http_response_incomplete"));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
88
105
|
const status = response.statusCode ?? 0;
|
|
89
106
|
resolve({
|
|
90
107
|
// The far end's own word, which is all HTTP can offer. A caller
|
|
@@ -119,7 +136,7 @@ export class HttpRuntime {
|
|
|
119
136
|
};
|
|
120
137
|
response.on("end", finish);
|
|
121
138
|
response.on("close", finish);
|
|
122
|
-
response.on("error", reject);
|
|
139
|
+
response.on("error", (error) => reject(outcomeUnknown(error.message)));
|
|
123
140
|
});
|
|
124
141
|
// Whether the request made it out. A socket error before this is a
|
|
125
142
|
// request that never happened; after it, the far end may have done the
|
|
@@ -127,19 +144,25 @@ export class HttpRuntime {
|
|
|
127
144
|
// `bytesWritten` is not the signal — it counts headers buffered on a
|
|
128
145
|
// socket that never connected, so ECONNREFUSED reports a nonzero count.
|
|
129
146
|
const failed = (error) => {
|
|
147
|
+
// When every pinned address refuses, Node reports one AggregateError
|
|
148
|
+
// with an empty message; the per-address errors say what happened.
|
|
149
|
+
if (error instanceof AggregateError && error.message === "") {
|
|
150
|
+
error = new Error(error.errors.map((inner) => inner.message).join("; "));
|
|
151
|
+
}
|
|
130
152
|
reject(call.writableFinished ? outcomeUnknown(error.message) : error);
|
|
131
153
|
};
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
call
|
|
135
|
-
|
|
136
|
-
|
|
154
|
+
// One deadline for the whole exchange. Node's own `timeout` only notices
|
|
155
|
+
// a socket that goes quiet, so a server sending a byte at a time held the
|
|
156
|
+
// call, and any verification or reconciliation waiting on it, for as
|
|
157
|
+
// long as it liked.
|
|
158
|
+
deadline = setTimeout(() => call.destroy(new Error("http_timeout")), operation.timeoutMs);
|
|
159
|
+
deadline.unref();
|
|
137
160
|
call.on("error", failed);
|
|
138
161
|
signal?.addEventListener("abort", () => call.destroy(new Error("task_cancelled")), { once: true });
|
|
139
162
|
if (operation.content !== undefined)
|
|
140
163
|
call.write(operation.content);
|
|
141
164
|
call.end();
|
|
142
|
-
});
|
|
165
|
+
}).finally(() => clearTimeout(deadline));
|
|
143
166
|
}
|
|
144
167
|
}
|
|
145
168
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAC3C,sCAAsC;AAEtC,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,YAAY,CAAC;AAGrD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EACL,qBAAqB,EACrB,iBAAiB,GAGlB,MAAM,oBAAoB,CAAC;AAU5B
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@melra/http-runtime",
|
|
3
|
-
"version": "0.3.0-alpha.
|
|
3
|
+
"version": "0.3.0-alpha.13",
|
|
4
4
|
"description": "HTTP/API reference effect adapter for the MELRA kernel.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -21,13 +21,13 @@
|
|
|
21
21
|
"dist"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@melra/policy-core": "0.3.0-alpha.
|
|
25
|
-
"@melra/protocol": "0.3.0-alpha.
|
|
24
|
+
"@melra/policy-core": "0.3.0-alpha.13",
|
|
25
|
+
"@melra/protocol": "0.3.0-alpha.13"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build": "tsc -p tsconfig.json",
|
|
29
29
|
"clean": "rm -rf dist",
|
|
30
30
|
"test": "vitest run",
|
|
31
|
-
"typecheck": "tsc -p tsconfig.json
|
|
31
|
+
"typecheck": "tsc -p tsconfig.test.json"
|
|
32
32
|
}
|
|
33
33
|
}
|
package/dist/index.test.d.ts
DELETED
package/dist/index.test.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":""}
|
package/dist/index.test.js
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
// Copyright 2026 XAGI Labs Private Limited
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
import { createServer } from "node:http";
|
|
4
|
-
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
5
|
-
import { HttpOperationSchema } from "@melra/protocol";
|
|
6
|
-
import { HttpRuntime } from "./index.js";
|
|
7
|
-
let server;
|
|
8
|
-
let origin;
|
|
9
|
-
/** Last request the fixture saw, so header and body handling is assertable. */
|
|
10
|
-
let seen;
|
|
11
|
-
beforeAll(async () => {
|
|
12
|
-
server = createServer((req, res) => {
|
|
13
|
-
const chunks = [];
|
|
14
|
-
req.on("data", (chunk) => chunks.push(chunk));
|
|
15
|
-
req.on("end", () => {
|
|
16
|
-
seen = {
|
|
17
|
-
method: req.method ?? "",
|
|
18
|
-
url: req.url ?? "",
|
|
19
|
-
headers: req.headers,
|
|
20
|
-
body: Buffer.concat(chunks).toString("utf8"),
|
|
21
|
-
};
|
|
22
|
-
if (req.url === "/teapot") {
|
|
23
|
-
res.writeHead(418).end("no coffee");
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
if (req.url === "/big") {
|
|
27
|
-
res.writeHead(200).end("x".repeat(50_000));
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
// Accepts the request and never answers, which is the case MELRA must not
|
|
31
|
-
// report as "the effect did not happen".
|
|
32
|
-
if (req.url === "/silent")
|
|
33
|
-
return;
|
|
34
|
-
res.writeHead(201, { "content-type": "application/json" });
|
|
35
|
-
res.end(JSON.stringify({ created: true }));
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
39
|
-
origin = `http://127.0.0.1:${server.address().port}`;
|
|
40
|
-
});
|
|
41
|
-
afterAll(async () => {
|
|
42
|
-
await new Promise((resolve) => server.close(() => resolve()));
|
|
43
|
-
});
|
|
44
|
-
const runtime = () => new HttpRuntime({ allowedDomains: ["*"], allowLocalhost: true });
|
|
45
|
-
const operation = (fields) => HttpOperationSchema.parse({ kind: "http", action: "request", ...fields });
|
|
46
|
-
describe("http runtime", () => {
|
|
47
|
-
it("posts a body and reports the response status", async () => {
|
|
48
|
-
const result = await runtime().execute(operation({
|
|
49
|
-
method: "POST",
|
|
50
|
-
url: `${origin}/orders`,
|
|
51
|
-
content: JSON.stringify({ item: "widget" }),
|
|
52
|
-
headers: { "content-type": "application/json" },
|
|
53
|
-
idempotencyKey: "order-42",
|
|
54
|
-
}));
|
|
55
|
-
expect(seen.method).toBe("POST");
|
|
56
|
-
expect(seen.body).toBe('{"item":"widget"}');
|
|
57
|
-
expect(seen.headers["idempotency-key"]).toBe("order-42");
|
|
58
|
-
expect(result.status).toBe(201);
|
|
59
|
-
expect(result.success).toBe(true);
|
|
60
|
-
expect(result.content).toBe('{"created":true}');
|
|
61
|
-
});
|
|
62
|
-
it("reports a failing status as an unsuccessful effect", async () => {
|
|
63
|
-
const result = await runtime().execute(operation({ url: `${origin}/teapot` }));
|
|
64
|
-
expect(result.status).toBe(418);
|
|
65
|
-
// The adapter never asserts its own success: a 418 is a completed call and
|
|
66
|
-
// a failed effect, and verification is what turns that into `partial`.
|
|
67
|
-
expect(result.success).toBe(false);
|
|
68
|
-
});
|
|
69
|
-
it("stops reading a body past the cap instead of buffering it", async () => {
|
|
70
|
-
const result = await runtime().execute(operation({ url: `${origin}/big`, maxResponseBytes: 1_000 }));
|
|
71
|
-
expect(result.truncated).toBe(true);
|
|
72
|
-
expect(String(result.content).length).toBeLessThan(50_000);
|
|
73
|
-
});
|
|
74
|
-
it("refuses a private destination before any socket opens", async () => {
|
|
75
|
-
const guarded = new HttpRuntime({
|
|
76
|
-
allowedDomains: ["*"],
|
|
77
|
-
allowLocalhost: false,
|
|
78
|
-
});
|
|
79
|
-
await expect(guarded.execute(operation({ url: "http://169.254.169.254/latest/meta-data/" }))).rejects.toThrow("destination_private_blocked");
|
|
80
|
-
});
|
|
81
|
-
it("refuses a host outside the allowlist", async () => {
|
|
82
|
-
const guarded = new HttpRuntime({
|
|
83
|
-
allowedDomains: ["example.com"],
|
|
84
|
-
allowLocalhost: false,
|
|
85
|
-
});
|
|
86
|
-
await expect(guarded.execute(operation({ url: "http://attacker.test/" }))).rejects.toThrow("destination_domain_not_allowed");
|
|
87
|
-
});
|
|
88
|
-
it("rejects an unknown field", () => {
|
|
89
|
-
expect(() => operation({ url: `${origin}/`, followRedirects: true })).toThrow();
|
|
90
|
-
});
|
|
91
|
-
it("cancels in flight when the task signal aborts", async () => {
|
|
92
|
-
const controller = new AbortController();
|
|
93
|
-
const pending = runtime().execute(operation({ url: `${origin}/orders` }), controller.signal);
|
|
94
|
-
controller.abort();
|
|
95
|
-
await expect(pending).rejects.toThrow("task_cancelled");
|
|
96
|
-
});
|
|
97
|
-
it("marks the outcome unknown when a sent request goes unanswered", async () => {
|
|
98
|
-
// The far end has the request and may already have acted on it. Claiming
|
|
99
|
-
// failure here would be the kernel asserting something it cannot know.
|
|
100
|
-
await expect(runtime().execute(operation({
|
|
101
|
-
method: "POST",
|
|
102
|
-
url: `${origin}/silent`,
|
|
103
|
-
content: "charge",
|
|
104
|
-
timeoutMs: 300,
|
|
105
|
-
}))).rejects.toThrow("effect_outcome_unknown:http_timeout");
|
|
106
|
-
});
|
|
107
|
-
it("reports a plain failure when the request never left the machine", async () => {
|
|
108
|
-
const closed = createServer();
|
|
109
|
-
await new Promise((resolve) => closed.listen(0, "127.0.0.1", resolve));
|
|
110
|
-
const port = closed.address().port;
|
|
111
|
-
await new Promise((resolve) => closed.close(() => resolve()));
|
|
112
|
-
const attempt = runtime().execute(operation({ method: "POST", url: `http://127.0.0.1:${port}/charges` }));
|
|
113
|
-
// Nothing reached a server, so this one really did not happen — and must
|
|
114
|
-
// not be parked for reconciliation.
|
|
115
|
-
await expect(attempt).rejects.toThrow(/ECONNREFUSED/);
|
|
116
|
-
await expect(attempt).rejects.not.toThrow(/effect_outcome_unknown/);
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
//# sourceMappingURL=index.test.js.map
|
package/dist/index.test.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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"}
|