@ai-ecoverse/slicc-kernel 1.59.0 → 1.60.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/dist/node.js CHANGED
@@ -1323,6 +1323,11 @@ function blockedReason(url, e) {
1323
1323
  }
1324
1324
  return `${url}: ${e instanceof Error ? e.message : String(e)}`;
1325
1325
  }
1326
+ function unopened(url, code) {
1327
+ return Object.assign(new Error(`${url}: the WebSocket closed (${code}) before it opened`), {
1328
+ status: 502
1329
+ });
1330
+ }
1326
1331
  function openWebSocket(Socket, request, withHeaders) {
1327
1332
  const { url, protocols, signal } = request;
1328
1333
  signal.throwIfAborted();
@@ -1336,13 +1341,22 @@ function openWebSocket(Socket, request, withHeaders) {
1336
1341
  const queue = new MessageQueue(() => socket.close(1e3, "too many waiting messages"));
1337
1342
  return new Promise((resolve, reject) => {
1338
1343
  let open = false;
1344
+ let settled = false;
1345
+ const fail3 = (error) => {
1346
+ if (settled) return;
1347
+ settled = true;
1348
+ signal.removeEventListener("abort", abort);
1349
+ reject(error);
1350
+ };
1339
1351
  const abort = () => {
1340
1352
  socket.close(1e3, "aborted");
1341
- reject(signal.reason);
1353
+ fail3(signal.reason);
1342
1354
  };
1343
1355
  signal.addEventListener("abort", abort, { once: true });
1344
1356
  socket.addEventListener("open", () => {
1357
+ if (settled) return;
1345
1358
  open = true;
1359
+ settled = true;
1346
1360
  signal.removeEventListener("abort", abort);
1347
1361
  resolve({
1348
1362
  protocol: socket.protocol,
@@ -1358,15 +1372,19 @@ function openWebSocket(Socket, request, withHeaders) {
1358
1372
  socket.addEventListener("message", (event) => {
1359
1373
  queue.push(typeof event.data === "string" ? event.data : new Uint8Array(event.data));
1360
1374
  });
1375
+ socket.addEventListener("error", () => {
1376
+ if (open || settled) return;
1377
+ try {
1378
+ socket.close();
1379
+ } catch {
1380
+ }
1381
+ queue.end({ code: 1006, reason: "" });
1382
+ fail3(unopened(url, 1006));
1383
+ });
1361
1384
  socket.addEventListener("close", (event) => {
1362
1385
  queue.end({ code: event.code, reason: event.reason });
1363
1386
  if (open) return;
1364
- signal.removeEventListener("abort", abort);
1365
- reject(
1366
- Object.assign(new Error(`${url}: the WebSocket closed (${event.code}) before it opened`), {
1367
- status: 502
1368
- })
1369
- );
1387
+ fail3(unopened(url, event.code));
1370
1388
  });
1371
1389
  });
1372
1390
  }
@@ -1756,6 +1774,29 @@ function userEnv(account, system) {
1756
1774
  PATH: `${pnpm}/bin:/usr/local/bin:/usr/bin:/bin`
1757
1775
  };
1758
1776
  }
1777
+ function groupsOf(groups, names) {
1778
+ return names.map((g) => {
1779
+ const row = groups.find((r) => r.name === g);
1780
+ if (!row) throw fsError("ENOENT", `no group ${g}`);
1781
+ return row;
1782
+ });
1783
+ }
1784
+ function homeModeOf(mode) {
1785
+ if (mode === void 0) return void 0;
1786
+ if (!Number.isInteger(mode) || mode < 0 || mode > 4095) {
1787
+ throw fsError("EINVAL", `not a mode: ${String(mode)}`);
1788
+ }
1789
+ return mode;
1790
+ }
1791
+ function joined(name2, rows) {
1792
+ let changed = false;
1793
+ for (const g of rows) {
1794
+ if (g.members.includes(name2)) continue;
1795
+ g.members.push(name2);
1796
+ changed = true;
1797
+ }
1798
+ return changed;
1799
+ }
1759
1800
  function homeGroupOf(groups, name2) {
1760
1801
  if (name2 === void 0) return void 0;
1761
1802
  const found = groups.find((g) => g.name === name2);
@@ -1771,14 +1812,14 @@ var UserDb = class {
1771
1812
  this.fs = fs;
1772
1813
  this.owners = owners;
1773
1814
  }
1774
- async stampHome(row, shared) {
1815
+ async stampHome(row, shared, homeMode) {
1775
1816
  const home = this.fs.resolvePath("/", row.home);
1776
1817
  if (!this.owners || !this.fs.chown || !HOME_DIR.test(home)) return;
1777
1818
  if (!await this.realDir(home)) return;
1778
1819
  const gid = shared?.gid ?? row.gid;
1779
1820
  await this.fs.chown(home, { uid: row.uid, gid, follow: false, kind: "adopt" });
1780
1821
  const scoop = row.name.startsWith("scoop-");
1781
- await this.fs.chmod(home, shared ? 1512 : scoop ? 488 : 448);
1822
+ await this.fs.chmod(home, homeMode ?? (shared ? 1512 : scoop ? 488 : 448));
1782
1823
  const pnpm = `${home}/.local/share/pnpm`;
1783
1824
  if (await this.realDir(pnpm)) await this.fs.chmod(pnpm, scoop ? 488 : 448);
1784
1825
  }
@@ -1892,31 +1933,33 @@ var UserDb = class {
1892
1933
  const groups = parsed(group);
1893
1934
  const found = users.find((r) => r.name === name2);
1894
1935
  const shared = homeGroupOf(groups, options.homeGroup);
1936
+ const homeMode = homeModeOf(options.homeMode);
1895
1937
  if (found) {
1938
+ const more = groupsOf(groups, options.groups ?? []);
1939
+ if (more.some((g) => g.gid === found.gid)) {
1940
+ throw fsError("EINVAL", `a user is no member of its own group ${name2}`);
1941
+ }
1896
1942
  if (found.uid >= FIRST_UID && found.uid <= LAST_UID) {
1897
1943
  await this.provision(found);
1898
- await this.stampHome(found, shared);
1944
+ await this.stampHome(found, shared, homeMode);
1899
1945
  }
1946
+ if (joined(name2, more)) await this.fs.writeFile(GROUP, formatGroup(group));
1900
1947
  remember();
1901
1948
  return this.account(found, groups);
1902
1949
  }
1903
1950
  const home = homeOf(options.home ?? `/home/${name2}`);
1904
- const extra = [USERS_GROUP, ...options.groups ?? []].map((g) => {
1905
- const row2 = groups.find((r) => r.name === g);
1906
- if (!row2) throw fsError("ENOENT", `no group ${g}`);
1907
- return row2;
1908
- });
1951
+ const extra = groupsOf(groups, [USERS_GROUP, ...options.groups ?? []]);
1909
1952
  if (groups.some((r) => r.name === name2)) throw fsError("EEXIST", `group ${name2} exists`);
1910
1953
  const uid = freeId(users, groups);
1911
1954
  const row = { name: name2, uid, gid: uid, gecos: name2, home, shell: "/bin/bash" };
1912
- for (const g of extra) if (!g.members.includes(name2)) g.members.push(name2);
1955
+ joined(name2, extra);
1913
1956
  group.push({ name: name2, gid: uid, members: [] });
1914
1957
  passwd.push(row);
1915
1958
  const fresh = !await this.fs.exists(home);
1916
1959
  await this.fs.mkdir(home, { recursive: true });
1917
1960
  try {
1918
1961
  await this.provision(row);
1919
- await this.stampHome(row, shared);
1962
+ await this.stampHome(row, shared, homeMode);
1920
1963
  await this.fs.mkdir("/etc", { recursive: true });
1921
1964
  await this.commit(formatPasswd(passwd), formatGroup(group));
1922
1965
  } catch (err) {
@@ -1927,6 +1970,31 @@ var UserDb = class {
1927
1970
  return this.account(row, parsed(group));
1928
1971
  });
1929
1972
  }
1973
+ async groups() {
1974
+ const { group } = await this.tables();
1975
+ return parsed(group).map(({ name: name2, gid, members }) => ({ name: name2, gid, members: [...members] }));
1976
+ }
1977
+ addMember(group, user) {
1978
+ return this.member(group, user, true);
1979
+ }
1980
+ removeMember(group, user) {
1981
+ return this.member(group, user, false);
1982
+ }
1983
+ member(name2, user, on) {
1984
+ return this.serial(async () => {
1985
+ const { passwd, group } = await this.tables();
1986
+ const row = parsed(group).find((g) => g.name === name2);
1987
+ if (!row) throw fsError("ENOENT", `no group ${String(name2)}`);
1988
+ const who = parsed(passwd).find((u) => u.name === user);
1989
+ if (!who) throw fsError("ENOENT", `no user ${String(user)}`);
1990
+ if (who.gid === row.gid) throw fsError("EINVAL", `${name2} is the own group of ${user}`);
1991
+ if (row.members.includes(user) !== on) {
1992
+ row.members = on ? [...row.members, user] : row.members.filter((m) => m !== user);
1993
+ await this.fs.writeFile(GROUP, formatGroup(group));
1994
+ }
1995
+ return { name: row.name, gid: row.gid, members: [...row.members] };
1996
+ });
1997
+ }
1930
1998
  remove(name2, options = {}) {
1931
1999
  return this.serial(async () => {
1932
2000
  const { passwd, group } = await this.tables();
@@ -8399,7 +8467,7 @@ function enableNetwork(net, options) {
8399
8467
  let proxy;
8400
8468
  const transport = options.transport ?? missingTransport();
8401
8469
  net.activate({ family: "inet", host: "127.0.0.1", port: REALM_PROXY_PORT }, () => {
8402
- const tls = new TlsTerminator({
8470
+ const tls2 = new TlsTerminator({
8403
8471
  ca: options.ca,
8404
8472
  engine: options.engine,
8405
8473
  ...options.now ? { now: options.now } : {}
@@ -8407,7 +8475,7 @@ function enableNetwork(net, options) {
8407
8475
  const started = new RealmProxy({
8408
8476
  net,
8409
8477
  transport,
8410
- tunnel: tls.handler,
8478
+ tunnel: tls2.handler,
8411
8479
  tunnelReady: async () => {
8412
8480
  await options.engine();
8413
8481
  }
@@ -8415,7 +8483,7 @@ function enableNetwork(net, options) {
8415
8483
  proxy = started;
8416
8484
  void started.closed.then(async () => {
8417
8485
  if (proxy === started) proxy = void 0;
8418
- await tls.close();
8486
+ await tls2.close();
8419
8487
  });
8420
8488
  });
8421
8489
  return () => proxy;
@@ -8434,6 +8502,11 @@ var TOKEN3 = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
8434
8502
  function message(e) {
8435
8503
  return e instanceof Error ? e.message : String(e);
8436
8504
  }
8505
+ var ERRNO = /^E[A-Z]+$|^EAI_[A-Z]+$/;
8506
+ function errnoOf(e) {
8507
+ const code = e?.code;
8508
+ return typeof code === "string" && ERRNO.test(code) ? code : "ECONNREFUSED";
8509
+ }
8437
8510
  function fail(errno, text) {
8438
8511
  return { ok: false, errno, message: text };
8439
8512
  }
@@ -8477,13 +8550,13 @@ var WsHandles = class {
8477
8550
  let socket;
8478
8551
  try {
8479
8552
  socket = await transport.websocket({
8480
- url: req.url,
8553
+ url: toHostLoopback(req.url),
8481
8554
  protocols: req.protocols,
8482
8555
  headers: req.headers,
8483
8556
  signal: entry.abort.signal
8484
8557
  });
8485
8558
  } catch (e) {
8486
- return fail("ECONNREFUSED", message(e));
8559
+ return fail(errnoOf(e), message(e));
8487
8560
  }
8488
8561
  if (!this.open.has(handle)) {
8489
8562
  socket.close(1001, "the program went away");
@@ -8838,14 +8911,14 @@ var Resolver = class {
8838
8911
  if (hit && hit.expires > this.now()) return hit.addresses;
8839
8912
  const signal = blocking?.();
8840
8913
  if (signal?.aborted) throw new KernelError("EINTR");
8841
- let lookup = this.pending.get(id2);
8842
- if (!lookup) {
8914
+ let lookup2 = this.pending.get(id2);
8915
+ if (!lookup2) {
8843
8916
  const abort = new AbortController();
8844
- lookup = { work: this.lookup(key, asked, id2, abort.signal), abort, waiters: 0 };
8845
- this.pending.set(id2, lookup);
8846
- void lookup.work.then(() => this.pending.delete(id2));
8917
+ lookup2 = { work: this.lookup(key, asked, id2, abort.signal), abort, waiters: 0 };
8918
+ this.pending.set(id2, lookup2);
8919
+ void lookup2.work.then(() => this.pending.delete(id2));
8847
8920
  }
8848
- return waitFor(lookup, signal);
8921
+ return waitFor(lookup2, signal);
8849
8922
  }
8850
8923
  async lookup(name2, family, id2, abandoned) {
8851
8924
  const uplink = this.uplink;
@@ -8886,17 +8959,17 @@ var Resolver = class {
8886
8959
  }
8887
8960
  }
8888
8961
  };
8889
- function waitFor(lookup, signal) {
8890
- lookup.waiters++;
8962
+ function waitFor(lookup2, signal) {
8963
+ lookup2.waiters++;
8891
8964
  return new Promise((resolve, reject) => {
8892
8965
  const abort = () => {
8893
- if (--lookup.waiters === 0) lookup.abort.abort(new KernelError("EINTR"));
8966
+ if (--lookup2.waiters === 0) lookup2.abort.abort(new KernelError("EINTR"));
8894
8967
  reject(new KernelError("EINTR"));
8895
8968
  };
8896
8969
  signal?.addEventListener("abort", abort, { once: true });
8897
- void lookup.work.then((value) => {
8970
+ void lookup2.work.then((value) => {
8898
8971
  if (signal?.aborted) return;
8899
- lookup.waiters--;
8972
+ lookup2.waiters--;
8900
8973
  signal?.removeEventListener("abort", abort);
8901
8974
  resolve(value);
8902
8975
  });
@@ -14490,6 +14563,62 @@ function isMemoryRoot(handle) {
14490
14563
  return handle instanceof MemoryDirectory;
14491
14564
  }
14492
14565
 
14566
+ // src/node/ws-diagnose.ts
14567
+ import { lookup } from "node:dns/promises";
14568
+ import { isIP, connect as tcp } from "node:net";
14569
+ import { connect as tls } from "node:tls";
14570
+ var PROBE_MS = 3e3;
14571
+ var PASSED = /* @__PURE__ */ new Set(["ECONNREFUSED", "EHOSTUNREACH", "ENETUNREACH", "ETIMEDOUT", "ECONNRESET"]);
14572
+ var NO_NAME = /* @__PURE__ */ new Set(["ENOTFOUND", "EAI_AGAIN", "EAI_NONAME", "EAI_FAIL", "ENODATA"]);
14573
+ function codeOf5(e) {
14574
+ const code = e?.code;
14575
+ return typeof code === "string" ? code : void 0;
14576
+ }
14577
+ function probe(secure, host, port, ms) {
14578
+ return new Promise((resolve) => {
14579
+ const socket = secure ? tls({ host, port, ...isIP(host) ? {} : { servername: host } }) : tcp({ host, port });
14580
+ const done4 = (code) => {
14581
+ clearTimeout(timer);
14582
+ socket.destroy();
14583
+ resolve(code);
14584
+ };
14585
+ const timer = setTimeout(() => done4("ETIMEDOUT"), ms);
14586
+ socket.once(secure ? "secureConnect" : "connect", () => done4("EPROTO"));
14587
+ socket.once("error", (e) => {
14588
+ const code = codeOf5(e) ?? "";
14589
+ done4(PASSED.has(code) ? code : "EPROTO");
14590
+ });
14591
+ });
14592
+ }
14593
+ async function diagnose(url, ms = PROBE_MS) {
14594
+ const at = new URL(url);
14595
+ const host = at.hostname.replace(/^\[(.*)\]$/, "$1");
14596
+ const secure = at.protocol === "wss:";
14597
+ const port = Number(at.port) || (secure ? 443 : 80);
14598
+ try {
14599
+ await lookup(host);
14600
+ } catch (e) {
14601
+ return NO_NAME.has(codeOf5(e) ?? "") ? "EAI_NONAME" : void 0;
14602
+ }
14603
+ return probe(secure, host, port, ms);
14604
+ }
14605
+ function diagnosing(transport, check = diagnose) {
14606
+ const open = transport.websocket;
14607
+ if (!open) return transport;
14608
+ return {
14609
+ ...transport,
14610
+ websocket: async (request) => {
14611
+ try {
14612
+ return await open(request);
14613
+ } catch (e) {
14614
+ if (request.signal.aborted || !(e instanceof Error) || codeOf5(e)) throw e;
14615
+ const code = await check(request.url).catch(() => void 0);
14616
+ throw code ? Object.assign(e, { code }) : e;
14617
+ }
14618
+ }
14619
+ };
14620
+ }
14621
+
14493
14622
  // src/kernel/net/remote-transport.ts
14494
14623
  var TransportError = class extends Error {
14495
14624
  status;
@@ -14752,10 +14881,10 @@ var ByteSource = class {
14752
14881
  async more() {
14753
14882
  const { done: done4, value } = await this.reader.read();
14754
14883
  if (done4) return false;
14755
- const joined = new Uint8Array(this.buffered.length + value.length);
14756
- joined.set(this.buffered);
14757
- joined.set(value, this.buffered.length);
14758
- this.buffered = joined;
14884
+ const joined2 = new Uint8Array(this.buffered.length + value.length);
14885
+ joined2.set(this.buffered);
14886
+ joined2.set(value, this.buffered.length);
14887
+ this.buffered = joined2;
14759
14888
  return true;
14760
14889
  }
14761
14890
  async until(marker) {
@@ -15284,7 +15413,7 @@ function hostfsFetchOf(options) {
15284
15413
  }
15285
15414
  function nodeTransport() {
15286
15415
  const transport = fetchTransport({ webSocket: true, webSocketHeaders: true, redirect: "manual" });
15287
- return { ...transport, traits: { ...transport.traits, crossOrigin: "any" } };
15416
+ return diagnosing({ ...transport, traits: { ...transport.traits, crossOrigin: "any" } });
15288
15417
  }
15289
15418
  var TERMINATED = "the kernel is terminated";
15290
15419
  var EXIT_GRACE_MS = 50;
@@ -15443,6 +15572,11 @@ async function createNodeKernel(options = {}) {
15443
15572
  remove: (name2, options2) => guard(() => launcher.users.remove(name2, options2)),
15444
15573
  list: () => guard(() => launcher.users.list())
15445
15574
  },
15575
+ groups: {
15576
+ list: () => guard(() => launcher.users.groups()),
15577
+ addMember: (group, user) => guard(() => launcher.users.addMember(group, user)),
15578
+ removeMember: (group, user) => guard(() => launcher.users.removeMember(group, user))
15579
+ },
15446
15580
  async connect({ user } = {}) {
15447
15581
  if (terminated) throw new Error(TERMINATED);
15448
15582
  if (user !== void 0 && !await launcher.users.lookup(user)) {