session-peer 0.1.0-preview.0 → 0.1.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.
@@ -0,0 +1,9 @@
1
+ export declare const VERSION = "0.1.0";
2
+ export declare const VERSION_LINE = "session-peer 0.1.0 (typescript)";
3
+ export declare function host(value: string): string;
4
+ export declare function reply(value: string): {
5
+ to: string;
6
+ host?: string;
7
+ home?: string;
8
+ };
9
+ export declare function envelope(text: string, noFrom: boolean, address?: string): string;
package/dist/protocol.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Refusal } from './discovery.js';
2
2
  import { uuid } from './writer.js';
3
- export const VERSION = '0.1.0-preview.0';
3
+ export const VERSION = '0.1.0';
4
4
  export const VERSION_LINE = `session-peer ${VERSION} (typescript)`;
5
5
  export function host(value) {
6
6
  if (value.length > 255 || !/^(?:[A-Za-z0-9_][A-Za-z0-9_.-]*@)?[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(value))
package/dist/send.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ export type SendOptions = {
2
+ to: string;
3
+ home?: string;
4
+ codexBin?: string;
5
+ message: string;
6
+ dryRun?: boolean;
7
+ };
8
+ export declare function checkMessage(text: string, codex?: boolean): void;
9
+ export declare function postSocket(path: string, text: string): Promise<void>;
10
+ export declare function send(options: SendOptions): Promise<Record<string, unknown>>;
package/dist/send.js CHANGED
@@ -2,6 +2,7 @@ import { createConnection } from 'node:net';
2
2
  import { canonical, claude, Refusal } from './discovery.js';
3
3
  import { executable, run, UnknownOutcome } from './process.js';
4
4
  import { resolveWriter, uuid } from './writer.js';
5
+ import { postWindowsPipe } from './windows.js';
5
6
  export function checkMessage(text, codex = false) {
6
7
  if (!text.trim() || text.includes('\0') || [...text].length > 1_000_000 ||
7
8
  (codex && Buffer.byteLength(text, 'utf8') > 32768))
@@ -83,6 +84,9 @@ export async function send(options) {
83
84
  const rechecked = select();
84
85
  if (row.pid !== rechecked.pid || row.socket !== rechecked.socket)
85
86
  throw new Refusal('target_changed', 1);
86
- await postSocket(String(row.socket), options.message);
87
+ if (process.platform === 'win32')
88
+ await postWindowsPipe(String(row.socket), Number(row.pid), options.message);
89
+ else
90
+ await postSocket(String(row.socket), options.message);
87
91
  return result;
88
92
  }
@@ -0,0 +1,9 @@
1
+ export type WindowsProcess = {
2
+ pid: number;
3
+ uid: string;
4
+ command: string;
5
+ start: string;
6
+ };
7
+ export declare function inspectWindows(mode: 'identity' | 'openers', value: string): WindowsProcess[];
8
+ export declare function windowsProcessStart(pid: number): bigint | undefined;
9
+ export declare function postWindowsPipe(pipe: string, pid: number, text: string): Promise<void>;
@@ -0,0 +1,171 @@
1
+ import { readdirSync, readFileSync, lstatSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { createConnection } from 'node:net';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { Refusal } from './discovery.js';
6
+ import { UnknownOutcome } from './process.js';
7
+ // Get-CimInstance and Restart Manager are Windows read-only APIs. The script
8
+ // emits only PID, SID, executable basename and creation time. No arguments,
9
+ // command line, message body, token or file contents are returned.
10
+ const inspectScript = String.raw `
11
+ $ErrorActionPreference = 'Stop'
12
+ Add-Type -TypeDefinition @'
13
+ using System;
14
+ using System.Text;
15
+ using System.Runtime.InteropServices;
16
+ public static class Reader {
17
+ [StructLayout(LayoutKind.Sequential)] public struct Time { public uint Lo; public uint Hi; }
18
+ [StructLayout(LayoutKind.Sequential)] public struct Unique { public uint Pid; public Time Start; }
19
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
20
+ public struct Info {
21
+ public Unique Process;
22
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst=256)] public string App;
23
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst=64)] public string Service;
24
+ public uint Kind; public uint Status; public uint Session;
25
+ [MarshalAs(UnmanagedType.Bool)] public bool Restartable;
26
+ }
27
+ [DllImport("Rstrtmgr.dll", CharSet=CharSet.Unicode)] public static extern int RmStartSession(out uint session, uint flags, StringBuilder key);
28
+ [DllImport("Rstrtmgr.dll", CharSet=CharSet.Unicode)] public static extern int RmRegisterResources(uint session, uint files, string[] paths, uint apps, IntPtr app, uint services, IntPtr svc);
29
+ [DllImport("Rstrtmgr.dll")] public static extern int RmGetList(uint session, out uint needed, ref uint count, [In,Out] Info[] info, ref uint reasons);
30
+ [DllImport("Rstrtmgr.dll")] public static extern int RmEndSession(uint session);
31
+ [DllImport("kernel32.dll", SetLastError=true)] static extern IntPtr OpenProcess(uint access, bool inherit, uint pid);
32
+ [DllImport("kernel32.dll", SetLastError=true)] static extern bool GetProcessTimes(IntPtr handle, out Time creation, out Time exit, out Time kernel, out Time user);
33
+ [DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
34
+ public static ulong Start(uint pid) {
35
+ var handle = OpenProcess(0x1000, false, pid);
36
+ if (handle == IntPtr.Zero) throw new Exception("process_unavailable");
37
+ try {
38
+ Time created, exited, kernel, user;
39
+ if (!GetProcessTimes(handle, out created, out exited, out kernel, out user)) throw new Exception("process_unavailable");
40
+ return ((ulong)created.Hi << 32) | created.Lo;
41
+ } finally { CloseHandle(handle); }
42
+ }
43
+ }
44
+ '@
45
+ function Identity([uint32]$identifier, [string]$expected = '') {
46
+ $item = Get-CimInstance Win32_Process -Filter "ProcessId = $identifier"
47
+ if (!$item) { throw 'process_unavailable' }
48
+ $start = [string][Reader]::Start($identifier)
49
+ if ($expected -and $start -ne $expected) { throw 'process_replaced' }
50
+ $owner = Invoke-CimMethod -InputObject $item -MethodName GetOwnerSid
51
+ if (!$owner.Sid) { throw 'owner_unavailable' }
52
+ return @{pid=[int]$identifier; uid=[string]$owner.Sid; command=[string]$item.Name; start=$start}
53
+ }
54
+ function Openers([string]$location) {
55
+ [uint32]$session = 0
56
+ $key = New-Object System.Text.StringBuilder 33
57
+ if ([Reader]::RmStartSession([ref]$session, 0, $key) -ne 0) { throw 'rm_start_failed' }
58
+ try {
59
+ $paths = [string[]]@($location)
60
+ if ([Reader]::RmRegisterResources($session, 1, $paths, 0, [IntPtr]::Zero, 0, [IntPtr]::Zero) -ne 0) { throw 'rm_register_failed' }
61
+ [uint32]$needed = 0; [uint32]$count = 128; [uint32]$reasons = 0
62
+ $rows = New-Object 'Reader+Info[]' 128
63
+ if ([Reader]::RmGetList($session, [ref]$needed, [ref]$count, $rows, [ref]$reasons) -ne 0 -or $count -gt 128) { throw 'rm_list_failed' }
64
+ $result = @()
65
+ for ($i=0; $i -lt $count; $i++) {
66
+ $ticks = ([uint64]$rows[$i].Process.Start.Hi -shl 32) -bor $rows[$i].Process.Start.Lo
67
+ $result += Identity $rows[$i].Process.Pid ([string]$ticks)
68
+ }
69
+ return ,$result
70
+ } finally { [void][Reader]::RmEndSession($session) }
71
+ }
72
+ `;
73
+ export function inspectWindows(mode, value) {
74
+ if (process.platform !== 'win32')
75
+ throw new Refusal('windows_inspection_unavailable', 1);
76
+ const encodedValue = Buffer.from(value, 'utf8').toString('base64');
77
+ const code = inspectScript + `
78
+ $value = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedValue}'))
79
+ try {
80
+ $result = if ('${mode}' -eq 'identity') { @(Identity ([uint32]$value)) } else { @(Openers $value) }
81
+ ConvertTo-Json -InputObject @($result) -Compress -Depth 4
82
+ } catch { exit 1 }
83
+ `;
84
+ const done = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command',
85
+ '$script = [Console]::In.ReadToEnd(); Invoke-Expression $script'], { input: code, encoding: 'utf8', timeout: 8000, maxBuffer: 65536, windowsHide: true });
86
+ if (done.status !== 0 || done.error)
87
+ throw new Refusal('windows_owner_inspection_failed', 1);
88
+ let rows;
89
+ try {
90
+ rows = JSON.parse(done.stdout);
91
+ }
92
+ catch {
93
+ throw new Refusal('windows_owner_inspection_failed', 1);
94
+ }
95
+ if (!Array.isArray(rows) || rows.length > 128 || rows.some(row => !row || !Number.isSafeInteger(row.pid) || row.pid <= 0 ||
96
+ typeof row.uid !== 'string' || !row.uid.startsWith('S-') ||
97
+ typeof row.command !== 'string' || !row.command ||
98
+ typeof row.start !== 'string' || !/^\d+$/.test(row.start))) {
99
+ throw new Refusal('windows_owner_inspection_failed', 1);
100
+ }
101
+ return rows;
102
+ }
103
+ export function windowsProcessStart(pid) {
104
+ try {
105
+ const rows = inspectWindows('identity', String(pid));
106
+ return rows.length === 1 ? BigInt(rows[0].start) : undefined;
107
+ }
108
+ catch {
109
+ return undefined;
110
+ }
111
+ }
112
+ function authLine(pid) {
113
+ const directory = join(process.env.CLAUDE_CONFIG_DIR || process.env.ANTHROPIC_CONFIG_DIR ||
114
+ join(process.env.USERPROFILE || '', '.claude'), 'sessions');
115
+ let names;
116
+ try {
117
+ names = readdirSync(directory).filter(name => new RegExp(`^${pid}\\.[^.]+\\.key$`).test(name));
118
+ }
119
+ catch {
120
+ throw new Refusal('windows_inbox_auth_unavailable', 1);
121
+ }
122
+ if (names.length !== 1)
123
+ throw new Refusal('windows_inbox_auth_unavailable', 1);
124
+ const file = join(directory, names[0]);
125
+ try {
126
+ const entry = lstatSync(file);
127
+ if (!entry.isFile() || entry.isSymbolicLink() || entry.size > 16384)
128
+ throw new Error('invalid_key');
129
+ const value = JSON.parse(readFileSync(file, 'utf8'));
130
+ const token = value?.peerToken;
131
+ if (typeof token !== 'string' || !token.trim())
132
+ throw new Error('invalid_key');
133
+ return JSON.stringify({ type: 'auth', token }) + '\n';
134
+ }
135
+ catch {
136
+ throw new Refusal('windows_inbox_auth_unavailable', 1);
137
+ }
138
+ }
139
+ export async function postWindowsPipe(pipe, pid, text) {
140
+ if (!/^\\\\\.\\pipe\\[^\r\n]+$/i.test(pipe))
141
+ throw new Refusal('invalid_windows_pipe', 1);
142
+ const auth = authLine(pid);
143
+ const payload = auth + JSON.stringify({ type: 'user', message: { role: 'user', content: text } }) + '\n';
144
+ return new Promise((resolve, reject) => {
145
+ let connected = false, finished = false;
146
+ const socket = createConnection(pipe);
147
+ const timeout = setTimeout(() => finish(new Error('timeout')), 10000);
148
+ const finish = (error) => {
149
+ if (finished)
150
+ return;
151
+ finished = true;
152
+ clearTimeout(timeout);
153
+ socket.destroy();
154
+ if (error)
155
+ reject(connected ? new UnknownOutcome() : new Refusal('inbox_unreachable', 1));
156
+ else
157
+ resolve();
158
+ };
159
+ socket.once('error', error => finish(error));
160
+ socket.once('connect', () => {
161
+ connected = true;
162
+ socket.write(payload, error => {
163
+ if (error)
164
+ return finish(error);
165
+ const drain = setTimeout(() => finish(), 2000);
166
+ socket.once('close', () => { clearTimeout(drain); finish(); });
167
+ socket.once('data', () => { clearTimeout(drain); finish(); });
168
+ });
169
+ });
170
+ });
171
+ }
@@ -0,0 +1,8 @@
1
+ export declare const uuid: (value: string) => boolean;
2
+ export declare function probeLock(path: string): Promise<'free' | 'held' | 'absent' | 'unknown'>;
3
+ export declare function inspectWriter(home: string, id: string): Promise<{
4
+ activity: string;
5
+ identity: string;
6
+ }>;
7
+ export declare function homes(selected: string): string[];
8
+ export declare function resolveWriter(selected: string, id: string): Promise<string>;
package/dist/writer.js CHANGED
@@ -4,6 +4,7 @@ import { isAbsolute, join } from 'node:path';
4
4
  import { setTimeout as delay } from 'node:timers/promises';
5
5
  import { canonical, Refusal } from './discovery.js';
6
6
  import { executable, run } from './process.js';
7
+ import { inspectWindows } from './windows.js';
7
8
  const missing = (error) => ['ENOENT', 'ENOTDIR'].includes(error.code ?? '');
8
9
  export const uuid = (value) => /^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/i.test(value);
9
10
  function snapshot(path) {
@@ -16,18 +17,30 @@ export async function probeLock(path) {
16
17
  const before = lstatSync(path);
17
18
  if (!before.isFile() || before.isSymbolicLink())
18
19
  return 'unknown';
19
- const { flockSync } = await import('fs-ext-extra-prebuilt');
20
- fd = openSync(path, constants.O_RDWR | constants.O_NOFOLLOW);
20
+ const locks = await import('fs-ext-extra-prebuilt');
21
+ fd = openSync(path, constants.O_RDWR | (process.platform === 'win32' ? 0 : constants.O_NOFOLLOW));
21
22
  const after = fstatSync(fd);
22
23
  if (before.dev !== after.dev || before.ino !== after.ino)
23
24
  return 'unknown';
24
- try {
25
- flockSync(fd, 'exnb');
25
+ if (process.platform === 'win32') {
26
+ try {
27
+ locks.lockFileExSync(fd, locks.constants.LOCKFILE_EXCLUSIVE_LOCK | locks.constants.LOCKFILE_FAIL_IMMEDIATELY, 0, 0, 0xffffffff, 0xffffffff);
28
+ }
29
+ catch (error) {
30
+ return ['EAGAIN', 'EACCES', 'EWOULDBLOCK'].includes(error.code ?? '') ||
31
+ error.errno === 33 ? 'held' : 'unknown';
32
+ }
33
+ locks.unlockFileExSync(fd, 0, 0, 0xffffffff, 0xffffffff);
26
34
  }
27
- catch (error) {
28
- return ['EAGAIN', 'EACCES', 'EWOULDBLOCK'].includes(error.code ?? '') ? 'held' : 'unknown';
35
+ else {
36
+ try {
37
+ locks.flockSync(fd, 'exnb');
38
+ }
39
+ catch (error) {
40
+ return ['EAGAIN', 'EACCES', 'EWOULDBLOCK'].includes(error.code ?? '') ? 'held' : 'unknown';
41
+ }
42
+ locks.flockSync(fd, 'un');
29
43
  }
30
- flockSync(fd, 'un');
31
44
  return 'free';
32
45
  }
33
46
  catch (error) {
@@ -43,6 +56,10 @@ async function sample(path) {
43
56
  if (state !== 'held')
44
57
  return { state, fingerprint: '', owners: [], valid: true };
45
58
  const fingerprint = snapshot(path);
59
+ if (process.platform === 'win32') {
60
+ const owners = inspectWindows('openers', path);
61
+ return { state, fingerprint, owners, valid: true };
62
+ }
46
63
  const listed = await run(executable('lsof'), ['-nP', '-F0pcu', '--', path]);
47
64
  const owners = [];
48
65
  let current;
@@ -76,9 +93,10 @@ export async function inspectWriter(home, id) {
76
93
  await delay(250);
77
94
  const after = await sample(path);
78
95
  const owner = after.owners[0];
96
+ const ownId = process.platform === 'win32' ? inspectWindows('identity', String(process.pid))[0]?.uid : process.getuid?.();
79
97
  if (!before.valid || !after.valid || before.state !== after.state || before.fingerprint !== after.fingerprint ||
80
98
  before.owners.length !== 1 || after.owners.length !== 1 || JSON.stringify(before.owners) !== JSON.stringify(after.owners) ||
81
- !owner?.start || owner.uid !== process.getuid?.() || !/^codex(?:-|$)/i.test(owner.command ?? '')) {
99
+ !owner?.start || !ownId || owner.uid !== ownId || !/^codex(?:\.exe|-|$)/i.test(owner.command ?? '')) {
82
100
  throw new Refusal('active_writer_unverified', 1);
83
101
  }
84
102
  return { activity: 'live_writer', identity: JSON.stringify({ fingerprint: after.fingerprint, ...owner }) };
package/package.json CHANGED
@@ -1,16 +1,20 @@
1
1
  {
2
2
  "name": "session-peer",
3
- "version": "0.1.0-preview.0",
3
+ "version": "0.1.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/",
7
7
  "access": "public",
8
- "tag": "preview"
8
+ "tag": "latest"
9
9
  },
10
10
  "repository": { "type": "git", "url": "git+https://github.com/abruption/session-peer-ts.git" },
11
- "os": ["darwin", "linux"],
12
- "description": "Python-free local and SSH client for Claude Code and Codex sessions (preview)",
11
+ "os": ["darwin", "linux", "win32"],
12
+ "description": "Python-free local and SSH client for Claude Code and Codex sessions",
13
13
  "type": "module",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" }
17
+ },
14
18
  "license": "MIT",
15
19
  "engines": {
16
20
  "node": "^22.13.0 || ^24.0.0"
@@ -31,8 +35,10 @@
31
35
  ],
32
36
  "scripts": {
33
37
  "build": "tsc -p tsconfig.json",
34
- "test": "npm run build && node --test test/*.test.mjs",
35
- "test:package": "npm run build && node test/package-smoke.mjs"
38
+ "test": "npm run build && npm run test:types && node --experimental-strip-types --test test/*.test.ts",
39
+ "test:types": "tsc -p tsconfig.test.json",
40
+ "test:windows": "npm run build && npm run test:types && node --experimental-strip-types --test test/windows.test.ts",
41
+ "test:package": "npm run build && node --experimental-strip-types test/package-smoke.ts"
36
42
  },
37
43
  "devDependencies": {
38
44
  "@types/node": "22.13.10",