@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/runtime",
3
- "version": "0.40.0",
3
+ "version": "0.42.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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(br_ec_get_default(), &pk, pub, sk) != 0) {
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()(br_ec_get_default(), sbc_hash(bits), h, sk, sig);
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()(br_ec_get_default(), h, sbc_hlen(bits), &pk, sig, sl)) *ok = 1;
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 br_ec_get_default()->mul(q, pl, one, 1, sbc_curve_br(curve)) != 0;
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 = br_ec_get_default();
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
- if (br_ec_get_default()->mul(pt, pl, sk->x, sk->xlen, sk->curve)) {
10063
- size_t xl; size_t xo = br_ec_get_default()->xoff(sk->curve, &xl);
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);
@@ -12,10 +12,10 @@ uses it.
12
12
 
13
13
  | Need | Source | Licence |
14
14
  | --- | --- | --- |
15
- | SHA-1/256/384/512, HMAC, HKDF | BearSSL 0.6 | MIT |
16
- | RSA (private and public operations, key generation, PKCS#1 v1.5 padding), RSA-OAEP | BearSSL 0.6 | MIT |
17
- | ECDSA and ECDH on P-256, P-384, P-521, EC key generation | BearSSL 0.6 | MIT |
18
- | AES (constant-time), CBC, CTR, GCM | BearSSL 0.6 | MIT |
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
@@ -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(br_ec_get_default(), &pk, pub, sk) != 0) {
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()(br_ec_get_default(), sbc_hash(bits), h, sk, sig);
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()(br_ec_get_default(), h, sbc_hlen(bits), &pk, sig, sl)) *ok = 1;
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 br_ec_get_default()->mul(q, pl, one, 1, sbc_curve_br(curve)) != 0;
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 = br_ec_get_default();
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
- if (br_ec_get_default()->mul(pt, pl, sk->x, sk->xlen, sk->curve)) {
889
- size_t xl; size_t xo = br_ec_get_default()->xoff(sk->curve, &xl);
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-0.6/bearssl-0.6",
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).