@cabane/companion 0.5.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/README.md +218 -0
- package/dist/cli.js +9936 -0
- package/dist/pairing-config.js +410 -0
- package/dist/runtime.js +8928 -0
- package/dist/static/app.js +585 -0
- package/dist/static/index.html +116 -0
- package/dist/static/styles.css +537 -0
- package/package.json +54 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
renameSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
existsSync
|
|
10
|
+
} from "fs";
|
|
11
|
+
import { homedir, userInfo } from "os";
|
|
12
|
+
import { dirname, join } from "path";
|
|
13
|
+
import { z as z3 } from "zod";
|
|
14
|
+
|
|
15
|
+
// src/errors.ts
|
|
16
|
+
var BridgeError = class extends Error {
|
|
17
|
+
constructor(message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "BridgeError";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var ApiError = class extends BridgeError {
|
|
23
|
+
constructor(status, message, body) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.status = status;
|
|
26
|
+
this.body = body;
|
|
27
|
+
this.name = "ApiError";
|
|
28
|
+
}
|
|
29
|
+
status;
|
|
30
|
+
body;
|
|
31
|
+
};
|
|
32
|
+
var ConfigError = class extends BridgeError {
|
|
33
|
+
constructor(message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "ConfigError";
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/pairing.ts
|
|
40
|
+
import { z } from "zod";
|
|
41
|
+
var PAIRING_VERSION = 1;
|
|
42
|
+
var DEVICE_TOKEN_PREFIX = "cabdev_";
|
|
43
|
+
function isAllowedBaseUrl(raw) {
|
|
44
|
+
let url;
|
|
45
|
+
try {
|
|
46
|
+
url = new URL(raw);
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (url.protocol === "https:") return true;
|
|
51
|
+
if (url.protocol === "http:") {
|
|
52
|
+
const host = url.hostname.toLowerCase();
|
|
53
|
+
return host === "localhost" || host.endsWith(".localhost") || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
var pairingSchema = z.object({
|
|
58
|
+
// Bumped if the wire shape changes incompatibly. We only accept v1.
|
|
59
|
+
v: z.literal(PAIRING_VERSION),
|
|
60
|
+
baseUrl: z.string().url().refine(isAllowedBaseUrl, {
|
|
61
|
+
message: "baseUrl must be https (loopback http is allowed for local dev only)"
|
|
62
|
+
}),
|
|
63
|
+
// The `cabdev_` device token plaintext — the bridge's one durable credential.
|
|
64
|
+
deviceToken: z.string().min(1).startsWith(DEVICE_TOKEN_PREFIX, {
|
|
65
|
+
message: 'deviceToken must be a cabane device token (starts with "cabdev_")'
|
|
66
|
+
}),
|
|
67
|
+
// Optional identity hints the app may include for nicer local display. The
|
|
68
|
+
// bridge also learns these from the first assignments pull, so they're not
|
|
69
|
+
// required.
|
|
70
|
+
deviceId: z.string().min(1).optional(),
|
|
71
|
+
deviceLabel: z.string().min(1).optional()
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// src/prepare-hook.ts
|
|
75
|
+
import { spawn } from "child_process";
|
|
76
|
+
import { z as z2 } from "zod";
|
|
77
|
+
var prepareHookSchema = z2.object({
|
|
78
|
+
command: z2.string().min(1),
|
|
79
|
+
args: z2.array(z2.string()).optional(),
|
|
80
|
+
// Extra env handed to the hook process itself (merged over process.env).
|
|
81
|
+
env: z2.record(z2.string(), z2.string()).optional(),
|
|
82
|
+
// Wall-clock cap for the hook. Provisioning is slow (minutes), so the
|
|
83
|
+
// default is generous; a hook that hangs past this is killed and the turn
|
|
84
|
+
// fails with a clear timeout message rather than pinning the bridge.
|
|
85
|
+
timeoutMs: z2.number().int().positive().optional()
|
|
86
|
+
}).strict();
|
|
87
|
+
var DEFAULT_TIMEOUT_MS = 10 * 6e4;
|
|
88
|
+
var prepareResultSchema = z2.object({
|
|
89
|
+
cwd: z2.string().min(1),
|
|
90
|
+
env: z2.record(z2.string(), z2.string()).optional()
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// src/config.ts
|
|
94
|
+
var realHomeCache;
|
|
95
|
+
function realAccountHome() {
|
|
96
|
+
if (realHomeCache === void 0) {
|
|
97
|
+
try {
|
|
98
|
+
realHomeCache = userInfo().homedir;
|
|
99
|
+
} catch {
|
|
100
|
+
realHomeCache = null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return realHomeCache;
|
|
104
|
+
}
|
|
105
|
+
function cabaneDir() {
|
|
106
|
+
const dir = join(process.env.CABANE_BRIDGE_HOME || homedir(), ".cabane");
|
|
107
|
+
if (process.env.VITEST) {
|
|
108
|
+
const real = realAccountHome();
|
|
109
|
+
if (real && dir === join(real, ".cabane")) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`cabaneDir() resolved to the real ${dir} during a test run. Bridge tests must swap process.env.HOME to a tmp dir before touching the config dir; this guard prevents wiping the operator's real bridge config (see apps/bridge/test/setup-home.ts).`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return dir;
|
|
116
|
+
}
|
|
117
|
+
function configPath() {
|
|
118
|
+
return join(cabaneDir(), "config.json");
|
|
119
|
+
}
|
|
120
|
+
var localAgentConfigSchema = z3.object({
|
|
121
|
+
cwd: z3.string().optional(),
|
|
122
|
+
prepareHook: prepareHookSchema.optional(),
|
|
123
|
+
// CT289: the Claude Code auto-memory escape hatch. Auto-memory is forced OFF
|
|
124
|
+
// by default on every bridge (memory belongs in the Cabane workspace, and the
|
|
125
|
+
// house bridge would otherwise share one cwd-keyed memory dir across users).
|
|
126
|
+
// On a bridge you run yourself, set `claudeCode: { autoMemory: true }` to hand
|
|
127
|
+
// auto-memory back to your own `~/.claude/settings.json` — Cabane then stops
|
|
128
|
+
// injecting the off switch and your normal Claude Code memory workflow applies
|
|
129
|
+
// (in coding mode, where the checkout's project settings are read).
|
|
130
|
+
claudeCode: z3.object({ autoMemory: z3.boolean().optional() }).strict().optional()
|
|
131
|
+
}).strict();
|
|
132
|
+
var bridgeConfigSchema = z3.object({
|
|
133
|
+
// The cabane instance this device is paired with. SJ515: https-enforced
|
|
134
|
+
// (loopback exempt) so a hand-edited config can't smuggle a plaintext-http
|
|
135
|
+
// base URL onto the MITM-able channel the device token + prompt ride.
|
|
136
|
+
baseUrl: z3.string().url().refine(isAllowedBaseUrl, {
|
|
137
|
+
message: "baseUrl must be https (loopback http is allowed for local dev only)"
|
|
138
|
+
}),
|
|
139
|
+
// The `cabdev_` device token plaintext — the bridge's one durable credential,
|
|
140
|
+
// presented as `Authorization: Bearer <token>` on the device-facing pull +
|
|
141
|
+
// heartbeat endpoints. Optional so `cabane-companion logout` can strip it (a
|
|
142
|
+
// "paired but logged out" state the supervisor refuses to run) while keeping
|
|
143
|
+
// the rest of the config; `pair` always writes one.
|
|
144
|
+
deviceToken: z3.string().optional(),
|
|
145
|
+
// Identity hints, learned from the pairing string and refreshed on the first
|
|
146
|
+
// assignments pull. Cosmetic — used for `status`/dashboard display only.
|
|
147
|
+
deviceId: z3.string().optional(),
|
|
148
|
+
deviceLabel: z3.string().optional(),
|
|
149
|
+
// Optional per-agent machine-local overrides (cwd / prepareHook), keyed by
|
|
150
|
+
// agentId / username / `slug/username`. Hand-added by the operator; the bridge
|
|
151
|
+
// never writes this (it only persists credentials + the device, elsewhere).
|
|
152
|
+
agents: z3.record(z3.string(), localAgentConfigSchema).optional(),
|
|
153
|
+
// Dashboard settings (all optional). dashboardPort: preferred bind port (next
|
|
154
|
+
// free one if taken); autoOpen: whether `start` opens the browser (the
|
|
155
|
+
// `--no-open` flag / `BRIDGE_NO_OPEN=1` override per-run); logLevel: pino
|
|
156
|
+
// level, live-editable from the dashboard settings panel.
|
|
157
|
+
dashboardPort: z3.number().int().min(1).max(65535).optional(),
|
|
158
|
+
autoOpen: z3.boolean().optional(),
|
|
159
|
+
logLevel: z3.enum(["warn", "info", "debug"]).optional(),
|
|
160
|
+
// CT270: the opencode runtime, when the operator runs one on this machine. The
|
|
161
|
+
// operator installs opencode, starts `opencode serve` (auth via opencode's own
|
|
162
|
+
// `/connect` — Cabane never sees provider keys), and points the bridge at it
|
|
163
|
+
// here. Setting this makes the device advertise the `opencode` runtime on its
|
|
164
|
+
// heartbeat manifest (so the server offers DeepSeek/opencode models here and
|
|
165
|
+
// routes those turns to this device) AND registers the opencode adapter in the
|
|
166
|
+
// dispatcher. Absent → the device is claude-code-only, exactly as before.
|
|
167
|
+
opencode: z3.object({
|
|
168
|
+
serverUrl: z3.string().url()
|
|
169
|
+
}).strict().optional(),
|
|
170
|
+
// CT481: the codex runtime, when the operator runs Codex on this machine. Unlike
|
|
171
|
+
// opencode (a long-lived `opencode serve` addressed by URL), Codex is a local CLI
|
|
172
|
+
// the `@openai/codex-sdk` spawns per turn — so the config is just an opt-in flag,
|
|
173
|
+
// not a server address. The operator installs Codex + logs in (`codex login`, or
|
|
174
|
+
// `CODEX_API_KEY` — Cabane never sees the key) and sets `{ "codex": { "enabled":
|
|
175
|
+
// true } }`. Presence of the block enables it (an explicit `enabled: false`
|
|
176
|
+
// keeps the block but turns it off). Enabling makes the device advertise the
|
|
177
|
+
// `codex` runtime on its heartbeat manifest AND registers the codex adapter in
|
|
178
|
+
// the dispatcher. Absent → the device doesn't offer codex, exactly as before.
|
|
179
|
+
codex: z3.object({
|
|
180
|
+
enabled: z3.boolean().optional()
|
|
181
|
+
}).strict().optional()
|
|
182
|
+
});
|
|
183
|
+
function loadConfig() {
|
|
184
|
+
const path = configPath();
|
|
185
|
+
if (!existsSync(path)) return null;
|
|
186
|
+
let raw;
|
|
187
|
+
try {
|
|
188
|
+
raw = readFileSync(path, "utf8");
|
|
189
|
+
} catch (err) {
|
|
190
|
+
throw new ConfigError(
|
|
191
|
+
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
if (raw.trim().length === 0) return null;
|
|
195
|
+
let parsed;
|
|
196
|
+
try {
|
|
197
|
+
parsed = JSON.parse(raw);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
throw new ConfigError(
|
|
200
|
+
`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
const result = bridgeConfigSchema.safeParse(parsed);
|
|
204
|
+
if (!result.success) {
|
|
205
|
+
const agentIssue = result.error.issues.find((i) => i.path[0] === "agents");
|
|
206
|
+
if (agentIssue) {
|
|
207
|
+
throw new ConfigError(
|
|
208
|
+
`${path}: invalid "agents" config at \`${agentIssue.path.join(".")}\` \u2014 ${agentIssue.message}. Fix the agents block in the config and retry.`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
throw new ConfigError(
|
|
212
|
+
`${path} is from an incompatible or older version of the bridge, or was hand-edited. Run \`cabane-companion pair\` to re-pair, or \`cabane-companion logout --purge\` to reset.`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return result.data;
|
|
216
|
+
}
|
|
217
|
+
function loadConfigTolerant() {
|
|
218
|
+
const path = configPath();
|
|
219
|
+
const empty = {};
|
|
220
|
+
if (!existsSync(path)) return { local: empty, note: null };
|
|
221
|
+
let raw;
|
|
222
|
+
try {
|
|
223
|
+
raw = readFileSync(path, "utf8");
|
|
224
|
+
} catch {
|
|
225
|
+
return { local: empty, note: null };
|
|
226
|
+
}
|
|
227
|
+
if (raw.trim().length === 0) return { local: empty, note: null };
|
|
228
|
+
let parsed;
|
|
229
|
+
try {
|
|
230
|
+
parsed = JSON.parse(raw);
|
|
231
|
+
} catch {
|
|
232
|
+
return { local: empty, note: `${path} was unreadable (invalid JSON) and has been reset.` };
|
|
233
|
+
}
|
|
234
|
+
const strict = bridgeConfigSchema.safeParse(parsed);
|
|
235
|
+
if (strict.success) {
|
|
236
|
+
const { agents: agents2, dashboardPort, autoOpen, logLevel } = strict.data;
|
|
237
|
+
return {
|
|
238
|
+
local: {
|
|
239
|
+
...agents2 !== void 0 ? { agents: agents2 } : {},
|
|
240
|
+
...dashboardPort !== void 0 ? { dashboardPort } : {},
|
|
241
|
+
...autoOpen !== void 0 ? { autoOpen } : {},
|
|
242
|
+
...logLevel !== void 0 ? { logLevel } : {}
|
|
243
|
+
},
|
|
244
|
+
note: null
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const obj = parsed && typeof parsed === "object" ? parsed : {};
|
|
248
|
+
const local = {};
|
|
249
|
+
const agents = z3.record(z3.string(), localAgentConfigSchema).safeParse(obj.agents);
|
|
250
|
+
if (agents.success) local.agents = agents.data;
|
|
251
|
+
if (typeof obj.dashboardPort === "number") local.dashboardPort = obj.dashboardPort;
|
|
252
|
+
if (typeof obj.autoOpen === "boolean") local.autoOpen = obj.autoOpen;
|
|
253
|
+
if (obj.logLevel === "warn" || obj.logLevel === "info" || obj.logLevel === "debug") {
|
|
254
|
+
local.logLevel = obj.logLevel;
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
local,
|
|
258
|
+
note: `the existing ${path} was from an older or incompatible bridge; re-pairing rewrote it.`
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function saveConfig(cfg) {
|
|
262
|
+
const path = configPath();
|
|
263
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
264
|
+
try {
|
|
265
|
+
chmodSync(cabaneDir(), 448);
|
|
266
|
+
} catch {
|
|
267
|
+
}
|
|
268
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
269
|
+
try {
|
|
270
|
+
writeFileSync(tmp, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
|
|
271
|
+
try {
|
|
272
|
+
chmodSync(tmp, 384);
|
|
273
|
+
} catch {
|
|
274
|
+
}
|
|
275
|
+
renameSync(tmp, path);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
try {
|
|
278
|
+
rmSync(tmp, { force: true });
|
|
279
|
+
} catch {
|
|
280
|
+
}
|
|
281
|
+
throw err;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/enrollment.ts
|
|
286
|
+
function trimBase(baseUrl) {
|
|
287
|
+
return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
288
|
+
}
|
|
289
|
+
async function postJson(baseUrl, path, body) {
|
|
290
|
+
let res;
|
|
291
|
+
try {
|
|
292
|
+
res = await fetch(`${trimBase(baseUrl)}${path}`, {
|
|
293
|
+
method: "POST",
|
|
294
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
295
|
+
body: JSON.stringify(body)
|
|
296
|
+
});
|
|
297
|
+
} catch (err) {
|
|
298
|
+
throw new BridgeError(
|
|
299
|
+
`couldn't reach cabane at ${baseUrl}: ${err instanceof Error ? err.message : String(err)}. Check the server URL (pass --server <url>) and your connection.`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
const raw = await res.text();
|
|
303
|
+
let parsed = void 0;
|
|
304
|
+
if (raw.length > 0) {
|
|
305
|
+
try {
|
|
306
|
+
parsed = JSON.parse(raw);
|
|
307
|
+
} catch {
|
|
308
|
+
parsed = raw;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (res.status >= 400) {
|
|
312
|
+
if (res.status === 404 && path.endsWith("/code")) {
|
|
313
|
+
throw new BridgeError(
|
|
314
|
+
`this cabane server (${baseUrl}) doesn't support device-flow pairing yet. Update the server, or use \`cabane-companion pair --legacy\` with a pairing string from the app.`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
const msg = parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : `${res.status}`;
|
|
318
|
+
throw new ApiError(res.status, `${res.status} ${msg}`, parsed);
|
|
319
|
+
}
|
|
320
|
+
return parsed;
|
|
321
|
+
}
|
|
322
|
+
function requestEnrollmentCode(baseUrl) {
|
|
323
|
+
return postJson(baseUrl, "/api/bridge-enrollment/code", {});
|
|
324
|
+
}
|
|
325
|
+
function pollOnce(baseUrl, deviceCode) {
|
|
326
|
+
return postJson(baseUrl, "/api/bridge-enrollment/poll", { deviceCode });
|
|
327
|
+
}
|
|
328
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
329
|
+
async function runDeviceFlow(baseUrl, print, opts = {}) {
|
|
330
|
+
const now = opts.now ?? (() => Date.now());
|
|
331
|
+
const wait = opts.sleepMs ?? sleep;
|
|
332
|
+
if (opts.signal?.aborted) throw new BridgeError("pairing was cancelled.");
|
|
333
|
+
const code = await requestEnrollmentCode(baseUrl);
|
|
334
|
+
if (opts.onCode) await opts.onCode(code);
|
|
335
|
+
print("");
|
|
336
|
+
print(` Enter this code at ${code.verificationUri}`);
|
|
337
|
+
print("");
|
|
338
|
+
print(` ${code.userCode}`);
|
|
339
|
+
print("");
|
|
340
|
+
print(` or open: ${code.verificationUriComplete}`);
|
|
341
|
+
print("");
|
|
342
|
+
print("Waiting for you to confirm it in cabane\u2026");
|
|
343
|
+
const deadline = now() + code.expiresIn * 1e3;
|
|
344
|
+
let intervalMs = Math.max(1, code.interval) * 1e3;
|
|
345
|
+
while (now() < deadline) {
|
|
346
|
+
await wait(intervalMs);
|
|
347
|
+
if (opts.signal?.aborted) throw new BridgeError("pairing was cancelled.");
|
|
348
|
+
const res = await pollOnce(baseUrl, code.deviceCode);
|
|
349
|
+
if (res.status === "complete") {
|
|
350
|
+
if (!res.deviceToken || !res.baseUrl) {
|
|
351
|
+
throw new BridgeError("the server reported the pairing complete but returned no token.");
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
baseUrl: res.baseUrl,
|
|
355
|
+
deviceToken: res.deviceToken,
|
|
356
|
+
...res.deviceId ? { deviceId: res.deviceId } : {},
|
|
357
|
+
...res.deviceLabel ? { deviceLabel: res.deviceLabel } : {}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
if (res.status === "expired") {
|
|
361
|
+
throw new BridgeError(
|
|
362
|
+
"this pairing code expired before it was confirmed. Run `cabane-companion pair` again for a fresh one."
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
if (res.status === "slow_down") intervalMs += 1e3;
|
|
366
|
+
}
|
|
367
|
+
throw new BridgeError(
|
|
368
|
+
"this pairing code expired before it was confirmed. Run `cabane-companion pair` again for a fresh one."
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/pairing-config.ts
|
|
373
|
+
var DEFAULT_BASE_URL = "https://cabane.ai";
|
|
374
|
+
function resolvePairBaseUrl(server) {
|
|
375
|
+
const raw = (server ?? process.env.CABANE_BASE_URL ?? DEFAULT_BASE_URL).trim();
|
|
376
|
+
if (!isAllowedBaseUrl(raw)) {
|
|
377
|
+
throw new BridgeError(
|
|
378
|
+
`invalid server URL "${raw}": must be an https URL (loopback http is allowed for local dev). Pass it as \`cabane-companion pair --server https://your-cabane\`.`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
return raw;
|
|
382
|
+
}
|
|
383
|
+
function writePairedConfig(paired) {
|
|
384
|
+
const { local, note } = loadConfigTolerant();
|
|
385
|
+
const config = {
|
|
386
|
+
...local,
|
|
387
|
+
baseUrl: paired.baseUrl,
|
|
388
|
+
deviceToken: paired.deviceToken,
|
|
389
|
+
...paired.deviceId ? { deviceId: paired.deviceId } : {},
|
|
390
|
+
...paired.deviceLabel ? { deviceLabel: paired.deviceLabel } : {}
|
|
391
|
+
};
|
|
392
|
+
saveConfig(config);
|
|
393
|
+
return { config, note };
|
|
394
|
+
}
|
|
395
|
+
function isDevicePaired() {
|
|
396
|
+
try {
|
|
397
|
+
return !!loadConfig()?.deviceToken;
|
|
398
|
+
} catch {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
export {
|
|
403
|
+
ApiError,
|
|
404
|
+
BridgeError,
|
|
405
|
+
isDevicePaired,
|
|
406
|
+
requestEnrollmentCode,
|
|
407
|
+
resolvePairBaseUrl,
|
|
408
|
+
runDeviceFlow,
|
|
409
|
+
writePairedConfig
|
|
410
|
+
};
|