@arcadiasystems/morse-cli 0.9.0 → 0.10.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.
- package/CHANGELOG.md +20 -0
- package/README.md +44 -7
- package/dist/index.js +81 -11
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@ All notable changes to `@arcadiasystems/morse-cli` are documented here. The
|
|
|
4
4
|
format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.10.1] - 2026-09-10
|
|
8
|
+
|
|
9
|
+
Tooling only. No behaviour changes.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `bun run docs-gate`, wired into `bun run check` alongside the coverage gate. It fails when the README status banner does not name the current version, or when CHANGELOG's newest entry does not match `package.json`. The banner silently claimed v0.3.0 for two whole releases, and nothing in lint, typecheck, tests or coverage could catch it.
|
|
14
|
+
|
|
15
|
+
## [0.10.0] - 2026-09-10
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **Encrypted commands work on mainnet when you bring a Seal credential.** Mainnet pins no key servers because every operator there is commercial, and the CLI previously had no way to be pointed at one, so `entry add-encrypted`, `entry decrypt` and `file upload --encrypt` simply could not run there.
|
|
20
|
+
|
|
21
|
+
`MORSE_SEAL_API_KEY` supplies a credential for Mysten's committee, whose object id and aggregator the SDK already exports; `MORSE_SEAL_API_KEY_NAME` overrides the header for operators that want something other than `X-API-Key`. `MORSE_SEAL_KEY_SERVERS` takes a JSON array of `{ objectId, weight }` for a non-committee operator and wins when both are set. Neither is a flag: a credential in argv leaks through `ps` and shell history, the same reason the signing key is not one.
|
|
22
|
+
|
|
23
|
+
- **The credential is checked before anything is encrypted.** Seal reads key-server public keys from chain but fetches key shares from the operator, so a wrong credential would let `encrypt` succeed and fail only at `decrypt`, leaving the user paying to store content nobody can read. That exact shape shipped once as the mainnet Seal default, and an unchecked API key would have reintroduced it user-side. Only an explicit `401` or `403` refuses; any other outcome, including the check being unreachable, proceeds, because a flaky pre-flight must never block a working key.
|
|
24
|
+
|
|
25
|
+
Verified live: a full encrypt and decrypt round trip on testnet through `MORSE_SEAL_KEY_SERVERS` (the same code path a credential takes), and a bogus `MORSE_SEAL_API_KEY` refused against the real Mysten aggregator with the pre-flight message rather than producing unreadable ciphertext.
|
|
26
|
+
|
|
7
27
|
## [0.9.0] - 2026-09-10
|
|
8
28
|
### Fixed (config state, found in a whole-package bug hunt)
|
|
9
29
|
|
package/README.md
CHANGED
|
@@ -6,15 +6,14 @@ content entries from your terminal, signing with a locally encrypted key.
|
|
|
6
6
|
Content is stored on [Walrus](https://walrus.xyz); private entries are encrypted
|
|
7
7
|
with [Seal](https://github.com/MystenLabs/seal).
|
|
8
8
|
|
|
9
|
-
> Status: v0.
|
|
9
|
+
> Status: v0.10.1. Mainnet and testnet are both supported for public content;
|
|
10
10
|
> the command surface is stable.
|
|
11
11
|
>
|
|
12
|
-
> **Encrypted commands
|
|
13
|
-
>
|
|
14
|
-
>
|
|
15
|
-
>
|
|
16
|
-
>
|
|
17
|
-
> mainnet with an explanation.
|
|
12
|
+
> **Encrypted commands need Seal key servers.** Testnet has an open set, so
|
|
13
|
+
> they work there out of the box. Mainnet has none, because every mainnet Seal
|
|
14
|
+
> operator is commercial: supply your own credential via `MORSE_SEAL_API_KEY`
|
|
15
|
+
> (see [Seal on mainnet](#seal-on-mainnet)), or these commands exit 2 with an
|
|
16
|
+
> explanation.
|
|
18
17
|
>
|
|
19
18
|
> Everything that does not encrypt works on both networks, including
|
|
20
19
|
> `file upload --public`, `file download` of a public file, and the whole
|
|
@@ -114,6 +113,9 @@ Environment variables (override the config file, overridden by flags):
|
|
|
114
113
|
| `MORSE_NETWORK` | `--network` | `mainnet`, `testnet`, or `localnet`. |
|
|
115
114
|
| `MORSE_WALRUS_UPLOAD_RELAY` | `--upload-relay` | Upload through a Walrus relay instead of the direct fanout. `auto` picks the canonical relay for the network. |
|
|
116
115
|
| `MORSE_WALRUS_MAX_TIP` | `--max-tip` | Cap the relay's per-upload tip, in MIST. Defaults to 10000000 (0.01 SUI). |
|
|
116
|
+
| `MORSE_SEAL_API_KEY` | (none) | Credential for Mysten's Seal committee, enabling encrypted commands on mainnet. |
|
|
117
|
+
| `MORSE_SEAL_API_KEY_NAME` | (none) | Header name for that credential. Defaults to `X-API-Key`. |
|
|
118
|
+
| `MORSE_SEAL_KEY_SERVERS` | (none) | JSON array of `{ objectId, weight }` for a non-committee Seal operator. Wins over `MORSE_SEAL_API_KEY`. |
|
|
117
119
|
| `MORSE_RPC_URL` | `--rpc` | Sui RPC URL override. |
|
|
118
120
|
| `MORSE_ADDRESS` | (no flag) | Active account address, selecting which keystore to use. |
|
|
119
121
|
| `MORSE_PUBLICATION` | `-P, --publication` | Active publication id. |
|
|
@@ -372,6 +374,41 @@ for works. These are aliases, not separate commands:
|
|
|
372
374
|
|
|
373
375
|
`publication` also answers to `pub`.
|
|
374
376
|
|
|
377
|
+
## Seal on mainnet
|
|
378
|
+
|
|
379
|
+
Encrypted commands need Seal key servers. Testnet pins an open set, so they
|
|
380
|
+
work there with no setup. Mainnet pins none: every mainnet Seal operator is
|
|
381
|
+
commercial, so you bring your own.
|
|
382
|
+
|
|
383
|
+
The usual route is a credential for Mysten's decentralized committee, which
|
|
384
|
+
the CLI already knows the object id and aggregator for:
|
|
385
|
+
|
|
386
|
+
```sh
|
|
387
|
+
export MORSE_SEAL_API_KEY=your-key-here
|
|
388
|
+
morse --network mainnet file upload ./secret.pdf --encrypt -r 0xrecipient
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
For an operator that is not the committee, give the servers directly:
|
|
392
|
+
|
|
393
|
+
```sh
|
|
394
|
+
export MORSE_SEAL_KEY_SERVERS='[{"objectId":"0x...","weight":1}]'
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Entries may carry `aggregatorUrl`, `apiKeyName` and `apiKey`; Seal requires
|
|
398
|
+
`aggregatorUrl` on a committee-mode server and forbids it on an independent
|
|
399
|
+
one. `MORSE_SEAL_KEY_SERVERS` takes precedence when both are set.
|
|
400
|
+
|
|
401
|
+
**Neither is a flag, deliberately.** A credential in argv leaks through `ps`
|
|
402
|
+
and shell history, the same reason the signing key is never a flag either.
|
|
403
|
+
|
|
404
|
+
Before encrypting anything, the CLI checks the credential against the operator
|
|
405
|
+
and refuses if it comes back `401` or `403`. This matters more than it looks:
|
|
406
|
+
Seal reads key-server public keys from chain but fetches key *shares* from the
|
|
407
|
+
operator, so a bad credential would let encryption succeed and fail only at
|
|
408
|
+
decryption, leaving you paying to store content nobody can ever read. Any other
|
|
409
|
+
outcome, including the check being offline, proceeds normally, because a flaky
|
|
410
|
+
check must never block a working key.
|
|
411
|
+
|
|
375
412
|
## When uploads fail: the upload relay
|
|
376
413
|
|
|
377
414
|
A Walrus upload pushes slivers to every storage node in the committee at once,
|
package/dist/index.js
CHANGED
|
@@ -148,7 +148,7 @@ import { Command } from "commander";
|
|
|
148
148
|
// package.json
|
|
149
149
|
var package_default = {
|
|
150
150
|
name: "@arcadiasystems/morse-cli",
|
|
151
|
-
version: "0.
|
|
151
|
+
version: "0.10.1",
|
|
152
152
|
description: "Command-line interface for the Morse decentralized CMS on Sui.",
|
|
153
153
|
license: "MIT",
|
|
154
154
|
type: "module",
|
|
@@ -191,6 +191,7 @@ var package_default = {
|
|
|
191
191
|
start: "bun src/index.ts",
|
|
192
192
|
build: "bun build src/index.ts --target node --packages external --outfile dist/index.js",
|
|
193
193
|
typecheck: "tsc --noEmit",
|
|
194
|
+
"docs-gate": "bun scripts/docs-gate.ts",
|
|
194
195
|
lint: "biome check .",
|
|
195
196
|
"lint:fix": "biome check --write .",
|
|
196
197
|
test: "bun test",
|
|
@@ -199,11 +200,11 @@ var package_default = {
|
|
|
199
200
|
"test:coverage": "bun test --coverage",
|
|
200
201
|
"test:e2e": "MORSE_E2E=1 bun test test/e2e/",
|
|
201
202
|
coverage: "bun scripts/coverage-gate.ts",
|
|
202
|
-
check: "tsc --noEmit && biome check . && bun scripts/coverage-gate.ts",
|
|
203
|
+
check: "tsc --noEmit && biome check . && bun scripts/docs-gate.ts && bun scripts/coverage-gate.ts",
|
|
203
204
|
prepublishOnly: "bun run check && bun run build"
|
|
204
205
|
},
|
|
205
206
|
dependencies: {
|
|
206
|
-
"@arcadiasystems/morse-sdk": "^0.8.
|
|
207
|
+
"@arcadiasystems/morse-sdk": "^0.8.1",
|
|
207
208
|
"@mysten/seal": "1.1.3",
|
|
208
209
|
"@mysten/sui": "2.16.2",
|
|
209
210
|
"@mysten/walrus": "1.1.6",
|
|
@@ -989,6 +990,72 @@ import {
|
|
|
989
990
|
} from "@arcadiasystems/morse-sdk";
|
|
990
991
|
import { SuiGrpcClient } from "@mysten/sui/grpc";
|
|
991
992
|
|
|
993
|
+
// src/config/seal.ts
|
|
994
|
+
import { MAINNET_SEAL_COMMITTEE } from "@arcadiasystems/morse-sdk";
|
|
995
|
+
var DEFAULT_API_KEY_NAME = "X-API-Key";
|
|
996
|
+
function sealServersFromEnv(env = process.env) {
|
|
997
|
+
const explicit = env.MORSE_SEAL_KEY_SERVERS;
|
|
998
|
+
if (explicit !== undefined && explicit.length > 0) {
|
|
999
|
+
return { servers: parseKeyServers(explicit) };
|
|
1000
|
+
}
|
|
1001
|
+
const apiKey = env.MORSE_SEAL_API_KEY;
|
|
1002
|
+
if (apiKey === undefined || apiKey.length === 0) {
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
const apiKeyName = env.MORSE_SEAL_API_KEY_NAME ?? DEFAULT_API_KEY_NAME;
|
|
1006
|
+
return {
|
|
1007
|
+
servers: MAINNET_SEAL_COMMITTEE.map((server) => ({
|
|
1008
|
+
...server,
|
|
1009
|
+
apiKeyName,
|
|
1010
|
+
apiKey
|
|
1011
|
+
})),
|
|
1012
|
+
...MAINNET_SEAL_COMMITTEE[0]?.aggregatorUrl === undefined ? {} : { verifyUrl: MAINNET_SEAL_COMMITTEE[0].aggregatorUrl },
|
|
1013
|
+
apiKeyName,
|
|
1014
|
+
apiKey
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
async function assertSealCredentialAccepted(env, fetchImpl = fetch) {
|
|
1018
|
+
if (env.verifyUrl === undefined || env.apiKey === undefined) {
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
let status;
|
|
1022
|
+
try {
|
|
1023
|
+
const response = await fetchImpl(`${env.verifyUrl}/v1/service`, {
|
|
1024
|
+
headers: { [env.apiKeyName ?? DEFAULT_API_KEY_NAME]: env.apiKey }
|
|
1025
|
+
});
|
|
1026
|
+
status = response.status;
|
|
1027
|
+
} catch {
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
if (status === 401 || status === 403) {
|
|
1031
|
+
throw new UsageError(`The Seal operator at ${env.verifyUrl} rejected your credential (HTTP ${status}). Encrypting anyway would produce content you could never decrypt, so this stops here. Check MORSE_SEAL_API_KEY.`);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
function parseKeyServers(raw) {
|
|
1035
|
+
let parsed;
|
|
1036
|
+
try {
|
|
1037
|
+
parsed = JSON.parse(raw);
|
|
1038
|
+
} catch (cause) {
|
|
1039
|
+
throw new UsageError(`MORSE_SEAL_KEY_SERVERS is not valid JSON: ${String(cause)}`);
|
|
1040
|
+
}
|
|
1041
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
1042
|
+
throw new UsageError('MORSE_SEAL_KEY_SERVERS must be a non-empty JSON array of { objectId, weight } entries, e.g. [{"objectId":"0x...","weight":1}].');
|
|
1043
|
+
}
|
|
1044
|
+
return parsed.map((entry, index) => {
|
|
1045
|
+
if (typeof entry !== "object" || entry === null) {
|
|
1046
|
+
throw new UsageError(`MORSE_SEAL_KEY_SERVERS[${index}] is not an object.`);
|
|
1047
|
+
}
|
|
1048
|
+
const { objectId, weight } = entry;
|
|
1049
|
+
if (typeof objectId !== "string" || !objectId.startsWith("0x")) {
|
|
1050
|
+
throw new UsageError(`MORSE_SEAL_KEY_SERVERS[${index}].objectId must be a 0x-prefixed object id.`);
|
|
1051
|
+
}
|
|
1052
|
+
if (typeof weight !== "number" || !Number.isInteger(weight) || weight < 1) {
|
|
1053
|
+
throw new UsageError(`MORSE_SEAL_KEY_SERVERS[${index}].weight must be a positive integer.`);
|
|
1054
|
+
}
|
|
1055
|
+
return entry;
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
|
|
992
1059
|
// src/cli/graphql-events.ts
|
|
993
1060
|
function canonicalGraphqlUrl(network) {
|
|
994
1061
|
if (network === "mainnet" || network === "testnet") {
|
|
@@ -1114,11 +1181,12 @@ async function buildContentContext(command) {
|
|
|
1114
1181
|
walrus
|
|
1115
1182
|
};
|
|
1116
1183
|
}
|
|
1117
|
-
function
|
|
1118
|
-
|
|
1119
|
-
|
|
1184
|
+
function buildSeal(config, client) {
|
|
1185
|
+
const fromEnv = sealServersFromEnv();
|
|
1186
|
+
if (fromEnv === undefined && config.sealKeyServers.length === 0) {
|
|
1187
|
+
throw new CliError(`Encrypted commands are not available on ${config.network}: it pins no Seal key servers, because every operator there is commercial. Set MORSE_SEAL_API_KEY if you have a credential for Mysten's committee, or MORSE_SEAL_KEY_SERVERS to a JSON array for another operator. Otherwise use --network testnet.`, ExitCode.Usage);
|
|
1120
1188
|
}
|
|
1121
|
-
|
|
1189
|
+
return DefaultSealAdapter.fromMorseConfig(config, fromEnv === undefined ? {} : { serverConfigs: fromEnv.servers }, client);
|
|
1122
1190
|
}
|
|
1123
1191
|
function walrusWriteConfig(base, network) {
|
|
1124
1192
|
const host = resolveUploadRelay(base.settings.uploadRelay, network);
|
|
@@ -1133,6 +1201,10 @@ function walrusWriteConfig(base, network) {
|
|
|
1133
1201
|
}
|
|
1134
1202
|
async function buildEncryptContext(command) {
|
|
1135
1203
|
const ctx = await buildContentContext(command);
|
|
1204
|
+
const fromEnv = sealServersFromEnv();
|
|
1205
|
+
if (fromEnv !== undefined) {
|
|
1206
|
+
await assertSealCredentialAccepted(fromEnv);
|
|
1207
|
+
}
|
|
1136
1208
|
return { ...ctx, seal: lazySeal(ctx) };
|
|
1137
1209
|
}
|
|
1138
1210
|
function walrusReadAdapter(base, network, viaAggregator) {
|
|
@@ -1161,8 +1233,7 @@ async function buildDecryptContext(command, opts = {}) {
|
|
|
1161
1233
|
if (network === "localnet") {
|
|
1162
1234
|
throw new CliError("Seal decryption is not available on localnet. Use testnet or mainnet.", ExitCode.Usage);
|
|
1163
1235
|
}
|
|
1164
|
-
|
|
1165
|
-
const seal = DefaultSealAdapter.fromMorseConfig(base.config, {}, base.client);
|
|
1236
|
+
const seal = buildSeal(base.config, base.client);
|
|
1166
1237
|
return {
|
|
1167
1238
|
...base,
|
|
1168
1239
|
keypair,
|
|
@@ -1190,8 +1261,7 @@ async function buildFileDownloadContext(command, opts = {}) {
|
|
|
1190
1261
|
function lazySeal(base) {
|
|
1191
1262
|
let adapter;
|
|
1192
1263
|
return () => {
|
|
1193
|
-
|
|
1194
|
-
adapter ??= DefaultSealAdapter.fromMorseConfig(base.config, {}, base.client);
|
|
1264
|
+
adapter ??= buildSeal(base.config, base.client);
|
|
1195
1265
|
return adapter;
|
|
1196
1266
|
};
|
|
1197
1267
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcadiasystems/morse-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Command-line interface for the Morse decentralized CMS on Sui.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"start": "bun src/index.ts",
|
|
44
44
|
"build": "bun build src/index.ts --target node --packages external --outfile dist/index.js",
|
|
45
45
|
"typecheck": "tsc --noEmit",
|
|
46
|
+
"docs-gate": "bun scripts/docs-gate.ts",
|
|
46
47
|
"lint": "biome check .",
|
|
47
48
|
"lint:fix": "biome check --write .",
|
|
48
49
|
"test": "bun test",
|
|
@@ -51,11 +52,11 @@
|
|
|
51
52
|
"test:coverage": "bun test --coverage",
|
|
52
53
|
"test:e2e": "MORSE_E2E=1 bun test test/e2e/",
|
|
53
54
|
"coverage": "bun scripts/coverage-gate.ts",
|
|
54
|
-
"check": "tsc --noEmit && biome check . && bun scripts/coverage-gate.ts",
|
|
55
|
+
"check": "tsc --noEmit && biome check . && bun scripts/docs-gate.ts && bun scripts/coverage-gate.ts",
|
|
55
56
|
"prepublishOnly": "bun run check && bun run build"
|
|
56
57
|
},
|
|
57
58
|
"dependencies": {
|
|
58
|
-
"@arcadiasystems/morse-sdk": "^0.8.
|
|
59
|
+
"@arcadiasystems/morse-sdk": "^0.8.1",
|
|
59
60
|
"@mysten/seal": "1.1.3",
|
|
60
61
|
"@mysten/sui": "2.16.2",
|
|
61
62
|
"@mysten/walrus": "1.1.6",
|