agmsg-cloud 0.1.1 → 0.1.2

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.
@@ -90,6 +90,30 @@ export function verificationUrlIsSafe(url, endpoint) {
90
90
  // operator who named their own endpoint owns their own topology.
91
91
  return true;
92
92
  }
93
+ /**
94
+ * The console to send someone to, or null when we do not know one.
95
+ *
96
+ * Null is the answer for a self-hosted endpoint, and it is the whole reason
97
+ * this is a function rather than a constant anyone can print. The pin above
98
+ * exists because the official console's origin is a name we ship; a
99
+ * self-hosted deployment's is "not ours to know", and guessing `app.<their
100
+ * domain>` sends an operator to a service that may not be theirs — or may be
101
+ * someone else's.
102
+ *
103
+ * So a caller that wants to end a message with "and manage it at …" has to
104
+ * handle the null. That is deliberate: a message with no URL is worse than one
105
+ * with a wrong URL only until the wrong URL is followed.
106
+ */
107
+ export function officialConsoleFor(endpoint) {
108
+ try {
109
+ return new URL(endpoint).origin === new URL(DEFAULT_ENDPOINT).origin
110
+ ? OFFICIAL_CONSOLE_ORIGIN
111
+ : null;
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ }
93
117
  export function openUrl(url) {
94
118
  const opener = openerFor(url);
95
119
  return new Promise((resolve) => {
@@ -3,6 +3,8 @@ import { DEFAULT_ENDPOINT, armEnterToOpen, verificationUrlIsSafe, } from '../bro
3
3
  import { credentialsForOrigin, isOrgAddress, originOf, writeCredential } from '../credentials.js';
4
4
  import { settleMachineName, validateMachineName } from '../machine-name.js';
5
5
  import { resolveScriptsDir } from '../config.js';
6
+ import { packageInstall } from '../version.js';
7
+ import { pathLookupCommand } from '../preflight.js';
6
8
  import { teamsBoundTo } from '../oss.js';
7
9
  // `login` — the device-authorization flow, from this machine's side.
8
10
  //
@@ -360,7 +362,43 @@ async function pollUntilDecided(grant, ctx) {
360
362
  // will look up again. Checked here so nothing durable happens first, and
361
363
  // checked again in `keyFor` so a future caller cannot route around this.
362
364
  if (!isOrgAddress(issued.org)) {
363
- throw new Error('the server answered with an org address this build does not recognise — nothing was stored');
365
+ // WHAT THIS SENTENCE HAS TO GET RIGHT: whose fault it is, and what to
366
+ // do next. The previous wording — "the server answered with an org
367
+ // address this build does not recognise" — put the server first, and
368
+ // the person who hit it concluded the service was broken and stopped
369
+ // (#476). It was not broken: their machine was running a build from
370
+ // before `isOrgAddress` learned the second form.
371
+ //
372
+ // `credentials.ts` predicts this window in as many words: the server
373
+ // cutover "does not land until `npm view agmsg-cloud version` resolves
374
+ // to a build whose `isOrgAddress` accepts the new form". The window was
375
+ // designed for. What was never written was the sentence someone reads
376
+ // from inside it.
377
+ //
378
+ // The version AND the directory, from `packageInstall()`, which walks
379
+ // once and returns both. That pairing is the point here rather than a
380
+ // detail: the machine that hit this had TWO `agmsg-cloud` on PATH, and
381
+ // the one that answered `-v` was not the one that ran. A version alone
382
+ // would have been the same ambiguity in a new place. Reading the
383
+ // directory back against the shell's own lookup is what settles it.
384
+ //
385
+ // The lookup command comes from `preflight.ts`, which already had to
386
+ // decide it: `where` on Windows, `which` elsewhere. Printing `which`
387
+ // unconditionally would hand a Windows reader a broken command at the
388
+ // exact moment they have hit the PATH confusion this sentence exists to
389
+ // resolve. Raised in review, and taken from the module that RUNS it so
390
+ // the printed advice and the executed lookup cannot drift apart.
391
+ //
392
+ // No version number is named as the one to upgrade TO. Naming it needs
393
+ // a claim about which release first accepted this form, that claim
394
+ // decays every release, and a reader on that exact version is told to
395
+ // install what they already have. "Update, then run this again" is
396
+ // true whatever the answer is.
397
+ const { version, directory } = packageInstall();
398
+ throw new Error(`this build (${version}, from ${directory}) does not recognise the org address ` +
399
+ 'the server sent — nothing was stored. Update with `npm i -g agmsg-cloud` and run ' +
400
+ 'this command again. If the version above is not the one you expected, another ' +
401
+ `\`agmsg-cloud\` is ahead of it on PATH — compare it with \`${pathLookupCommand()} agmsg-cloud\`.`);
364
402
  }
365
403
  return finish(issued, endpoint, post, out);
366
404
  }
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
2
2
  import { hostname } from 'node:os';
3
3
  import { CourierClient } from '../api.js';
4
4
  import { originOf, readCredential } from '../credentials.js';
5
+ import { officialConsoleFor } from '../browser.js';
5
6
  import { publicKeyOf } from '../oss.js';
6
7
  import { deviceIdentityPath } from '../paths.js';
7
8
  import { NEEDS, ensurePreflight, preflight } from '../preflight.js';
@@ -217,8 +218,22 @@ export async function cmdSync(config, opts) {
217
218
  // authenticated, by machine.
218
219
  await fetch(config, { team: opts.team });
219
220
  // The last thing said, because "what now" is the question the screen leaves
220
- // otherwise — and here the answer is that there is no next command. Saying
221
- // so is the point: someone who has just run five steps and watched a code
221
+ // otherwise: someone who has just run five steps and watched a code
222
222
  // comparison has every reason to expect a sixth.
223
- out(`\n"${opts.team}" is on this machine, unlocked and syncing. Nothing further to run.\n`);
223
+ //
224
+ // "Nothing further to run" was true and was read as "nothing further to do",
225
+ // and the walk stopped there (#479). The two are different sentences. There
226
+ // is no sixth COMMAND, and there is somewhere to go — the console is where a
227
+ // team is managed once it syncs, and the CLI was the only thing that knew
228
+ // that and did not say it.
229
+ //
230
+ // The console is named only for the official endpoint. `browser.ts` pins that
231
+ // origin because it is a name we ship, and refuses to guess a self-hosted
232
+ // one — "its layout is not ours to know". Printing `app.<their domain>` would
233
+ // send an operator to a service that need not be theirs, so a self-hosted
234
+ // deployment gets the true half of the sentence and no invented URL.
235
+ const console_ = officialConsoleFor(config.baseUrl);
236
+ out(`\n"${opts.team}" is on this machine, unlocked and syncing. No further command to run` +
237
+ (console_ ? ` — manage it at ${console_}` : '') +
238
+ '.\n');
224
239
  }
@@ -97,11 +97,29 @@ function remoteSupportsConnect(scriptsDir) {
97
97
  return /\bconnect\b/.test(text);
98
98
  }
99
99
  }
100
+ /**
101
+ * What a person types to ask their shell which executable would run.
102
+ *
103
+ * `where` on Windows, `which` everywhere else. Exported and taking its platform
104
+ * as an argument because this answer is now needed in two places and both of
105
+ * them are wrong if they disagree: this module RUNS it, and `login` PRINTS it
106
+ * for an operator to run themselves (#476). Telling a Windows reader to run
107
+ * `which` hands them a broken command at the exact moment they have hit the
108
+ * PATH confusion the message exists to resolve — raised in review.
109
+ *
110
+ * A parameter rather than a module-level constant so both branches are
111
+ * reachable from a test on one machine. A constant bound at import time can
112
+ * only ever be checked on the platform the suite happens to run on, which is
113
+ * the branch nobody needed to check.
114
+ */
115
+ export function pathLookupCommand(on = platform) {
116
+ return on === 'win32' ? 'where' : 'which';
117
+ }
100
118
  function onPath(command) {
101
119
  try {
102
120
  // `command -v` through the shell would re-interpret the name; execFile with
103
121
  // a fixed argv does not.
104
- execFileSync(platform === 'win32' ? 'where' : 'which', [command], { stdio: 'ignore' });
122
+ execFileSync(pathLookupCommand(), [command], { stdio: 'ignore' });
105
123
  return true;
106
124
  }
107
125
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agmsg-cloud",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Companion CLI for the agmsg cloud service: connect a team, join from another machine, and back up its keys.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",