@veilio-inc/cli 0.5.4 → 0.6.1

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/README.md +62 -15
  2. package/dist/args.d.ts +20 -2
  3. package/dist/args.d.ts.map +1 -1
  4. package/dist/args.js +48 -3
  5. package/dist/args.js.map +1 -1
  6. package/dist/client/index.d.ts +9 -0
  7. package/dist/client/index.d.ts.map +1 -0
  8. package/dist/client/index.js +13 -0
  9. package/dist/client/index.js.map +1 -0
  10. package/dist/client/keys.d.ts +22 -0
  11. package/dist/client/keys.d.ts.map +1 -0
  12. package/dist/client/keys.js +170 -0
  13. package/dist/client/keys.js.map +1 -0
  14. package/dist/client/maps.d.ts +115 -0
  15. package/dist/client/maps.d.ts.map +1 -0
  16. package/dist/client/maps.js +798 -0
  17. package/dist/client/maps.js.map +1 -0
  18. package/dist/client/personal-maps-key.d.ts +42 -0
  19. package/dist/client/personal-maps-key.d.ts.map +1 -0
  20. package/dist/client/personal-maps-key.js +168 -0
  21. package/dist/client/personal-maps-key.js.map +1 -0
  22. package/dist/client/project-scope.d.ts +87 -0
  23. package/dist/client/project-scope.d.ts.map +1 -0
  24. package/dist/client/project-scope.js +132 -0
  25. package/dist/client/project-scope.js.map +1 -0
  26. package/dist/client/projects.d.ts +53 -0
  27. package/dist/client/projects.d.ts.map +1 -0
  28. package/dist/client/projects.js +129 -0
  29. package/dist/client/projects.js.map +1 -0
  30. package/dist/client/status.d.ts +21 -0
  31. package/dist/client/status.d.ts.map +1 -0
  32. package/dist/client/status.js +71 -0
  33. package/dist/client/status.js.map +1 -0
  34. package/dist/client/team-key-bootstrap.d.ts +55 -0
  35. package/dist/client/team-key-bootstrap.d.ts.map +1 -0
  36. package/dist/client/team-key-bootstrap.js +127 -0
  37. package/dist/client/team-key-bootstrap.js.map +1 -0
  38. package/dist/client/vault-key.d.ts +5 -0
  39. package/dist/client/vault-key.d.ts.map +1 -0
  40. package/dist/client/vault-key.js +35 -0
  41. package/dist/client/vault-key.js.map +1 -0
  42. package/dist/client/wording.d.ts +62 -0
  43. package/dist/client/wording.d.ts.map +1 -0
  44. package/dist/client/wording.js +358 -0
  45. package/dist/client/wording.js.map +1 -0
  46. package/dist/cloud-commands.d.ts +34 -43
  47. package/dist/cloud-commands.d.ts.map +1 -1
  48. package/dist/cloud-commands.js +282 -466
  49. package/dist/cloud-commands.js.map +1 -1
  50. package/dist/cloud.d.ts +101 -2
  51. package/dist/cloud.d.ts.map +1 -1
  52. package/dist/cloud.js +91 -7
  53. package/dist/cloud.js.map +1 -1
  54. package/dist/commands.d.ts +14 -1
  55. package/dist/commands.d.ts.map +1 -1
  56. package/dist/commands.js +24 -0
  57. package/dist/commands.js.map +1 -1
  58. package/dist/credential.d.ts +7 -0
  59. package/dist/credential.d.ts.map +1 -1
  60. package/dist/credential.js +10 -1
  61. package/dist/credential.js.map +1 -1
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +24 -5
  64. package/dist/index.js.map +1 -1
  65. package/dist/team-namespace.d.ts +74 -2
  66. package/dist/team-namespace.d.ts.map +1 -1
  67. package/dist/team-namespace.js +78 -18
  68. package/dist/team-namespace.js.map +1 -1
  69. package/package.json +4 -3
@@ -6,15 +6,22 @@
6
6
  // with no account touches nothing (FR-002). Putting these here is what keeps
7
7
  // that walk meaningful instead of something to be deleted the moment it becomes
8
8
  // inconvenient.
9
- import { CloudError, startDeviceLogin, exchangeDeviceCode, createMap, deleteMap, getMap, reservePlaceholders, getVault, getUserKeys, getTeamKeyWraps, listMaps, listRules, request, } from './cloud.js';
10
- import { checkVaultVerifier, decryptMapFromVault, deriveVaultKey, unwrapPrivateKey, unwrapTeamKey, exportTeamKey, encryptMapForVault, fromBase64, parseVaultEnvelope, importTeamKey, encryptTeamMap, decryptTeamMapWithAny, } from '@veilio-inc/engine';
11
- import { loadMap, loadRemote, saveMap } from './store.js';
9
+ import { CloudError, startDeviceLogin, exchangeDeviceCode, reservePlaceholders, getUserKeys, getTeamKeyWraps, getMyTeam, listMaps, listRules, request, } from './cloud.js';
10
+ import { unwrapTeamKey, exportTeamKey } from '@veilio-inc/engine';
12
11
  import { resolveTeamNamespace, teamLayerNote } from './team-namespace.js';
13
- import { readTeamUnlock, removeTeamUnlock, writeTeamUnlock, teamUnlockPath, expiryFrom, } from './team-unlock.js';
12
+ import { removeTeamUnlock, writeTeamUnlock, teamUnlockPath, expiryFrom, } from './team-unlock.js';
14
13
  import { readCredential, removeCredential, writeCredential } from './credential.js';
15
14
  import { mergeRules, removeRules, rulesPath, writeRules } from './rules.js';
16
15
  import { EXIT_ERROR, EXIT_OK } from './commands.js';
17
16
  import { DEFAULT_INSTANCE } from './cloud.js';
17
+ import { createCloudMap, listCloudMaps, pullCloudMap, removeCloudMap, updateCloudMap, } from './client/maps.js';
18
+ import { refusal, renderRefusal } from './client/wording.js';
19
+ import { bootstrapTeamKey, openOwnKeypair } from './client/team-key-bootstrap.js';
20
+ import { vaultLock, vaultUnlock } from './client/keys.js';
21
+ import { vaultKeyFor } from './client/vault-key.js';
22
+ import { formatProjectsList, listTeamProjects, useProject, usedSentence, } from './client/projects.js';
23
+ import { PROJECT_ENV, overriddenSentence, projectOverride, } from './client/project-scope.js';
24
+ import { personalMapsKeyPath, removePersonalMapsKey } from './client/personal-maps-key.js';
18
25
  import { VERSION } from './version.js';
19
26
  /** The endpoint a sign-in is checked against. See `runLogin`. */
20
27
  const ENTITLED_PROBE = '/api/maps';
@@ -81,7 +88,7 @@ export async function runDeviceLogin(base, io) {
81
88
  const account = await accountFor(base, token, io);
82
89
  if (account === null)
83
90
  return EXIT_ERROR;
84
- writeCredential({ token, account, instance: base }, io.home);
91
+ writeCredential(keptProject({ token, account, instance: base }, io), io.home);
85
92
  io.stdout(`Signed in to ${base} as ${account}\n`);
86
93
  return EXIT_OK;
87
94
  }
@@ -112,6 +119,25 @@ export async function runDeviceLogin(base, io) {
112
119
  }
113
120
  }
114
121
  }
122
+ /**
123
+ * A new sign-in keeps the project chosen under the previous one only when it
124
+ * is the same account on the same instance (spec 052, founder 2026-10-25).
125
+ * A project id from another account or instance means nothing here, and
126
+ * keeping it would point every team map call at somebody else's choice. It is
127
+ * dropped, and that is said, so nobody wonders why they are back to every
128
+ * project's maps.
129
+ */
130
+ function keptProject(next, io) {
131
+ const prior = readCredential(io.home);
132
+ if (!prior?.project)
133
+ return next;
134
+ if (prior.instance === next.instance && prior.account === next.account) {
135
+ return { ...next, project: prior.project };
136
+ }
137
+ io.stderr(`veilio: project ${prior.project} was chosen for ${prior.account} on ${prior.instance}, so it does not ` +
138
+ 'carry over to this sign-in. Run `veilio projects use <name>` to choose one.\n');
139
+ return next;
140
+ }
115
141
  /** Who the new session belongs to, asked of the server rather than assumed. */
116
142
  async function accountFor(base, token, io) {
117
143
  try {
@@ -215,7 +241,7 @@ export async function runLogin(instance, io, opts = {}) {
215
241
  catch (err) {
216
242
  return reportLoginFailure(err, io);
217
243
  }
218
- writeCredential(credential, io.home);
244
+ writeCredential(keptProject(credential, io), io.home);
219
245
  io.stdout(`Signed in to ${base} as ${credential.account}\n`);
220
246
  return EXIT_OK;
221
247
  }
@@ -248,6 +274,7 @@ export async function runLogout(io) {
248
274
  // branch where signing out quietly kept the more dangerous half.
249
275
  try {
250
276
  removeTeamUnlock(io.home);
277
+ removePersonalMapsKey(io.home);
251
278
  removeRules(io.home);
252
279
  removeCredential(io.home);
253
280
  }
@@ -277,6 +304,15 @@ export async function runLogout(io) {
277
304
  `(${err instanceof Error ? err.message : String(err)}). Delete ${teamUnlockPath(io.home)} by hand.\n`);
278
305
  return EXIT_ERROR;
279
306
  }
307
+ // The personal-maps key, for the same reason as the team keys.
308
+ try {
309
+ removePersonalMapsKey(io.home);
310
+ }
311
+ catch (err) {
312
+ io.stderr(`veilio: the session was revoked, but the unlocked personal-maps key could not be removed ` +
313
+ `(${err instanceof Error ? err.message : String(err)}). Delete ${personalMapsKeyPath(io.home)} by hand.\n`);
314
+ return EXIT_ERROR;
315
+ }
280
316
  // The rules are the account's too, and a whitelist among them keeps names
281
317
  // readable: they go with the session rather than outliving it.
282
318
  try {
@@ -314,7 +350,16 @@ export function runWhoami(io) {
314
350
  io.stdout('Not signed in. Run `veilio login` to connect a Veilio Cloud account.\n');
315
351
  return EXIT_OK;
316
352
  }
317
- io.stdout(`${credential.account} on ${credential.instance}\n`);
353
+ // Still no request: the variable is reported as set, not resolved - which
354
+ // project it names is `veilio projects`'s question, and needs the network.
355
+ const override = projectOverride(io.env);
356
+ io.stdout(`${credential.account} on ${credential.instance}` +
357
+ (override
358
+ ? `, project ${override} (from ${PROJECT_ENV})`
359
+ : credential.project
360
+ ? `, project ${credential.project}`
361
+ : '') +
362
+ '\n');
318
363
  return EXIT_OK;
319
364
  }
320
365
  /**
@@ -365,277 +410,121 @@ async function requirePassword(io, label) {
365
410
  throw new Error('this build cannot read a password interactively');
366
411
  return io.password(label);
367
412
  }
368
- // ─── maps: list, pull, push ──────────────────────────────────────────────────
413
+ // ─── maps: list, pull, push, update, rm ──────────────────────────────────────
414
+ //
415
+ // The work is done in `client/maps.ts`, which the MCP server's map tools call
416
+ // too (spec 052 §3). What is left here is the terminal's half: asking for the
417
+ // passphrase, and saying what happened - results on stdout, refusals on
418
+ // stderr, in the words of the shared table.
419
+ /** The client's context for a terminal run: the person can be asked. */
420
+ function clientContext(io) {
421
+ return {
422
+ home: io.home,
423
+ passphrase: io.password ? () => io.password('Vault passphrase: ') : undefined,
424
+ env: io.env,
425
+ };
426
+ }
427
+ /** A refusal on stderr, and the exit code that goes with it. */
428
+ function refused(refusal, io) {
429
+ io.stderr(renderRefusal(refusal));
430
+ return EXIT_ERROR;
431
+ }
432
+ function notes(lines, io) {
433
+ for (const line of lines)
434
+ io.stderr(`veilio: ${line}\n`);
435
+ }
369
436
  /**
370
437
  * List what the account holds.
371
438
  *
372
439
  * Prints the id, because `pull` takes one. A name would be friendlier and is
373
440
  * wrong: names are not unique, and picking "the first one that matches" is how
374
- * somebody pulls a different map over the one they were using.
441
+ * somebody pulls a different map over the one they were using. A teammate's
442
+ * team map is marked, because it is theirs to update or remove.
375
443
  */
376
444
  export async function runMapsList(io) {
377
- const credential = readCredential(io.home);
378
- if (!credential)
379
- return notSignedIn(io);
380
- let list;
381
- try {
382
- list = await listMaps(credential);
383
- }
384
- catch (err) {
385
- return reportCloudFailure(err, io);
386
- }
387
- const all = [...list.personalMaps, ...list.teamMaps];
388
- if (all.length === 0) {
445
+ const listed = await listCloudMaps(clientContext(io));
446
+ if (!listed.ok)
447
+ return refused(listed.refusal, io);
448
+ const { maps, project, projectFromEnv } = listed.value;
449
+ if (maps.length === 0) {
389
450
  io.stdout('No maps in Cloud yet. `veilio maps push <name>` uploads the local one.\n');
390
451
  return EXIT_OK;
391
452
  }
392
- for (const map of all) {
393
- const count = map.identifier_count === null ? '?' : String(map.identifier_count);
394
- io.stdout(`${map.id} ${map.scope.padEnd(8)} ${count.padStart(5)} symbols ${map.name}\n`);
395
- }
453
+ for (const map of maps) {
454
+ const count = map.count === null ? '?' : String(map.count);
455
+ const whose = map.scope === 'team' && !map.mine ? ' (a teammate’s)' : '';
456
+ io.stdout(`${map.id} ${map.scope.padEnd(8)} ${count.padStart(5)} symbols ${map.name}${whose}\n`);
457
+ }
458
+ // Said on stderr so the listing itself stays parseable: which project's team
459
+ // maps these are, or that no project was chosen.
460
+ if (project)
461
+ io.stderr(projectFromEnv
462
+ ? `veilio: team maps of project ${project} - ${PROJECT_ENV} chose it, so \`veilio projects use\` does not change it here\n`
463
+ : `veilio: team maps of project ${project} (\`veilio projects\` to change)\n`);
396
464
  return EXIT_OK;
397
465
  }
398
466
  /**
399
- * Open a team map with the team keys `veilio team unlock` stored.
400
- *
401
- * The same keys the MCP server reads, for the same reason: this machine may
402
- * have nobody present to type a vault passphrase, and the team key - not the
403
- * vault key - is what a team map is sealed under. Reports and returns null
404
- * when it cannot, so the caller writes nothing.
405
- */
406
- async function openTeamEnvelope(credential, mapData, io) {
407
- const unlock = readTeamUnlock({ instance: credential.instance, account: credential.account }, io.home);
408
- if (!unlock || unlock.keys.length === 0) {
409
- io.stderr('veilio: that is a team map, and no team key is unlocked on this machine. ' +
410
- 'Run `veilio team unlock` first. Nothing was written.\n');
411
- return null;
412
- }
413
- const keys = [];
414
- for (const stored of unlock.keys) {
415
- try {
416
- keys.push({ version: stored.version, key: await importTeamKey(stored.key) });
417
- }
418
- catch {
419
- // A damaged entry; the others may still open the map.
420
- continue;
421
- }
422
- }
423
- try {
424
- return await decryptTeamMapWithAny(keys, JSON.parse(mapData));
425
- }
426
- catch (err) {
427
- io.stderr(`veilio: that team map could not be opened with the unlocked team keys — ` +
428
- `${err instanceof Error ? err.message : String(err)}. ` +
429
- 'If the team key was rotated since, run `veilio team unlock` again. Nothing was written.\n');
430
- return null;
431
- }
432
- }
433
- /**
434
- * Pull a map into the local store: decrypt FIRST, write second.
435
- *
436
- * The ordering is the control (Constitution IV). Writing before decrypting means
437
- * a wrong passphrase leaves a half-built store behind — and the store is the one
438
- * thing that can restore text already anonymized, so a partial write there is
439
- * not an inconvenience, it is unrecoverable text.
440
- *
441
- * A personal map is opened HERE. The server holds the salt and a verifier and
442
- * nothing else; the passphrase never leaves this machine, and there is no code
443
- * path in this package that could send it.
467
+ * Pull a map into the local store: decrypt FIRST, write second (see
468
+ * `pullCloudMap`). A personal map is opened HERE - the passphrase never leaves
469
+ * this machine, and there is no code path in this package that could send it.
444
470
  */
445
471
  export async function runMapsPull(id, mapPath, io, options = {}) {
446
- const credential = readCredential(io.home);
447
- if (!credential)
448
- return notSignedIn(io);
449
- if (!id) {
450
- io.stderr('veilio: which map? Run `veilio maps list` for the ids.\n');
451
- return EXIT_ERROR;
452
- }
453
- let remote;
454
- try {
455
- remote = await getMap(credential, id);
456
- }
457
- catch (err) {
458
- return reportCloudFailure(err, io);
459
- }
460
- // Every map is an envelope the server cannot open: a personal map under the
461
- // vault key, a team map under the team key. Which one is written on the
462
- // envelope itself.
463
- if (typeof remote.map_data !== 'string') {
464
- io.stderr('veilio: that map is not an encrypted envelope, so it was not written.\n');
465
- return EXIT_ERROR;
466
- }
467
- let envelope;
468
- try {
469
- envelope = JSON.parse(remote.map_data);
470
- }
471
- catch {
472
- io.stderr('veilio: that map is not an encrypted envelope, so it was not written.\n');
473
- return EXIT_ERROR;
474
- }
475
- let map;
476
- if (envelope.alg === 'AES-256-GCM-TEAM') {
477
- const opened = await openTeamEnvelope(credential, remote.map_data, io);
478
- if (!opened)
479
- return EXIT_ERROR;
480
- map = opened;
481
- }
482
- else {
483
- let vault;
484
- try {
485
- vault = await getVault(credential);
486
- }
487
- catch (err) {
488
- return reportCloudFailure(err, io);
489
- }
490
- if (!vault.initialized) {
491
- // Stated plainly rather than surfaced as a decryption failure. There is
492
- // nothing to try a passphrase against, and inviting one would be asking
493
- // somebody to guess at a lock that does not exist.
494
- io.stderr('veilio: this account has no vault, so there is no key for that map. ' +
495
- 'Create one in the web app first — it is the passphrase your maps are encrypted under.\n');
496
- return EXIT_ERROR;
497
- }
498
- const passphrase = io.password ? await io.password('Vault passphrase: ') : '';
499
- if (passphrase === '') {
500
- io.stderr('veilio: no passphrase given\n');
501
- return EXIT_ERROR;
502
- }
503
- const key = await deriveVaultKey(passphrase, fromBase64(vault.salt), vault.kdf ? { name: 'PBKDF2-SHA256', iterations: vault.kdf.iterations } : undefined);
504
- // The verifier answers "is this the right passphrase" before anything is
505
- // decrypted, so a wrong one is named as a wrong passphrase rather than
506
- // surfacing as an authentication-tag failure that reads like corruption.
507
- if (!(await checkVaultVerifier(key, vault.verifier))) {
508
- io.stderr('veilio: that vault passphrase is not right. Nothing was written.\n');
509
- return EXIT_ERROR;
510
- }
511
- try {
512
- map = await decryptMapFromVault(key, parseVaultEnvelope(remote.map_data));
513
- }
514
- catch (err) {
515
- io.stderr(`veilio: that map could not be opened — ${err instanceof Error ? err.message : String(err)}. ` +
516
- 'Nothing was written.\n');
517
- return EXIT_ERROR;
518
- }
519
- }
520
- // Only now is anything touched on disk.
521
- const existing = loadRemote(mapPath);
522
- const local = loadMap(mapPath);
523
- const hasLocal = Object.keys(local).length > 0;
524
- const diverged = hasLocal &&
525
- existing !== undefined &&
526
- existing.id === remote.id &&
527
- existing.updatedAt !== remote.updated_at &&
528
- JSON.stringify(local) !== JSON.stringify(map);
529
- if (diverged && options.force !== true) {
530
- // Reported, not resolved. Overwriting either side silently loses entries
531
- // that some text out there still depends on.
532
- io.stderr(`veilio: this map changed in Cloud (${existing.updatedAt} → ${remote.updated_at}) and ` +
533
- 'locally since it was pulled. Neither copy has been touched.\n' +
534
- `Pass --force to overwrite the local copy, or export it first with \`veilio map --json\`.\n`);
535
- return EXIT_ERROR;
536
- }
537
- // Whether or not the Cloud copy changed: a local entry the pulled map lacks,
538
- // or gives another name, is a placeholder some masked text still depends on
539
- // (staging walk, 2026-10-01 - a scrub after a pull was lost on the next pull).
540
- const dropped = Object.keys(local).filter((p) => map[p] !== local[p]);
541
- if (dropped.length > 0 && options.force !== true) {
542
- const shown = dropped.slice(0, 5).join(', ') + (dropped.length > 5 ? ` and ${dropped.length - 5} more` : '');
543
- io.stderr(`veilio: pulling "${remote.name}" would drop ${dropped.length} placeholder(s) this project's map has: ${shown}. ` +
544
- 'Nothing was written.\n' +
545
- `Push this map first (\`veilio maps push\`), pass --force to replace it, or export it with \`veilio map --json\`.\n`);
546
- return EXIT_ERROR;
472
+ const pulled = await pullCloudMap(clientContext(io), id, { mapPath, force: options.force });
473
+ if (!pulled.ok)
474
+ return refused(pulled.refusal, io);
475
+ const { name, count, path } = pulled.value;
476
+ io.stdout(`Pulled "${name}" (${count} symbols) into ${path}\n`);
477
+ return EXIT_OK;
478
+ }
479
+ /** Upload the local map, personal or the team's. Same endpoint, same quotas,
480
+ * same validation as the web app. */
481
+ export async function runMapsPush(name, mapPath, io, toTeam = false) {
482
+ const created = await createCloudMap(clientContext(io), {
483
+ name,
484
+ scope: toTeam ? 'team' : 'personal',
485
+ mapPath,
486
+ });
487
+ if (!created.ok)
488
+ return refused(created.refusal, io);
489
+ const c = created.value;
490
+ notes(c.notes, io);
491
+ if (c.scope === 'personal') {
492
+ io.stdout(`Pushed ${c.count} symbols to Cloud as ${c.id}\n`);
493
+ return EXIT_OK;
547
494
  }
548
- const now = new Date().toISOString();
549
- saveMap(mapPath, map, {
550
- force: true,
551
- remote: { id: remote.id, updatedAt: remote.updated_at, pulledAt: now },
495
+ const disputed = c.disputed ?? 0;
496
+ io.stdout(`Published "${c.name}" (${c.count} symbols) to the team as ${c.id}\n` +
497
+ `The team's shared set now holds ${c.teamTotal} identifiers` +
498
+ (disputed > 0
499
+ ? `, ${disputed} of which ${disputed === 1 ? 'is' : 'are'} disputed and will not restore\n`
500
+ : ', none disputed\n'));
501
+ return EXIT_OK;
502
+ }
503
+ /** Replace a Cloud map's entries with the local map's, in place (spec 052). */
504
+ export async function runMapsUpdate(id, mapPath, io, options = {}) {
505
+ const updated = await updateCloudMap(clientContext(io), id, {
506
+ mapPath,
507
+ allowDrop: options.allowDrop,
552
508
  });
553
- io.stdout(`Pulled "${remote.name}" (${Object.keys(map).length} symbols) into ${mapPath}\n`);
509
+ if (!updated.ok)
510
+ return refused(updated.refusal, io);
511
+ const u = updated.value;
512
+ notes(u.notes, io);
513
+ io.stdout(`Updated "${u.name}" (${u.id}) in place: ${u.count} symbols` +
514
+ (u.dropped > 0 ? `, ${u.dropped} dropped as asked` : '') +
515
+ `${u.scope === 'team' ? ', sealed with the team key' : ''}\n`);
554
516
  return EXIT_OK;
555
517
  }
556
518
  /**
557
- * Placeholders the local map and the team's namespace use for DIFFERENT names.
558
- *
559
- * Publishing one of these creates a DISPUTED placeholder, and
560
- * `restoreLayers.disputedIn` then refuses to restore it for every member of
561
- * the team until somebody deletes a map. One person's push, a cost paid by
562
- * everyone, and nothing errors at the time - so it is checked before anything
563
- * is sealed rather than reported afterwards.
564
- *
565
- * An identifier the team holds under a DIFFERENT placeholder is an alias, not
566
- * a conflict: both still restore (spec 017), so those pass.
567
- *
568
- * A map anonymized with `reservedAnonymizeOverTeam` cannot reach this, because
569
- * its numbers came out of the team's own allocation. This is for maps that
570
- * predate it - numbered locally, possibly before this account joined a team.
519
+ * Remove a map from Cloud. Renaming and removing are allowed while a key
520
+ * rotation is owed, because neither stores anything under the key.
571
521
  */
572
- export function conflictsWithTeam(local, teamNamespace) {
573
- const out = [];
574
- for (const [placeholder, mine] of Object.entries(local)) {
575
- const theirs = teamNamespace[placeholder];
576
- if (theirs !== undefined && theirs !== mine)
577
- out.push({ placeholder, mine, theirs });
578
- }
579
- return out;
580
- }
581
- /** Upload the local map. Same endpoint, same quotas, same validation. */
582
- export async function runMapsPush(name, mapPath, io, toTeam = false) {
583
- const credential = readCredential(io.home);
584
- if (!credential)
585
- return notSignedIn(io);
586
- const map = loadMap(mapPath);
587
- if (Object.keys(map).length === 0) {
588
- io.stderr(`veilio: there is no local map at ${mapPath} to push.\n`);
589
- return EXIT_ERROR;
590
- }
591
- if (toTeam)
592
- return pushToTeam(name, mapPath, map, credential, io);
593
- let vault;
594
- try {
595
- vault = await getVault(credential);
596
- }
597
- catch (err) {
598
- return reportCloudFailure(err, io);
599
- }
600
- if (!vault.initialized) {
601
- io.stderr('veilio: this account has no vault, so there is no key to encrypt with. ' +
602
- 'Create one in the web app first.\n');
603
- return EXIT_ERROR;
604
- }
605
- const passphrase = io.password ? await io.password('Vault passphrase: ') : '';
606
- if (passphrase === '') {
607
- io.stderr('veilio: no passphrase given\n');
608
- return EXIT_ERROR;
609
- }
610
- const key = await deriveVaultKey(passphrase, fromBase64(vault.salt), vault.kdf ? { name: 'PBKDF2-SHA256', iterations: vault.kdf.iterations } : undefined);
611
- if (!(await checkVaultVerifier(key, vault.verifier))) {
612
- // Caught before anything is uploaded. Pushing under the wrong key would
613
- // store a map the account can never open again.
614
- io.stderr('veilio: that vault passphrase is not right. Nothing was uploaded.\n');
615
- return EXIT_ERROR;
616
- }
617
- const envelope = await encryptMapForVault(key, map);
618
- let created;
619
- try {
620
- created = await createMap(credential, {
621
- name: name ?? 'from the terminal',
622
- map_data: JSON.stringify(envelope),
623
- identifier_count: Object.keys(map).length,
624
- scope: 'personal',
625
- });
626
- }
627
- catch (err) {
628
- return reportCloudFailure(err, io);
629
- }
630
- saveMap(mapPath, map, {
631
- force: true,
632
- remote: {
633
- id: created.id,
634
- updatedAt: new Date().toISOString(),
635
- pushedAt: new Date().toISOString(),
636
- },
637
- });
638
- io.stdout(`Pushed ${Object.keys(map).length} symbols to Cloud as ${created.id}\n`);
522
+ export async function runMapsRm(id, io) {
523
+ const removed = await removeCloudMap(clientContext(io), id);
524
+ if (!removed.ok)
525
+ return refused(removed.refusal, io);
526
+ const { name } = removed.value;
527
+ io.stdout(name ? `Removed "${name}" (${removed.value.id})\n` : `Removed ${removed.value.id}\n`);
639
528
  return EXIT_OK;
640
529
  }
641
530
  function notSignedIn(io) {
@@ -710,43 +599,57 @@ export async function runTeamUnlock(io, days) {
710
599
  return EXIT_ERROR;
711
600
  }
712
601
  const mine = await getUserKeys(credential);
713
- if (!mine.initialized) {
714
- io.stderr('veilio: this account has no keypair yet. Open the web app once — that is where a ' +
715
- 'keypair is created and where a teammate grants you the team key.\n');
716
- return EXIT_ERROR;
717
- }
718
- const { wraps } = await getTeamKeyWraps(credential, teamId);
602
+ // No keypair: there is nothing to unwrap a team key with, nor to wrap a
603
+ // first one to. The web app makes it on the first vault unlock.
604
+ if (!mine.initialized)
605
+ return refused(refusal('keys_not_set_up'), io);
606
+ let { wraps } = await getTeamKeyWraps(credential, teamId);
607
+ const derived = await vaultKeyFor(credential, clientContext(io).passphrase);
608
+ if (!derived.ok)
609
+ return refused(derived.refusal, io);
610
+ const vaultKey = derived.value;
611
+ // The keypair, and its public half computed from the private one: a
612
+ // public key the server hands back is never trusted on its own word, not
613
+ // for a wrap's context and above all not for wrapping a key to (security
614
+ // review). A server answer that differs stops everything.
615
+ const own = await openOwnKeypair(vaultKey, mine.privateKeyEncrypted, mine.publicKey);
616
+ if (!own.ok)
617
+ return refused(refusal('own_public_key_mismatch'), io);
618
+ const privateKey = own.privateKey;
619
+ const myPublicKey = own.publicKey;
620
+ // None held here: the team may have no key at all - a team of one always
621
+ // starts that way - so try to mint its first one, as the web app does.
622
+ // The server says whether that is a bootstrap; a refusal means a key
623
+ // exists (or this member may not start one), never a reason to mint again.
624
+ let minted = null;
719
625
  if (wraps.length === 0) {
720
- io.stderr('veilio: no teammate has granted you the team key yet. Ask someone already in the ' +
721
- 'team to open the web app, which is what performs the grant.\n');
722
- return EXIT_ERROR;
723
- }
724
- const vault = await getVault(credential);
725
- if (!vault.initialized) {
726
- io.stderr('veilio: this account has no vault, so there is no key to unwrap anything with. ' +
727
- 'Create one in the web app first.\n');
728
- return EXIT_ERROR;
729
- }
730
- const passphrase = io.password ? await io.password('Vault passphrase: ') : '';
731
- if (passphrase === '') {
732
- io.stderr('veilio: no passphrase given\n');
733
- return EXIT_ERROR;
734
- }
735
- const vaultKey = await deriveVaultKey(passphrase, fromBase64(vault.salt), vault.kdf ? { name: 'PBKDF2-SHA256', iterations: vault.kdf.iterations } : undefined);
736
- // Named as a wrong passphrase here rather than surfacing later as an
737
- // authentication-tag failure that reads like corruption.
738
- if (!(await checkVaultVerifier(vaultKey, vault.verifier))) {
739
- io.stderr('veilio: that vault passphrase is not right. Nothing was written.\n');
740
- return EXIT_ERROR;
626
+ const tried = await bootstrapTeamKey(credential, teamId, {
627
+ privateKey,
628
+ publicKey: myPublicKey,
629
+ });
630
+ if (tried.status === 'readback_mismatch')
631
+ return refused(refusal('team_key_readback_mismatch'), io);
632
+ if (tried.status === 'refused') {
633
+ if (tried.reason && /owner or an admin/i.test(tried.reason))
634
+ return refused(refusal('team_key_not_startable', { detail: tried.reason }), io);
635
+ // A team of one has nobody to ask, and must not be told to.
636
+ if (await isTeamOfOne(credential)) {
637
+ return refused(refusal('team_key_solo_unavailable', {
638
+ detail: tried.reason ?? 'the server did not accept a first key',
639
+ }), io);
640
+ }
641
+ return refused(refusal('team_key_waiting'), io);
642
+ }
643
+ minted = { version: tried.version };
644
+ ({ wraps } = await getTeamKeyWraps(credential, teamId));
741
645
  }
742
- const privateKey = await unwrapPrivateKey(vaultKey, mine.privateKeyEncrypted);
743
646
  const keys = [];
744
647
  for (const wrap of wraps) {
745
648
  try {
746
649
  const teamKey = await unwrapTeamKey(wrap.wrapped_key, privateKey, {
747
650
  teamId,
748
651
  version: wrap.version,
749
- myPublicKey: mine.publicKey,
652
+ myPublicKey,
750
653
  });
751
654
  keys.push({ version: wrap.version, key: await exportTeamKey(teamKey) });
752
655
  }
@@ -759,7 +662,7 @@ export async function runTeamUnlock(io, days) {
759
662
  }
760
663
  if (keys.length === 0) {
761
664
  io.stderr('veilio: none of the team key wraps held for this account could be opened. ' +
762
- 'Ask a teammate to grant access again from the web app.\n');
665
+ 'Grant it again from the team page in the web app, then run `veilio team unlock` again.\n');
763
666
  return EXIT_ERROR;
764
667
  }
765
668
  const now = new Date();
@@ -773,6 +676,10 @@ export async function runTeamUnlock(io, days) {
773
676
  keys,
774
677
  }, io.home);
775
678
  const versions = keys.map((k) => `v${k.version}`).join(', ');
679
+ if (minted) {
680
+ io.stdout(`Set up the team key (v${minted.version}): the team had none, so this account made its first one ` +
681
+ 'and holds it. It was never sent anywhere unwrapped.\n');
682
+ }
776
683
  io.stdout(`Unlocked ${keys.length === 1 ? 'the team key' : 'team keys'} (${versions}) until ` +
777
684
  `${expiresAt}.\n` +
778
685
  `Stored in ${teamUnlockPath(io.home)}, readable only by you. ` +
@@ -783,6 +690,17 @@ export async function runTeamUnlock(io, days) {
783
690
  return reportCloudFailure(err, io);
784
691
  }
785
692
  }
693
+ /** Whether this account is its team's only member. Unknown counts as not:
694
+ * the general wording is still true for a team of one, just less helpful. */
695
+ async function isTeamOfOne(credential) {
696
+ try {
697
+ const members = (await getMyTeam(credential)).team?.members;
698
+ return Array.isArray(members) && members.length === 1;
699
+ }
700
+ catch {
701
+ return false;
702
+ }
703
+ }
786
704
  /** Remove the unlocked team keys. */
787
705
  export function runTeamLock(io) {
788
706
  try {
@@ -800,16 +718,84 @@ export function runTeamLock(io) {
800
718
  io.stdout('Team keys locked.\n');
801
719
  return EXIT_OK;
802
720
  }
721
+ // ─── projects ────────────────────────────────────────────────────────────────
722
+ /** The team's projects, the one in use marked with `*`. */
723
+ export async function runProjectsList(io) {
724
+ const listed = await listTeamProjects(clientContext(io));
725
+ if (!listed.ok)
726
+ return refused(listed.refusal, io);
727
+ const { lines, note } = formatProjectsList(listed.value, { surface: 'cli' });
728
+ for (const line of lines)
729
+ io.stdout(`${line}\n`);
730
+ if (note)
731
+ io.stderr(`veilio: ${note}\n`);
732
+ return EXIT_OK;
733
+ }
734
+ /** Choose the project this sign-in works in. */
735
+ export async function runProjectsUse(ref, io) {
736
+ const used = await useProject(clientContext(io), ref);
737
+ if (!used.ok)
738
+ return refused(used.refusal, io);
739
+ io.stdout(`${usedSentence(used.value, { surface: 'cli' })}\n`);
740
+ if (used.value.overriddenBy !== null)
741
+ io.stderr(`veilio: ${overriddenSentence(used.value, 'shell')}\n`);
742
+ return EXIT_OK;
743
+ }
744
+ // ─── vault: unlock, lock ─────────────────────────────────────────────────────
745
+ /**
746
+ * Unlock the personal-maps key for processes that cannot ask for a passphrase
747
+ * (spec 052 FR-011). See `client/keys.ts` for what is kept: that key, and
748
+ * nothing else the vault protects.
749
+ */
750
+ export async function runVaultUnlock(io, days) {
751
+ const unlocked = await vaultUnlock(clientContext(io), { days });
752
+ if (!unlocked.ok)
753
+ return refused(unlocked.refusal, io);
754
+ const u = unlocked.value;
755
+ io.stdout(`${u.created ? 'Created and unlocked' : 'Unlocked'} the personal-maps key until ${u.expiresAt}.\n` +
756
+ `Stored in ${u.path}, readable only by you. It opens your personal maps and nothing else; ` +
757
+ 'the vault passphrase and key were not kept. Run `veilio vault lock` to remove it.\n' +
758
+ (u.resealed > 0
759
+ ? `Re-sealed ${u.resealed} personal map${u.resealed === 1 ? '' : 's'} under it, so the MCP server can open ${u.resealed === 1 ? 'it' : 'them'}.\n`
760
+ : ''));
761
+ if (u.failed.length > 0) {
762
+ io.stderr(`veilio: ${u.failed.length} personal map${u.failed.length === 1 ? '' : 's'} could not be re-sealed and ` +
763
+ `${u.failed.length === 1 ? 'is' : 'are'} still under the vault key, which the MCP server cannot open:\n` +
764
+ u.failed.map((f) => ` "${f.name}" (${f.id}): ${f.reason}\n`).join('') +
765
+ 'Run `veilio vault unlock` again to retry them; the key itself is unlocked.\n');
766
+ return EXIT_ERROR;
767
+ }
768
+ return EXIT_OK;
769
+ }
770
+ /** Remove the personal-maps key from this machine. */
771
+ export function runVaultLock(io) {
772
+ try {
773
+ if (!vaultLock(clientContext(io))) {
774
+ io.stdout('No personal-maps key was unlocked.\n');
775
+ return EXIT_OK;
776
+ }
777
+ }
778
+ catch (err) {
779
+ io.stderr(`veilio: the personal-maps key could not be removed ` +
780
+ `(${err instanceof Error ? err.message : String(err)}). ` +
781
+ `Delete ${personalMapsKeyPath(io.home)} by hand.\n`);
782
+ return EXIT_ERROR;
783
+ }
784
+ io.stdout('Personal-maps key locked.\n');
785
+ return EXIT_OK;
786
+ }
803
787
  // ─── restore: the team layer ────────────────────────────────────────────────
804
788
  /**
805
789
  * The team's maps for `veilio restore` (spec 028), handed to the local command
806
790
  * as a callback so that command stays unlinked from the network. Signed out:
807
791
  * no request, no layer - a local restore, as before.
808
792
  */
809
- export async function teamLayerFor(home, missing) {
793
+ export async function teamLayerFor(home, missing, env) {
810
794
  // Signed out, the resolver answers 'local' without a request. At most 5 s: a
811
795
  // hung instance must not stall a `... | veilio restore > file` pipeline.
812
- const found = await resolveTeamNamespace(home, { deadlineMs: RESTORE_CLOUD_WAIT_MS });
796
+ const found = await resolveTeamNamespace(home, { deadlineMs: RESTORE_CLOUD_WAIT_MS, env });
797
+ if (found.status === 'refused')
798
+ return { team: null, note: null, refused: renderRefusal(found.refusal) };
813
799
  if (found.status === 'team' && found.conflictDetection)
814
800
  return { team: found.analysis, note: null };
815
801
  return { team: null, note: teamLayerNote(found, missing) };
@@ -817,8 +803,10 @@ export async function teamLayerFor(home, missing) {
817
803
  const RESTORE_CLOUD_WAIT_MS = 5000;
818
804
  /** What `veilio scrub` numbers from (spec 030): the team's namespace, or this
819
805
  * project's map with the reason. Signed out or in no team: no request, no note. */
820
- export async function teamNamespaceForScrub(home) {
821
- const found = await resolveTeamNamespace(home, { deadlineMs: RESTORE_CLOUD_WAIT_MS });
806
+ export async function teamNamespaceForScrub(home, env) {
807
+ const found = await resolveTeamNamespace(home, { deadlineMs: RESTORE_CLOUD_WAIT_MS, env });
808
+ if (found.status === 'refused')
809
+ return { team: null, note: null, refused: renderRefusal(found.refusal) };
822
810
  if (found.status === 'team') {
823
811
  const credential = readCredential(home);
824
812
  const teamId = found.teamId;
@@ -853,179 +841,7 @@ export async function teamNamespaceForScrub(home) {
853
841
  };
854
842
  return { team: null, note: null };
855
843
  }
856
- /**
857
- * Publish the local map as the TEAM's, from the terminal (spec 011 US1).
858
- *
859
- * Seals what is on disk AS IT IS NUMBERED. It does not renumber: by the time
860
- * anybody runs this, the local map's placeholders are already present in the
861
- * files they masked, and renumbering here would publish a map that disagrees
862
- * with every one of them - while `saveMap(force)` rewrote the local map too,
863
- * breaking local restore as well. Numbers come out of the team's allocation at
864
- * ANONYMIZE time (`reservedAnonymizeOverTeam`), which is where the text and
865
- * the numbers can still change together.
866
- *
867
- * So the ordering here is: establish that publishing is safe, THEN seal, THEN
868
- * upload. Every refusal happens before the first byte leaves (FR-007).
869
- */
870
- async function pushToTeam(name, mapPath, map, credential, io) {
871
- // 1. The key. Read from the unlock file, never derived here: publishing
872
- // needs the TEAM key, and nobody's passphrase can produce it.
873
- const unlock = readTeamUnlock({ instance: credential.instance, account: credential.account }, io.home);
874
- if (!unlock || unlock.keys.length === 0) {
875
- io.stderr('veilio: no team key is unlocked on this machine, so this map cannot be sealed for ' +
876
- 'the team. Run `veilio team unlock` first. Nothing was uploaded.\n');
877
- return EXIT_ERROR;
878
- }
879
- // Newest first: a team mid-rotation holds two, and new content belongs under
880
- // the newer one. The version is declared on upload so the server can refuse
881
- // a write under a key a departed member knew (spec 014).
882
- const newest = [...unlock.keys].sort((a, b) => b.version - a.version)[0];
883
- let teamKey;
884
- try {
885
- teamKey = await importTeamKey(newest.key);
886
- }
887
- catch (err) {
888
- io.stderr(`veilio: the unlocked team key could not be read (${err instanceof Error ? err.message : String(err)}). ` +
889
- 'Run `veilio team unlock` again. Nothing was uploaded.\n');
890
- return EXIT_ERROR;
891
- }
892
- // 2. The team's namespace, for the conflict check and for the report. A
893
- // failure here is NOT fallback-to-local: publishing blind is what the
894
- // check exists to prevent.
895
- const found = await resolveTeamNamespace(io.home);
896
- if (found.status === 'local') {
897
- io.stderr('veilio: this account is not in a team, so there is no shared map to publish to. ' +
898
- 'Nothing was uploaded.\n');
899
- return EXIT_ERROR;
900
- }
901
- if (found.status === 'locked') {
902
- io.stderr('veilio: the team key on this machine does not match this team. ' +
903
- 'Run `veilio team unlock` again. Nothing was uploaded.\n');
904
- return EXIT_ERROR;
905
- }
906
- if (found.status === 'unavailable') {
907
- io.stderr(`veilio: could not read the team's maps (${found.reason}), so whether this map ` +
908
- 'conflicts with the team cannot be known. Nothing was uploaded.\n');
909
- return EXIT_ERROR;
910
- }
911
- // 3. The conflict check. Before sealing, so a refusal costs nothing.
912
- const clashes = conflictsWithTeam(map, found.analysis.namespace);
913
- if (clashes.length > 0) {
914
- io.stderr(`veilio: ${clashes.length} placeholder${clashes.length === 1 ? '' : 's'} in this map already ` +
915
- `mean something else to the team. Publishing would leave ${clashes.length === 1 ? 'it' : 'them'} ` +
916
- `unrestorable for every member, so nothing was uploaded:\n`);
917
- for (const { placeholder, mine, theirs } of clashes.slice(0, 10)) {
918
- io.stderr(` ${placeholder}: this map says ${mine}, the team says ${theirs}\n`);
919
- }
920
- if (clashes.length > 10)
921
- io.stderr(` … and ${clashes.length - 10} more\n`);
922
- io.stderr('Re-run `veilio scrub --team` to number this project out of the team\u2019s own ' +
923
- 'allocation, then push the result.\n');
924
- return EXIT_ERROR;
925
- }
926
- // 4. Seal and upload. Nothing above this line has sent anything.
927
- const envelope = await encryptTeamMap(teamKey, map);
928
- let created;
929
- try {
930
- created = await createMap(credential, {
931
- name: name ?? 'from the terminal',
932
- map_data: JSON.stringify(envelope),
933
- identifier_count: Object.keys(map).length,
934
- scope: 'team',
935
- key_version: newest.version,
936
- });
937
- }
938
- catch (err) {
939
- return reportTeamWriteFailure(err, io);
940
- }
941
- // The local map is NOT rewritten with a remote id: a team map is not this
942
- // project's copy of anything, and recording it as `remote` would make the
943
- // next `maps pull` treat the team's map as this project's own.
944
- // A union, not a sum: a name already in the team that this map also holds is
945
- // ONE identifier in the shared set, and the pre-flight above has just checked
946
- // that every such overlap agrees. Adding the two counts said "about" three
947
- // for two (spec 011 T030).
948
- const total = new Set([...Object.keys(found.analysis.namespace), ...Object.keys(map)]).size;
949
- const disputed = Object.keys(found.analysis.conflicts).length;
950
- // The name the person chose, beside the id they would type to remove it (T014).
951
- const label = name ?? 'from the terminal';
952
- io.stdout(`Published "${label}" (${Object.keys(map).length} symbols) to the team as ${created.id}\n` +
953
- `The team's shared set now holds ${total} identifiers` +
954
- (disputed > 0
955
- ? `, ${disputed} of which ${disputed === 1 ? 'is' : 'are'} disputed and will not restore\n`
956
- : ', none disputed\n'));
957
- return EXIT_OK;
958
- }
959
- /**
960
- * Remove a map from Cloud.
961
- *
962
- * Who may remove a team map is the server's decision and is not second-guessed
963
- * here - this says what each refusal means and changes nothing locally either
964
- * way. Renaming and removing are allowed while a key rotation is owed, because
965
- * neither stores anything under the key.
966
- */
967
- export async function runMapsRm(id, io) {
968
- const credential = readCredential(io.home);
969
- if (!credential)
970
- return notSignedIn(io);
971
- if (!id) {
972
- io.stderr('veilio: which map? Run `veilio maps list` for the ids.\n');
973
- return EXIT_ERROR;
974
- }
975
- // The name, read first, so the confirmation says WHICH map went: an id is
976
- // what a person types, the name is what they recognise (spec 011 T032). A
977
- // failed read never stops the removal - it only means the id is all there is
978
- // to report.
979
- let label;
980
- try {
981
- label = (await getMap(credential, id)).name || null;
982
- }
983
- catch {
984
- label = null;
985
- }
986
- try {
987
- await deleteMap(credential, id);
988
- }
989
- catch (err) {
990
- if (err instanceof CloudError && err.status === 404) {
991
- io.stderr(`veilio: there is no map ${id} on this account.\n`);
992
- return EXIT_ERROR;
993
- }
994
- if (err instanceof CloudError && err.status === 403) {
995
- io.stderr(`veilio: ${id} belongs to someone else on your team, so it is theirs to remove.\n`);
996
- return EXIT_ERROR;
997
- }
998
- return reportCloudFailure(err, io);
999
- }
1000
- io.stdout(label ? `Removed "${label}" (${id})\n` : `Removed ${id}\n`);
1001
- return EXIT_OK;
1002
- }
1003
- /**
1004
- * A failed team-map write, said in terms of what happened and who can fix it.
1005
- *
1006
- * The person reading this is often not the person who can clear it: a rotation
1007
- * is owed by whoever holds the current key, not by whoever happened to push.
1008
- * An unexplained refusal in a terminal reads as a broken tool (spec 011 US3).
1009
- */
1010
- function reportTeamWriteFailure(err, io) {
1011
- if (err instanceof CloudError && err.status === 409) {
1012
- io.stderr('veilio: a teammate left while holding the team key, so no new team content may be ' +
1013
- 'written until the key is changed. A member who holds the current key has to do ' +
1014
- 'that — it cannot be done from here. Nothing was uploaded.\n');
1015
- return EXIT_ERROR;
1016
- }
1017
- if (err instanceof CloudError && err.status === 413) {
1018
- io.stderr('veilio: this map is too large for Cloud to accept, so nothing was stored. ' +
1019
- 'Split it, or remove entries you no longer need.\n');
1020
- return EXIT_ERROR;
1021
- }
1022
- if (err instanceof CloudError && err.kind === 'forbidden' && /limit/i.test(err.message)) {
1023
- // The ceiling is an abuse limit, not a plan feature. Saying "upgrade" here
1024
- // would take money for a change that does nothing.
1025
- io.stderr(`veilio: ${err.message}\nThis ceiling is the same on every paid plan, so upgrading ` +
1026
- 'will not raise it. Nothing was uploaded.\n');
1027
- return EXIT_ERROR;
1028
- }
1029
- return reportCloudFailure(err, io);
1030
- }
844
+ /** Kept importable from here: tests and callers written before spec 052 moved
845
+ * it into the shared client. */
846
+ export { conflictsWithTeam } from './client/maps.js';
1031
847
  //# sourceMappingURL=cloud-commands.js.map