@volter/teams 0.1.6 → 0.1.7

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/home.mjs CHANGED
@@ -31,14 +31,19 @@ export function teamsHome(env = process.env) {
31
31
  return dir;
32
32
  }
33
33
 
34
+ /** A Windows named pipe (`\\.\pipe\…`): no file to make, chmod or remove; it vanishes with its last handle. */
35
+ export const isNamedPipe = (path) => /^\\\\[.?]\\pipe\\/i.test(path);
36
+
34
37
  /**
35
38
  * A Unix socket path for `name` in `dir`: the path itself when it fits a socket
36
39
  * address (104 bytes on macOS, 108 on Linux); otherwise, as tmux does, a
37
40
  * per-user directory under /tmp that only this user may enter, with the socket
38
- * named by a hash of the path it stands for.
41
+ * named by a hash of the path it stands for. Node on Windows listens only on
42
+ * named pipes, so there it is always a pipe named by that same hash.
39
43
  */
40
- export function shortSocketPath(dir, name) {
44
+ export function shortSocketPath(dir, name, platform = process.platform) {
41
45
  const inside = join(dir, name);
46
+ if (platform === 'win32') return `\\\\.\\pipe\\supercode-${createHash('sha256').update(inside).digest('hex').slice(0, 24)}`;
42
47
  if (Buffer.byteLength(inside) <= 100) return inside;
43
48
  const uid = typeof process.getuid === 'function' ? process.getuid() : 'user';
44
49
  return join('/tmp', `supercode-machine-${uid}`, `${createHash('sha256').update(inside).digest('hex').slice(0, 24)}.sock`);
@@ -49,6 +54,7 @@ export const machineSocketPath = (home) => shortSocketPath(home, 'machine.sock')
49
54
 
50
55
  /** The socket's directory exists; the shared /tmp fallback must be ours and admit only us, and anything else there is refused, never adopted. */
51
56
  export function ensureSocketDirectory(socketPath) {
57
+ if (isNamedPipe(socketPath)) return;
52
58
  const dir = dirname(socketPath);
53
59
  mkdirSync(dir, { recursive: true, mode: 0o700 });
54
60
  if (!basename(dir).startsWith('supercode-machine-') || dirname(dir) !== '/tmp') return;
package/machine-core.mjs CHANGED
@@ -15,7 +15,7 @@ import { Channel, NODE_PROTOCOL } from './channel.mjs';
15
15
  import { holds, admitFromGrants, LOCAL_OPERATOR } from './admission.mjs';
16
16
  import { findProduct, DAEMON_KINDS, PRODUCT_MAX_MESSAGE, productProof, sameSecret } from './products.mjs';
17
17
  import { loadOrCreateKeyPair, publicKeyId } from './identity.mjs';
18
- import { teamsHome, paths, writeJson, readLimits, AccountLog, ensureSocketDirectory } from './home.mjs';
18
+ import { teamsHome, paths, writeJson, readLimits, AccountLog, ensureSocketDirectory, isNamedPipe } from './home.mjs';
19
19
 
20
20
  export const MACHINE_VERSION = 2;
21
21
  const LINK_BACKOFF_MIN_MS = 1_000;
@@ -93,8 +93,9 @@ export class MachineCore extends EventEmitter {
93
93
  this.server.once('error', reject);
94
94
  this.server.listen(this.socketPath, () => { this.server.off('error', reject); resolveListen(); });
95
95
  });
96
- // Owner-only: the socket is the local door, and whoever opens it is this OS user.
97
- chmodSync(this.socketPath, 0o600);
96
+ // Owner-only: the socket is the local door, and whoever opens it is this OS user. A named pipe's default
97
+ // access already admits only its owner (and SYSTEM and Administrators) to connect.
98
+ if (!isNamedPipe(this.socketPath)) chmodSync(this.socketPath, 0o600);
98
99
  this.address = this.socketPath;
99
100
  writeJson(this.p.machine, { pid: process.pid, id: this.id, socket: this.socketPath, startedAt: Date.now(), version: MACHINE_VERSION, protocol: NODE_PROTOCOL, floor: this.floor, server: this.link?.server ?? null });
100
101
  this.emit('listening', this.socketPath);
@@ -115,7 +116,7 @@ export class MachineCore extends EventEmitter {
115
116
  for (const { channel } of this.connections.values()) channel.close();
116
117
  this.server?.close();
117
118
  await this.afterStop();
118
- rmSync(this.socketPath, { force: true });
119
+ if (!isNamedPipe(this.socketPath)) rmSync(this.socketPath, { force: true });
119
120
  if (existsSync(this.p.machine)) rmSync(this.p.machine);
120
121
  }
121
122
 
@@ -385,13 +386,14 @@ export class MachineCore extends EventEmitter {
385
386
  }
386
387
 
387
388
  async function clearStaleSocket(path) {
388
- if (!existsSync(path)) return;
389
+ // A named pipe has no file to leave behind: only a live daemon holds its name.
390
+ if (!isNamedPipe(path) && !existsSync(path)) return;
389
391
  const live = await new Promise((done) => {
390
392
  const probe = netConnect(path);
391
393
  probe.once('connect', () => { probe.destroy(); done(true); });
392
394
  probe.once('error', () => done(false));
393
395
  });
394
396
  if (live) throw Object.assign(new Error(`a machine daemon is already serving ${path}`), { code: 'EADDRINUSE' });
395
- rmSync(path, { force: true });
397
+ if (!isNamedPipe(path)) rmSync(path, { force: true });
396
398
  }
397
399
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/teams",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "description": "Volter Teams, the neutral team plane: machine identity and daemon core, the local door and server link, admission from machine grants, product doors, and the Teams server's neutral routes (company RFC 0008).",
6
6
  "license": "MIT",