@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.
@@ -3302,6 +3302,10 @@ typedef struct sb_wsc {
3302
3302
  uint64_t rng;
3303
3303
  char *host, *path, *headers, *protocols;
3304
3304
  int port, tls;
3305
+ // Raw TCP (cloudflare:sockets connect()): no WebSocket framing, bytes both ways.
3306
+ int raw, starttls_req, shutdown_req, want_drain;
3307
+ size_t unsent, window;
3308
+ char sni[256];
3305
3309
  } sb_wsc;
3306
3310
  static pthread_mutex_t sb_wsc_lock = PTHREAD_MUTEX_INITIALIZER;
3307
3311
  static sb_wsc* sb_wscs = 0;
@@ -3496,6 +3500,7 @@ static long sb_wsio_pump(sb_wsc* c, sb_wsio* io, unsigned char* buf, size_t cap,
3496
3500
  }
3497
3501
  }
3498
3502
 
3503
+ static int sb_wsio_start_tls(sb_wsio* io, const char* host, char* why, size_t whylen);
3499
3504
  static int sb_wsio_open(sb_wsio* io, const char* host, int port, int tls, char* why, size_t whylen) {
3500
3505
  memset(io, 0, sizeof(*io));
3501
3506
  io->fd = sb_tcp_connect(host, port, why, whylen);
@@ -3506,6 +3511,11 @@ static int sb_wsio_open(sb_wsio* io, const char* host, int port, int tls, char*
3506
3511
  fcntl(io->fd, F_SETFL, fcntl(io->fd, F_GETFL) | O_NONBLOCK);
3507
3512
  io->tls = tls;
3508
3513
  if (!tls) return 0;
3514
+ return sb_wsio_start_tls(io, host, why, whylen);
3515
+ }
3516
+ // Starts TLS on an open fd (connect with secureTransport "on", or startTls()).
3517
+ static int sb_wsio_start_tls(sb_wsio* io, const char* host, char* why, size_t whylen) {
3518
+ io->tls = 1;
3509
3519
  io->sc = (br_ssl_client_context*)malloc(sizeof(br_ssl_client_context));
3510
3520
  io->xc = (br_x509_minimal_context*)malloc(sizeof(br_x509_minimal_context));
3511
3521
  io->iobuf = (unsigned char*)malloc(BR_SSL_BUFSIZE_BIDI);
@@ -3759,6 +3769,91 @@ finish:
3759
3769
  return 0;
3760
3770
  }
3761
3771
 
3772
+ // Raw TCP: the thread for connect(). Events: 1 data, 3 error (reason in data), 4 connected (plain),
3773
+ // 6 end of input, 8 send queue drained (after a write left more than the high-water mark queued),
3774
+ // 9 TLS handshake finished.
3775
+ #define SB_TCP_READ_WINDOW (256u << 10)
3776
+ static void* sb_tcp_thread(void* arg) {
3777
+ sb_wsc* c = (sb_wsc*)arg;
3778
+ sb_wsio io;
3779
+ memset(&io, 0, sizeof(io));
3780
+ io.fd = -1;
3781
+ char why[512] = "";
3782
+ unsigned char buf[16384];
3783
+ unsigned char* wq = 0; size_t wq_len = 0, wq_cap = 0;
3784
+ int tls_waiting = 0, eof = 0, shut = 0;
3785
+ if (sb_wsio_open(&io, c->host, c->port, c->tls, why, sizeof(why)) != 0) { sb_wsc_push(c, 3, 0, why, strlen(why)); goto finish; }
3786
+ if (c->tls) tls_waiting = 1; else sb_wsc_push(c, 4, 0, "", 0);
3787
+ for (;;) {
3788
+ pthread_mutex_lock(&c->lock);
3789
+ int rel = c->released;
3790
+ size_t window = c->unacked;
3791
+ int st = c->starttls_req; c->starttls_req = 0;
3792
+ int sh = c->shutdown_req;
3793
+ char sni[256]; memcpy(sni, c->sni, sizeof(sni));
3794
+ if (c->outq_len) {
3795
+ if (wq_len + c->outq_len > wq_cap) {
3796
+ size_t cap = wq_cap ? wq_cap : 4096;
3797
+ while (cap < wq_len + c->outq_len) cap *= 2;
3798
+ unsigned char* grown = (unsigned char*)realloc(wq, cap);
3799
+ if (grown) { wq = grown; wq_cap = cap; }
3800
+ }
3801
+ if (wq_len + c->outq_len <= wq_cap) { memcpy(wq + wq_len, c->outq, c->outq_len); wq_len += c->outq_len; c->outq_len = 0; }
3802
+ }
3803
+ c->unsent = wq_len;
3804
+ pthread_mutex_unlock(&c->lock);
3805
+ if (rel) {
3806
+ if (wq_len) sb_wsio_pump(c, &io, buf, sizeof(buf), wq, &wq_len, 0, 0);
3807
+ goto finish;
3808
+ }
3809
+ if (st && !io.tls) {
3810
+ if (wq_len) sb_wsio_pump(c, &io, buf, sizeof(buf), wq, &wq_len, 0, 0);
3811
+ if (sb_wsio_start_tls(&io, sni[0] ? sni : c->host, why, sizeof(why)) != 0) { sb_wsc_push(c, 3, 0, why, strlen(why)); goto finish; }
3812
+ tls_waiting = 1;
3813
+ }
3814
+ long n = sb_wsio_pump(c, &io, buf, sizeof(buf), wq, &wq_len, !eof && window < SB_TCP_READ_WINDOW, tls_waiting ? 20 : -1);
3815
+ if (tls_waiting && io.tls && (br_ssl_engine_current_state(&io.sc->eng) & (BR_SSL_SENDAPP | BR_SSL_RECVAPP))) {
3816
+ tls_waiting = 0;
3817
+ sb_wsc_push(c, 9, 0, "", 0);
3818
+ }
3819
+ if (n == -2) continue;
3820
+ if (n < 0) {
3821
+ int err = io.tls ? br_ssl_engine_last_error(&io.sc->eng) : 0;
3822
+ if (err != 0 && err != BR_ERR_IO) { snprintf(why, sizeof(why), "TLS error %d", err); sb_wsc_push(c, 3, 0, why, strlen(why)); goto finish; }
3823
+ if (tls_waiting) { snprintf(why, sizeof(why), "connection closed during the TLS handshake"); sb_wsc_push(c, 3, 0, why, strlen(why)); goto finish; }
3824
+ if (!eof) { eof = 1; sb_wsc_push(c, 6, 0, "", 0); }
3825
+ // keep serving writes until released; with no reader the pump just waits for the wake pipe
3826
+ n = 0;
3827
+ }
3828
+ if (n == 0) sb_wsc_drain_wake(c);
3829
+ else sb_wsc_push(c, 1, 0, (const char*)buf, (size_t)n);
3830
+ if (sh && wq_len == 0 && !shut) {
3831
+ shut = 1;
3832
+ if (io.tls) br_ssl_engine_close(&io.sc->eng); else shutdown(io.fd, SHUT_WR);
3833
+ }
3834
+ int drained = 0;
3835
+ pthread_mutex_lock(&c->lock);
3836
+ c->unsent = wq_len;
3837
+ if (c->want_drain && c->outq_len + wq_len == 0) { c->want_drain = 0; drained = 1; }
3838
+ pthread_mutex_unlock(&c->lock);
3839
+ if (drained) sb_wsc_push(c, 8, 0, "", 0);
3840
+ }
3841
+ finish:
3842
+ sb_wsio_close(&io);
3843
+ free(wq);
3844
+ pthread_mutex_lock(&c->lock);
3845
+ c->finished = 1;
3846
+ int rel2 = c->released;
3847
+ pthread_mutex_unlock(&c->lock);
3848
+ if (rel2) {
3849
+ pthread_mutex_lock(&sb_wsc_lock);
3850
+ for (sb_wsc** at = &sb_wscs; *at; at = &(*at)->next) if (*at == c) { *at = c->next; break; }
3851
+ pthread_mutex_unlock(&sb_wsc_lock);
3852
+ sb_wsc_free(c);
3853
+ }
3854
+ return 0;
3855
+ }
3856
+
3762
3857
  static sb_wsc* sb_wsc_find(unsigned id) {
3763
3858
  pthread_mutex_lock(&sb_wsc_lock);
3764
3859
  sb_wsc* c = sb_wscs;
@@ -3787,6 +3882,52 @@ static void sb_wsc_drain(void) {
3787
3882
 
3788
3883
  // The JS side's ops. data is borrowed for the call.
3789
3884
  static long long sb_wsc_op(int op, unsigned id, const char* data, size_t len, size_t n, char* out, size_t cap) {
3885
+ if (op == 'T') {
3886
+ // data: host \n port \n mode (0 off, 1 on, 2 starttls) \n servername
3887
+ sb_wsc* c = (sb_wsc*)calloc(1, sizeof(sb_wsc));
3888
+ if (!c) return 0;
3889
+ char* copy = (char*)malloc(len + 1);
3890
+ if (!copy) { free(c); return 0; }
3891
+ memcpy(copy, data, len); copy[len] = 0;
3892
+ char* parts[4]; char* cur = copy;
3893
+ for (int i = 0; i < 4; i++) {
3894
+ parts[i] = cur;
3895
+ char* nl = strchr(cur, '\n');
3896
+ if (nl) { *nl = 0; cur = nl + 1; } else cur += strlen(cur);
3897
+ }
3898
+ c->raw = 1; c->window = SB_TCP_READ_WINDOW;
3899
+ c->host = strdup(parts[0]); c->port = atoi(parts[1]); c->tls = atoi(parts[2]) == 1;
3900
+ snprintf(c->sni, sizeof(c->sni), "%s", parts[3]);
3901
+ c->path = strdup(""); c->protocols = strdup(""); c->headers = strdup("");
3902
+ free(copy);
3903
+ if (!c->host || !c->path || !c->protocols || !c->headers || pipe(c->wake) != 0) { free(c->host); free(c->path); free(c->protocols); free(c->headers); free(c); return 0; }
3904
+ for (int k = 0; k < 2; k++) { fcntl(c->wake[k], F_SETFD, FD_CLOEXEC); fcntl(c->wake[k], F_SETFL, fcntl(c->wake[k], F_GETFL) | O_NONBLOCK); }
3905
+ pthread_mutex_init(&c->lock, 0);
3906
+ pthread_mutex_lock(&sb_wsc_lock);
3907
+ int live = 0;
3908
+ for (sb_wsc* k = sb_wscs; k; k = k->next) live++;
3909
+ if (live >= SB_WSC_MAX) { pthread_mutex_unlock(&sb_wsc_lock); sb_wsc_free(c); return 0; }
3910
+ sb_wsc_next = (sb_wsc_next + 1) & 0x7fffffffu;
3911
+ if (!sb_wsc_next) sb_wsc_next = 1;
3912
+ c->id = sb_wsc_next | SB_WSC_BIT;
3913
+ c->next = sb_wscs; sb_wscs = c;
3914
+ pthread_mutex_unlock(&sb_wsc_lock);
3915
+ pthread_attr_t attr;
3916
+ pthread_attr_init(&attr);
3917
+ pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
3918
+ pthread_attr_setstacksize(&attr, 512 * 1024);
3919
+ pthread_t thread;
3920
+ int failed = pthread_create(&thread, &attr, sb_tcp_thread, c);
3921
+ pthread_attr_destroy(&attr);
3922
+ if (failed) {
3923
+ pthread_mutex_lock(&sb_wsc_lock);
3924
+ for (sb_wsc** at = &sb_wscs; *at; at = &(*at)->next) if (*at == c) { *at = c->next; break; }
3925
+ pthread_mutex_unlock(&sb_wsc_lock);
3926
+ sb_wsc_free(c);
3927
+ return 0;
3928
+ }
3929
+ return (long long)c->id;
3930
+ }
3790
3931
  if (op == 'u') {
3791
3932
  // data: host \n port \n tls(0|1) \n path \n protocols \n header lines (to the end)
3792
3933
  sb_wsc* c = (sb_wsc*)calloc(1, sizeof(sb_wsc));
@@ -3836,7 +3977,40 @@ static long long sb_wsc_op(int op, unsigned id, const char* data, size_t len, si
3836
3977
  sb_wsc* c = sb_wsc_find(id);
3837
3978
  if (!c) return op == 'p' || op == 'k' || op == 'w' || op == 'r' ? 0 : -1;
3838
3979
  switch (op) {
3839
- case 't': case 'b': return sb_wsc_frame(c, op == 't' ? 1 : 2, data, len, 0, 0);
3980
+ case 'b': if (c->raw) {
3981
+ // raw bytes: queue them, say how many are still unsent, and ask for a drain event past the mark
3982
+ pthread_mutex_lock(&c->lock);
3983
+ size_t need = c->outq_len + len;
3984
+ if (need > c->outq_cap) {
3985
+ size_t cap = c->outq_cap ? c->outq_cap : 4096;
3986
+ while (cap < need) cap *= 2;
3987
+ unsigned char* grown = (unsigned char*)realloc(c->outq, cap);
3988
+ if (!grown) { pthread_mutex_unlock(&c->lock); return -1; }
3989
+ c->outq = grown; c->outq_cap = cap;
3990
+ }
3991
+ memcpy(c->outq + c->outq_len, data, len);
3992
+ c->outq_len += len;
3993
+ size_t queued = c->outq_len + c->unsent;
3994
+ if (queued > (256u << 10)) c->want_drain = 1;
3995
+ pthread_mutex_unlock(&c->lock);
3996
+ sb_wsc_wake_thread(c);
3997
+ return (long long)queued;
3998
+ }
3999
+ /* fall through */
4000
+ case 't': return sb_wsc_frame(c, op == 't' ? 1 : 2, data, len, 0, 0);
4001
+ case 'S':
4002
+ pthread_mutex_lock(&c->lock);
4003
+ snprintf(c->sni, sizeof(c->sni), "%.*s", (int)(len < sizeof(c->sni) - 1 ? len : sizeof(c->sni) - 1), data ? data : "");
4004
+ c->starttls_req = 1;
4005
+ pthread_mutex_unlock(&c->lock);
4006
+ sb_wsc_wake_thread(c);
4007
+ return 0;
4008
+ case 'H':
4009
+ pthread_mutex_lock(&c->lock);
4010
+ c->shutdown_req = 1;
4011
+ pthread_mutex_unlock(&c->lock);
4012
+ sb_wsc_wake_thread(c);
4013
+ return 0;
3840
4014
  case 'c': {
3841
4015
  pthread_mutex_lock(&c->lock);
3842
4016
  int already = c->close_queued;
@@ -4962,8 +5136,13 @@ function __sbEmbeddedDispatch(msg) {
4962
5136
  if (op === "secret.get") {
4963
5137
  // Secrets reach a standalone binary through the environment, the same
4964
5138
  // channel the launcher and the supervisor use; there is nothing to decrypt.
4965
- const value = __sbEnv(String(msg.name));
4966
- if (!value) throw new Error("secret not set: " + msg.name);
5139
+ const raw = __sbEnv(String(msg.name));
5140
+ if (!raw) throw new Error("secret not set: " + msg.name);
5141
+ // __sbEnv hands back the environment's bytes as a bytestring, one char per byte, so a UTF-8 secret ("é" is
5142
+ // C3 A9) reached the app as "é". From compatibility date 2026-11-07 it is decoded as UTF-8, as workerd's
5143
+ // secrets are text. Bytes that are not valid UTF-8 pass through unchanged (__sbFromUtf8's rule). The broker
5144
+ // transport is not touched: its secrets arrive as JSON, already real strings.
5145
+ const value = __sbCompatDate() >= "2026-11-07" ? __sbFromUtf8(raw) : raw;
4967
5146
  return { ok: true, value };
4968
5147
  }
4969
5148
 
package/src/wrap.ts CHANGED
@@ -137,6 +137,20 @@ function hasBindings(b: Bindings): boolean {
137
137
  * leaves the specifier external for this.
138
138
  */
139
139
  export function inlineWorkersImports(source: string): string {
140
+ // `cloudflare:sockets` (compatibility date 2026-11-17): connect() lives on a prelude global.
141
+ // postgres/cf loads it lazily: `await import("cloudflare:sockets")`.
142
+ source = source.replace(/\bimport\(\s*["']cloudflare:sockets["']\s*\)/g, "Promise.resolve(globalThis.__sbSockets)");
143
+ source = source.replace(
144
+ /^import\s*\{([^}]*)\}\s*from\s*["']cloudflare:sockets["'];?[ \t]*$/gm,
145
+ (_match, names: string) => {
146
+ const pairs = names
147
+ .split(",")
148
+ .map((part) => part.trim())
149
+ .filter(Boolean)
150
+ .map((part) => part.replace(/^(\S+)\s+as\s+(\S+)$/, "$1: $2"));
151
+ return `const { ${pairs.join(", ")} } = globalThis.__sbSockets;`;
152
+ },
153
+ );
140
154
  return source.replace(
141
155
  /^import\s*\{([^}]*)\}\s*from\s*["']cloudflare:workers["'];?[ \t]*$/gm,
142
156
  (_match, names: string) => {
package/test/bearssl.ts CHANGED
@@ -7,9 +7,10 @@ import { join, resolve } from "node:path";
7
7
  // The prelude carries crypto.subtle's native half on every transport, so a test that compiles a
8
8
  // handler using crypto.subtle needs BearSSL on the include path and link line, as the CLI arranges
9
9
  // for a build. Same pinned source as the CLI's src/bearssl.ts; built once into the toolchain cache.
10
- const VERSION = "0.6";
11
- const URL_ = `https://bearssl.org/bearssl-${VERSION}.tar.gz`;
12
- const SHA256 = "6705bba1714961b41a728dfc5debbe348d2966c117649392f8c8139efc83ff14";
10
+ const COMMIT = "7bea48e5e850ab4cafbe68d3765cdaba13a86d6f";
11
+ const VERSION = COMMIT.slice(0, 7);
12
+ const URL_ = `https://bearssl.org/gitweb/?p=BearSSL;a=snapshot;h=${COMMIT};sf=tgz`;
13
+ const SHA256 = "ed397832b1613582737d9c1ce01c1af74ad6a9e9c52a341e24433987cda0f372";
13
14
 
14
15
  const walk = async (dir: string): Promise<string[]> =>
15
16
  (
@@ -28,7 +29,7 @@ export async function bearsslEnv(): Promise<{ SB_EXTRA_CFLAGS: string; SB_EXTRA_
28
29
  process.env.SPROUTBOAT_TOOLCHAIN_CACHE ?? join(homedir(), ".cache/sproutboat"),
29
30
  `bearssl-${VERSION}-test`,
30
31
  );
31
- const tree = join(root, `bearssl-${VERSION}`);
32
+ const tree = join(root, `BearSSL-${VERSION}`);
32
33
  const objects = join(root, "obj");
33
34
  const marker = join(objects, ".complete");
34
35
  if (!existsSync(marker)) {