@matuto-ai/opencode-psst-connector 0.0.0-stage → 0.6.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +157 -2
  4. package/docs/active-conversation.md +68 -0
  5. package/docs/message-lifecycle.md +31 -0
  6. package/docs/migration-0.6.0.md +21 -0
  7. package/docs/native-automation.md +37 -0
  8. package/docs/presence-notices.md +21 -0
  9. package/docs/publishing.md +19 -0
  10. package/docs/reference-snapshot.sha256 +7 -0
  11. package/docs/setup-acceptance.md +96 -0
  12. package/docs/verification.md +39 -0
  13. package/package.json +42 -4
  14. package/plugin/index.mjs +18 -0
  15. package/plugin/tui.mjs +160 -0
  16. package/src/active-presence.mjs +97 -0
  17. package/src/approval-helper.mjs +11 -0
  18. package/src/config.mjs +50 -0
  19. package/src/enrollment.mjs +59 -0
  20. package/src/identity-recovery.mjs +94 -0
  21. package/src/installation.mjs +127 -0
  22. package/src/message-format.mjs +26 -0
  23. package/src/message-preview.mjs +29 -0
  24. package/src/native-delivery.mjs +169 -0
  25. package/src/native-inbox.mjs +37 -0
  26. package/src/native-incarnation.mjs +67 -0
  27. package/src/native-lock.mjs +17 -0
  28. package/src/native-metadata.mjs +15 -0
  29. package/src/native-plugin.mjs +335 -0
  30. package/src/native-setup.mjs +208 -0
  31. package/src/opencode-adapter.mjs +223 -0
  32. package/src/presence-notices.mjs +90 -0
  33. package/src/project-metadata.mjs +21 -0
  34. package/src/reference-pin.mjs +21 -0
  35. package/src/reference.mjs +20 -0
  36. package/src/setup-rpc.mjs +16 -0
  37. package/src/tool-schema.mjs +5 -0
  38. package/src/tool-scope.mjs +26 -0
  39. package/vendor/broker/LICENSE +21 -0
  40. package/vendor/broker/ORIGIN.md +12 -0
  41. package/vendor/broker/reference/client.mjs +439 -0
  42. package/vendor/broker/reference/errors.mjs +43 -0
  43. package/vendor/broker/reference/inbox.mjs +334 -0
  44. package/vendor/broker/reference/mcp-stdio.mjs +160 -0
  45. package/vendor/broker/reference/runtime.mjs +329 -0
  46. package/vendor/broker/reference/tools.mjs +88 -0
  47. package/vendor/broker/src/protocol.mjs +325 -0
  48. package/vendor/enrollment/LICENSE +21 -0
  49. package/vendor/enrollment/ORIGIN.md +20 -0
  50. package/vendor/enrollment/approval.mjs +181 -0
  51. package/vendor/enrollment/enrollment.mjs +137 -0
@@ -0,0 +1,18 @@
1
+ import { tool } from '../src/tool-schema.mjs';
2
+ import { loadBrokerReference } from '../src/reference.mjs';
3
+ import { readExistingConnection } from '../src/enrollment.mjs';
4
+ import { readInstallation } from '../src/installation.mjs';
5
+ import { setupNativePlugin } from '../src/native-plugin.mjs';
6
+
7
+ // Structurally the public @opencode/plugin Plugin.define({ id, setup }) contract.
8
+ // define is identity-only; the package never installs a host, runtime or CLI.
9
+ export default {
10
+ id: 'matuto.psst',
11
+ async setup(ctx) {
12
+ const reference = await loadBrokerReference();
13
+ const connectionFile = ctx.options?.connectionFile ?? process.env.PSST_CONFIG_FILE;
14
+ const runtime = await setupNativePlugin({ ctx, reference, tool, metadataOptions: ctx.options ?? {}, setupEnabled: !connectionFile,
15
+ loadSettings: readOptions => connectionFile ? readExistingConnection(connectionFile, readOptions) : readInstallation(undefined, readOptions) });
16
+ return runtime.dispose;
17
+ },
18
+ };
package/plugin/tui.mjs ADDED
@@ -0,0 +1,160 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { SETUP_RPC } from '../src/setup-rpc.mjs';
3
+ import { installationDirectory } from '../src/installation.mjs';
4
+ import { createPresenceWriter, presenceDirectory } from '../src/active-presence.mjs';
5
+
6
+ export default {
7
+ id: 'matuto.psst',
8
+ setup(ctx) {
9
+ let writer, directory, last, lastWrite = 0, warned = false;
10
+ const offline = () => {
11
+ if (!warned) ctx.ui?.toast?.show?.({ title: 'Psst offline', message: 'Active-chat messaging requires a local TUI sharing the server private directory.', variant: 'warning' });
12
+ warned = true;
13
+ };
14
+ if (ctx.app?.version !== '2.2.5' || typeof ctx.ui?.router?.current !== 'function' || typeof ctx.storage?.memory !== 'function') { offline(); return; }
15
+ const [memory, update] = ctx.storage.memory('psst-active-owner', { initial: { owner: randomUUID(), seq: 0 } });
16
+ const counter = { get seq() { return memory.seq; }, set seq(value) { update(draft => { draft.seq = value; }); } };
17
+ const tick = () => {
18
+ try {
19
+ const route = ctx.ui.router.current(), location = ctx.location;
20
+ if (!location) throw new Error('native_selection_location_unavailable');
21
+ const nextDirectory = presenceDirectory(installationDirectory(), location);
22
+ if (nextDirectory !== directory) {
23
+ writer?.stop(); writer = undefined; directory = undefined;
24
+ writer = createPresenceWriter({ directory: nextDirectory, owner: memory.owner, counter });
25
+ directory = nextDirectory; last = undefined;
26
+ }
27
+ const selected = route.type === 'session' ? route.sessionID : null;
28
+ if (selected !== last || Date.now() - lastWrite >= 1000) { writer.write(selected); last = selected; lastWrite = Date.now(); }
29
+ warned = false;
30
+ } catch { try { writer?.stop(); } catch {} writer = undefined; directory = undefined; offline(); }
31
+ };
32
+ const stopSetupUI = registerSetupUI(ctx, () => { tick(); return !!writer; });
33
+ tick(); const timer = setInterval(tick, 250); timer.unref?.();
34
+ return () => { stopSetupUI(); clearInterval(timer); try { writer?.stop(); } catch {} };
35
+ },
36
+ };
37
+
38
+
39
+ const stateMessages = {
40
+ setup_needed: 'Enter your broker URL with /psst-setup.',
41
+ pending: 'Waiting for human approval in the broker admin UI. Use /psst-status to check.',
42
+ approved: 'Broker authorization approved.',
43
+ denied: 'The approval request was denied. A new request requires confirmation in /psst-setup.',
44
+ expired: 'The approval request expired. A new request requires confirmation in /psst-setup.',
45
+ revoked: 'The authorization was revoked. A new request requires confirmation in /psst-setup.',
46
+ paused: 'Local setup is paused. Existing authorization and local data are retained.',
47
+ setup_blocked: 'Setup could not continue. Check /psst-status before retrying.',
48
+ setup_owner_recovery_required: 'Setup ownership needs recovery. Existing authorization and local data are retained.',
49
+ };
50
+ function brokerOrigin(value) {
51
+ if (typeof value !== 'string' || value.length > 2048 || /[\s\x00-\x1f\x7f-\x9f]/u.test(value)) throw Error('invalid_url');
52
+ let url;
53
+ try { url = new URL(value); } catch { throw Error('invalid_url'); }
54
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || url.pathname !== '/') throw Error('invalid_url');
55
+ return url.origin;
56
+ }
57
+ function statusMessage(result) {
58
+ // Never render exception strings or arbitrary server fields, including paths,
59
+ // credentials and terminal controls. Public approval URLs have one fixed path.
60
+ const setupMessage = stateMessages[result?.state];
61
+ let message = result?.connected === true
62
+ ? 'Psst is connected for this selected chat.' + (setupMessage && result.state !== 'setup_needed' ? ` Existing connection stays active. Setup: ${setupMessage}` : '')
63
+ : setupMessage ?? 'Psst is not connected. Use /psst-setup to configure it.';
64
+ if (result?.authorization_state === 'expired') message = 'The current authorization expired. Use /psst-setup for an explicitly approved replacement. Existing local data is retained.';
65
+ if (result?.authorization_state === 'rejected') message = 'The broker rejected the current authorization. Use /psst-setup for an explicitly approved replacement. Existing local data is retained.';
66
+ if (typeof result?.approval_url === 'string' && result.approval_url.length <= 2048) {
67
+ try {
68
+ const url = new URL(result.approval_url);
69
+ if (url.protocol === 'https:' && !url.username && !url.password && !url.search && !url.hash && url.pathname === '/admin') message += ` Approve at ${url.origin}/admin.`;
70
+ } catch {}
71
+ }
72
+ if (typeof result?.request_id === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(result.request_id)) message += ` Request: ${result.request_id}.`;
73
+ return message;
74
+ }
75
+ function registerSetupUI(ctx, available) {
76
+ // Presence-only hosts/tests still work; never touch the client without the
77
+ // supported native command and dialog APIs.
78
+ if (typeof ctx.keymap?.layer !== 'function' || typeof ctx.ui?.dialog?.prompt !== 'function'
79
+ || typeof ctx.ui?.dialog?.confirm !== 'function') return () => {};
80
+ let disposed = false, active;
81
+ const toast = (message, variant = 'info') => { if (!disposed) ctx.ui.toast.show({ title: 'Psst', message, variant, duration: 12000 }); };
82
+ const selection = () => {
83
+ const route = ctx.ui.router.current();
84
+ if (route?.type !== 'session' || typeof route.sessionID !== 'string' || !/^[A-Za-z0-9_.:-]{1,256}$/.test(route.sessionID) || !ctx.location) throw Error('selection_changed');
85
+ return { sessionID: route.sessionID, directory: ctx.location.directory, workspaceID: ctx.location.workspaceID };
86
+ };
87
+ const run = async kind => {
88
+ if (disposed) return;
89
+ if (active) { toast('Finish or dismiss the current Psst dialog first.', 'warning'); return; }
90
+ const controller = new AbortController();
91
+ active = controller;
92
+ let timer, check, blur;
93
+ try {
94
+ const selected = selection();
95
+ if (!available()) throw Error('local_unavailable');
96
+ const location = { ...ctx.location };
97
+ check = () => {
98
+ try { const current = selection();
99
+ if (JSON.stringify(current) !== JSON.stringify(selected)) controller.abort();
100
+ } catch { controller.abort(); }
101
+ };
102
+ blur = () => controller.abort();
103
+ ctx.renderer?.on?.('blur', blur);
104
+ ctx.renderer?.on?.('frame', check);
105
+ timer = setInterval(check, 50); timer.unref?.();
106
+ controller.signal.addEventListener('abort', () => {
107
+ clearInterval(timer);
108
+ ctx.renderer?.off?.('blur', blur);
109
+ ctx.renderer?.off?.('frame', check);
110
+ }, { once: true });
111
+ const verify = () => {
112
+ check();
113
+ if (disposed || controller.signal.aborted) throw Error('selection_changed');
114
+ if (!available()) throw Error('local_unavailable');
115
+ };
116
+ const rpc = ctx.client.rpc(SETUP_RPC);
117
+ const call = async (method, input) => {
118
+ verify();
119
+ const result = await rpc[method]({ sessionID: selected.sessionID, ...input }, { location, signal: AbortSignal.any([controller.signal, AbortSignal.timeout(15000)]) });
120
+ verify(); return result;
121
+ };
122
+ if (kind === 'setup') {
123
+ const value = await ctx.ui.dialog.prompt({ title: 'Set up Psst', description: 'Enter only your HTTPS broker URL. Approval happens in the broker admin UI; never paste a token or credential.', placeholder: 'https://broker.example.com' });
124
+ if (value === undefined) return;
125
+ verify();
126
+ const broker_url = brokerOrigin(value);
127
+ const status = await call('status', { broker_url });
128
+ let replacement_confirmed = false;
129
+ if (status?.replacement_confirmation_required === true) {
130
+ if (typeof status.request_id !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(status.request_id)) throw Error('invalid_status');
131
+ replacement_confirmed = await ctx.ui.dialog.confirm({ title: 'Replace Psst authorization?', message: 'This creates a new approval request and, after approval, new credential-bound addresses. Old addresses will not become the new identity. Old local data is retained. Continue?', label: { confirm: 'Create new request', cancel: 'Keep existing' } }) === true;
132
+ verify();
133
+ if (!replacement_confirmed) return;
134
+ }
135
+ const result = await call('setup', { broker_url, ...(replacement_confirmed ? { replacement_confirmed: true, replacement_request_id: status.request_id } : {}) });
136
+ toast(statusMessage(result));
137
+ } else toast(statusMessage(await call(kind, {})));
138
+ } catch (error) {
139
+ const message = error?.message === 'invalid_url'
140
+ ? 'Enter an HTTPS broker origin only, without credentials, path, query or fragment.'
141
+ : controller.signal.aborted || error?.message === 'selection_changed'
142
+ ? 'Psst action stopped because the selected chat or terminal focus changed. Run it again in the intended chat.'
143
+ : error?.message === 'local_unavailable'
144
+ ? 'Setup requires a local TUI sharing the server private directory.'
145
+ : 'Psst action could not complete. Check /psst-status before retrying; do not paste credentials.';
146
+ toast(message, 'warning');
147
+ } finally {
148
+ clearInterval(timer);
149
+ if (blur) ctx.renderer?.off?.('blur', blur);
150
+ if (check) ctx.renderer?.off?.('frame', check);
151
+ if (active === controller) active = undefined;
152
+ }
153
+ };
154
+ ctx.keymap.layer(() => ({ mode: 'global', commands: [
155
+ { id: 'psst-setup', title: 'Psst: Set up broker', group: 'Psst', palette: true, slash: { name: 'psst-setup' }, run: () => run('setup') },
156
+ { id: 'psst-status', title: 'Psst: Check status', group: 'Psst', palette: true, slash: { name: 'psst-status' }, run: () => run('status') },
157
+ { id: 'psst-cancel-setup', title: 'Psst: Pause setup', group: 'Psst', palette: true, slash: { name: 'psst-cancel-setup' }, run: () => run('cancel') },
158
+ ] }));
159
+ return () => { disposed = true; active?.abort(); };
160
+ }
@@ -0,0 +1,97 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { constants, openSync, closeSync, fstatSync, readFileSync, writeFileSync, renameSync, unlinkSync, lstatSync, realpathSync, readdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { privateDirectory } from './config.mjs';
5
+ import { canonical } from '../vendor/broker/src/protocol.mjs';
6
+
7
+ // Availability preference, NOT authentication. Processes with the same UID can
8
+ // forge this signal. It never supplies a binding, credential or tool authority.
9
+ export const PRESENCE_TTL = 5000;
10
+ const validID = s => typeof s === 'string' && /^[A-Za-z0-9_.:-]{1,256}$/.test(s);
11
+ const ownerID = s => typeof s === 'string' && /^[a-f0-9-]{36}$/.test(s);
12
+ export function processIdentity(pid = process.pid) {
13
+ if (!Number.isSafeInteger(pid) || pid < 1) throw new Error('native_selection_invalid');
14
+ if (process.platform !== 'linux') { process.kill(pid, 0); return { pid, start: null, boot: null }; }
15
+ const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
16
+ return { pid, start: stat.slice(stat.lastIndexOf(')') + 2).split(' ')[19], boot: readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim() };
17
+ }
18
+ function checkedDirectory(path) {
19
+ const s = lstatSync(path);
20
+ if (!s.isDirectory() || s.isSymbolicLink() || s.mode & 0o077 || process.getuid && s.uid !== process.getuid() || realpathSync(path) !== path) throw new Error('native_selection_path_unsafe');
21
+ return path;
22
+ }
23
+ export function presenceDirectory(root, location, { create = false } = {}) {
24
+ checkedDirectory(root);
25
+ const directory = realpathSync(location.directory);
26
+ const key = createHash('sha256').update(canonical({ directory, workspaceID: location.workspaceID ?? null })).digest('hex');
27
+ const path = join(root, 'active-tui-' + key);
28
+ if (create) privateDirectory(path);
29
+ return checkedDirectory(path);
30
+ }
31
+ function read(path) {
32
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
33
+ try {
34
+ const s = fstatSync(fd);
35
+ if (!s.isFile() || s.nlink !== 1 || s.mode & 0o077 || process.getuid && s.uid !== process.getuid() || s.size > 2048) throw new Error('native_selection_invalid');
36
+ return JSON.parse(readFileSync(fd, 'utf8'));
37
+ } finally { closeSync(fd); }
38
+ }
39
+ export function createPresenceWriter({ directory, owner = randomUUID(), counter = { seq: 0 }, now = Date.now }) {
40
+ checkedDirectory(directory);
41
+ if (!ownerID(owner)) throw new Error('native_selection_invalid');
42
+ const identity = processIdentity(), path = join(directory, owner + '.json');
43
+ const generation = randomUUID();
44
+ let stopped = false;
45
+ return {
46
+ owner,
47
+ write(sessionID) {
48
+ if (stopped || sessionID !== null && !validID(sessionID)) throw new Error('native_selection_invalid');
49
+ checkedDirectory(directory);
50
+ const value = { version: 1, owner, ...identity, generation, seq: ++counter.seq, issued: now(), sessionID };
51
+ const temp = join(directory, `.${owner}-${randomUUID()}.tmp`);
52
+ let fd;
53
+ try {
54
+ fd = openSync(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
55
+ writeFileSync(fd, JSON.stringify(value)); closeSync(fd); fd = undefined;
56
+ renameSync(temp, path);
57
+ } finally { if (fd !== undefined) closeSync(fd); try { unlinkSync(temp); } catch {} }
58
+ },
59
+ stop() { stopped = true; try { checkedDirectory(directory); if (read(path).generation === generation) unlinkSync(path); } catch (e) { if (e.code !== 'ENOENT') throw e; } },
60
+ };
61
+ }
62
+ export function createPresenceReader({ directory, now = Date.now }) {
63
+ const seen = new Map();
64
+ return {
65
+ selected() {
66
+ checkedDirectory(directory);
67
+ const names = readdirSync(directory).filter(n => n.endsWith('.json'));
68
+ if (names.length > 4096) throw new Error('native_selection_owner_limit');
69
+ const selected = new Set(); let liveOwners = 0;
70
+ for (const [id, value] of seen) if (value.expires <= now()) seen.delete(id);
71
+ for (const name of names) {
72
+ try {
73
+ const v = read(join(directory, name)), time = now();
74
+ // Dedicated ephemeral files only; never remove bindings or journals.
75
+ if (v.version === 1 && ownerID(v.owner) && name === v.owner + '.json' && Number.isSafeInteger(v.issued) && time - v.issued >= PRESENCE_TTL) {
76
+ if (canonical(read(join(directory, name))) === canonical(v)) unlinkSync(join(directory, name));
77
+ continue;
78
+ }
79
+ if (v.version !== 1 || !ownerID(v.owner) || name !== v.owner + '.json' || !Number.isSafeInteger(v.seq) || v.seq < 1
80
+ || !Number.isSafeInteger(v.issued) || v.issued > time || time - v.issued >= PRESENCE_TTL
81
+ || v.sessionID !== null && !validID(v.sessionID)) continue;
82
+ const identity = processIdentity(v.pid);
83
+ if (identity.start !== v.start || identity.boot !== v.boot) continue;
84
+ const previous = seen.get(v.owner);
85
+ if (previous && (v.seq < previous.seq || v.seq === previous.seq && canonical(v) !== previous.value)) continue;
86
+ liveOwners++;
87
+ if (!seen.has(v.owner) && seen.size >= 128) continue;
88
+ seen.set(v.owner, { seq: v.seq, value: canonical(v), expires: v.issued + PRESENCE_TTL });
89
+ if (v.sessionID) selected.add(v.sessionID);
90
+ } catch { /* A malformed/stale owner is not a reason to activate anything. */ }
91
+ }
92
+ for (const [id, value] of seen) if (value.expires <= now()) seen.delete(id);
93
+ if (liveOwners > 64) throw new Error('native_selection_owner_limit');
94
+ return selected;
95
+ },
96
+ };
97
+ }
@@ -0,0 +1,11 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstatSync, readFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ export const APPROVAL_SHA256 = 'a60c3a0ccd7deb50556586cd67841bb89a300ea3c7f1af3a91d437f856b7a2d5';
5
+ export async function loadApprovalHelper() {
6
+ const path = fileURLToPath(new URL('../vendor/enrollment/approval.mjs', import.meta.url));
7
+ const stat = lstatSync(path);
8
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 65536
9
+ || createHash('sha256').update(readFileSync(path)).digest('hex') !== APPROVAL_SHA256) throw new Error('approval_snapshot_mismatch');
10
+ return import('../vendor/enrollment/approval.mjs');
11
+ }
package/src/config.mjs ADDED
@@ -0,0 +1,50 @@
1
+ import { openSync, closeSync, readFileSync, fstatSync, constants, mkdirSync, lstatSync, realpathSync } from 'node:fs';
2
+ import { isAbsolute, dirname, resolve } from 'node:path';
3
+ const fail = () => { throw new Error('psst_configuration_invalid'); };
4
+ const plain = value => value && typeof value === 'object' && !Array.isArray(value);
5
+ export function readConfig(path) {
6
+ if (typeof path !== 'string' || !isAbsolute(path)) fail();
7
+ let fd, marked = false;
8
+ try {
9
+ fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
10
+ const stat = fstatSync(fd);
11
+ if (!stat.isFile() || stat.mode & 0o077 || stat.size > 16384 || process.getuid && stat.uid !== process.getuid()) fail();
12
+ let value = JSON.parse(readFileSync(fd, 'utf8'));
13
+ if (value?.format === 'broker-client-bundle-v1') {
14
+ const allowed = ['format', 'protocol_version', 'broker_url', 'token_file', 'file_paths_relative_to', 'authorization_header', 'authorization_scheme', 'insecure_development', 'allowed_client_types', 'scope', 'ca_file', 'enrollment_receipt_file', 'registration_version'];
15
+ if (Object.keys(value).some(key => !allowed.includes(key)) || value.protocol_version !== '1'
16
+ || value.registration_version !== undefined && ![1,2].includes(value.registration_version)
17
+ || value.file_paths_relative_to !== 'connection_file_directory' || value.authorization_header !== 'Authorization'
18
+ || value.authorization_scheme !== 'Bearer' || value.insecure_development !== false
19
+ || !Array.isArray(value.allowed_client_types) || value.allowed_client_types.length !== 1 || value.allowed_client_types[0] !== 'opencode'
20
+ || value.enrollment_receipt_file !== undefined && value.enrollment_receipt_file !== 'enrollment.json'
21
+ || value.token_file !== 'token' || typeof value.scope !== 'string') fail();
22
+ marked = value.enrollment_receipt_file === 'enrollment.json';
23
+ const base = dirname(realpathSync(path));
24
+ value = { broker: { url: value.broker_url, tokenFile: resolve(base, value.token_file),
25
+ ...(value.registration_version !== undefined ? { registrationVersion: value.registration_version } : {}),
26
+ ...(value.ca_file ? { caFile: resolve(base, value.ca_file) } : {}) }, stateDirectory: resolve(base, 'state') };
27
+ }
28
+ if (!plain(value) || Object.keys(value).some(key => !['broker', 'stateDirectory', 'name', 'project', 'projectEnv'].includes(key))
29
+ || !plain(value.broker) || Object.keys(value.broker).some(key => !['url', 'tokenFile', 'caFile', 'registrationVersion'].includes(key))
30
+ || typeof value.stateDirectory !== 'string' || !isAbsolute(value.stateDirectory)
31
+ || value.broker.registrationVersion !== undefined && ![1, 2].includes(value.broker.registrationVersion)
32
+ || typeof value.broker.tokenFile !== 'string' || !isAbsolute(value.broker.tokenFile)
33
+ || value.broker.caFile !== undefined && (typeof value.broker.caFile !== 'string' || !isAbsolute(value.broker.caFile))
34
+ || value.project !== undefined && (typeof value.project !== 'string' || !value.project.trim() || [...value.project].length > 256 || /[\x00-\x1f\x7f-\x9f]/.test(value.project))
35
+ || value.projectEnv !== undefined && (typeof value.projectEnv !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(value.projectEnv))
36
+ || value.name !== undefined && (typeof value.name !== 'string' || !value.name.trim() || value.name.length > 100 || /[\x00-\x1f\x7f]/.test(value.name))) fail();
37
+ const url = new URL(value.broker.url);
38
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || url.pathname !== '/') fail();
39
+ if (marked) Object.defineProperty(value, 'enrollmentReceiptRequired', { value: true });
40
+ return value;
41
+ } catch { fail(); }
42
+ finally { if (fd !== undefined) closeSync(fd); }
43
+ }
44
+ export function privateDirectory(path) {
45
+ mkdirSync(path, { recursive: true, mode: 0o700 });
46
+ const stat = lstatSync(path);
47
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.mode & 0o077 || realpathSync(path) !== path
48
+ || process.getuid && stat.uid !== process.getuid()) fail();
49
+ return path;
50
+ }
@@ -0,0 +1,59 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync, lstatSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join, resolve, isAbsolute } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { readConfig } from './config.mjs';
7
+
8
+ export const ENROLLMENT_SHA256 = 'afeab00d7c2d6768d3537bd7c350e249768897bf81eff9b4ae9796f7c3f9dbe9';
9
+ async function helper() {
10
+ const path = fileURLToPath(new URL('../vendor/enrollment/enrollment.mjs', import.meta.url));
11
+ const stat = lstatSync(path);
12
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 65536
13
+ || createHash('sha256').update(readFileSync(path)).digest('hex') !== ENROLLMENT_SHA256) throw new Error('enrollment_snapshot_mismatch');
14
+ return import('../vendor/enrollment/enrollment.mjs');
15
+ }
16
+
17
+ /** Legacy environment mode may load an existing bundle, never issue credentials. */
18
+ export async function readExistingConnection(path, { identityRecovery = false } = {}) {
19
+ const settings = readConfig(path);
20
+ if (settings.enrollmentReceiptRequired) {
21
+ try { settings.enrollment = (await helper()).readEnrollmentReceipt({ connectionFile: path, clientType: 'opencode' }); }
22
+ catch (error) {
23
+ // The pinned helper checks receipt/token/type/origin integrity before its
24
+ // final expiry check. This mode is only for existing identity discovery,
25
+ // never connection, activation, selection or enrollment.
26
+ if (!identityRecovery || error.code !== 'credential_expired') throw error;
27
+ settings.authorizationExpired = true;
28
+ }
29
+ }
30
+ return settings;
31
+ }
32
+
33
+ /** Only the native explicit-connect command calls this. No model tool accepts a secret. */
34
+ export function createConnectionLoader({ directory, stateHome = process.env.XDG_STATE_HOME || join(homedir(), '.local', 'state'),
35
+ loadHelper = helper } = {}) {
36
+ if (!isAbsolute(directory) || !isAbsolute(stateHome)) throw new Error('private_directory_required');
37
+ return async function connectFile(path, { clientType, sessionID }) {
38
+ if (clientType !== 'opencode' || typeof path !== 'string' || !isAbsolute(path)
39
+ || /[\x00-\x1f\x7f]/.test(path) || typeof sessionID !== 'string' || !sessionID) throw new Error('private_file_required');
40
+ let settings;
41
+ try { settings = readConfig(path); } catch { /* The shared helper independently validates pairing files. */ }
42
+ if (settings) {
43
+ // The explicit bundle marker makes missing/corrupt receipts fatal.
44
+ if (settings.enrollmentReceiptRequired) {
45
+ const api = await loadHelper();
46
+ settings.enrollment = api.readEnrollmentReceipt({ connectionFile: path, clientType });
47
+ }
48
+ } else {
49
+ const api = await loadHelper();
50
+ const key = createHash('sha256').update(JSON.stringify([resolve(directory), sessionID])).digest('hex');
51
+ const result = await api.connectPairingFile({ pairingFile: path,
52
+ directory: join(stateHome, 'psst', 'opencode', key), clientType });
53
+ settings = readConfig(result.connectionFile);
54
+ settings.enrollment = api.readEnrollmentReceipt({ connectionFile: result.connectionFile, clientType });
55
+ }
56
+ if (settings.enrollment && Date.parse(settings.enrollment.expiresAt) <= Date.now()) throw new Error('credential_expired');
57
+ return settings;
58
+ };
59
+ }
@@ -0,0 +1,94 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { readdirSync, lstatSync, openSync, closeSync, readFileSync, fstatSync, constants } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { canonical, validateUuid } from '../vendor/broker/src/protocol.mjs';
5
+ import { privateDirectory } from './config.mjs';
6
+ import { nativeSessionBinding } from './native-delivery.mjs';
7
+
8
+ // Private persisted names remain compatible; the public plugin is only matuto.psst.
9
+ export function readNativeBindings(settings) {
10
+ if (!settings) return [];
11
+ try { lstatSync(settings.stateDirectory); } catch (error) { if (error.code === 'ENOENT') return []; throw error; }
12
+ privateDirectory(settings.stateDirectory);
13
+ const names=readdirSync(settings.stateDirectory).filter(name=>name.startsWith('opencode-v2-'));
14
+ if(names.length>256)throw new Error('identity_recovery_scan_limit');
15
+ const records=[];
16
+ for(const name of names){
17
+ if(!/^opencode-v2-[a-f0-9]{64}$/.test(name))throw new Error('identity_recovery_invalid');
18
+ const directory=privateDirectory(join(settings.stateDirectory,name));
19
+ const fd=openSync(join(directory,'binding.json'),constants.O_RDONLY|constants.O_NOFOLLOW);let record;
20
+ try{const stat=fstatSync(fd);if(!stat.isFile()||stat.mode&0o077||stat.size>8192||process.getuid&&stat.uid!==process.getuid())throw new Error('identity_recovery_invalid');record=JSON.parse(readFileSync(fd,'utf8'));}finally{closeSync(fd);}
21
+ if(record.version!==1||Object.keys(record).sort().join(',')!=='binding,broker,identity,version')throw new Error('identity_recovery_invalid');
22
+ const b=record.binding;
23
+ if(canonical(nativeSessionBinding({id:b?.sessionID,projectID:b?.projectID,location:b?.location,time:{created:b?.created}},b?.hostID))!==canonical(b))throw new Error('identity_recovery_invalid');
24
+ if(Object.keys(record.identity??{}).sort().join(',')!=='client_id,client_type,session_instance_id'||record.identity.client_type!=='opencode')throw new Error('identity_recovery_invalid');
25
+ validateUuid(record.identity.client_id);validateUuid(record.identity.session_instance_id);
26
+ const hash=createHash('sha256').update(canonical({binding:b,broker:record.broker})).digest('hex');
27
+ if(name!=='opencode-v2-'+hash)throw new Error('identity_recovery_invalid');
28
+ if(record.broker===settings.broker.url)records.push(record);
29
+ }
30
+ return records;
31
+ }
32
+
33
+ export async function recoverHostAnchor({storage,session,settings,directory,workspaceID}) {
34
+ const saved=await storage.get('psst-v2-host-id');
35
+ const records=readNativeBindings(settings);
36
+ // Shared approved installations may hold mailboxes belonging to other native
37
+ // hosts. An existing anchor is not replaced based on unrelated manifests.
38
+ if(saved!==undefined)return saved;
39
+ async function freshAnchor(){
40
+ let names=[];
41
+ try{if(settings)names=readdirSync(settings.stateDirectory);}catch(error){if(error.code!=='ENOENT')throw error;}
42
+ for(const name of names.filter(n=>n.startsWith('opencode-')&&!n.startsWith('opencode-v2-'))){
43
+ const path=join(settings.stateDirectory,name),stat=lstatSync(path);
44
+ if(stat.isSymbolicLink())throw new Error('identity_recovery_invalid');
45
+ if(!stat.isDirectory())continue;
46
+ privateDirectory(path);
47
+ // The old classic layout has no exact current native-session proof.
48
+ // Keep it intact instead of allocating a replacement identity.
49
+ throw new Error('identity_legacy_migration_required');
50
+ }
51
+ const id=randomUUID();await storage.set('psst-v2-host-id',id);return id;
52
+ }
53
+ const candidates=records.filter(r=>r.binding.location.directory===directory
54
+ && r.binding.location.workspaceID===workspaceID);
55
+ const anchors=new Set(),deadline=Date.now()+5000;
56
+ let index=0,incomplete=false,unproven=false;
57
+ await Promise.all(Array.from({length:Math.min(4,candidates.length)},async()=>{
58
+ while(index<candidates.length){
59
+ const record=candidates[index++],remaining=deadline-Date.now();
60
+ if(remaining<=0){incomplete=true;return;}
61
+ const signal=AbortSignal.timeout(Math.max(1,Math.min(2000,remaining)));let listener,info;
62
+ try{
63
+ info=await Promise.race([session.get({sessionID:record.binding.sessionID},{signal}),
64
+ new Promise((_,reject)=>{listener=()=>reject(new Error('identity_recovery_timeout'));signal.addEventListener('abort',listener,{once:true});})]);
65
+ }catch(error){
66
+ // The official public API tags absent sessions. Other failures are not
67
+ // evidence that a second anchor is unrelated; preserve and stop.
68
+ if(error?._tag!=='SessionNotFoundError'||error.sessionID!==record.binding.sessionID){incomplete=true;return;}
69
+ continue;
70
+ }finally{if(listener)signal.removeEventListener('abort',listener);}
71
+ // A successful lookup with a changed or unusable live binding is not
72
+ // absence. It must not authorize a fresh anchor or partial recovery.
73
+ if(!info||info.parentID||info.time?.archived){unproven=true;continue;}
74
+ try{
75
+ if(canonical(nativeSessionBinding(info,record.binding.hostID))===canonical(record.binding))anchors.add(record.binding.hostID);
76
+ else unproven=true;
77
+ }catch{unproven=true;}
78
+ }
79
+ }));
80
+ if(incomplete)throw new Error('identity_recovery_incomplete');
81
+ if(anchors.size>1)throw new Error('identity_recovery_ambiguous');
82
+ if(unproven)throw new Error('identity_recovery_required');
83
+ // No candidate, or every candidate positively absent in this native store:
84
+ // allocate an independent host without adopting or changing old mailboxes.
85
+ if(!anchors.size)return freshAnchor();
86
+ const [hostID]=anchors;
87
+ await storage.set('psst-v2-host-id',hostID);
88
+ return hostID;
89
+ }
90
+
91
+ export function assertExistingBinding(settings,binding){
92
+ const matches=readNativeBindings(settings).filter(r=>r.binding.sessionID===binding.sessionID);
93
+ if(matches.length>1||matches.some(r=>canonical(r.binding)!==canonical(binding)))throw new Error('identity_recovery_conflict');
94
+ }
@@ -0,0 +1,127 @@
1
+ import { openSync, closeSync, readFileSync, writeFileSync, fsyncSync, fstatSync, lstatSync, realpathSync, renameSync, unlinkSync, constants } from 'node:fs';
2
+ import { join, isAbsolute } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { DatabaseSync } from 'node:sqlite';
6
+ import { privateDirectory } from './config.mjs';
7
+ import { readExistingConnection } from './enrollment.mjs';
8
+
9
+ export function installationDirectory(env = process.env) {
10
+ const directory = env.PSST_OPENCODE_CONFIG_DIR || join(env.OPENCODE_CONFIG_DIR || join(env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'opencode'), 'psst');
11
+ if (!isAbsolute(directory)) throw new Error('psst_private_absolute_directory_required');
12
+ return directory;
13
+ }
14
+ function read(path) {
15
+ let fd;
16
+ try {
17
+ fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
18
+ const stat = fstatSync(fd);
19
+ if (!stat.isFile() || stat.mode & 0o077 || stat.size > 8192 || process.getuid && stat.uid !== process.getuid()) throw new Error();
20
+ const value = JSON.parse(readFileSync(fd, 'utf8'));
21
+ if (value?.format !== 'psst-opencode-installation-v1' || typeof value.revision !== 'string'
22
+ || !isAbsolute(value.connectionFile) || Object.keys(value).some(k => !['format', 'revision', 'connectionFile'].includes(k))) throw new Error();
23
+ return value;
24
+ } catch (error) { if (error.code === 'ENOENT') return null; throw new Error('psst_installation_invalid'); }
25
+ finally { if (fd !== undefined) closeSync(fd); }
26
+ }
27
+ export function readSelection(directory = installationDirectory()) {
28
+ try {
29
+ const stat = lstatSync(directory);
30
+ if (!isAbsolute(directory) || !stat.isDirectory() || stat.isSymbolicLink() || stat.mode & 0o077
31
+ || realpathSync(directory) !== directory || process.getuid && stat.uid !== process.getuid()) throw new Error('psst_installation_invalid');
32
+ } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
33
+ return read(join(directory, 'installation.json'));
34
+ }
35
+ export async function readInstallation(directory = installationDirectory(), readOptions) {
36
+ const selected = readSelection(directory);
37
+ return selected ? readExistingConnection(selected.connectionFile, readOptions) : undefined;
38
+ }
39
+ /** No network. Compare-and-swap fences concurrent UI flows; never deletes old bundles/inboxes. */
40
+ export async function selectInstallation({ directory = installationDirectory(), connectionFile, expectedRevision, signal, pendingFlowId, verify }) {
41
+ await readExistingConnection(connectionFile); // exact broker approval receipt, type, URL and token binding
42
+ if (verify !== undefined && (typeof verify !== 'function' || !await verify())) throw new Error('psst_setup_binding_changed');
43
+ return withSelectionLock(directory, () => {
44
+ let temp;
45
+ try {
46
+ if (signal?.aborted) throw new Error('psst_setup_cancelled');
47
+ if (pendingFlowId) {
48
+ const pending = readPendingFile(directory);
49
+ if (!pending || pending.paused || pending.flowId !== pendingFlowId) throw new Error('psst_setup_cancelled');
50
+ }
51
+ const previous = readSelection(directory);
52
+ if (pendingFlowId && previous?.connectionFile === connectionFile) {
53
+ writePending(directory, { ...readPendingFile(directory), paused: true });
54
+ return previous; // Reconcile crash after committed selection, not a replacement.
55
+ }
56
+ if ((previous?.revision ?? null) !== expectedRevision) throw new Error('psst_selection_changed');
57
+ const value = { format: 'psst-opencode-installation-v1', revision: randomUUID(), connectionFile };
58
+ temp = join(directory, `.selection-${randomUUID()}`);
59
+ const out = openSync(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
60
+ try { writeFileSync(out, JSON.stringify(value) + '\n'); fsyncSync(out); } finally { closeSync(out); }
61
+ renameSync(temp, join(directory, 'installation.json')); temp = null;
62
+ const dir = openSync(directory, constants.O_RDONLY); try { fsyncSync(dir); } finally { closeSync(dir); }
63
+ if (pendingFlowId) writePending(directory, { ...readPendingFile(directory), paused: true });
64
+ return value;
65
+ } finally { if (temp) unlinkSync(temp); }
66
+ });
67
+ }
68
+
69
+ function readPendingFile(directory) {
70
+ let fd;
71
+ try {
72
+ fd = openSync(join(directory, 'pending.json'), constants.O_RDONLY | constants.O_NOFOLLOW);
73
+ const stat = fstatSync(fd);
74
+ if (!stat.isFile() || stat.mode & 0o077 || stat.size > 8192 || process.getuid && stat.uid !== process.getuid()) throw new Error();
75
+ const value = JSON.parse(readFileSync(fd, 'utf8'));
76
+ const url = new URL(value.brokerUrl);
77
+ if (value.format !== 'psst-opencode-pending-v1' || typeof value.flowId !== 'string' || typeof value.paused !== 'boolean'
78
+ || value.expectedRevision !== null && typeof value.expectedRevision !== 'string'
79
+ || value.requestId !== null && typeof value.requestId !== 'string'
80
+ || value.previousRequestId !== undefined && value.previousRequestId !== null && typeof value.previousRequestId !== 'string'
81
+ || value.owner !== undefined && (!value.owner || Object.keys(value.owner).sort().join(',') !== 'binding,sessionID'
82
+ || typeof value.owner.sessionID !== 'string' || value.owner.sessionID.length > 256
83
+ || typeof value.owner.binding !== 'string' || value.owner.binding.length > 4096)
84
+ || url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || url.pathname !== '/') throw new Error();
85
+ return value;
86
+ } catch (error) { if (error.code === 'ENOENT') return null; throw new Error('psst_pending_invalid'); }
87
+ finally { if (fd !== undefined) closeSync(fd); }
88
+ }
89
+ export function readPending(directory = installationDirectory()) {
90
+ readSelection(directory); // existing directory ownership/canonical-path check, no creation
91
+ return readPendingFile(directory);
92
+ }
93
+ function withSelectionLock(directory, action) {
94
+ privateDirectory(directory);
95
+ const lock = join(directory, 'installation-lock.sqlite');
96
+ try { const fd = openSync(lock, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600); closeSync(fd); }
97
+ catch (error) { if (error.code !== 'EEXIST') throw new Error('psst_selection_busy'); }
98
+ const stat = lstatSync(lock);
99
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.mode & 0o077 || process.getuid && stat.uid !== process.getuid()) throw new Error('psst_installation_invalid');
100
+ let db;
101
+ try {
102
+ db = new DatabaseSync(lock); db.exec('PRAGMA busy_timeout=0; BEGIN IMMEDIATE');
103
+ const result = action(); db.exec('COMMIT'); return result;
104
+ } finally { db?.close(); } // OS releases the lock even after a killed process; no stale-lock reset.
105
+ }
106
+ function writePending(directory, value) {
107
+ const temp = join(directory, `.pending-${randomUUID()}`);
108
+ const fd = openSync(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
109
+ try { writeFileSync(fd, JSON.stringify(value) + '\n'); fsyncSync(fd); } finally { closeSync(fd); }
110
+ renameSync(temp, join(directory, 'pending.json'));
111
+ const dir = openSync(directory, constants.O_RDONLY); try { fsyncSync(dir); } finally { closeSync(dir); }
112
+ }
113
+ export function savePending({ directory = installationDirectory(), brokerUrl, expectedRevision, requestId = null, previousRequestId, owner }) {
114
+ return withSelectionLock(directory, () => {
115
+ readPendingFile(directory); // Explicit setup is not permission to reset corrupt state.
116
+ const value = { format: 'psst-opencode-pending-v1', flowId: randomUUID(), brokerUrl, expectedRevision, requestId, paused: false, ...(owner ? { owner } : {}), ...(previousRequestId ? { previousRequestId } : {}) };
117
+ writePending(directory, value); return value;
118
+ });
119
+ }
120
+ export function updatePending({ directory = installationDirectory(), flowId, paused, requestId }) {
121
+ return withSelectionLock(directory, () => {
122
+ const value = readPendingFile(directory);
123
+ if (!value || value.flowId !== flowId) return false;
124
+ writePending(directory, { ...value, ...(paused !== undefined ? { paused } : {}), ...(requestId !== undefined ? { requestId } : {}) });
125
+ return true;
126
+ });
127
+ }