@moxxy/plugin-channel-signal 0.27.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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/dist/channel/chunker.d.ts +69 -0
  3. package/dist/channel/chunker.d.ts.map +1 -0
  4. package/dist/channel/chunker.js +133 -0
  5. package/dist/channel/chunker.js.map +1 -0
  6. package/dist/channel/turn-runner.d.ts +33 -0
  7. package/dist/channel/turn-runner.d.ts.map +1 -0
  8. package/dist/channel/turn-runner.js +70 -0
  9. package/dist/channel/turn-runner.js.map +1 -0
  10. package/dist/channel/voice.d.ts +37 -0
  11. package/dist/channel/voice.d.ts.map +1 -0
  12. package/dist/channel/voice.js +88 -0
  13. package/dist/channel/voice.js.map +1 -0
  14. package/dist/channel.d.ts +154 -0
  15. package/dist/channel.d.ts.map +1 -0
  16. package/dist/channel.js +468 -0
  17. package/dist/channel.js.map +1 -0
  18. package/dist/index.d.ts +28 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +215 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/jsonrpc.d.ts +53 -0
  23. package/dist/jsonrpc.d.ts.map +1 -0
  24. package/dist/jsonrpc.js +167 -0
  25. package/dist/jsonrpc.js.map +1 -0
  26. package/dist/keys.d.ts +37 -0
  27. package/dist/keys.d.ts.map +1 -0
  28. package/dist/keys.js +55 -0
  29. package/dist/keys.js.map +1 -0
  30. package/dist/pair-flow.d.ts +21 -0
  31. package/dist/pair-flow.d.ts.map +1 -0
  32. package/dist/pair-flow.js +136 -0
  33. package/dist/pair-flow.js.map +1 -0
  34. package/dist/permission.d.ts +21 -0
  35. package/dist/permission.d.ts.map +1 -0
  36. package/dist/permission.js +27 -0
  37. package/dist/permission.js.map +1 -0
  38. package/dist/schema.d.ts +4295 -0
  39. package/dist/schema.d.ts.map +1 -0
  40. package/dist/schema.js +84 -0
  41. package/dist/schema.js.map +1 -0
  42. package/dist/setup-wizard.d.ts +14 -0
  43. package/dist/setup-wizard.d.ts.map +1 -0
  44. package/dist/setup-wizard.js +85 -0
  45. package/dist/setup-wizard.js.map +1 -0
  46. package/dist/sidecar.d.ts +137 -0
  47. package/dist/sidecar.d.ts.map +1 -0
  48. package/dist/sidecar.js +421 -0
  49. package/dist/sidecar.js.map +1 -0
  50. package/package.json +89 -0
  51. package/src/channel/chunker.test.ts +135 -0
  52. package/src/channel/chunker.ts +147 -0
  53. package/src/channel/turn-runner.ts +97 -0
  54. package/src/channel/voice.test.ts +136 -0
  55. package/src/channel/voice.ts +118 -0
  56. package/src/channel.test.ts +364 -0
  57. package/src/channel.ts +607 -0
  58. package/src/index.ts +289 -0
  59. package/src/jsonrpc.test.ts +128 -0
  60. package/src/jsonrpc.ts +198 -0
  61. package/src/keys.test.ts +45 -0
  62. package/src/keys.ts +56 -0
  63. package/src/pair-flow.ts +161 -0
  64. package/src/permission.ts +36 -0
  65. package/src/schema.ts +98 -0
  66. package/src/setup-wizard.ts +98 -0
  67. package/src/sidecar.test.ts +276 -0
  68. package/src/sidecar.ts +511 -0
  69. package/src/subcommands.test.ts +206 -0
@@ -0,0 +1,206 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import * as fsSync from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
+ import { VaultStore, createStaticKeySource, deriveKey, generateSalt } from '@moxxy/plugin-vault';
7
+ import type { ChannelDef } from '@moxxy/sdk';
8
+ import { buildSignalPlugin } from './index.js';
9
+ import { SIGNAL_ACCOUNT_KEY, SIGNAL_ALLOWED_SENDERS_KEY } from './keys.js';
10
+
11
+ let tmp: string;
12
+ let vault: VaultStore;
13
+ let signalDef: ChannelDef;
14
+ let writeOut: string[];
15
+ let writeErr: string[];
16
+ let origStdoutWrite: typeof process.stdout.write;
17
+ let origStderrWrite: typeof process.stderr.write;
18
+
19
+ beforeEach(async () => {
20
+ tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'mox-sig-sub-'));
21
+ vault = new VaultStore({
22
+ filePath: path.join(tmp, 'vault.json'),
23
+ keySource: createStaticKeySource(deriveKey('test', generateSalt())),
24
+ });
25
+ const plugin = buildSignalPlugin({ vault });
26
+ signalDef = plugin.channels![0]!;
27
+ writeOut = [];
28
+ writeErr = [];
29
+ origStdoutWrite = process.stdout.write.bind(process.stdout);
30
+ origStderrWrite = process.stderr.write.bind(process.stderr);
31
+ process.stdout.write = ((chunk: string | Uint8Array) => {
32
+ writeOut.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString());
33
+ return true;
34
+ }) as typeof process.stdout.write;
35
+ process.stderr.write = ((chunk: string | Uint8Array) => {
36
+ writeErr.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString());
37
+ return true;
38
+ }) as typeof process.stderr.write;
39
+ // Tests must never find a REAL signal-cli (CI may or may not have one).
40
+ vi.stubEnv('PATH', tmp);
41
+ vi.stubEnv('MOXXY_SIGNAL_ACCOUNT', '');
42
+ });
43
+
44
+ afterEach(async () => {
45
+ process.stdout.write = origStdoutWrite;
46
+ process.stderr.write = origStderrWrite;
47
+ vi.unstubAllEnvs();
48
+ await fs.rm(tmp, { recursive: true, force: true });
49
+ });
50
+
51
+ function ctx(
52
+ overrides: {
53
+ startChannel?: () => Promise<number>;
54
+ session?: { setPermissionResolver: (r: unknown) => void };
55
+ } = {},
56
+ ) {
57
+ return {
58
+ deps: { cwd: tmp, vault, logger: undefined, options: {} },
59
+ args: { positional: [], flags: {} },
60
+ startChannel: overrides.startChannel ?? (async () => 0),
61
+ session: overrides.session ?? { setPermissionResolver: () => {} },
62
+ } as never;
63
+ }
64
+
65
+ describe('signal ChannelDef shape', () => {
66
+ it('declares a dedicated runner with the signal session source', () => {
67
+ expect(signalDef.name).toBe('signal');
68
+ expect(signalDef.dedicatedRunner).toBe(true);
69
+ expect(signalDef.sessionSource).toBe('signal');
70
+ expect(signalDef.interactiveCommand).toBe('setup');
71
+ expect(Object.keys(signalDef.subcommands!)).toEqual(
72
+ expect.arrayContaining(['setup', 'pair', 'status', 'unpair']),
73
+ );
74
+ expect(signalDef.config?.fields.map((f) => f.vaultKey)).toEqual([SIGNAL_ACCOUNT_KEY]);
75
+ expect(signalDef.config?.connect?.kind).toBe('qr');
76
+ });
77
+ });
78
+
79
+ describe('isAvailable', () => {
80
+ it('returns an install hint (never throws) when signal-cli is missing', async () => {
81
+ const availability = await signalDef.isAvailable!({ cwd: tmp, vault });
82
+ expect(availability.ok).toBe(false);
83
+ expect(availability.reason).toMatch(/brew install signal-cli/);
84
+ });
85
+
86
+ it('asks for an account once the binary exists', async () => {
87
+ fsSync.writeFileSync(path.join(tmp, 'signal-cli'), '#!/bin/sh\n', { mode: 0o755 });
88
+ const availability = await signalDef.isAvailable!({ cwd: tmp, vault });
89
+ expect(availability.ok).toBe(false);
90
+ expect(availability.reason).toMatch(/signal setup/);
91
+ });
92
+
93
+ it('is available with binary + env account', async () => {
94
+ fsSync.writeFileSync(path.join(tmp, 'signal-cli'), '#!/bin/sh\n', { mode: 0o755 });
95
+ vi.stubEnv('MOXXY_SIGNAL_ACCOUNT', '+15551234567');
96
+ const availability = await signalDef.isAvailable!({ cwd: tmp, vault });
97
+ expect(availability).toEqual({ ok: true });
98
+ });
99
+
100
+ it('is available with binary + vault account', async () => {
101
+ fsSync.writeFileSync(path.join(tmp, 'signal-cli'), '#!/bin/sh\n', { mode: 0o755 });
102
+ await vault.set(SIGNAL_ACCOUNT_KEY, '+15551234567');
103
+ const availability = await signalDef.isAvailable!({ cwd: tmp, vault });
104
+ expect(availability).toEqual({ ok: true });
105
+ });
106
+ });
107
+
108
+ describe('signal channel subcommands', () => {
109
+ it('`status` reports unconfigured state as JSON', async () => {
110
+ const code = await signalDef.subcommands!.status!.run(ctx());
111
+ expect(code).toBe(0);
112
+ const parsed = JSON.parse(writeOut.join(''));
113
+ expect(parsed).toEqual({
114
+ binaryFound: false,
115
+ account: null,
116
+ linked: null,
117
+ allowedSenders: [],
118
+ dataDir: expect.stringContaining('signal-cli'),
119
+ });
120
+ });
121
+
122
+ it('`status` surfaces the stored account + allow-list', async () => {
123
+ await vault.set(SIGNAL_ACCOUNT_KEY, '+15551234567');
124
+ await vault.set(SIGNAL_ALLOWED_SENDERS_KEY, JSON.stringify(['+14440002222']));
125
+ const code = await signalDef.subcommands!.status!.run(ctx());
126
+ expect(code).toBe(0);
127
+ const parsed = JSON.parse(writeOut.join(''));
128
+ expect(parsed.account).toBe('+15551234567');
129
+ expect(parsed.allowedSenders).toEqual(['+14440002222']);
130
+ });
131
+
132
+ it('`unpair` clears account + allow-list and points at the phone for full unlink', async () => {
133
+ await vault.set(SIGNAL_ACCOUNT_KEY, '+15551234567');
134
+ await vault.set(SIGNAL_ALLOWED_SENDERS_KEY, JSON.stringify(['+14440002222']));
135
+ const code = await signalDef.subcommands!.unpair!.run(ctx());
136
+ expect(code).toBe(0);
137
+ expect(writeOut.join('')).toContain('unpaired');
138
+ expect(writeOut.join('')).toContain('Linked Devices');
139
+ expect(await vault.get(SIGNAL_ACCOUNT_KEY)).toBeNull();
140
+ expect(await vault.get(SIGNAL_ALLOWED_SENDERS_KEY)).toBeNull();
141
+ });
142
+
143
+ it('`unpair` is a no-op when nothing is configured', async () => {
144
+ const code = await signalDef.subcommands!.unpair!.run(ctx());
145
+ expect(code).toBe(0);
146
+ expect(writeOut.join('')).toContain('no account was configured');
147
+ });
148
+
149
+ it('`pair` refuses to start without a TTY (interactive-only flow)', async () => {
150
+ const originalIsTTY = process.stdin.isTTY;
151
+ Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true });
152
+ try {
153
+ const startChannel = vi.fn(async () => 0);
154
+ const code = await signalDef.subcommands!.pair!.run(ctx({ startChannel }));
155
+ expect(code).toBe(1);
156
+ expect(startChannel).not.toHaveBeenCalled();
157
+ expect(writeErr.join('')).toMatch(/TTY/);
158
+ } finally {
159
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
160
+ }
161
+ });
162
+
163
+ it('`pair` drives the in-process link flow on a TTY (fails fast without the binary)', async () => {
164
+ const originalIsTTY = process.stdin.isTTY;
165
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
166
+ try {
167
+ const startChannel = vi.fn(async () => 0);
168
+ const setPermissionResolver = vi.fn();
169
+ // No signal-cli on the stubbed PATH -> channel.start throws the install
170
+ // hint before any process could spawn. We assert `pair` wires the
171
+ // session's resolver (the in-process flow) instead of delegating.
172
+ await expect(
173
+ signalDef.subcommands!.pair!.run(ctx({ startChannel, session: { setPermissionResolver } })),
174
+ ).rejects.toThrow(/signal-cli not found/);
175
+ expect(setPermissionResolver).toHaveBeenCalledTimes(1);
176
+ expect(startChannel).not.toHaveBeenCalled();
177
+ } finally {
178
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
179
+ }
180
+ });
181
+
182
+ it('`setup` starts the channel directly when headless', async () => {
183
+ const originalIsTTY = process.stdin.isTTY;
184
+ Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true });
185
+ try {
186
+ const startChannel = vi.fn(async () => 0);
187
+ const code = await signalDef.subcommands!.setup!.run(ctx({ startChannel }));
188
+ expect(code).toBe(0);
189
+ expect(startChannel).toHaveBeenCalledTimes(1);
190
+ } finally {
191
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
192
+ }
193
+ });
194
+
195
+ it('subcommands return 1 when vault is unavailable', async () => {
196
+ const badCtx = {
197
+ deps: { cwd: tmp, vault: undefined, logger: undefined, options: {} },
198
+ args: { positional: [], flags: {} },
199
+ startChannel: async () => 0,
200
+ session: { setPermissionResolver: () => {} },
201
+ } as never;
202
+ const code = await signalDef.subcommands!.status!.run(badCtx);
203
+ expect(code).toBe(1);
204
+ expect(writeErr.join('')).toContain('vault unavailable');
205
+ });
206
+ });