@tapicker/bridge 1.0.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/LICENSE +21 -0
- package/README.md +46 -0
- package/dist/bridge/bridge-service.d.ts +46 -0
- package/dist/bridge/bridge-service.js +245 -0
- package/dist/bridge/control.d.ts +7 -0
- package/dist/bridge/control.js +211 -0
- package/dist/bridge/daemon.d.ts +16 -0
- package/dist/bridge/daemon.js +97 -0
- package/dist/bridge/extension-session.d.ts +43 -0
- package/dist/bridge/extension-session.js +204 -0
- package/dist/bridge/pairing-state.d.ts +22 -0
- package/dist/bridge/pairing-state.js +76 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +835 -0
- package/dist/commands.d.ts +33 -0
- package/dist/commands.js +123 -0
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +10 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.js +52 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +8 -0
- package/dist/mcp.d.ts +13 -0
- package/dist/mcp.js +300 -0
- package/dist/protocol/schema.d.ts +149 -0
- package/dist/protocol/schema.js +155 -0
- package/dist/server.d.ts +63 -0
- package/dist/server.js +486 -0
- package/dist/workflow-watch.d.ts +31 -0
- package/dist/workflow-watch.js +304 -0
- package/docs/cli.md +112 -0
- package/docs/http-api.md +149 -0
- package/docs/mcp.md +90 -0
- package/docs/operations.md +85 -0
- package/docs/workflow-watch.md +86 -0
- package/package.json +55 -0
- package/src/bridge/bridge-service.ts +291 -0
- package/src/bridge/control.ts +281 -0
- package/src/bridge/daemon.ts +102 -0
- package/src/bridge/extension-session.ts +286 -0
- package/src/bridge/pairing-state.ts +92 -0
- package/src/cli.ts +1019 -0
- package/src/commands.ts +158 -0
- package/src/constants.ts +10 -0
- package/src/errors.ts +72 -0
- package/src/index.ts +35 -0
- package/src/mcp.ts +386 -0
- package/src/protocol/schema.ts +202 -0
- package/src/server.ts +623 -0
- package/src/workflow-watch.ts +364 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, readFile, rename, unlink } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { BridgeError } from '../errors.js';
|
|
6
|
+
const BridgeDaemonStateSchema = z
|
|
7
|
+
.object({
|
|
8
|
+
pid: z.number().int().positive(),
|
|
9
|
+
bridgeId: z.uuid(),
|
|
10
|
+
httpUrl: z.url(),
|
|
11
|
+
startedAt: z.number().int().positive(),
|
|
12
|
+
})
|
|
13
|
+
.strict();
|
|
14
|
+
export function getBridgeDaemonStatePath(dataDir) {
|
|
15
|
+
return join(dataDir, 'bridge.pid');
|
|
16
|
+
}
|
|
17
|
+
export function getBridgeDaemonLogPath(dataDir) {
|
|
18
|
+
return join(dataDir, 'bridge.log');
|
|
19
|
+
}
|
|
20
|
+
export async function readBridgeDaemonState(dataDir) {
|
|
21
|
+
const filePath = getBridgeDaemonStatePath(dataDir);
|
|
22
|
+
let content;
|
|
23
|
+
try {
|
|
24
|
+
content = await readFile(filePath, 'utf8');
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error.code === 'ENOENT')
|
|
28
|
+
return undefined;
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
let value;
|
|
32
|
+
try {
|
|
33
|
+
value = JSON.parse(content);
|
|
34
|
+
}
|
|
35
|
+
catch (cause) {
|
|
36
|
+
throw new BridgeError('InvalidDaemonState', 'Saved Bridge process state is not valid JSON', {
|
|
37
|
+
status: 500,
|
|
38
|
+
cause,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const parsed = BridgeDaemonStateSchema.safeParse(value);
|
|
42
|
+
if (!parsed.success) {
|
|
43
|
+
throw new BridgeError('InvalidDaemonState', 'Saved Bridge process state has an unsupported format', {
|
|
44
|
+
status: 500,
|
|
45
|
+
details: parsed.error.issues,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return parsed.data;
|
|
49
|
+
}
|
|
50
|
+
export async function writeBridgeDaemonState(dataDir, state) {
|
|
51
|
+
const filePath = getBridgeDaemonStatePath(dataDir);
|
|
52
|
+
const directory = dirname(filePath);
|
|
53
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
54
|
+
const temporaryPath = join(directory, `.bridge-${process.pid}-${randomUUID()}.tmp`);
|
|
55
|
+
const content = JSON.stringify(state, null, 2);
|
|
56
|
+
let file;
|
|
57
|
+
try {
|
|
58
|
+
file = await open(temporaryPath, 'wx', 0o600);
|
|
59
|
+
await file.writeFile(content, 'utf8');
|
|
60
|
+
await file.sync();
|
|
61
|
+
await file.close();
|
|
62
|
+
file = undefined;
|
|
63
|
+
await rename(temporaryPath, filePath);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
await file?.close().catch(() => undefined);
|
|
67
|
+
await unlink(temporaryPath).catch(() => undefined);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export async function removeBridgeDaemonState(dataDir, expectedPid) {
|
|
72
|
+
const state = await readBridgeDaemonState(dataDir);
|
|
73
|
+
if (state?.pid !== expectedPid)
|
|
74
|
+
return;
|
|
75
|
+
try {
|
|
76
|
+
await unlink(getBridgeDaemonStatePath(dataDir));
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error.code !== 'ENOENT')
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export function isProcessRunning(pid) {
|
|
84
|
+
try {
|
|
85
|
+
process.kill(pid, 0);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
const code = error.code;
|
|
90
|
+
if (code === 'ESRCH')
|
|
91
|
+
return false;
|
|
92
|
+
if (code === 'EPERM')
|
|
93
|
+
return true;
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=daemon.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import type { WebSocket } from 'ws';
|
|
3
|
+
import type { ExtensionPairInput } from '../protocol/schema.js';
|
|
4
|
+
export type ExtensionSessionStatus = {
|
|
5
|
+
connected: boolean;
|
|
6
|
+
ready: boolean;
|
|
7
|
+
pairingRequired?: boolean;
|
|
8
|
+
clientId?: string;
|
|
9
|
+
clientName?: string;
|
|
10
|
+
capabilities: string[];
|
|
11
|
+
};
|
|
12
|
+
export type ExtensionSessionOptions = {
|
|
13
|
+
bridgeVersion: string;
|
|
14
|
+
protocolVersion: number;
|
|
15
|
+
capabilities: string[];
|
|
16
|
+
remoteAddress: string;
|
|
17
|
+
verifyCredential: (clientId: string, credential: string) => boolean;
|
|
18
|
+
pair: (input: ExtensionPairInput, remoteAddress: string) => Promise<string>;
|
|
19
|
+
};
|
|
20
|
+
export declare class ExtensionSession extends EventEmitter {
|
|
21
|
+
private readonly socket;
|
|
22
|
+
private readonly options;
|
|
23
|
+
private ready;
|
|
24
|
+
private pairingRequired;
|
|
25
|
+
private clientId;
|
|
26
|
+
private clientName;
|
|
27
|
+
private capabilities;
|
|
28
|
+
private pendingRequests;
|
|
29
|
+
constructor(socket: WebSocket, options: ExtensionSessionOptions);
|
|
30
|
+
start(): void;
|
|
31
|
+
getStatus(): ExtensionSessionStatus;
|
|
32
|
+
request<T>(type: string, data: unknown, timeout?: number): Promise<T>;
|
|
33
|
+
close(code?: number, reason?: string): void;
|
|
34
|
+
private handleMessage;
|
|
35
|
+
private handleHello;
|
|
36
|
+
private handlePair;
|
|
37
|
+
private authorize;
|
|
38
|
+
private respond;
|
|
39
|
+
private send;
|
|
40
|
+
private rejectPending;
|
|
41
|
+
private reportError;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=extension-session.d.ts.map
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { DEFAULT_REQUEST_TIMEOUT } from '../constants.js';
|
|
3
|
+
import { BridgeError, safeError, safeSuccess } from '../errors.js';
|
|
4
|
+
import { BridgeHelloDataSchema, BridgePairDataSchema, ExtensionHelloInputSchema, ExtensionPairInputSchema, SafeResultSchema, createEnvelope, parseEnvelope, } from '../protocol/schema.js';
|
|
5
|
+
export class ExtensionSession extends EventEmitter {
|
|
6
|
+
socket;
|
|
7
|
+
options;
|
|
8
|
+
ready = false;
|
|
9
|
+
pairingRequired = false;
|
|
10
|
+
clientId;
|
|
11
|
+
clientName;
|
|
12
|
+
capabilities = [];
|
|
13
|
+
pendingRequests = new Map();
|
|
14
|
+
constructor(socket, options) {
|
|
15
|
+
super();
|
|
16
|
+
this.socket = socket;
|
|
17
|
+
this.options = options;
|
|
18
|
+
}
|
|
19
|
+
start() {
|
|
20
|
+
this.socket.on('message', (data) => void this.handleMessage(data).catch((error) => this.reportError(error)));
|
|
21
|
+
this.socket.on('error', (error) => this.reportError(error));
|
|
22
|
+
this.socket.on('close', (code, reason) => {
|
|
23
|
+
this.rejectPending(new BridgeError('ExtensionDisconnected', 'Tapicker Extension disconnected', { status: 503 }));
|
|
24
|
+
this.emit('close', { code, reason: reason.toString() });
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
getStatus() {
|
|
28
|
+
return {
|
|
29
|
+
connected: this.socket.readyState === this.socket.OPEN,
|
|
30
|
+
ready: this.ready,
|
|
31
|
+
pairingRequired: this.pairingRequired,
|
|
32
|
+
clientId: this.clientId,
|
|
33
|
+
clientName: this.clientName,
|
|
34
|
+
capabilities: this.ready ? this.capabilities : [],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
request(type, data, timeout = DEFAULT_REQUEST_TIMEOUT) {
|
|
38
|
+
if (!this.ready || this.socket.readyState !== this.socket.OPEN) {
|
|
39
|
+
return Promise.reject(new BridgeError('ExtensionUnavailable', 'No paired Tapicker Extension is connected', { status: 503 }));
|
|
40
|
+
}
|
|
41
|
+
const envelope = createEnvelope(type, data);
|
|
42
|
+
const responseType = `${type}:res`;
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const timer = setTimeout(() => {
|
|
45
|
+
this.pendingRequests.delete(envelope.id);
|
|
46
|
+
reject(new BridgeError('ExtensionRequestTimeout', `Extension did not respond to ${type}`, {
|
|
47
|
+
status: 504,
|
|
48
|
+
details: { timeout },
|
|
49
|
+
}));
|
|
50
|
+
}, timeout);
|
|
51
|
+
this.pendingRequests.set(envelope.id, {
|
|
52
|
+
type: responseType,
|
|
53
|
+
timer,
|
|
54
|
+
resolve: (value) => resolve(value),
|
|
55
|
+
reject,
|
|
56
|
+
});
|
|
57
|
+
try {
|
|
58
|
+
this.send(envelope);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
this.pendingRequests.delete(envelope.id);
|
|
63
|
+
reject(error);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
close(code = 1000, reason = 'Bridge session closed') {
|
|
68
|
+
if (this.socket.readyState === this.socket.OPEN || this.socket.readyState === this.socket.CONNECTING) {
|
|
69
|
+
this.socket.close(code, reason);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async handleMessage(raw) {
|
|
73
|
+
const parsed = parseEnvelope(raw);
|
|
74
|
+
if (!parsed.success) {
|
|
75
|
+
this.reportError(parsed.error);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const message = parsed.data;
|
|
79
|
+
if (message.type === 'pong')
|
|
80
|
+
return;
|
|
81
|
+
const pending = this.pendingRequests.get(message.id);
|
|
82
|
+
if (pending && pending.type === message.type) {
|
|
83
|
+
this.pendingRequests.delete(message.id);
|
|
84
|
+
clearTimeout(pending.timer);
|
|
85
|
+
const result = SafeResultSchema.safeParse(message.data);
|
|
86
|
+
if (!result.success) {
|
|
87
|
+
pending.reject(new BridgeError('InvalidExtensionResponse', 'Extension returned an invalid response', { status: 502 }));
|
|
88
|
+
}
|
|
89
|
+
else if (result.data.ok) {
|
|
90
|
+
pending.resolve(result.data.data);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
pending.reject(new BridgeError(result.data.error.code, result.data.error.message, {
|
|
94
|
+
status: 502,
|
|
95
|
+
details: result.data.error.details,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (message.type === 'ping') {
|
|
101
|
+
this.respond(message, 'pong', undefined);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (message.type === 'extension.hello') {
|
|
105
|
+
await this.handleHello(message);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (message.type === 'extension.pair') {
|
|
109
|
+
await this.handlePair(message);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
this.emit('message', message);
|
|
113
|
+
}
|
|
114
|
+
async handleHello(message) {
|
|
115
|
+
const parsed = ExtensionHelloInputSchema.safeParse(message.data);
|
|
116
|
+
if (!parsed.success) {
|
|
117
|
+
this.respond(message, `${message.type}:res`, safeError(new BridgeError('InvalidHello', 'Invalid Extension hello', {
|
|
118
|
+
status: 400,
|
|
119
|
+
details: parsed.error.issues,
|
|
120
|
+
})));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const hello = parsed.data;
|
|
124
|
+
this.clientId = hello.clientId;
|
|
125
|
+
this.clientName = hello.clientName;
|
|
126
|
+
this.capabilities = hello.capabilities;
|
|
127
|
+
const accepted = Boolean(hello.credential && this.options.verifyCredential(hello.clientId, hello.credential));
|
|
128
|
+
this.pairingRequired = !accepted;
|
|
129
|
+
const response = BridgeHelloDataSchema.parse({
|
|
130
|
+
accepted,
|
|
131
|
+
requiresPairing: !accepted,
|
|
132
|
+
bridgeVersion: this.options.bridgeVersion,
|
|
133
|
+
protocolVersion: this.options.protocolVersion,
|
|
134
|
+
capabilities: this.options.capabilities,
|
|
135
|
+
});
|
|
136
|
+
this.respond(message, `${message.type}:res`, safeSuccess(response));
|
|
137
|
+
if (accepted)
|
|
138
|
+
this.authorize(hello);
|
|
139
|
+
}
|
|
140
|
+
async handlePair(message) {
|
|
141
|
+
const parsed = ExtensionPairInputSchema.safeParse(message.data);
|
|
142
|
+
if (!parsed.success) {
|
|
143
|
+
this.respond(message, `${message.type}:res`, safeError(new BridgeError('InvalidPairingRequest', 'Invalid pairing request', {
|
|
144
|
+
status: 400,
|
|
145
|
+
details: parsed.error.issues,
|
|
146
|
+
})));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const input = parsed.data;
|
|
150
|
+
if (!this.clientId || input.clientId !== this.clientId || input.clientName !== this.clientName) {
|
|
151
|
+
this.respond(message, `${message.type}:res`, safeError(new BridgeError('PairingSessionMismatch', 'Pairing request does not match the Extension hello', {
|
|
152
|
+
status: 400,
|
|
153
|
+
})));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const credential = await this.options.pair(input, this.options.remoteAddress);
|
|
158
|
+
const response = BridgePairDataSchema.parse({
|
|
159
|
+
accepted: true,
|
|
160
|
+
clientId: input.clientId,
|
|
161
|
+
credential,
|
|
162
|
+
});
|
|
163
|
+
this.respond(message, `${message.type}:res`, safeSuccess(response));
|
|
164
|
+
this.authorize({ ...input, capabilities: this.capabilities });
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
this.respond(message, `${message.type}:res`, safeError(error));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
authorize(hello) {
|
|
171
|
+
this.ready = true;
|
|
172
|
+
this.pairingRequired = false;
|
|
173
|
+
this.clientId = hello.clientId;
|
|
174
|
+
this.clientName = hello.clientName;
|
|
175
|
+
this.capabilities = hello.capabilities;
|
|
176
|
+
this.emit('ready', {
|
|
177
|
+
clientId: this.clientId,
|
|
178
|
+
clientName: this.clientName,
|
|
179
|
+
capabilities: this.capabilities,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
respond(request, type, data) {
|
|
183
|
+
this.send(createEnvelope(type, data, { id: request.id, callback: false }));
|
|
184
|
+
}
|
|
185
|
+
send(envelope) {
|
|
186
|
+
if (this.socket.readyState !== this.socket.OPEN) {
|
|
187
|
+
throw new BridgeError('ExtensionUnavailable', 'Tapicker Extension WebSocket is not open', { status: 503 });
|
|
188
|
+
}
|
|
189
|
+
this.socket.send(JSON.stringify(envelope));
|
|
190
|
+
}
|
|
191
|
+
rejectPending(error) {
|
|
192
|
+
for (const pending of this.pendingRequests.values()) {
|
|
193
|
+
clearTimeout(pending.timer);
|
|
194
|
+
pending.reject(error);
|
|
195
|
+
}
|
|
196
|
+
this.pendingRequests.clear();
|
|
197
|
+
}
|
|
198
|
+
reportError(error) {
|
|
199
|
+
if (this.listenerCount('error') > 0) {
|
|
200
|
+
this.emit('error', error);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=extension-session.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
declare const PairingStateSchema: z.ZodObject<{
|
|
3
|
+
version: z.ZodLiteral<1>;
|
|
4
|
+
clients: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
5
|
+
clientId: z.ZodUUID;
|
|
6
|
+
credential: z.ZodUUID;
|
|
7
|
+
}, z.core.$strict>>>;
|
|
8
|
+
}, z.core.$strict>;
|
|
9
|
+
export type PairedExtensionIdentity = z.infer<typeof PairingStateSchema>['clients'][number];
|
|
10
|
+
export interface PairingStatePersistence {
|
|
11
|
+
load(): Promise<PairedExtensionIdentity[]>;
|
|
12
|
+
save(identities: PairedExtensionIdentity[]): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export declare class PairingStateFile implements PairingStatePersistence {
|
|
15
|
+
private readonly filePath;
|
|
16
|
+
constructor(filePath: string);
|
|
17
|
+
load(): Promise<PairedExtensionIdentity[]>;
|
|
18
|
+
save(identities: PairedExtensionIdentity[]): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export declare function getDefaultBridgeDataDir(home?: string): string;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=pairing-state.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, readFile, rename, unlink } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { BridgeError } from '../errors.js';
|
|
7
|
+
const PairingStateSchema = z
|
|
8
|
+
.object({
|
|
9
|
+
version: z.literal(1),
|
|
10
|
+
clients: z
|
|
11
|
+
.array(z
|
|
12
|
+
.object({
|
|
13
|
+
clientId: z.uuid(),
|
|
14
|
+
credential: z.uuid(),
|
|
15
|
+
})
|
|
16
|
+
.strict())
|
|
17
|
+
.default([]),
|
|
18
|
+
})
|
|
19
|
+
.strict();
|
|
20
|
+
export class PairingStateFile {
|
|
21
|
+
filePath;
|
|
22
|
+
constructor(filePath) {
|
|
23
|
+
this.filePath = filePath;
|
|
24
|
+
}
|
|
25
|
+
async load() {
|
|
26
|
+
await mkdir(dirname(this.filePath), { recursive: true, mode: 0o700 });
|
|
27
|
+
let content;
|
|
28
|
+
try {
|
|
29
|
+
content = await readFile(this.filePath, 'utf8');
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error.code === 'ENOENT')
|
|
33
|
+
return [];
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
let value;
|
|
37
|
+
try {
|
|
38
|
+
value = JSON.parse(content);
|
|
39
|
+
}
|
|
40
|
+
catch (cause) {
|
|
41
|
+
throw new BridgeError('InvalidPairingState', 'Saved Bridge pairing state is not valid JSON', {
|
|
42
|
+
status: 500,
|
|
43
|
+
cause,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const parsed = PairingStateSchema.safeParse(value);
|
|
47
|
+
if (!parsed.success) {
|
|
48
|
+
throw new BridgeError('InvalidPairingState', `Saved Bridge pairing state at ${this.filePath} has an unsupported format. This pre-release version does not migrate old state automatically; update it to the version 1 clients-array format or remove it and pair Extensions again.`, { status: 500, details: parsed.error.issues });
|
|
49
|
+
}
|
|
50
|
+
return parsed.data.clients;
|
|
51
|
+
}
|
|
52
|
+
async save(identities) {
|
|
53
|
+
const directory = dirname(this.filePath);
|
|
54
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
55
|
+
const temporaryPath = join(directory, `.paired-clients-${process.pid}-${randomUUID()}.tmp`);
|
|
56
|
+
const content = JSON.stringify({ version: 1, clients: identities }, null, 2);
|
|
57
|
+
let file;
|
|
58
|
+
try {
|
|
59
|
+
file = await open(temporaryPath, 'wx', 0o600);
|
|
60
|
+
await file.writeFile(content, 'utf8');
|
|
61
|
+
await file.sync();
|
|
62
|
+
await file.close();
|
|
63
|
+
file = undefined;
|
|
64
|
+
await rename(temporaryPath, this.filePath);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
await file?.close().catch(() => undefined);
|
|
68
|
+
await unlink(temporaryPath).catch(() => undefined);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function getDefaultBridgeDataDir(home = homedir()) {
|
|
74
|
+
return join(home, '.tapicker');
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=pairing-state.js.map
|
package/dist/cli.d.ts
ADDED