@skrr-ai/cli 0.1.83 → 0.1.84

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 (48) hide show
  1. package/README.md +2 -2
  2. package/dist/base-command.js +2 -2
  3. package/dist/commands/agents/endpoint/connect.d.ts +24 -0
  4. package/dist/commands/agents/endpoint/connect.js +93 -0
  5. package/dist/commands/agents/endpoint/disable.d.ts +18 -0
  6. package/dist/commands/agents/endpoint/disable.js +44 -0
  7. package/dist/commands/agents/endpoint/enable.d.ts +18 -0
  8. package/dist/commands/agents/endpoint/enable.js +46 -0
  9. package/dist/commands/agents/endpoint/show.d.ts +16 -0
  10. package/dist/commands/agents/endpoint/show.js +45 -0
  11. package/dist/commands/harnesses/install-token.d.ts +4 -4
  12. package/dist/commands/harnesses/install-token.js +4 -4
  13. package/dist/commands/store/acquisitions/list.d.ts +18 -0
  14. package/dist/commands/store/acquisitions/list.js +58 -0
  15. package/dist/commands/store/governance/requests.js +6 -0
  16. package/dist/commands/store/maintainers/remove.d.ts +8 -1
  17. package/dist/commands/store/maintainers/remove.js +29 -4
  18. package/dist/commands/store/reports/confirm-ban.d.ts +19 -0
  19. package/dist/commands/store/reports/confirm-ban.js +57 -0
  20. package/dist/commands/store/reports/disposition.d.ts +21 -0
  21. package/dist/commands/store/reports/disposition.js +76 -0
  22. package/dist/commands/store/reports/list.d.ts +17 -0
  23. package/dist/commands/store/reports/list.js +79 -0
  24. package/dist/commands/store/reports/withdraw-ban.d.ts +18 -0
  25. package/dist/commands/store/reports/withdraw-ban.js +48 -0
  26. package/dist/commands/store/updates.js +9 -5
  27. package/dist/commands/workspaces/audit.d.ts +30 -0
  28. package/dist/commands/workspaces/audit.js +145 -0
  29. package/dist/lib/agent-endpoints.d.ts +62 -0
  30. package/dist/lib/agent-endpoints.js +64 -0
  31. package/dist/lib/auth-storage.d.ts +1 -1
  32. package/dist/lib/auth-storage.js +1 -1
  33. package/dist/lib/daemonBroker.d.ts +1 -1
  34. package/dist/lib/daemonBroker.js +3 -4
  35. package/dist/lib/harness-context.js +1 -5
  36. package/dist/lib/store-acquisition.d.ts +2 -0
  37. package/dist/lib/store-acquisition.js +18 -1
  38. package/dist/lib/store-moderation.d.ts +95 -0
  39. package/dist/lib/store-moderation.js +51 -0
  40. package/dist/lib/workspaces.d.ts +73 -0
  41. package/dist/lib/workspaces.js +21 -0
  42. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonPair.js +1 -1
  43. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonPair.js +1 -1
  44. package/dist/node_modules/@skrr-ai/data-provider/index.js +4324 -4294
  45. package/oclif.manifest.json +22000 -21244
  46. package/package.json +4 -3
  47. package/dist/commands/pair.d.ts +0 -67
  48. package/dist/commands/pair.js +0 -351
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skrr-ai/cli",
3
- "version": "0.1.83",
3
+ "version": "0.1.84",
4
4
  "description": "Command-line interface for skrr",
5
5
  "license": "ISC",
6
6
  "author": "skrr",
@@ -53,7 +53,6 @@
53
53
  "@oclif/plugin-warn-if-update-available": "^3.1.0",
54
54
  "@skrr-ai/auth-core": "^0.4.12",
55
55
  "proper-lockfile": "^4.1.2",
56
- "qrcode-terminal": "^0.12.0",
57
56
  "socket.io-client": "^4.8.3",
58
57
  "tweetnacl": "^1.0.3"
59
58
  },
@@ -61,7 +60,6 @@
61
60
  "@oclif/plugin-legacy": "^2.0.37",
62
61
  "@types/node": "^20.11.0",
63
62
  "@types/proper-lockfile": "^4.1.4",
64
- "@types/qrcode-terminal": "^0.12.2",
65
63
  "esbuild": "^0.25.12",
66
64
  "oclif": "^4.23.29",
67
65
  "rimraf": "^5.0.5",
@@ -157,6 +155,9 @@
157
155
  "agents:goals": {
158
156
  "description": "List the goals an agent is on the hook for (agent-scoped facade over `goals`)"
159
157
  },
158
+ "agents:endpoint": {
159
+ "description": "Your agent as a hosted MCP endpoint: show its URL and payer, switch it on for a Store copy, and mint an agents:invoke key"
160
+ },
160
161
  "agents:tone": {
161
162
  "description": "Read and set how an agent WRITES (its tone; audio timbre is `voiceId`)"
162
163
  },
@@ -1,67 +0,0 @@
1
- import { BaseCommand } from '../base-command';
2
- interface DecryptedPlaintext {
3
- token: string;
4
- refreshToken: string;
5
- daemonId: string | null;
6
- dataEncryptionKey: string | null;
7
- }
8
- /**
9
- * Canonical AAD bytes for the pair-response (C4). Mobile builds the
10
- * same string, ASCII-encodes, prepends a 2-byte big-endian length, and
11
- * appends to the ciphertext bundle. The CLI rebuilds locally and
12
- * compares before decrypting — so a tampered or misdirected ciphertext
13
- * fails AAD before tweetnacl ever sees it.
14
- *
15
- * Format: `oversky:pair:v1:daemonId=<daemonId>:responseUserId=<responseUserId>`
16
- *
17
- * The version tag (`v1`) lets us evolve the AAD shape later without
18
- * silently accepting old payloads.
19
- */
20
- export declare function buildPairAad(daemonId: string, responseUserId: string): Uint8Array;
21
- export default class Pair extends BaseCommand {
22
- static description: string;
23
- static hidden: boolean;
24
- static examples: string[];
25
- static flags: {
26
- 'no-qr': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
27
- 'poll-timeout': import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
28
- };
29
- run(): Promise<void>;
30
- }
31
- export interface DecryptOptions {
32
- expectedDaemonId: string;
33
- expectedResponseUserId: string;
34
- }
35
- /**
36
- * Decrypt the pairing response. Returns null on any failure (bad format,
37
- * decryption mismatch, malformed JSON, AAD mismatch) — the caller surfaces
38
- * a uniform "decrypt failed" error to the user. See
39
- * `docs/daemon-pair-protocol.md` for the byte layout.
40
- *
41
- * Bundle layout (mobile-encoded):
42
- * nonce (24 bytes) || mobilePublicKey (32 bytes) || ciphertext
43
- * || lenAad (2 bytes, big-endian) || aad
44
- *
45
- * The trailing `lenAad || aad` is XSalsa20-Poly1305's "no native AAD slot"
46
- * workaround (C4): the canonical AAD is sealed alongside the ciphertext so
47
- * any tampered/misdirected payload fails verification BEFORE tweetnacl
48
- * sees a single byte of plaintext. The CLI rebuilds the AAD locally from
49
- * `expectedDaemonId` + `expectedResponseUserId` and constant-time compares
50
- * — a leaked QR + race attack that swapped the daemonId at the server gets
51
- * rejected here.
52
- */
53
- export declare function decryptResponse(responseB64: string, cliSecretKey: Uint8Array, opts?: DecryptOptions): DecryptedPlaintext | null;
54
- /**
55
- * Persist the pairing credentials. Token + refreshToken go to the
56
- * canonical auth backend (keychain or file); the optional
57
- * dataEncryptionKey is preserved alongside (C4) — until Tier 1 #1's
58
- * mobile half ships, this field will typically be null and the daemon
59
- * derives its machine-DEK locally instead.
60
- *
61
- * Exported for tests so the persistence path can be exercised without
62
- * spinning the full oclif runtime.
63
- */
64
- export declare function persistPairCredentials(plaintext: DecryptedPlaintext, serverOrigin: string): void;
65
- /** base64url encode (no padding) — what the protocol uses for publicKey. */
66
- export declare function base64urlEncode(bytes: Uint8Array): string;
67
- export {};
@@ -1,351 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.buildPairAad = buildPairAad;
7
- exports.decryptResponse = decryptResponse;
8
- exports.persistPairCredentials = persistPairCredentials;
9
- exports.base64urlEncode = base64urlEncode;
10
- const core_1 = require("@oclif/core");
11
- const tweetnacl_1 = __importDefault(require("tweetnacl"));
12
- const qrcode_terminal_1 = __importDefault(require("qrcode-terminal"));
13
- const auth_storage_1 = require("../lib/auth-storage");
14
- const base_command_1 = require("../base-command");
15
- const api_fetch_1 = require("../lib/api-fetch");
16
- const PUBLIC_KEY_BYTES = tweetnacl_1.default.box.publicKeyLength; // 32
17
- const SECRET_KEY_BYTES = tweetnacl_1.default.box.secretKeyLength; // 32
18
- const NONCE_BYTES = tweetnacl_1.default.box.nonceLength; // 24
19
- // H16 — feature-flag check evaluated AT MODULE LOAD so oclif sees a
20
- // stable `hidden` value when it builds the manifest. Toggling at
21
- // runtime would require a manifest rebuild anyway; capturing once
22
- // matches oclif's static-class-field model.
23
- const PAIR_FEATURE_ENABLED = process.env.OVERSKY_ENABLE_PAIR === '1';
24
- /**
25
- * Canonical AAD bytes for the pair-response (C4). Mobile builds the
26
- * same string, ASCII-encodes, prepends a 2-byte big-endian length, and
27
- * appends to the ciphertext bundle. The CLI rebuilds locally and
28
- * compares before decrypting — so a tampered or misdirected ciphertext
29
- * fails AAD before tweetnacl ever sees it.
30
- *
31
- * Format: `oversky:pair:v1:daemonId=<daemonId>:responseUserId=<responseUserId>`
32
- *
33
- * The version tag (`v1`) lets us evolve the AAD shape later without
34
- * silently accepting old payloads.
35
- */
36
- function buildPairAad(daemonId, responseUserId) {
37
- const canonical = `oversky:pair:v1:daemonId=${daemonId}:responseUserId=${responseUserId}`;
38
- return new TextEncoder().encode(canonical);
39
- }
40
- class Pair extends base_command_1.BaseCommand {
41
- static description = PAIR_FEATURE_ENABLED
42
- ? 'Pair this daemon to a skrr account via the mobile app.'
43
- : "Pair this daemon to a skrr account via the mobile app (mobile app integration pending; use 'skrr login' for now).";
44
- // H16 — hide from `--help` output unless the feature flag is on at
45
- // module load. oclif consults this static field when generating the
46
- // top-level command list.
47
- static hidden = !PAIR_FEATURE_ENABLED;
48
- static examples = ['<%= config.bin %> pair', '<%= config.bin %> pair --no-qr'];
49
- static flags = {
50
- 'no-qr': core_1.Flags.boolean({
51
- description: 'Skip the QR rendering and only print the pairing URL.',
52
- default: false,
53
- }),
54
- 'poll-timeout': core_1.Flags.integer({
55
- description: 'Maximum seconds to poll before giving up (default: 300).',
56
- default: 300,
57
- }),
58
- };
59
- async run() {
60
- // H16 — runtime gate. Even if the user discovered the command name,
61
- // we explicitly bail with a clear message and `EX_USAGE` (64) so
62
- // CI scripts can distinguish "user error" from "command failed".
63
- if (!PAIR_FEATURE_ENABLED) {
64
- this.error('`skrr pair` is not yet available — mobile app integration is pending. Use `oversky login` instead.', { exit: 64 });
65
- }
66
- const { flags } = await this.parse(Pair);
67
- // Step 1 — generate the CLI's ephemeral X25519 box keypair.
68
- const keypair = tweetnacl_1.default.box.keyPair();
69
- const publicKeyB64url = base64urlEncode(keypair.publicKey);
70
- // Step 2 — ask the server to open a pairing window.
71
- let start;
72
- try {
73
- start = await (0, api_fetch_1.apiFetch)('/api/auth/pair/start', {
74
- method: 'POST',
75
- body: { publicKey: publicKeyB64url },
76
- // The /start endpoint is anonymous; no credential needed. Pass
77
- // the resolved one anyway — apiFetch tolerates an authed call to
78
- // an anon endpoint.
79
- credential: this.resolvedCredential,
80
- });
81
- }
82
- catch (err) {
83
- if (err instanceof api_fetch_1.ApiFetchError && err.status === 409) {
84
- this.error('A pairing request already exists for this CLI session — wait for it to expire or restart the CLI.', { exit: 1 });
85
- }
86
- if (err instanceof api_fetch_1.ApiFetchError && err.status === 503) {
87
- // H5 — global active-row cap. Surface the Retry-After hint.
88
- this.error('skrr pair-request capacity is temporarily exhausted. Wait ~60s and try `skrr pair` again.', { exit: 1 });
89
- }
90
- this.handleApiError(err);
91
- }
92
- if (typeof start.pairId !== 'string' ||
93
- start.pairId.length === 0 ||
94
- typeof start.pairSecret !== 'string' ||
95
- start.pairSecret.length === 0) {
96
- this.error('Server did not return a pairId/pairSecret. Update your skrr server.', {
97
- exit: 1,
98
- });
99
- }
100
- const expiresAt = new Date(start.expiresAt);
101
- const pairingUrl = `oversky://pair?pk=${start.publicKey}`;
102
- // Step 3 — render the QR + URL.
103
- process.stderr.write('\n');
104
- process.stderr.write(' Open the skrr mobile app and scan this QR code to pair this daemon:\n');
105
- process.stderr.write('\n');
106
- if (!flags['no-qr']) {
107
- const qr = renderQr(pairingUrl);
108
- if (qr)
109
- process.stderr.write(qr);
110
- }
111
- process.stderr.write(' Or open this URL on your phone:\n');
112
- process.stderr.write(` ${pairingUrl}\n`);
113
- process.stderr.write('\n');
114
- process.stderr.write(` Pairing window expires at ${expiresAt.toISOString()}.\n`);
115
- process.stderr.write('\n');
116
- process.stderr.write(' Waiting for approval...\n');
117
- // Step 4 — poll for the response.
118
- const pollIntervalMs = Math.max(500, start.pollIntervalMs || 1000);
119
- const pollDeadline = Math.min(Date.now() + flags['poll-timeout'] * 1000, expiresAt.getTime());
120
- let pollResult = null;
121
- while (Date.now() < pollDeadline) {
122
- await sleep(pollIntervalMs);
123
- try {
124
- const res = await (0, api_fetch_1.apiFetch)('/api/auth/pair/poll', {
125
- method: 'POST',
126
- // H5 — authenticate by holder-of-secret, not holder-of-publicKey.
127
- // The pairSecret was returned to us once on /start; the server
128
- // stored only its hash and compares constant-time.
129
- body: { pairId: start.pairId, pairSecret: start.pairSecret },
130
- credential: this.resolvedCredential,
131
- });
132
- if (res.state === 'authorized') {
133
- pollResult = res;
134
- break;
135
- }
136
- if (res.state === 'expired') {
137
- this.error('Pairing window expired before the mobile app responded. Run `skrr pair` again.', {
138
- exit: 1,
139
- });
140
- }
141
- // 'pending' — keep polling.
142
- }
143
- catch (err) {
144
- if (err instanceof api_fetch_1.ApiFetchError && err.status === 429) {
145
- // Back off doubly on rate limit.
146
- await sleep(pollIntervalMs);
147
- continue;
148
- }
149
- this.handleApiError(err);
150
- }
151
- }
152
- if (!pollResult || !pollResult.response) {
153
- this.error('Pairing timed out without a response. Run `skrr pair` again.', { exit: 1 });
154
- }
155
- // Step 5 — verify AAD, then decrypt the response (C4).
156
- if (!pollResult.daemonId || !pollResult.responseUserId) {
157
- this.error('Pairing response missing daemonId or responseUserId — refusing to decrypt without AAD context.', { exit: 1 });
158
- }
159
- const plaintext = decryptResponse(pollResult.response, keypair.secretKey, {
160
- expectedDaemonId: pollResult.daemonId,
161
- expectedResponseUserId: pollResult.responseUserId,
162
- });
163
- if (!plaintext) {
164
- this.error('Pairing response failed to decrypt — the mobile app may be sending a malformed payload, or the AAD did not match.', { exit: 1 });
165
- }
166
- // Step 6 — install the credentials via the canonical token-save path.
167
- // C4 — persist the dataEncryptionKey when the mobile app sends one.
168
- // Until Tier 1 #1's mobile half ships, the field will be null and
169
- // the daemon's machine-DEK path takes over instead.
170
- persistPairCredentials(plaintext, this.cliConfig.baseURL);
171
- process.stderr.write('\n');
172
- process.stderr.write(' Pairing complete. Daemon ready.\n');
173
- if (plaintext.daemonId) {
174
- process.stderr.write(` Daemon ID: ${plaintext.daemonId}\n`);
175
- }
176
- if (plaintext.dataEncryptionKey) {
177
- process.stderr.write(' Data encryption key installed.\n');
178
- }
179
- process.stderr.write('\n');
180
- }
181
- }
182
- exports.default = Pair;
183
- /**
184
- * Decrypt the pairing response. Returns null on any failure (bad format,
185
- * decryption mismatch, malformed JSON, AAD mismatch) — the caller surfaces
186
- * a uniform "decrypt failed" error to the user. See
187
- * `docs/daemon-pair-protocol.md` for the byte layout.
188
- *
189
- * Bundle layout (mobile-encoded):
190
- * nonce (24 bytes) || mobilePublicKey (32 bytes) || ciphertext
191
- * || lenAad (2 bytes, big-endian) || aad
192
- *
193
- * The trailing `lenAad || aad` is XSalsa20-Poly1305's "no native AAD slot"
194
- * workaround (C4): the canonical AAD is sealed alongside the ciphertext so
195
- * any tampered/misdirected payload fails verification BEFORE tweetnacl
196
- * sees a single byte of plaintext. The CLI rebuilds the AAD locally from
197
- * `expectedDaemonId` + `expectedResponseUserId` and constant-time compares
198
- * — a leaked QR + race attack that swapped the daemonId at the server gets
199
- * rejected here.
200
- */
201
- function decryptResponse(responseB64, cliSecretKey, opts) {
202
- if (typeof responseB64 !== 'string' || responseB64.length === 0)
203
- return null;
204
- if (cliSecretKey.length !== SECRET_KEY_BYTES)
205
- return null;
206
- let bundle;
207
- try {
208
- bundle = base64Decode(responseB64);
209
- }
210
- catch {
211
- return null;
212
- }
213
- if (bundle.length < NONCE_BYTES + PUBLIC_KEY_BYTES + 1)
214
- return null;
215
- let cipher;
216
- let trailerAad = null;
217
- if (opts) {
218
- // C4 — bundle MUST carry a trailing `lenAad || aad`. The 2-byte
219
- // length is big-endian unsigned — caps AAD at 65535 bytes (way
220
- // more headroom than the canonical AAD ever needs).
221
- const expectedAad = buildPairAad(opts.expectedDaemonId, opts.expectedResponseUserId);
222
- if (bundle.length < NONCE_BYTES + PUBLIC_KEY_BYTES + 2 + 1)
223
- return null;
224
- const lenOffset = bundle.length - expectedAad.length - 2;
225
- if (lenOffset < NONCE_BYTES + PUBLIC_KEY_BYTES + 1)
226
- return null;
227
- const lenAad = (bundle[lenOffset] << 8) | bundle[lenOffset + 1];
228
- if (lenAad !== expectedAad.length)
229
- return null;
230
- const trailerStart = lenOffset;
231
- const aadStart = lenOffset + 2;
232
- if (trailerStart < NONCE_BYTES + PUBLIC_KEY_BYTES + 1)
233
- return null;
234
- trailerAad = bundle.slice(aadStart, aadStart + lenAad);
235
- if (!constantTimeBytesEqual(trailerAad, expectedAad)) {
236
- // AAD mismatch — refuse to decrypt. Do NOT log or surface the
237
- // mismatch shape; uniform null fits the "uniform terminal state"
238
- // model the rest of the command relies on.
239
- return null;
240
- }
241
- cipher = bundle.slice(NONCE_BYTES + PUBLIC_KEY_BYTES, trailerStart);
242
- }
243
- else {
244
- cipher = bundle.slice(NONCE_BYTES + PUBLIC_KEY_BYTES);
245
- }
246
- const nonce = bundle.slice(0, NONCE_BYTES);
247
- const mobilePublicKey = bundle.slice(NONCE_BYTES, NONCE_BYTES + PUBLIC_KEY_BYTES);
248
- const plaintextBytes = tweetnacl_1.default.box.open(cipher, nonce, mobilePublicKey, cliSecretKey);
249
- if (!plaintextBytes)
250
- return null;
251
- let json;
252
- try {
253
- json = JSON.parse(Buffer.from(plaintextBytes).toString('utf8'));
254
- }
255
- catch {
256
- return null;
257
- }
258
- if (!isValidPlaintext(json))
259
- return null;
260
- return json;
261
- }
262
- /**
263
- * Persist the pairing credentials. Token + refreshToken go to the
264
- * canonical auth backend (keychain or file); the optional
265
- * dataEncryptionKey is preserved alongside (C4) — until Tier 1 #1's
266
- * mobile half ships, this field will typically be null and the daemon
267
- * derives its machine-DEK locally instead.
268
- *
269
- * Exported for tests so the persistence path can be exercised without
270
- * spinning the full oclif runtime.
271
- */
272
- function persistPairCredentials(plaintext, serverOrigin) {
273
- (0, auth_storage_1.writeToBackend)({
274
- token: plaintext.token,
275
- ...(plaintext.refreshToken ? { refreshToken: plaintext.refreshToken } : {}),
276
- }, { serverOrigin, clearReauth: true, provenance: 'user_login' });
277
- if (plaintext.dataEncryptionKey) {
278
- // The unified `writeToBackend` API only carries token+refresh
279
- // today. For now, persist the DEK as a sibling field on the next
280
- // bundle write so it survives a CLI restart. When the daemon
281
- // machine-DEK seam lands, this is the call site that flips to the
282
- // L12 Keychain helper.
283
- const existing = (0, auth_storage_1.readFromBackend)(serverOrigin);
284
- if (existing.bundle) {
285
- (0, auth_storage_1.writeToBackend)({
286
- ...existing.bundle,
287
- // dataEncryptionKey is not in the AuthBundle type yet; this is
288
- // the deliberate seam for the daemon-side L12 path. We append
289
- // through a typed cast so the bundle survives a round-trip.
290
- ...{ dataEncryptionKey: plaintext.dataEncryptionKey },
291
- });
292
- }
293
- }
294
- // TODO: mobile DEK not yet available — daemon machine-DEK path will
295
- // skip this branch entirely until Tier 1 #1's mobile half ships.
296
- }
297
- function isValidPlaintext(v) {
298
- if (!v || typeof v !== 'object')
299
- return false;
300
- const o = v;
301
- if (typeof o.token !== 'string' || o.token.length === 0)
302
- return false;
303
- if (typeof o.refreshToken !== 'string' || o.refreshToken.length === 0)
304
- return false;
305
- if (o.daemonId !== null && typeof o.daemonId !== 'string')
306
- return false;
307
- if (o.dataEncryptionKey !== null && typeof o.dataEncryptionKey !== 'string')
308
- return false;
309
- return true;
310
- }
311
- /** base64url encode (no padding) — what the protocol uses for publicKey. */
312
- function base64urlEncode(bytes) {
313
- return Buffer.from(bytes)
314
- .toString('base64')
315
- .replace(/\+/g, '-')
316
- .replace(/\//g, '_')
317
- .replace(/=+$/, '');
318
- }
319
- /** Standard base64 decode — what the protocol uses for the response bundle. */
320
- function base64Decode(s) {
321
- // Permissive: the protocol says base64 (with padding), but a well-meaning
322
- // mobile-side base64url encoder would also work. Accept both.
323
- const normalized = s.replace(/-/g, '+').replace(/_/g, '/');
324
- const buf = Buffer.from(normalized, 'base64');
325
- return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
326
- }
327
- /** Constant-time Uint8Array equality. */
328
- function constantTimeBytesEqual(a, b) {
329
- if (a.length !== b.length)
330
- return false;
331
- let diff = 0;
332
- for (let i = 0; i < a.length; i++) {
333
- diff |= a[i] ^ b[i];
334
- }
335
- return diff === 0;
336
- }
337
- function renderQr(text) {
338
- try {
339
- let out = '';
340
- qrcode_terminal_1.default.generate(text, { small: true }, (rendered) => {
341
- out = rendered;
342
- });
343
- return out;
344
- }
345
- catch {
346
- return '';
347
- }
348
- }
349
- function sleep(ms) {
350
- return new Promise((resolve) => setTimeout(resolve, ms));
351
- }