permaura 1.3.26 → 1.3.27
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/bin/cli.mjs +22 -22
- package/bin/lib.mjs +5 -5
- package/package.json +1 -1
package/bin/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// permaura
|
|
2
|
+
// permaura: the dead-easy way to run the Permaura gateway.
|
|
3
3
|
// npx -y permaura start # install-or-upgrade, run, print URL + token
|
|
4
4
|
// npx -y permaura start --pair PGW-XXXX # …and connect it to your permaura.com account
|
|
5
5
|
// npx -y permaura status # …or any other permaura command, against a running one
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// newer binary when one ships (skip with --no-upgrade / PERMAURA_NO_UPGRADE=1).
|
|
12
12
|
//
|
|
13
13
|
// This is a launcher, not a second CLI. Everything but the install/upgrade half is handed to the
|
|
14
|
-
// binary verbatim, so there is exactly one implementation of every command
|
|
14
|
+
// binary verbatim, so there is exactly one implementation of every command, including `--pair`,
|
|
15
15
|
// which the binary performs itself once it is up.
|
|
16
16
|
import os from "node:os";
|
|
17
17
|
import fs from "node:fs";
|
|
@@ -58,7 +58,7 @@ const warn = (s) => console.log(`${C.yellow("!")} ${s}`);
|
|
|
58
58
|
/**
|
|
59
59
|
* The binary's own subcommands. The launcher keeps the list so a typo costs nothing: a mistyped
|
|
60
60
|
* command is rejected here, before anything downloads a binary or binds a port. It is the same list
|
|
61
|
-
* the binary's clap definition carries, and the two drift only when a command is added
|
|
61
|
+
* the binary's clap definition carries, and the two drift only when a command is added, at which
|
|
62
62
|
* point the worst case is that the new one has to be run through the installed binary directly.
|
|
63
63
|
*/
|
|
64
64
|
const KNOWN = new Set([
|
|
@@ -90,7 +90,7 @@ const VALUE_FLAGS = new Set(["--url", "--token"]);
|
|
|
90
90
|
* people arrive by and wrong for the other one that installs this file: `npm i -g permaura` puts the
|
|
91
91
|
* launcher on PATH, and telling someone to reach it through npx then sends them back out to the
|
|
92
92
|
* registry to fetch a copy of what they are already running. It is not in the docs, but it is the
|
|
93
|
-
* reflex for anything published on npm, and it works
|
|
93
|
+
* reflex for anything published on npm, and it works, so the help has to know.
|
|
94
94
|
*
|
|
95
95
|
* npx runs the package out of its own cache (`~/.npm/_npx/<hash>/node_modules/permaura/bin/cli.mjs`),
|
|
96
96
|
* so the path this module was loaded from is the answer; nothing else needs to be probed or asked.
|
|
@@ -98,7 +98,7 @@ const VALUE_FLAGS = new Set(["--url", "--token"]);
|
|
|
98
98
|
* reader on PATH gets a command that works, and an npx user gets one their shell will tell them
|
|
99
99
|
* about immediately.
|
|
100
100
|
*
|
|
101
|
-
* The `curl | sh` route never reaches here at all
|
|
101
|
+
* The `curl | sh` route never reaches here at all: it installs the Rust binary, whose own clap help
|
|
102
102
|
* has said `permaura <command>` throughout.
|
|
103
103
|
*/
|
|
104
104
|
function invocation() {
|
|
@@ -106,7 +106,7 @@ function invocation() {
|
|
|
106
106
|
return self.includes(`${path.sep}_npx${path.sep}`) ? "npx -y permaura" : "permaura";
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
const usage = (me = invocation()) => `permaura
|
|
109
|
+
const usage = (me = invocation()) => `permaura: the dead-easy way to run the Permaura gateway.
|
|
110
110
|
|
|
111
111
|
Usage:
|
|
112
112
|
${me} start install-or-upgrade, run, print URL + token
|
|
@@ -116,7 +116,7 @@ Usage:
|
|
|
116
116
|
${me} --version print the launcher + installed-binary versions
|
|
117
117
|
${me} --help print this
|
|
118
118
|
|
|
119
|
-
Commands (${[...KNOWN].sort().join(", ")}) are the binary's own
|
|
119
|
+
Commands (${[...KNOWN].sort().join(", ")}) are the binary's own: run
|
|
120
120
|
\`${me} help <command>\` for one of them.
|
|
121
121
|
|
|
122
122
|
Launcher options:
|
|
@@ -138,7 +138,7 @@ Environment:
|
|
|
138
138
|
*
|
|
139
139
|
* NO arguments prints help and starts nothing. A multi-command tool that boots a long-running server
|
|
140
140
|
* when invoked bare is a surprise, and this is the first command most people type. An unrecognised
|
|
141
|
-
* subcommand is an ERROR for the same reason it always was
|
|
141
|
+
* subcommand is an ERROR for the same reason it always was: `--help`, and every typo, used to
|
|
142
142
|
* silently boot a gateway and bind a port.
|
|
143
143
|
*
|
|
144
144
|
* Everything else is forwarded to the binary untouched (minus the launcher-only flags), so the
|
|
@@ -272,7 +272,7 @@ function sha256File(p) {
|
|
|
272
272
|
* The release signing keys this launcher accepts (Ed25519, SPKI PEM), newest first.
|
|
273
273
|
*
|
|
274
274
|
* Why this matters: `asset.sha256` comes from the SAME manifest as `asset.url`. That is integrity
|
|
275
|
-
* against a corrupted download, not authenticity
|
|
275
|
+
* against a corrupted download, not authenticity: anyone who can change the manifest changes both
|
|
276
276
|
* numbers and the checksum cheerfully confirms their binary. Only a signature made by a key we pinned
|
|
277
277
|
* out-of-band says "Permaura built this".
|
|
278
278
|
*
|
|
@@ -280,7 +280,7 @@ function sha256File(p) {
|
|
|
280
280
|
* gateway binary is, so whatever is pinned here sits on people's machines until they think to run
|
|
281
281
|
* `npm update -g permaura`. With one key, rotating it breaks every one of those installs at once, and
|
|
282
282
|
* the break is unrecoverable from the client side: the thing that would carry the new key is itself
|
|
283
|
-
* signed with it. A set gives rotation an overlap window
|
|
283
|
+
* signed with it. A set gives rotation an overlap window. See RELEASING.md, but the shape is: add
|
|
284
284
|
* the next key here, ship, wait, and only then sign with it.
|
|
285
285
|
*
|
|
286
286
|
* MUST stay in step with the other three pin sites, or an install verifies on one surface and fails on
|
|
@@ -315,7 +315,7 @@ const REQUIRE_SIGNATURE = RELEASE_PUBKEY_PEMS.length > 0;
|
|
|
315
315
|
* parse.
|
|
316
316
|
*
|
|
317
317
|
* Returns true when verified, false when no key is configured (nothing to verify against), and throws
|
|
318
|
-
* when a key IS configured and the signature is missing or bad
|
|
318
|
+
* when a key IS configured and the signature is missing or bad: that is an attack, not a hiccup.
|
|
319
319
|
*/
|
|
320
320
|
async function verifyManifestSignature(rawManifest, sigUrl) {
|
|
321
321
|
if (!REQUIRE_SIGNATURE) return false;
|
|
@@ -344,13 +344,13 @@ async function ensureBinary({ noUpgrade, force }) {
|
|
|
344
344
|
const key = platformKey();
|
|
345
345
|
if (!key) {
|
|
346
346
|
throw new Error(
|
|
347
|
-
`unsupported platform ${process.platform}/${process.arch}
|
|
347
|
+
`unsupported platform ${process.platform}/${process.arch}: prebuilt gateways cover macOS, Linux and Windows on arm64/x64. https://permaura.com/docs`,
|
|
348
348
|
);
|
|
349
349
|
}
|
|
350
350
|
fs.mkdirSync(path.join(DIR, "bin"), { recursive: true });
|
|
351
351
|
|
|
352
352
|
// Sweep binaries parked by a previous rename-aside upgrade (D9 cleanup-on-start; no-op when no
|
|
353
|
-
// live binary exists
|
|
353
|
+
// live binary exists: a lone .old may be the only working copy after a failed swap).
|
|
354
354
|
sweepOldBinaries(BIN);
|
|
355
355
|
|
|
356
356
|
const have = installedVersion();
|
|
@@ -368,7 +368,7 @@ async function ensureBinary({ noUpgrade, force }) {
|
|
|
368
368
|
throw new Error(`bad manifest JSON: ${e.message}`);
|
|
369
369
|
}
|
|
370
370
|
} catch (e) {
|
|
371
|
-
// A signature failure is never "the network was flaky"
|
|
371
|
+
// A signature failure is never "the network was flaky": it must not fall back to a cached binary
|
|
372
372
|
// and carry on as though nothing happened.
|
|
373
373
|
if (REQUIRE_SIGNATURE && /signature|signed|release key/i.test(e.message)) throw e;
|
|
374
374
|
if (haveBinary && !force) {
|
|
@@ -379,7 +379,7 @@ async function ensureBinary({ noUpgrade, force }) {
|
|
|
379
379
|
}
|
|
380
380
|
if (!REQUIRE_SIGNATURE) {
|
|
381
381
|
warn(
|
|
382
|
-
"This build cannot verify that the gateway binary came from Permaura
|
|
382
|
+
"This build cannot verify that the gateway binary came from Permaura: no release key is pinned yet. The download is checksummed, but a checksum from an unsigned manifest only proves the file arrived intact, not who made it.",
|
|
383
383
|
);
|
|
384
384
|
}
|
|
385
385
|
|
|
@@ -427,14 +427,14 @@ async function ensureBinary({ noUpgrade, force }) {
|
|
|
427
427
|
|
|
428
428
|
fs.chmodSync(tmp, 0o755);
|
|
429
429
|
// Rename-aside swap (the rustup pattern, D9): park the current binary at `.old`, move the verified
|
|
430
|
-
// download into place, restore the parked copy if that fails
|
|
430
|
+
// download into place, restore the parked copy if that fails: a runnable binary exists at BIN on
|
|
431
431
|
// every exit path. A gateway already running the old inode keeps working on every platform; the
|
|
432
432
|
// sweep above collects parked files on a later run.
|
|
433
433
|
swapBinaryIntoPlace(BIN, tmp);
|
|
434
434
|
// Quarantine is macOS's own "you didn't make this" flag, so stripping it from an artifact we could
|
|
435
435
|
// not authenticate throws away the last check standing. Once a release key is pinned this only runs
|
|
436
436
|
// for a signature-verified binary; until then it must still run, because the gateway binary is not
|
|
437
|
-
// notarized and Gatekeeper would otherwise refuse to execute it at all
|
|
437
|
+
// notarized and Gatekeeper would otherwise refuse to execute it at all: removing the strip today
|
|
438
438
|
// would simply break every macOS install. Un-gating it is part of the signing work, not separate
|
|
439
439
|
// from it (see HANDBACK, § H-04).
|
|
440
440
|
if (process.platform === "darwin" && (manifestVerified || !REQUIRE_SIGNATURE)) {
|
|
@@ -450,7 +450,7 @@ async function ensureBinary({ noUpgrade, force }) {
|
|
|
450
450
|
|
|
451
451
|
/**
|
|
452
452
|
* The operator bearer for this install: an explicit PERMAURA_TOKEN, else the one the gateway minted
|
|
453
|
-
* into `operator.token` on clean bootstrap. Returns null when neither exists
|
|
453
|
+
* into `operator.token` on clean bootstrap. Returns null when neither exists: a gateway initialised
|
|
454
454
|
* by an older build has a bearer we can't read, and inventing a shared one is exactly the hole this
|
|
455
455
|
* closes. Callers degrade to manual pairing instead.
|
|
456
456
|
*/
|
|
@@ -488,7 +488,7 @@ async function waitHealthz(addr, tries = 20) {
|
|
|
488
488
|
/**
|
|
489
489
|
* Best-effort graceful stop: `POST /v1/shutdown` (operator-authed, loopback-only) asks the gateway to
|
|
490
490
|
* drain in-flight requests and exit on its own. Resolves true on 202. Falls through to the caller's
|
|
491
|
-
* hard kill on any failure
|
|
491
|
+
* hard kill on any failure, e.g. a clean (non-seeded) gateway where the demo token isn't valid.
|
|
492
492
|
*/
|
|
493
493
|
function postShutdown(addr, token) {
|
|
494
494
|
return new Promise((resolve) => {
|
|
@@ -533,7 +533,7 @@ async function main() {
|
|
|
533
533
|
|
|
534
534
|
if (!cmd) {
|
|
535
535
|
if (forward.length) {
|
|
536
|
-
// Arguments but no command
|
|
536
|
+
// Arguments but no command: a mistyped flag. It must not fall through to the bare-invocation
|
|
537
537
|
// help and exit 0, and it certainly must not start anything.
|
|
538
538
|
console.error(`${C.red("✗")} unknown argument${forward.length > 1 ? "s" : ""}: ${forward.join(", ")}\n`);
|
|
539
539
|
console.error(usage());
|
|
@@ -546,7 +546,7 @@ async function main() {
|
|
|
546
546
|
}
|
|
547
547
|
|
|
548
548
|
if (!KNOWN.has(cmd)) {
|
|
549
|
-
// Exit before anything binds a port or downloads a binary
|
|
549
|
+
// Exit before anything binds a port or downloads a binary: a mistyped command should cost
|
|
550
550
|
// nothing.
|
|
551
551
|
console.error(`${C.red("✗")} unknown command: ${cmd}\n`);
|
|
552
552
|
console.error(usage());
|
|
@@ -598,7 +598,7 @@ async function main() {
|
|
|
598
598
|
const token = readOperatorToken();
|
|
599
599
|
const pairing = forward.some((a) => a === "--pair" || a.startsWith("--pair="));
|
|
600
600
|
if (!token) {
|
|
601
|
-
// The gateway is up and healthy
|
|
601
|
+
// The gateway is up and healthy: only adding it to an account needs the bearer. Say so plainly
|
|
602
602
|
// rather than guessing at a token.
|
|
603
603
|
warn(
|
|
604
604
|
`This gateway was initialised by an earlier build, so its operator token isn't readable here.\n Re-pair from Console → Settings → Gateways, or re-run with PERMAURA_TOKEN=<token> set.`,
|
package/bin/lib.mjs
CHANGED
|
@@ -34,7 +34,7 @@ export function assetName(key) {
|
|
|
34
34
|
/**
|
|
35
35
|
* Sweep binaries parked by a previous rename-aside upgrade (D9 cleanup-on-start). Only sweeps when
|
|
36
36
|
* a live binary exists at `binPath`: after a failed swap the newest `.old` may be the only working
|
|
37
|
-
* copy on disk, and it must survive until a later successful swap replaces it. Best-effort
|
|
37
|
+
* copy on disk, and it must survive until a later successful swap replaces it. Best-effort: on
|
|
38
38
|
* Windows a still-running old gateway keeps its file locked and is collected on a later run.
|
|
39
39
|
*/
|
|
40
40
|
export function sweepOldBinaries(binPath, fsOps = fs) {
|
|
@@ -46,7 +46,7 @@ export function sweepOldBinaries(binPath, fsOps = fs) {
|
|
|
46
46
|
try {
|
|
47
47
|
fsOps.rmSync(path.join(dir, f), { force: true });
|
|
48
48
|
} catch {
|
|
49
|
-
/* locked by a running process
|
|
49
|
+
/* locked by a running process, leave it for next time */
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -55,7 +55,7 @@ export function sweepOldBinaries(binPath, fsOps = fs) {
|
|
|
55
55
|
/**
|
|
56
56
|
* Rename-aside swap (D9, the rustup pattern): park the current binary at `.old` (Windows can't
|
|
57
57
|
* overwrite a running executable but CAN rename it), then move the verified download into place.
|
|
58
|
-
* If that final rename fails, the parked binary is restored
|
|
58
|
+
* If that final rename fails, the parked binary is restored: there is a runnable binary at
|
|
59
59
|
* `binPath` on every exit path, success or throw.
|
|
60
60
|
*/
|
|
61
61
|
export function swapBinaryIntoPlace(binPath, tmpPath, fsOps = fs) {
|
|
@@ -80,7 +80,7 @@ export function swapBinaryIntoPlace(binPath, tmpPath, fsOps = fs) {
|
|
|
80
80
|
try {
|
|
81
81
|
fsOps.renameSync(parkedAt, binPath);
|
|
82
82
|
} catch {
|
|
83
|
-
/* restore failed
|
|
83
|
+
/* restore failed: surface the original swap error below */
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
throw e;
|
|
@@ -91,7 +91,7 @@ export function swapBinaryIntoPlace(binPath, tmpPath, fsOps = fs) {
|
|
|
91
91
|
* Compare versions by semver precedence. `<0` if a<b, `0` equal, `>0` if a>b.
|
|
92
92
|
*
|
|
93
93
|
* The pre-release suffix is part of the order, not noise to be discarded. It used to be dropped, so
|
|
94
|
-
* `1.2.5-dev.1` compared EQUAL to the released `1.2.5` and the upgrade was never offered
|
|
94
|
+
* `1.2.5-dev.1` compared EQUAL to the released `1.2.5` and the upgrade was never offered: a
|
|
95
95
|
* dev build of a version permanently masked that version's real release, on exactly the machines
|
|
96
96
|
* most likely to be running one.
|
|
97
97
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "permaura",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.27",
|
|
4
4
|
"description": "Run the Permaura gateway with one command. Give your AI agents permission, not keys. `npm i -g permaura` then `permaura start --pair <code>` installs the gateway, starts it, and connects it to your permaura.com account.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|