@sproutboat/runtime 0.40.0 → 0.42.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/CHANGELOG.md +18 -0
- package/package.json +1 -1
- package/src/native-fetch-prelude.js +303 -9
- package/src/sockets.test.ts +42 -0
- package/src/source.ts +15 -0
- package/src/subtle/README.md +4 -4
- package/src/subtle/subtle.c +19 -7
- package/src/subtle/subtle.test.ts +1 -1
- package/src/transport-broker.js +471 -0
- package/src/transport-embedded.js +182 -3
- package/src/wrap.ts +14 -0
- package/test/bearssl.ts +5 -4
- package/test/native-sockets-broker.ts +460 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @sproutboat/runtime
|
|
2
2
|
|
|
3
|
+
## 0.42.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2baeae9: `connect()` from `cloudflare:sockets`, from compatibility date 2026-11-17: plain TCP, `secureTransport: "on"` and `"starttls"` with `startTls()`, `readable`/`writable` byte streams with write backpressure, `opened`, `closed`, `close()`, half-close, workerd's error shapes, and port 25 refused. It runs on both transports: the embedded one (standalone and sproutd) opens the connection in a thread per socket and does TLS with BearSSL (extra roots through `SB_CA_BUNDLE`); the broker one tunnels through the broker, which vets every address with the `fetch()` policy (`SB_EGRESS_ALLOW`, private ranges refused), terminates TLS, and hands the tunnel to the egress service where the broker has no network. Limits: 16 sockets per app. From the same date `navigator.userAgent` is `"Cloudflare-Workers"` and a bundled `Buffer`/`process` is accepted, so unmodified `pg`, `postgres` and Redis clients run. The toolchain gains three Porffor fixes that those drivers hit: bindings named like libc macros, call arguments evaluated in order, and a writable `navigator`.
|
|
8
|
+
|
|
9
|
+
## 0.41.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 1eacf43: Secrets read from the environment (a standalone binary, a sproutd worker) are decoded as UTF-8 from the proposed compatibility date 2026-11-07. Before, the handler got the environment's bytes one char per byte, so `é` arrived as `é`. The broker transport is unchanged: its secrets already arrive as real strings. Bytes that are not valid UTF-8 pass through as before.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- f74835a: `crypto.subtle` signs and verifies P-256 (ES256 and ECDH) with BearSSL's 64-bit `ec_p256_m64` where the target has a 128-bit multiply, and keeps the portable code everywhere else (and for P-384 and P-521). BearSSL never picks m64 by itself, so the C asks for it with `br_ec_p256_m64_get()`, which returns null on a target without one. Through the runtime on macOS arm64, ES256 signs 2.2x faster (2,563 to 5,621 per second) and verifies 3.1x faster (1,318 to 4,050 per second). Results are unchanged, so there is no new compatibility date.
|
|
18
|
+
|
|
19
|
+
This needs the BearSSL that the CLI pins from the release that ships with it: m64 is not in BearSSL 0.6, so a CLI that still links 0.6 cannot link this runtime.
|
|
20
|
+
|
|
3
21
|
## 0.40.0
|
|
4
22
|
|
|
5
23
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -11,6 +11,8 @@ function __sbEnvValue() {
|
|
|
11
11
|
// Setters, not bare assignments in the tail: the template and every app must compile the prelude identically.
|
|
12
12
|
function __sbSetCompat(date) {
|
|
13
13
|
__sbCompatNow = date;
|
|
14
|
+
// navigator.userAgent as workerd has it: pg picks its Workers socket code on exactly this string.
|
|
15
|
+
if (date >= "2026-11-17") navigator.userAgent = "Cloudflare-Workers";
|
|
14
16
|
Request._cd = date;
|
|
15
17
|
if (date >= "2026-11-15" && Error.captureStackTrace === undefined) Error.captureStackTrace = __sbCaptureStackTrace;
|
|
16
18
|
}
|
|
@@ -9397,6 +9399,14 @@ static const sbc_u8 sbc_oid_x25519[] = { 0x2b, 0x65, 0x6e };
|
|
|
9397
9399
|
static int sbc_oid_eq(const sbc_u8* a, size_t al, const sbc_u8* b, size_t bl) { return al == bl && memcmp(a, b, al) == 0; }
|
|
9398
9400
|
|
|
9399
9401
|
static int sbc_curve_br(int c) { return c == 1 ? BR_EC_secp256r1 : c == 2 ? BR_EC_secp384r1 : c == 3 ? BR_EC_secp521r1 : 0; }
|
|
9402
|
+
|
|
9403
|
+
static const br_ec_impl* sbc_ec(int br_curve) {
|
|
9404
|
+
if (br_curve == BR_EC_secp256r1) {
|
|
9405
|
+
const br_ec_impl* fast = br_ec_p256_m64_get();
|
|
9406
|
+
if (fast) return fast;
|
|
9407
|
+
}
|
|
9408
|
+
return br_ec_get_default();
|
|
9409
|
+
}
|
|
9400
9410
|
static int sbc_curve_from_br(int c) { return c == BR_EC_secp256r1 ? 1 : c == BR_EC_secp384r1 ? 2 : c == BR_EC_secp521r1 ? 3 : 0; }
|
|
9401
9411
|
static size_t sbc_curve_size(int c) { return c == 1 ? 32 : c == 2 ? 48 : c == 3 ? 66 : 0; }
|
|
9402
9412
|
static void sbc_curve_oid(int c, const sbc_u8** o, size_t* n) {
|
|
@@ -9579,7 +9589,7 @@ static int sbc_decode(int pkcs8, const sbc_u8* der, size_t len, sbc_buf* out) {
|
|
|
9579
9589
|
if (cv) {
|
|
9580
9590
|
sbc_u8 pub[133];
|
|
9581
9591
|
br_ec_public_key pk;
|
|
9582
|
-
if (br_ec_compute_pub(
|
|
9592
|
+
if (br_ec_compute_pub(sbc_ec(sk->curve), &pk, pub, sk) != 0) {
|
|
9583
9593
|
sbc_put1(out, SBC_K_EC); sbc_put1(out, (sbc_u8)cv); sbc_put1(out, 1);
|
|
9584
9594
|
sbc_field(out, pk.q, pk.qlen);
|
|
9585
9595
|
|
|
@@ -9862,7 +9872,7 @@ static int sbc_ecdsa_sign(int bits, const sbc_u8* key, size_t kl, const sbc_u8*
|
|
|
9862
9872
|
sbc_u8 h[64], sig[132];
|
|
9863
9873
|
rc = SBC_OPERATION;
|
|
9864
9874
|
if (sbc_digest(bits, m, ml, h)) {
|
|
9865
|
-
size_t n = br_ecdsa_sign_raw_get_default()(
|
|
9875
|
+
size_t n = br_ecdsa_sign_raw_get_default()(sbc_ec(sk->curve), sbc_hash(bits), h, sk, sig);
|
|
9866
9876
|
if (n) { sbc_put(out, sig, n); rc = SBC_OK; }
|
|
9867
9877
|
}
|
|
9868
9878
|
free(dc);
|
|
@@ -9877,7 +9887,7 @@ static int sbc_ecdsa_verify(int bits, int curve, const sbc_u8* point, size_t pl,
|
|
|
9877
9887
|
if (sl != 2 * fs) return SBC_OK;
|
|
9878
9888
|
br_ec_public_key pk;
|
|
9879
9889
|
pk.curve = sbc_curve_br(curve); pk.q = (sbc_u8*)point; pk.qlen = pl;
|
|
9880
|
-
if (br_ecdsa_vrfy_raw_get_default()(
|
|
9890
|
+
if (br_ecdsa_vrfy_raw_get_default()(sbc_ec(pk.curve), h, sbc_hlen(bits), &pk, sig, sl)) *ok = 1;
|
|
9881
9891
|
return SBC_OK;
|
|
9882
9892
|
}
|
|
9883
9893
|
|
|
@@ -9887,12 +9897,12 @@ static int sbc_ec_point_ok(int curve, const sbc_u8* point, size_t pl) {
|
|
|
9887
9897
|
sbc_u8 q[133];
|
|
9888
9898
|
memcpy(q, point, pl);
|
|
9889
9899
|
sbc_u8 one[1] = { 1 };
|
|
9890
|
-
return
|
|
9900
|
+
return sbc_ec(sbc_curve_br(curve))->mul(q, pl, one, 1, sbc_curve_br(curve)) != 0;
|
|
9891
9901
|
}
|
|
9892
9902
|
static int sbc_ec_keygen(int curve, sbc_buf* out) {
|
|
9893
9903
|
br_ec_private_key sk; br_ec_public_key pk;
|
|
9894
9904
|
sbc_u8 kb[BR_EC_KBUF_PRIV_MAX_SIZE], pb[BR_EC_KBUF_PUB_MAX_SIZE];
|
|
9895
|
-
const br_ec_impl* impl =
|
|
9905
|
+
const br_ec_impl* impl = sbc_ec(sbc_curve_br(curve));
|
|
9896
9906
|
if (!br_ec_keygen(&sbc_prng, impl, &sk, kb, sbc_curve_br(curve))) return SBC_OPERATION;
|
|
9897
9907
|
if (!br_ec_compute_pub(impl, &pk, pb, &sk)) return SBC_OPERATION;
|
|
9898
9908
|
size_t need = br_encode_ec_pkcs8_der(0, &sk, &pk);
|
|
@@ -10059,8 +10069,9 @@ static int sbc_ecdh(int curve, const sbc_u8* pkcs8, size_t kl, const sbc_u8* pee
|
|
|
10059
10069
|
if (sk->curve == sbc_curve_br(curve) && fs && pl == 1 + 2 * fs && peer[0] == 4) {
|
|
10060
10070
|
sbc_u8 pt[133];
|
|
10061
10071
|
memcpy(pt, peer, pl);
|
|
10062
|
-
|
|
10063
|
-
|
|
10072
|
+
const br_ec_impl* impl = sbc_ec(sk->curve);
|
|
10073
|
+
if (impl->mul(pt, pl, sk->x, sk->xlen, sk->curve)) {
|
|
10074
|
+
size_t xl; size_t xo = impl->xoff(sk->curve, &xl);
|
|
10064
10075
|
sbc_put(out, pt + xo, xl); rc = SBC_OK;
|
|
10065
10076
|
}
|
|
10066
10077
|
}
|
|
@@ -14153,7 +14164,7 @@ function __sbWsOp(op, id, data, n) {
|
|
|
14153
14164
|
const unsigned int __id = id.type == 1 && id.val > 0 ? (unsigned int)id.val : 0;
|
|
14154
14165
|
const size_t __n = n.type == 1 && n.val > 0 ? (size_t)n.val : 0;
|
|
14155
14166
|
char __r[64]; __r[0] = 0;
|
|
14156
|
-
if (__op == 'u' || __op == 'W' || (__id & 0x80000000u)) {
|
|
14167
|
+
if (__op == 'u' || __op == 'T' || __op == 'W' || (__id & 0x80000000u)) {
|
|
14157
14168
|
long long __w = -1;
|
|
14158
14169
|
if (!sb_wsc_hook) snprintf(__r, sizeof(__r), "0");
|
|
14159
14170
|
else if (__op == 'W') snprintf(__r, sizeof(__r), "%d", sb_wsc_wakeable_hook && sb_wsc_wakeable_hook() ? 1 : 0);
|
|
@@ -14172,7 +14183,7 @@ function __sbWsOp(op, id, data, n) {
|
|
|
14172
14183
|
const char* __d = 0; size_t __dl = 0;
|
|
14173
14184
|
if (porf_native_fetch_read_raw_bytes(data, &__d, &__dl) == 0) __w = sb_wsc_hook('b', __id, __d, __dl, 0, 0, 0);
|
|
14174
14185
|
snprintf(__r, sizeof(__r), "%lld", __w);
|
|
14175
|
-
} else if (__op == 't' || __op == 'c' || __op == 'u') {
|
|
14186
|
+
} else if (__op == 't' || __op == 'c' || __op == 'u' || __op == 'T' || __op == 'S') {
|
|
14176
14187
|
const char* __d; size_t __dl; char* __do = 0;
|
|
14177
14188
|
porf_native_fetch_read_value(data, &__d, &__dl, &__do);
|
|
14178
14189
|
__w = sb_wsc_hook(__op, __id, __d, __dl, __n, 0, 0);
|
|
@@ -14564,6 +14575,289 @@ function __sbWsPump(ws) {
|
|
|
14564
14575
|
}
|
|
14565
14576
|
}
|
|
14566
14577
|
|
|
14578
|
+
// ---------------------------------------------------------------------------
|
|
14579
|
+
// cloudflare:sockets connect(), from compatibility date 2026-11-17. A socket is a host
|
|
14580
|
+
// socket in the outbound WebSocket table (sb_wsc, raw mode); this end speaks the __sbWsOp ops:
|
|
14581
|
+
// "T" open (host, port, secureTransport mode, server name), "b" queue bytes (returns how many
|
|
14582
|
+
// are still unsent), "S" start TLS, "H" shut the write side, "p" "i" take an event, "k" ack,
|
|
14583
|
+
// "w" park a timer, "r" release. Events: 1 data, 3 error, 4 connected, 6 end of input,
|
|
14584
|
+
// 8 send queue drained, 9 TLS handshake done.
|
|
14585
|
+
const __SB_SOCKETS_DATE = "2026-11-17";
|
|
14586
|
+
const __SB_TCP_HWM = 262144;
|
|
14587
|
+
let __sbTcpOpen = 0;
|
|
14588
|
+
|
|
14589
|
+
function __sbTcpBadInt() {
|
|
14590
|
+
return new TypeError("The value cannot be converted because it is not an integer.");
|
|
14591
|
+
}
|
|
14592
|
+
function __sbTcpTarget(address) {
|
|
14593
|
+
let host;
|
|
14594
|
+
let port;
|
|
14595
|
+
if (__sbIsStr(address)) {
|
|
14596
|
+
let rest = address;
|
|
14597
|
+
let cut;
|
|
14598
|
+
if (rest.charAt(0) === "[") {
|
|
14599
|
+
const close = rest.indexOf("]");
|
|
14600
|
+
cut = close >= 0 && rest.charAt(close + 1) === ":" ? close + 1 : -1;
|
|
14601
|
+
} else cut = rest.lastIndexOf(":");
|
|
14602
|
+
if (cut < 0) {
|
|
14603
|
+
if (rest === "") throw new TypeError("Specified address is missing hostname.");
|
|
14604
|
+
throw new TypeError("Specified address is missing port.");
|
|
14605
|
+
}
|
|
14606
|
+
host = rest.slice(0, cut);
|
|
14607
|
+
port = rest.slice(cut + 1);
|
|
14608
|
+
} else if (address != null && __sbIsObj(address)) {
|
|
14609
|
+
host = address.hostname;
|
|
14610
|
+
port = address.port;
|
|
14611
|
+
if (host === undefined || host === "") throw new TypeError("Specified address is missing hostname.");
|
|
14612
|
+
if (port === undefined) throw new TypeError("Specified address is missing port.");
|
|
14613
|
+
} else throw new TypeError("connect() takes an address string or { hostname, port }.");
|
|
14614
|
+
const n = Number(port);
|
|
14615
|
+
if (!(Number.isInteger(n)) || String(port).trim() === "") throw __sbTcpBadInt();
|
|
14616
|
+
if (n < 0 || n > 65535) throw new RangeError("Port out of range.");
|
|
14617
|
+
return { host: String(host), port: n };
|
|
14618
|
+
}
|
|
14619
|
+
|
|
14620
|
+
function __sbTcpChunk(chunk) {
|
|
14621
|
+
if (__sbIsStr(chunk)) return new TextEncoder().encode(chunk);
|
|
14622
|
+
if (chunk instanceof ArrayBuffer) return new Uint8Array(chunk);
|
|
14623
|
+
if (ArrayBuffer.isView(chunk)) return new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
14624
|
+
throw new TypeError("This TransformStream is being used as a byte stream, but received an object of non-ArrayBuffer/ArrayBufferView type on its writable side.");
|
|
14625
|
+
}
|
|
14626
|
+
|
|
14627
|
+
// Takes every event the host has, then parks a timer (or polls, where nothing can wake the loop).
|
|
14628
|
+
function __sbTcpPump(st) {
|
|
14629
|
+
if (st.id === 0) return;
|
|
14630
|
+
for (;;) {
|
|
14631
|
+
const head = __sbWsOp("p", st.id, "", 0);
|
|
14632
|
+
if (head === "") {
|
|
14633
|
+
if (st.id === 0) return;
|
|
14634
|
+
if (__sbWsCanWake()) {
|
|
14635
|
+
st.timer = setTimeout(() => __sbTcpPump(st), 2147483647);
|
|
14636
|
+
__sbWsOp("w", st.id, "", st.timer);
|
|
14637
|
+
} else st.timer = setTimeout(() => __sbTcpPump(st), 10);
|
|
14638
|
+
return;
|
|
14639
|
+
}
|
|
14640
|
+
const parts = head.split(" ");
|
|
14641
|
+
const kind = Number(parts[0]);
|
|
14642
|
+
const length = Number(parts[1]);
|
|
14643
|
+
const bytes = new Uint8Array(length);
|
|
14644
|
+
if (length > 0) __sbWsOp("i", st.id, bytes, 0);
|
|
14645
|
+
else __sbWsOp("i", st.id, new Uint8Array(0), 0);
|
|
14646
|
+
if (kind === 1) {
|
|
14647
|
+
st.inbox.push(bytes);
|
|
14648
|
+
} else if (kind === 4 || kind === 9) {
|
|
14649
|
+
if (st.opening) {
|
|
14650
|
+
const o = st.opening;
|
|
14651
|
+
st.opening = null;
|
|
14652
|
+
o.resolve({ remoteAddress: st.host + ":" + st.port, localAddress: null });
|
|
14653
|
+
}
|
|
14654
|
+
} else if (kind === 6) {
|
|
14655
|
+
st.eof = true;
|
|
14656
|
+
if (!st.allowHalfOpen) st.writeEnd = true;
|
|
14657
|
+
} else if (kind === 8) {
|
|
14658
|
+
const d = st.drain;
|
|
14659
|
+
st.drain = null;
|
|
14660
|
+
if (d) d();
|
|
14661
|
+
} else if (kind === 3) {
|
|
14662
|
+
// workerd words every connect failure the same; the host's reason stays on st.reason
|
|
14663
|
+
st.reason = new TextDecoder().decode(bytes);
|
|
14664
|
+
st.failed = new Error("proxy request failed, cannot connect to the specified address");
|
|
14665
|
+
st.eof = true;
|
|
14666
|
+
}
|
|
14667
|
+
__sbTcpSettle(st);
|
|
14668
|
+
if (st.id === 0) return;
|
|
14669
|
+
}
|
|
14670
|
+
}
|
|
14671
|
+
|
|
14672
|
+
// Applies state changes to the streams and promises.
|
|
14673
|
+
function __sbTcpSettle(st) {
|
|
14674
|
+
if (st.opening && st.failed) {
|
|
14675
|
+
const o = st.opening;
|
|
14676
|
+
st.opening = null;
|
|
14677
|
+
o.reject(st.failed);
|
|
14678
|
+
}
|
|
14679
|
+
const wake = st.pullWake;
|
|
14680
|
+
if (wake && (st.inbox.length > 0 || st.eof)) {
|
|
14681
|
+
st.pullWake = null;
|
|
14682
|
+
wake();
|
|
14683
|
+
}
|
|
14684
|
+
if (st.eof && st.inbox.length === 0 && !st.settled) {
|
|
14685
|
+
if (st.failed) {
|
|
14686
|
+
st.settled = true;
|
|
14687
|
+
st.closedReject(st.failed);
|
|
14688
|
+
st.release();
|
|
14689
|
+
} else if (!st.allowHalfOpen) {
|
|
14690
|
+
st.settled = true;
|
|
14691
|
+
st.closedResolve(undefined);
|
|
14692
|
+
if (st.writableController) st.writableController.error(new TypeError("This WritableStream has been closed."));
|
|
14693
|
+
st.release();
|
|
14694
|
+
}
|
|
14695
|
+
}
|
|
14696
|
+
}
|
|
14697
|
+
|
|
14698
|
+
function __sbTcpRelease(st) {
|
|
14699
|
+
const id = st.id;
|
|
14700
|
+
if (id === 0) return;
|
|
14701
|
+
st.id = 0;
|
|
14702
|
+
if (st.timer !== undefined) clearTimeout(st.timer);
|
|
14703
|
+
__sbWsOp("r", id, "", 0);
|
|
14704
|
+
__sbTcpOpen--;
|
|
14705
|
+
}
|
|
14706
|
+
|
|
14707
|
+
// One socket facade over a host connection. `st` is shared with the socket startTls() returns.
|
|
14708
|
+
function __sbTcpFacade(st, mode) {
|
|
14709
|
+
const closed = new Promise((resolve, reject) => {
|
|
14710
|
+
st.closedResolve = resolve;
|
|
14711
|
+
st.closedReject = reject;
|
|
14712
|
+
});
|
|
14713
|
+
// A rejected `closed` nobody awaits must not surface as an unhandled rejection.
|
|
14714
|
+
closed.catch(() => {});
|
|
14715
|
+
const opened = new Promise((resolve, reject) => {
|
|
14716
|
+
st.opening = { resolve, reject };
|
|
14717
|
+
});
|
|
14718
|
+
opened.catch(() => {});
|
|
14719
|
+
const readable = new ReadableStream(
|
|
14720
|
+
{
|
|
14721
|
+
start(controller) {
|
|
14722
|
+
st.readableController = controller;
|
|
14723
|
+
},
|
|
14724
|
+
pull(controller) {
|
|
14725
|
+
if (st.inbox.length > 0) {
|
|
14726
|
+
const chunk = st.inbox.shift();
|
|
14727
|
+
controller.enqueue(chunk);
|
|
14728
|
+
if (st.id !== 0) __sbWsOp("k", st.id, "", chunk.length);
|
|
14729
|
+
return undefined;
|
|
14730
|
+
}
|
|
14731
|
+
if (st.eof) {
|
|
14732
|
+
if (st.failed && !st.closedByUser) {
|
|
14733
|
+
// a failed connect reads as an empty stream, as in workerd
|
|
14734
|
+
}
|
|
14735
|
+
controller.close();
|
|
14736
|
+
return undefined;
|
|
14737
|
+
}
|
|
14738
|
+
return new Promise((resolve) => {
|
|
14739
|
+
st.pullWake = () => {
|
|
14740
|
+
if (st.inbox.length > 0) {
|
|
14741
|
+
const chunk = st.inbox.shift();
|
|
14742
|
+
controller.enqueue(chunk);
|
|
14743
|
+
if (st.id !== 0) __sbWsOp("k", st.id, "", chunk.length);
|
|
14744
|
+
} else controller.close();
|
|
14745
|
+
resolve();
|
|
14746
|
+
};
|
|
14747
|
+
});
|
|
14748
|
+
},
|
|
14749
|
+
cancel() {
|
|
14750
|
+
return undefined;
|
|
14751
|
+
},
|
|
14752
|
+
},
|
|
14753
|
+
{ highWaterMark: 0 },
|
|
14754
|
+
);
|
|
14755
|
+
const writable = new WritableStream(
|
|
14756
|
+
{
|
|
14757
|
+
start(controller) {
|
|
14758
|
+
st.writableController = controller;
|
|
14759
|
+
},
|
|
14760
|
+
write(chunk) {
|
|
14761
|
+
const bytes = __sbTcpChunk(chunk);
|
|
14762
|
+
if (st.id === 0) throw new TypeError("This WritableStream has been closed.");
|
|
14763
|
+
const queued = Number(__sbWsOp("b", st.id, __sbToBytes(bytes), 0));
|
|
14764
|
+
if (queued > __SB_TCP_HWM) return new Promise((resolve) => (st.drain = resolve));
|
|
14765
|
+
return undefined;
|
|
14766
|
+
},
|
|
14767
|
+
close() {
|
|
14768
|
+
if (st.id !== 0) __sbWsOp("H", st.id, "", 0);
|
|
14769
|
+
st.writeClosed = true;
|
|
14770
|
+
return undefined;
|
|
14771
|
+
},
|
|
14772
|
+
abort() {
|
|
14773
|
+
return undefined;
|
|
14774
|
+
},
|
|
14775
|
+
},
|
|
14776
|
+
{ highWaterMark: 1 },
|
|
14777
|
+
);
|
|
14778
|
+
const socket = {
|
|
14779
|
+
readable,
|
|
14780
|
+
writable,
|
|
14781
|
+
opened,
|
|
14782
|
+
closed,
|
|
14783
|
+
upgraded: false,
|
|
14784
|
+
secureTransport: mode,
|
|
14785
|
+
close() {
|
|
14786
|
+
st.closedByUser = true;
|
|
14787
|
+
if (st.writableController) st.writableController.error(new TypeError("This WritableStream belongs to an object that is closing."));
|
|
14788
|
+
const wake = st.pullWake;
|
|
14789
|
+
st.pullWake = null;
|
|
14790
|
+
if (!st.settled) {
|
|
14791
|
+
st.settled = true;
|
|
14792
|
+
st.closedResolve(undefined);
|
|
14793
|
+
}
|
|
14794
|
+
st.inbox.length = 0;
|
|
14795
|
+
st.eof = true;
|
|
14796
|
+
if (wake && st.readableController) st.readableController.error(new TypeError("This socket has been closed."));
|
|
14797
|
+
__sbTcpRelease(st);
|
|
14798
|
+
return Promise.resolve();
|
|
14799
|
+
},
|
|
14800
|
+
startTls(options) {
|
|
14801
|
+
if (mode !== "starttls" || st.upgradedOnce)
|
|
14802
|
+
throw new TypeError(st.upgradedOnce ? "The connection was closed before startTls could be started." : "The `secureTransport` socket option must be set to 'starttls' for startTls to be used.");
|
|
14803
|
+
if (readable.locked || writable.locked) throw new TypeError("Cannot startTls while the streams are locked.");
|
|
14804
|
+
st.upgradedOnce = true;
|
|
14805
|
+
let sni = st.host;
|
|
14806
|
+
if (options != null && options.expectedServerHostname != null) sni = String(options.expectedServerHostname);
|
|
14807
|
+
__sbWsOp("S", st.id, sni, 0);
|
|
14808
|
+
// the old socket ends once the upgrade is under way (workerd resolves its `closed`)
|
|
14809
|
+
const oldResolve = st.closedResolve;
|
|
14810
|
+
const next = __sbTcpFacade(st, "on");
|
|
14811
|
+
setTimeout(() => oldResolve(undefined), 0);
|
|
14812
|
+
return next;
|
|
14813
|
+
},
|
|
14814
|
+
};
|
|
14815
|
+
return socket;
|
|
14816
|
+
}
|
|
14817
|
+
|
|
14818
|
+
function __sbConnect(address, options) {
|
|
14819
|
+
if (__sbCompatDate() < __SB_SOCKETS_DATE)
|
|
14820
|
+
throw new Error("connect() needs compatibility_date " + __SB_SOCKETS_DATE + " or later");
|
|
14821
|
+
const target = __sbTcpTarget(address);
|
|
14822
|
+
let mode = "off";
|
|
14823
|
+
let allowHalfOpen = false;
|
|
14824
|
+
if (options != null) {
|
|
14825
|
+
if (options.secureTransport !== undefined) {
|
|
14826
|
+
mode = String(options.secureTransport);
|
|
14827
|
+
if (mode !== "off" && mode !== "on" && mode !== "starttls")
|
|
14828
|
+
throw new TypeError("Unsupported secureTransport value: " + mode);
|
|
14829
|
+
}
|
|
14830
|
+
allowHalfOpen = options.allowHalfOpen === true;
|
|
14831
|
+
}
|
|
14832
|
+
const st = {
|
|
14833
|
+
id: 0, host: target.host, port: target.port, inbox: [], eof: false, failed: null, settled: false,
|
|
14834
|
+
allowHalfOpen, pullWake: null, drain: null, timer: undefined, opening: null, upgradedOnce: false,
|
|
14835
|
+
closedByUser: false, writableController: null, readableController: null, reason: "", writeClosed: false, writeEnd: false,
|
|
14836
|
+
release: null, closedResolve: null, closedReject: null,
|
|
14837
|
+
};
|
|
14838
|
+
st.release = () => __sbTcpRelease(st);
|
|
14839
|
+
const socket = __sbTcpFacade(st, mode);
|
|
14840
|
+
if (target.port === 25) {
|
|
14841
|
+
st.failed = new Error("proxy request failed, cannot connect to the specified address");
|
|
14842
|
+
st.eof = true;
|
|
14843
|
+
setTimeout(() => __sbTcpSettle(st), 0);
|
|
14844
|
+
return socket;
|
|
14845
|
+
}
|
|
14846
|
+
const spec = target.host + "\n" + target.port + "\n" + (mode === "on" ? "1" : mode === "starttls" ? "2" : "0") + "\n" + target.host;
|
|
14847
|
+
const id = __sbTcpOpen >= 16 ? 0 : Number(__sbWsOp("T", 0, spec, 0));
|
|
14848
|
+
if (!(id > 0)) {
|
|
14849
|
+
st.failed = new Error(__sbTcpOpen >= 16 ? "Too many open connections" : "TCP sockets are not available on this host");
|
|
14850
|
+
st.eof = true;
|
|
14851
|
+
setTimeout(() => __sbTcpSettle(st), 0);
|
|
14852
|
+
return socket;
|
|
14853
|
+
}
|
|
14854
|
+
st.id = id;
|
|
14855
|
+
__sbTcpOpen++;
|
|
14856
|
+
__sbTcpPump(st);
|
|
14857
|
+
return socket;
|
|
14858
|
+
}
|
|
14859
|
+
globalThis.__sbSockets = { connect: __sbConnect };
|
|
14860
|
+
|
|
14567
14861
|
// ---------------------------------------------------------------------------
|
|
14568
14862
|
// Outbound WebSockets, from compatibility date 2026-11-01: new WebSocket(url, protocols) and
|
|
14569
14863
|
// fetch(url, { headers: { Upgrade: "websocket" } }), whose Response carries the connected
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { inlineWorkersImports } from "./wrap";
|
|
3
|
+
import { SOCKETS_DATE, validateHttpSyncSource } from "./source";
|
|
4
|
+
|
|
5
|
+
test("cloudflare:sockets imports become the prelude's connect", () => {
|
|
6
|
+
const out = inlineWorkersImports('import { connect } from "cloudflare:sockets";\nconst x = 1;');
|
|
7
|
+
expect(out).toContain("const { connect } = globalThis.__sbSockets;");
|
|
8
|
+
expect(inlineWorkersImports('import { connect as dial } from "cloudflare:sockets";')).toContain(
|
|
9
|
+
"const { connect: dial } = globalThis.__sbSockets;",
|
|
10
|
+
);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("a lazy import of cloudflare:sockets (postgres/cf, pg-cloudflare) resolves to the same object", () => {
|
|
14
|
+
const out = inlineWorkersImports('const { connect } = await import("cloudflare:sockets");');
|
|
15
|
+
expect(out).toBe("const { connect } = await Promise.resolve(globalThis.__sbSockets);");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const handler = "export default { fetch() { return new Response(Buffer.from('a').toString() + process.nextTick); } };";
|
|
19
|
+
|
|
20
|
+
test("Buffer and process in a bundle are refused before the sockets date and allowed from it", () => {
|
|
21
|
+
expect(validateHttpSyncSource(handler, "2026-09-14").ok).toBe(false);
|
|
22
|
+
expect(validateHttpSyncSource(handler, SOCKETS_DATE)).toEqual({ ok: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("a node: import specifier is still refused from the sockets date", () => {
|
|
26
|
+
const withNode = "export default { fetch() { return new Response('node:fs'.length + \"\"); } };".replace(
|
|
27
|
+
"'node:fs'",
|
|
28
|
+
'"node:fs"',
|
|
29
|
+
);
|
|
30
|
+
expect(validateHttpSyncSource(withNode, SOCKETS_DATE).ok).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("cloudflare:sockets needs the sockets date", () => {
|
|
34
|
+
const app =
|
|
35
|
+
'import { connect } from "cloudflare:sockets";\nexport default { fetch() { return new Response(String(typeof connect)); } };';
|
|
36
|
+
const before = validateHttpSyncSource(app, "2026-11-16");
|
|
37
|
+
expect(before.ok).toBe(false);
|
|
38
|
+
expect(before.ok ? [] : before.errors).toContain(
|
|
39
|
+
`cloudflare:sockets needs compatibility_date ${SOCKETS_DATE} or later`,
|
|
40
|
+
);
|
|
41
|
+
expect(validateHttpSyncSource(app, SOCKETS_DATE)).toEqual({ ok: true });
|
|
42
|
+
});
|
package/src/source.ts
CHANGED
|
@@ -67,6 +67,14 @@ function usesNodeGlobalUnguarded(module: string): boolean {
|
|
|
67
67
|
/** `new WebSocket(url)` connects out from this compatibility date (earlier, only WebSocketPair makes one). */
|
|
68
68
|
export const OUTBOUND_WEBSOCKET_DATE = "2026-11-01";
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* From this date `cloudflare:sockets` connect() exists, and so does the bundled Buffer and process
|
|
72
|
+
* stub a database driver brings: the bundler inlines both, so `Buffer.from` and `process.nextTick`
|
|
73
|
+
* in the output are the bundle's own code, not a host API.
|
|
74
|
+
*/
|
|
75
|
+
export const SOCKETS_DATE = "2026-11-17";
|
|
76
|
+
const driverAllowed = (message: string) => message === "Node, Bun, and Deno APIs are not supported";
|
|
77
|
+
|
|
70
78
|
export function validateHttpSyncSource(module: string, compatibilityDate?: string): SourceValidation {
|
|
71
79
|
// `cloudflare:workers` is the one import allowed past bundling: the compiler turns it
|
|
72
80
|
// into globals the same way (see inlineWorkersImports).
|
|
@@ -85,8 +93,15 @@ export function validateHttpSyncSource(module: string, compatibilityDate?: strin
|
|
|
85
93
|
"handler must default-export an object with fetch(request), scheduled() or queue(), or a WorkerEntrypoint class",
|
|
86
94
|
);
|
|
87
95
|
}
|
|
96
|
+
if (
|
|
97
|
+
/["']cloudflare:sockets["']/.test(module) &&
|
|
98
|
+
!(compatibilityDate !== undefined && compatibilityDate >= SOCKETS_DATE)
|
|
99
|
+
)
|
|
100
|
+
errors.push(`cloudflare:sockets needs compatibility_date ${SOCKETS_DATE} or later`);
|
|
88
101
|
const guardedGlobals = compatibilityDate !== undefined && compatibilityDate >= GUARDED_GLOBALS_DATE;
|
|
102
|
+
const sockets = compatibilityDate !== undefined && compatibilityDate >= SOCKETS_DATE;
|
|
89
103
|
for (const [pattern, message] of alwaysForbidden) {
|
|
104
|
+
if (sockets && driverAllowed(message) && !/['"`]node:/.test(source)) continue;
|
|
90
105
|
if (pattern === NODE_GLOBAL_USE && guardedGlobals ? !usesNodeGlobalUnguarded(source) : !pattern.test(source))
|
|
91
106
|
continue;
|
|
92
107
|
errors.push(message);
|
package/src/subtle/README.md
CHANGED
|
@@ -12,10 +12,10 @@ uses it.
|
|
|
12
12
|
|
|
13
13
|
| Need | Source | Licence |
|
|
14
14
|
| --- | --- | --- |
|
|
15
|
-
| SHA-1/256/384/512, HMAC, HKDF | BearSSL
|
|
16
|
-
| RSA (private and public operations, key generation, PKCS#1 v1.5 padding), RSA-OAEP | BearSSL
|
|
17
|
-
| ECDSA and ECDH on P-256, P-384, P-521, EC key generation | BearSSL
|
|
18
|
-
| AES (constant-time), CBC, CTR, GCM | BearSSL
|
|
15
|
+
| SHA-1/256/384/512, HMAC, HKDF | BearSSL (master 7bea48e) | MIT |
|
|
16
|
+
| RSA (private and public operations, key generation, PKCS#1 v1.5 padding), RSA-OAEP | BearSSL (master 7bea48e) | MIT |
|
|
17
|
+
| ECDSA and ECDH on P-256, P-384, P-521, EC key generation | BearSSL (master 7bea48e) | MIT |
|
|
18
|
+
| AES (constant-time), CBC, CTR, GCM | BearSSL (master 7bea48e) | MIT |
|
|
19
19
|
| Ed25519, X25519 | Monocypher 4.0.2, vendored in `vendor/` | BSD-2-Clause or CC0-1.0 |
|
|
20
20
|
|
|
21
21
|
BearSSL is already linked into every embedded sprout (standalone binaries and sproutd) for
|
package/src/subtle/subtle.c
CHANGED
|
@@ -183,6 +183,17 @@ static int sbc_oid_eq(const sbc_u8* a, size_t al, const sbc_u8* b, size_t bl) {
|
|
|
183
183
|
|
|
184
184
|
/* Curve codes used on the wire to JS: 1 = P-256, 2 = P-384, 3 = P-521. */
|
|
185
185
|
static int sbc_curve_br(int c) { return c == 1 ? BR_EC_secp256r1 : c == 2 ? BR_EC_secp384r1 : c == 3 ? BR_EC_secp521r1 : 0; }
|
|
186
|
+
/* BearSSL's default EC implementation is the portable m31 (m15 where there is no 64-bit multiply
|
|
187
|
+
result). P-256 has a 64-bit specialisation, m64, that is roughly 3x faster to sign and 5x to verify,
|
|
188
|
+
but BearSSL never selects it by itself. br_ec_p256_m64_get() returns null on a target without a
|
|
189
|
+
128-bit multiply, and m64 only knows P-256, so every other curve keeps the default. */
|
|
190
|
+
static const br_ec_impl* sbc_ec(int br_curve) {
|
|
191
|
+
if (br_curve == BR_EC_secp256r1) {
|
|
192
|
+
const br_ec_impl* fast = br_ec_p256_m64_get();
|
|
193
|
+
if (fast) return fast;
|
|
194
|
+
}
|
|
195
|
+
return br_ec_get_default();
|
|
196
|
+
}
|
|
186
197
|
static int sbc_curve_from_br(int c) { return c == BR_EC_secp256r1 ? 1 : c == BR_EC_secp384r1 ? 2 : c == BR_EC_secp521r1 ? 3 : 0; }
|
|
187
198
|
static size_t sbc_curve_size(int c) { return c == 1 ? 32 : c == 2 ? 48 : c == 3 ? 66 : 0; }
|
|
188
199
|
static void sbc_curve_oid(int c, const sbc_u8** o, size_t* n) {
|
|
@@ -380,7 +391,7 @@ static int sbc_decode(int pkcs8, const sbc_u8* der, size_t len, sbc_buf* out) {
|
|
|
380
391
|
if (cv) {
|
|
381
392
|
sbc_u8 pub[133];
|
|
382
393
|
br_ec_public_key pk;
|
|
383
|
-
if (br_ec_compute_pub(
|
|
394
|
+
if (br_ec_compute_pub(sbc_ec(sk->curve), &pk, pub, sk) != 0) {
|
|
384
395
|
sbc_put1(out, SBC_K_EC); sbc_put1(out, (sbc_u8)cv); sbc_put1(out, 1);
|
|
385
396
|
sbc_field(out, pk.q, pk.qlen);
|
|
386
397
|
/* The scalar, left padded to the field size as JWK d wants it. */
|
|
@@ -677,7 +688,7 @@ static int sbc_ecdsa_sign(int bits, const sbc_u8* key, size_t kl, const sbc_u8*
|
|
|
677
688
|
sbc_u8 h[64], sig[132];
|
|
678
689
|
rc = SBC_OPERATION;
|
|
679
690
|
if (sbc_digest(bits, m, ml, h)) {
|
|
680
|
-
size_t n = br_ecdsa_sign_raw_get_default()(
|
|
691
|
+
size_t n = br_ecdsa_sign_raw_get_default()(sbc_ec(sk->curve), sbc_hash(bits), h, sk, sig);
|
|
681
692
|
if (n) { sbc_put(out, sig, n); rc = SBC_OK; }
|
|
682
693
|
}
|
|
683
694
|
free(dc);
|
|
@@ -692,7 +703,7 @@ static int sbc_ecdsa_verify(int bits, int curve, const sbc_u8* point, size_t pl,
|
|
|
692
703
|
if (sl != 2 * fs) return SBC_OK;
|
|
693
704
|
br_ec_public_key pk;
|
|
694
705
|
pk.curve = sbc_curve_br(curve); pk.q = (sbc_u8*)point; pk.qlen = pl;
|
|
695
|
-
if (br_ecdsa_vrfy_raw_get_default()(
|
|
706
|
+
if (br_ecdsa_vrfy_raw_get_default()(sbc_ec(pk.curve), h, sbc_hlen(bits), &pk, sig, sl)) *ok = 1;
|
|
696
707
|
return SBC_OK;
|
|
697
708
|
}
|
|
698
709
|
/* A public point is valid when it is on the curve (the group is prime order, cofactor 1). */
|
|
@@ -702,12 +713,12 @@ static int sbc_ec_point_ok(int curve, const sbc_u8* point, size_t pl) {
|
|
|
702
713
|
sbc_u8 q[133];
|
|
703
714
|
memcpy(q, point, pl);
|
|
704
715
|
sbc_u8 one[1] = { 1 };
|
|
705
|
-
return
|
|
716
|
+
return sbc_ec(sbc_curve_br(curve))->mul(q, pl, one, 1, sbc_curve_br(curve)) != 0;
|
|
706
717
|
}
|
|
707
718
|
static int sbc_ec_keygen(int curve, sbc_buf* out) {
|
|
708
719
|
br_ec_private_key sk; br_ec_public_key pk;
|
|
709
720
|
sbc_u8 kb[BR_EC_KBUF_PRIV_MAX_SIZE], pb[BR_EC_KBUF_PUB_MAX_SIZE];
|
|
710
|
-
const br_ec_impl* impl =
|
|
721
|
+
const br_ec_impl* impl = sbc_ec(sbc_curve_br(curve));
|
|
711
722
|
if (!br_ec_keygen(&sbc_prng, impl, &sk, kb, sbc_curve_br(curve))) return SBC_OPERATION;
|
|
712
723
|
if (!br_ec_compute_pub(impl, &pk, pb, &sk)) return SBC_OPERATION;
|
|
713
724
|
size_t need = br_encode_ec_pkcs8_der(0, &sk, &pk);
|
|
@@ -885,8 +896,9 @@ static int sbc_ecdh(int curve, const sbc_u8* pkcs8, size_t kl, const sbc_u8* pee
|
|
|
885
896
|
if (sk->curve == sbc_curve_br(curve) && fs && pl == 1 + 2 * fs && peer[0] == 4) {
|
|
886
897
|
sbc_u8 pt[133];
|
|
887
898
|
memcpy(pt, peer, pl);
|
|
888
|
-
|
|
889
|
-
|
|
899
|
+
const br_ec_impl* impl = sbc_ec(sk->curve);
|
|
900
|
+
if (impl->mul(pt, pl, sk->x, sk->xlen, sk->curve)) {
|
|
901
|
+
size_t xl; size_t xo = impl->xoff(sk->curve, &xl);
|
|
890
902
|
sbc_put(out, pt + xo, xl); rc = SBC_OK;
|
|
891
903
|
}
|
|
892
904
|
}
|
|
@@ -9,7 +9,7 @@ import { assembleSubtleC } from "./assemble";
|
|
|
9
9
|
|
|
10
10
|
const bear = join(
|
|
11
11
|
process.env.SPROUTBOAT_TOOLCHAIN_CACHE ?? join(homedir(), ".cache/sproutboat"),
|
|
12
|
-
"bearssl-
|
|
12
|
+
"bearssl-7bea48e/BearSSL-7bea48e",
|
|
13
13
|
);
|
|
14
14
|
const lib = join(bear, "build/libbearssl.a");
|
|
15
15
|
// SUBTLE_DRIVER: a prebuilt driver binary (a machine with no C compiler, or a cross-built one).
|