@arcadiasystems/morse-cli 0.2.0 → 0.4.0
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 +67 -0
- package/README.md +36 -30
- package/dist/index.js +387 -346
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,73 @@ 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.4.0] - 2026-06-04
|
|
8
|
+
|
|
9
|
+
Ports the file commands to the SDK 0.4.0 `RecipientFile` primitive, which
|
|
10
|
+
replaces the per-wallet allowlist plus encrypted-file pair with a single object
|
|
11
|
+
that carries its recipient list inline. This is a breaking change to the `morse
|
|
12
|
+
file` surface and removes `morse allowlist` entirely.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `morse file recipient add|remove|list <file> [address]`: manage a file's
|
|
17
|
+
recipient list directly on the file object (replaces `morse allowlist`).
|
|
18
|
+
- Recipients on uploads and registration via repeatable `-r, --recipient <addr>`;
|
|
19
|
+
the sender is always included.
|
|
20
|
+
- Encrypted uploads emit a **share string** (`mf1.<fileId>.<prefix>.<nonce>`,
|
|
21
|
+
see `format/share.ts`) bundling everything a recipient needs to decrypt, plus
|
|
22
|
+
the raw `sealIdPrefix` and `sealNonce` (hex) in `--json`.
|
|
23
|
+
- `morse file download` accepts `--share <string>`, or `--prefix <hex> --nonce
|
|
24
|
+
<hex>`, to decrypt; the file id becomes an optional positional (the share
|
|
25
|
+
string carries it).
|
|
26
|
+
- `morse file register --encrypted --seal-prefix <hex>` / `--public`: register
|
|
27
|
+
on-chain metadata for an existing Walrus blob as a `RecipientFile`.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- Depends on `@arcadiasystems/morse-sdk` `^0.4.0`.
|
|
32
|
+
- `morse file list` reconciles `RecipientFile` events; `--accessible` now lists
|
|
33
|
+
files you can decrypt as a recipient (recipient-list membership), not allowlist
|
|
34
|
+
membership. The human listing shows a `recipients` count column.
|
|
35
|
+
- `morse file get` shows the recipient list instead of an allowlist reference.
|
|
36
|
+
|
|
37
|
+
### Removed
|
|
38
|
+
|
|
39
|
+
- `morse allowlist` and all its subcommands. Recipient access now lives on the
|
|
40
|
+
file via `morse file recipient`.
|
|
41
|
+
- `file upload -a <allowlist>` and `file download --seal-id <hex>`. Use
|
|
42
|
+
`--encrypt` / `--recipient` on upload and `--share` (or `--prefix`/`--nonce`)
|
|
43
|
+
on download.
|
|
44
|
+
|
|
45
|
+
## [0.3.0] - 2026-06-04
|
|
46
|
+
|
|
47
|
+
Event-based file listing, wrapping the SDK 0.3.0 reconcile helpers. Purely
|
|
48
|
+
additive; all existing commands are unchanged.
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- `morse file list`: list files owned by an address (default: the active
|
|
53
|
+
account), or, with `--accessible`, files decryptable via allowlist membership.
|
|
54
|
+
`--address <addr>` queries another address, `--hydrate` fetches the full record
|
|
55
|
+
per file (adds `blobId`; one read each), `--limit <n>` caps results, `--json`
|
|
56
|
+
emits the summary array.
|
|
57
|
+
- Event fetching via `cli/events.ts`: a paginator over `suix_queryEvents` feeding
|
|
58
|
+
the SDK's pure `reconcileFilesOwnedBy` / `reconcileFilesAccessibleBy` helpers.
|
|
59
|
+
`--indexer-url <url>` overrides the event source (any endpoint that speaks
|
|
60
|
+
`suix_queryEvents`).
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- Depends on `@arcadiasystems/morse-sdk` `^0.3.0`.
|
|
65
|
+
|
|
66
|
+
### Notes
|
|
67
|
+
|
|
68
|
+
- Listing reads `suix_queryEvents`, a deprecated Sui JSON-RPC endpoint Mysten is
|
|
69
|
+
sunsetting; on the public RPC it may degrade over time. Use `--indexer-url` to
|
|
70
|
+
point at your own indexer. Results are best-effort and eventually consistent.
|
|
71
|
+
- Summaries omit `blobId`/`blobObjectId` (not in the `FileCreated` event); use
|
|
72
|
+
`--hydrate` to fetch them.
|
|
73
|
+
|
|
7
74
|
## [0.2.0] - 2026-06-04
|
|
8
75
|
|
|
9
76
|
Wraps the allowlist + encrypted-file surface from `@arcadiasystems/morse-sdk`
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ 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.3.0, targeting Sui testnet. The command surface is stable; mainnet
|
|
10
10
|
> support lands when the contracts are frozen.
|
|
11
11
|
|
|
12
12
|
## Requirements
|
|
@@ -228,46 +228,52 @@ OwnerCap and PublisherCap IDs are auto-resolved from the active account when the
|
|
|
228
228
|
`--owner-cap` / `--publisher-cap` override is omitted. Destructive operations
|
|
229
229
|
(`delete`, `revoke`, `destroy`, `transfer`) confirm interactively unless `--yes`.
|
|
230
230
|
|
|
231
|
-
### allowlist
|
|
232
|
-
|
|
233
|
-
Per-wallet allowlists gate who can decrypt encrypted files. The admin Cap is
|
|
234
|
-
auto-resolved from the active account when `--cap` is omitted.
|
|
235
|
-
|
|
236
|
-
| Command | Purpose |
|
|
237
|
-
| --- | --- |
|
|
238
|
-
| `allowlist create --name <name>` | Create an allowlist; transfers its admin Cap to you. |
|
|
239
|
-
| `allowlist add-member <addr> -a <id> [--cap <id>]` | Add a wallet that may decrypt. |
|
|
240
|
-
| `allowlist remove-member <addr> -a <id> [--cap <id>]` | Remove a wallet. |
|
|
241
|
-
| `allowlist transfer-cap <recipient> -a <id> [--cap <id>] [-y]` | Hand off admin rights. |
|
|
242
|
-
| `allowlist delete -a <id> [--cap <id>] [-y]` | Delete an allowlist (dependent files become undecryptable). |
|
|
243
|
-
| `allowlist get <id>` | Show an allowlist's name and members. |
|
|
244
|
-
| `allowlist list-caps [address]` | List allowlist admin Caps held by an address. |
|
|
245
|
-
|
|
246
231
|
### file
|
|
247
232
|
|
|
233
|
+
A `RecipientFile` is a single object that carries its own recipient list: the
|
|
234
|
+
addresses allowed to decrypt it live inline on the file, managed by its owner.
|
|
235
|
+
There is no separate allowlist object.
|
|
236
|
+
|
|
248
237
|
| Command | Purpose |
|
|
249
238
|
| --- | --- |
|
|
250
|
-
| `file upload <path> --name <n>
|
|
251
|
-
| `file register --blob-id <id> --name <n> --content-type <m> --size <bytes>
|
|
252
|
-
| `file download
|
|
253
|
-
| `file
|
|
239
|
+
| `file upload <path> --name <n> (--public \| --encrypt \| -r <addr>...) [--content-type <m>] [--epochs <n>]` | Upload to Walrus and register. `--public` is world-readable; `--encrypt` (or supplying `-r`) encrypts, and the sender is always a recipient. One of the three is required. |
|
|
240
|
+
| `file register --blob-id <id> --name <n> --content-type <m> --size <bytes> (--public \| --encrypted --seal-prefix <hex>) [-r <addr>...] [--blob-object-id <id>]` | Register metadata for a blob already on Walrus. |
|
|
241
|
+
| `file download [file] [--out <path>] [--share <s>] [--prefix <hex> --nonce <hex>] [--via-aggregator]` | Download content; decrypts in place with a share string (or prefix + nonce). |
|
|
242
|
+
| `file list [--address <addr>] [--accessible] [--hydrate] [--limit <n>] [--indexer-url <url>]` | List files owned by (or, with `--accessible`, decryptable by) an address. |
|
|
243
|
+
| `file get <file>` | Fetch a file's on-chain metadata and recipient list. |
|
|
254
244
|
| `file update <file> --name <n> --content-type <m>` | Update name and MIME (owner only). |
|
|
255
|
-
| `file transfer-ownership <file> <newOwner> [-y]` | Transfer
|
|
245
|
+
| `file transfer-ownership <file> <newOwner> [-y]` | Transfer ownership (not recipient access, which the list governs). |
|
|
256
246
|
| `file delete <file> [-y]` | Delete the metadata record (the Walrus blob expires on its own lease). |
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
247
|
+
| `file recipient add <file> <address>` | Allow an address to decrypt the file. |
|
|
248
|
+
| `file recipient remove <file> <address>` | Revoke an address's access. |
|
|
249
|
+
| `file recipient list <file>` | List the file's recipients. |
|
|
250
|
+
|
|
251
|
+
An encrypted `file upload` prints a **share string** (`mf1.<fileId>.<prefix>.<nonce>`)
|
|
252
|
+
that bundles the file id and the Seal prefix and nonce. The prefix and nonce are
|
|
253
|
+
not recoverable from the ciphertext, so save the share string: `file download
|
|
254
|
+
--share <s>` needs it (plus being on the recipient list) to decrypt. `--json`
|
|
255
|
+
also returns the raw `sealIdPrefix` and `sealNonce` as hex, usable as `--prefix`
|
|
256
|
+
/ `--nonce` if you prefer to pass them separately.
|
|
257
|
+
|
|
258
|
+
`file list` reconstructs the file set from contract events. `RecipientFile`
|
|
259
|
+
objects have no on-chain owner index for the shared case, so listing is
|
|
260
|
+
event-derived, not a direct query. By default the command reads events via
|
|
261
|
+
`suix_queryEvents` on the configured Sui RPC. That endpoint is **deprecated**
|
|
262
|
+
(Mysten is sunsetting it), so listing may degrade or stop working on the public
|
|
263
|
+
RPC over time; point `--indexer-url <url>` at any source that speaks
|
|
264
|
+
`suix_queryEvents` (a self-hosted indexer, a third-party endpoint) to stay in
|
|
265
|
+
control. Results are best-effort and eventually consistent (subject to indexer
|
|
266
|
+
lag and retention). Summary rows omit `blobId`/`blobObjectId`; add `--hydrate`
|
|
267
|
+
to fetch the full record per file (one read each) when you need them.
|
|
263
268
|
|
|
264
269
|
## Output and scripting
|
|
265
270
|
|
|
266
271
|
- Human-readable output goes to stdout; progress, warnings, and prompts go to
|
|
267
272
|
stderr. Redirecting stdout yields only the result.
|
|
268
273
|
- `--json` emits a single JSON document on stdout (nothing else). `bigint` values
|
|
269
|
-
(e.g. `gasUsedMist`) are encoded as decimal strings and byte arrays (
|
|
270
|
-
|
|
274
|
+
(e.g. `gasUsedMist`) are encoded as decimal strings and byte arrays (quilt patch
|
|
275
|
+
ids) as `0x`-hex. Encrypted uploads also return `sealIdPrefix` / `sealNonce` as
|
|
276
|
+
hex and a `share` string.
|
|
271
277
|
- Color is emitted only to a TTY and honors `NO_COLOR` and `FORCE_COLOR`.
|
|
272
278
|
|
|
273
279
|
## Exit codes
|
|
@@ -290,7 +296,7 @@ no owner index); track file ids yourself or index the contract events.
|
|
|
290
296
|
- [`encrypt-decrypt.sh`](./examples/encrypt-decrypt.sh): encrypt with Seal and decrypt back.
|
|
291
297
|
- [`delegation.sh`](./examples/delegation.sh): issue a PublisherCap to a delegate, then revoke it.
|
|
292
298
|
- [`ci-noninteractive.sh`](./examples/ci-noninteractive.sh): env-var auth, `--yes`, and `--json` parsing.
|
|
293
|
-
- [`files.sh`](./examples/files.sh):
|
|
299
|
+
- [`files.sh`](./examples/files.sh): RecipientFile round-trip (upload an encrypted file, download/decrypt via its share string, manage recipients, plus a public file).
|
|
294
300
|
|
|
295
301
|
## Limitations
|
|
296
302
|
|
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.4.0",
|
|
152
152
|
description: "Command-line interface for the Morse decentralized CMS on Sui.",
|
|
153
153
|
license: "MIT",
|
|
154
154
|
type: "module",
|
|
@@ -203,7 +203,7 @@ var package_default = {
|
|
|
203
203
|
prepublishOnly: "bun run check && bun run build"
|
|
204
204
|
},
|
|
205
205
|
dependencies: {
|
|
206
|
-
"@arcadiasystems/morse-sdk": "^0.
|
|
206
|
+
"@arcadiasystems/morse-sdk": "^0.4.0",
|
|
207
207
|
"@mysten/seal": "1.1.3",
|
|
208
208
|
"@mysten/sui": "2.16.2",
|
|
209
209
|
"@mysten/walrus": "1.1.6",
|
|
@@ -889,29 +889,30 @@ async function resolveActiveAccount(gopts) {
|
|
|
889
889
|
return accountAddress(resolveSettings(gopts, await loadConfig()).account);
|
|
890
890
|
}
|
|
891
891
|
|
|
892
|
-
// src/commands/
|
|
892
|
+
// src/commands/cap.ts
|
|
893
893
|
import {
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
toAllowlistId,
|
|
894
|
+
destroyPublisherCap,
|
|
895
|
+
issuePublisherCap,
|
|
896
|
+
revokePublisherCap,
|
|
897
|
+
toPublisherCapId as toPublisherCapId2,
|
|
899
898
|
toSuiAddress as toSuiAddress3,
|
|
900
|
-
|
|
899
|
+
transferPublisherCap
|
|
901
900
|
} from "@arcadiasystems/morse-sdk";
|
|
902
901
|
|
|
903
902
|
// src/cli/context.ts
|
|
904
903
|
import {
|
|
904
|
+
buildRecipientFileEventTypes,
|
|
905
905
|
DefaultSealAdapter,
|
|
906
906
|
DefaultWalrusReadAdapter,
|
|
907
907
|
DefaultWalrusWriteAdapter,
|
|
908
908
|
HttpAggregatorReadAdapter,
|
|
909
909
|
KeypairAdapter,
|
|
910
910
|
morseConfig,
|
|
911
|
-
|
|
912
|
-
|
|
911
|
+
RpcPublicationReader,
|
|
912
|
+
RpcRecipientFilesReader
|
|
913
913
|
} from "@arcadiasystems/morse-sdk";
|
|
914
914
|
import { SuiGrpcClient } from "@mysten/sui/grpc";
|
|
915
|
+
import { SuiJsonRpcClient } from "@mysten/sui/jsonRpc";
|
|
915
916
|
async function buildReadContext(command) {
|
|
916
917
|
const opts = globalOptions(command);
|
|
917
918
|
const output = outputFor(command);
|
|
@@ -952,14 +953,9 @@ async function buildFilesReadContext(command) {
|
|
|
952
953
|
const base = await buildReadContext(command);
|
|
953
954
|
return {
|
|
954
955
|
...base,
|
|
955
|
-
filesReader:
|
|
956
|
-
|
|
957
|
-
}
|
|
958
|
-
async function buildAllowlistWriteContext(command) {
|
|
959
|
-
const base = await buildWriteContext(command);
|
|
960
|
-
return {
|
|
961
|
-
...base,
|
|
962
|
-
filesReader: RpcFilesReader.fromMorseConfig(base.config, base.client)
|
|
956
|
+
filesReader: RpcRecipientFilesReader.fromConfig(base.client, {
|
|
957
|
+
packageId: base.config.packageId
|
|
958
|
+
})
|
|
963
959
|
};
|
|
964
960
|
}
|
|
965
961
|
async function buildContentContext(command) {
|
|
@@ -1020,12 +1016,77 @@ async function buildFileDownloadContext(command, opts = {}) {
|
|
|
1020
1016
|
}
|
|
1021
1017
|
return {
|
|
1022
1018
|
...base,
|
|
1023
|
-
filesReader:
|
|
1019
|
+
filesReader: RpcRecipientFilesReader.fromConfig(base.client, {
|
|
1020
|
+
packageId: base.config.packageId
|
|
1021
|
+
}),
|
|
1024
1022
|
walrusRead: walrusReadAdapter(base, network, Boolean(opts.viaAggregator)),
|
|
1025
1023
|
seal: DefaultSealAdapter.fromMorseConfig(base.config, {}, base.client),
|
|
1026
1024
|
unlockSigner: () => resolveSigner(base.settings.account, process.env, base.signal)
|
|
1027
1025
|
};
|
|
1028
1026
|
}
|
|
1027
|
+
async function buildFileListContext(command, opts = {}) {
|
|
1028
|
+
const base = await buildReadContext(command);
|
|
1029
|
+
const originPackageId = base.config.recipientFileEventOriginPackageId;
|
|
1030
|
+
if (originPackageId === undefined) {
|
|
1031
|
+
throw new CliError("File listing is unavailable on this network: no recipientFileEventOriginPackageId in the config. Use testnet, or supply a config that sets it.", ExitCode.Usage);
|
|
1032
|
+
}
|
|
1033
|
+
const events = new SuiJsonRpcClient({
|
|
1034
|
+
network: base.settings.network,
|
|
1035
|
+
url: opts.indexerUrl ?? base.config.rpcUrl
|
|
1036
|
+
});
|
|
1037
|
+
return {
|
|
1038
|
+
...base,
|
|
1039
|
+
filesReader: RpcRecipientFilesReader.fromConfig(base.client, {
|
|
1040
|
+
packageId: base.config.packageId
|
|
1041
|
+
}),
|
|
1042
|
+
events,
|
|
1043
|
+
eventTypes: buildRecipientFileEventTypes(originPackageId)
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// src/cli/target.ts
|
|
1048
|
+
import { toPublicationId } from "@arcadiasystems/morse-sdk";
|
|
1049
|
+
var OBJECT_ID = /^0x[0-9a-f]{1,64}$/i;
|
|
1050
|
+
var SLUG_PAGE_LIMIT = 50;
|
|
1051
|
+
async function resolvePublication(ctx, override) {
|
|
1052
|
+
const value = override ?? ctx.settings.publication;
|
|
1053
|
+
if (value === undefined) {
|
|
1054
|
+
throw new UsageError("No publication selected. Pass --publication <slug|id> or run `morse use <slug|id>`.");
|
|
1055
|
+
}
|
|
1056
|
+
if (OBJECT_ID.test(value)) {
|
|
1057
|
+
return toPublicationId(value.toLowerCase());
|
|
1058
|
+
}
|
|
1059
|
+
return resolveSlug(ctx, value);
|
|
1060
|
+
}
|
|
1061
|
+
function resolveCollection(ctx, override) {
|
|
1062
|
+
const value = override ?? ctx.settings.collection;
|
|
1063
|
+
if (value === undefined) {
|
|
1064
|
+
throw new UsageError("No collection selected. Pass --collection <name> or run `morse use <slug|id> <collection>`.");
|
|
1065
|
+
}
|
|
1066
|
+
return value;
|
|
1067
|
+
}
|
|
1068
|
+
async function resolveSlug(ctx, slug) {
|
|
1069
|
+
if (ctx.ownerAddress === undefined) {
|
|
1070
|
+
throw new UsageError(`Cannot resolve the slug "${slug}" without an active account. Pass the publication id, or select an account.`);
|
|
1071
|
+
}
|
|
1072
|
+
ctx.output.info(`Resolving slug "${slug}" among owned publications...`);
|
|
1073
|
+
let cursor;
|
|
1074
|
+
do {
|
|
1075
|
+
const page = await ctx.reader.listPublicationsOwnedBy(ctx.ownerAddress, {
|
|
1076
|
+
limit: SLUG_PAGE_LIMIT,
|
|
1077
|
+
signal: ctx.signal,
|
|
1078
|
+
...cursor === undefined ? {} : { cursor }
|
|
1079
|
+
});
|
|
1080
|
+
for (const owned of page.results) {
|
|
1081
|
+
const publication = await ctx.reader.getPublication(owned.publicationId, ctx.signal);
|
|
1082
|
+
if (publication.slug === slug) {
|
|
1083
|
+
return publication.id;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
cursor = page.nextCursor ?? undefined;
|
|
1087
|
+
} while (cursor !== undefined);
|
|
1088
|
+
throw new UsageError(`No publication with slug "${slug}" owned by the active account. Pass the publication id instead.`);
|
|
1089
|
+
}
|
|
1029
1090
|
|
|
1030
1091
|
// src/format/ids.ts
|
|
1031
1092
|
var HEAD = 6;
|
|
@@ -1094,36 +1155,30 @@ function renderEntryList(entries) {
|
|
|
1094
1155
|
return entries.map((entry) => `#${entry.id} ${entry.name} (${entry.revisions.length} revisions)`).join(`
|
|
1095
1156
|
`);
|
|
1096
1157
|
}
|
|
1097
|
-
function
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
return [
|
|
1101
|
-
`${allowlist.name} (${shortId(allowlist.id)})`,
|
|
1102
|
-
`members (${allowlist.members.length}):`,
|
|
1103
|
-
members
|
|
1104
|
-
].join(`
|
|
1105
|
-
`);
|
|
1106
|
-
}
|
|
1107
|
-
function renderAllowlistCapList(caps) {
|
|
1108
|
-
if (caps.length === 0) {
|
|
1109
|
-
return "No allowlist caps held by this address.";
|
|
1158
|
+
function renderFileList(items) {
|
|
1159
|
+
if (items.length === 0) {
|
|
1160
|
+
return "No files.";
|
|
1110
1161
|
}
|
|
1111
|
-
|
|
1162
|
+
const header = "id name size recipients created";
|
|
1163
|
+
const rows = items.map((file) => {
|
|
1164
|
+
const created = new Date(file.createdAtMs).toISOString().slice(0, 10);
|
|
1165
|
+
return `${shortId(file.id)} ${file.name} ${file.size} ${file.members.length} ${created}`;
|
|
1166
|
+
});
|
|
1167
|
+
return [header, ...rows].join(`
|
|
1112
1168
|
`);
|
|
1113
1169
|
}
|
|
1114
|
-
function
|
|
1115
|
-
const
|
|
1170
|
+
function renderRecipientFile(file) {
|
|
1171
|
+
const recipients = file.members.length === 0 ? "(none)" : file.members.map((m) => ` ${m}`).join(`
|
|
1172
|
+
`);
|
|
1173
|
+
return [
|
|
1116
1174
|
`${file.name} (${shortId(file.id)})`,
|
|
1117
1175
|
`contentType: ${file.contentType}`,
|
|
1118
1176
|
`size: ${file.size}`,
|
|
1119
|
-
`encrypted: ${file.encrypted}`,
|
|
1120
1177
|
`blobId: ${file.blobId}`,
|
|
1121
|
-
`owner: ${file.owner}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
}
|
|
1126
|
-
return lines.join(`
|
|
1178
|
+
`owner: ${file.owner}`,
|
|
1179
|
+
`recipients (${file.members.length}):`,
|
|
1180
|
+
recipients
|
|
1181
|
+
].join(`
|
|
1127
1182
|
`);
|
|
1128
1183
|
}
|
|
1129
1184
|
function headLabel(value) {
|
|
@@ -1148,11 +1203,11 @@ function publisherCapOption(command) {
|
|
|
1148
1203
|
function ownerCapOption(command) {
|
|
1149
1204
|
return command.option("--owner-cap <id>", "OwnerCap ID (auto-resolved if omitted)");
|
|
1150
1205
|
}
|
|
1151
|
-
function
|
|
1152
|
-
return
|
|
1206
|
+
function collect(value, previous) {
|
|
1207
|
+
return [...previous, value];
|
|
1153
1208
|
}
|
|
1154
|
-
function
|
|
1155
|
-
return command.option("--
|
|
1209
|
+
function recipientOption(command) {
|
|
1210
|
+
return command.option("-r, --recipient <addr>", "Recipient address allowed to decrypt (repeatable); the sender is always added", collect, []);
|
|
1156
1211
|
}
|
|
1157
1212
|
function viaAggregatorOption(command) {
|
|
1158
1213
|
return command.option("--via-aggregator", "Fetch content via the Walrus aggregator HTTP service. More reliable when storage nodes are flaky; no client-side blob verification.");
|
|
@@ -1163,7 +1218,6 @@ function contentOptions(command) {
|
|
|
1163
1218
|
|
|
1164
1219
|
// src/commands/resolve.ts
|
|
1165
1220
|
import {
|
|
1166
|
-
toAllowlistCapId,
|
|
1167
1221
|
toOwnerCapId,
|
|
1168
1222
|
toPublisherCapId
|
|
1169
1223
|
} from "@arcadiasystems/morse-sdk";
|
|
@@ -1206,25 +1260,6 @@ async function resolvePublisherCap(reader, address, publicationId, override, sig
|
|
|
1206
1260
|
} while (cursor !== undefined);
|
|
1207
1261
|
throw new CliError(`No PublisherCap for ${publicationId} held by ${address}. Pass --publisher-cap, or check that the active account holds one.`, ExitCode.NotFound);
|
|
1208
1262
|
}
|
|
1209
|
-
async function resolveAllowlistCap(filesReader, address, allowlistId, override, signal) {
|
|
1210
|
-
if (override !== undefined) {
|
|
1211
|
-
return toAllowlistCapId(override);
|
|
1212
|
-
}
|
|
1213
|
-
let cursor;
|
|
1214
|
-
do {
|
|
1215
|
-
const page = await filesReader.listAllowlistCapsOwnedBy(address, {
|
|
1216
|
-
limit: PAGE_LIMIT,
|
|
1217
|
-
signal,
|
|
1218
|
-
...cursor === undefined ? {} : { cursor }
|
|
1219
|
-
});
|
|
1220
|
-
const match = page.results.find((c) => c.allowlistId === allowlistId);
|
|
1221
|
-
if (match !== undefined) {
|
|
1222
|
-
return match.id;
|
|
1223
|
-
}
|
|
1224
|
-
cursor = page.nextCursor ?? undefined;
|
|
1225
|
-
} while (cursor !== undefined);
|
|
1226
|
-
throw new CliError(`No allowlist Cap for ${allowlistId} held by ${address}. Pass --cap, or check that the active account administers it.`, ExitCode.NotFound);
|
|
1227
|
-
}
|
|
1228
1263
|
|
|
1229
1264
|
// src/commands/shared.ts
|
|
1230
1265
|
import { StorageMode } from "@arcadiasystems/morse-sdk";
|
|
@@ -1249,7 +1284,7 @@ function parseByteSize(value, name) {
|
|
|
1249
1284
|
if (!/^\d+$/.test(value)) {
|
|
1250
1285
|
throw new UsageError(`${name} must be a non-negative integer, got "${value}".`);
|
|
1251
1286
|
}
|
|
1252
|
-
return
|
|
1287
|
+
return Number(value);
|
|
1253
1288
|
}
|
|
1254
1289
|
function parseId(value, name) {
|
|
1255
1290
|
const id = Number(value);
|
|
@@ -1259,191 +1294,9 @@ function parseId(value, name) {
|
|
|
1259
1294
|
return id;
|
|
1260
1295
|
}
|
|
1261
1296
|
|
|
1262
|
-
// src/commands/allowlist.ts
|
|
1263
|
-
function requireAllowlistId(value) {
|
|
1264
|
-
if (value === undefined) {
|
|
1265
|
-
throw new UsageError("Pass --allowlist <id>.");
|
|
1266
|
-
}
|
|
1267
|
-
return toAllowlistId(value);
|
|
1268
|
-
}
|
|
1269
|
-
async function runAllowlistCreate(ctx, options) {
|
|
1270
|
-
ctx.output.info(`Creating allowlist "${options.name}"...`);
|
|
1271
|
-
const result = await createAllowlist(ctx.adapter, ctx.config, {
|
|
1272
|
-
name: options.name,
|
|
1273
|
-
signal: ctx.signal
|
|
1274
|
-
});
|
|
1275
|
-
const human = [
|
|
1276
|
-
`Created allowlist "${options.name}" (${result.allowlistId})`,
|
|
1277
|
-
` cap: ${result.capId}`,
|
|
1278
|
-
` tx: ${result.digest}`,
|
|
1279
|
-
"Save the cap id: it is the admin token for managing members."
|
|
1280
|
-
].join(`
|
|
1281
|
-
`);
|
|
1282
|
-
ctx.output.result(human, result);
|
|
1283
|
-
}
|
|
1284
|
-
async function runAllowlistAddMember(ctx, member, options) {
|
|
1285
|
-
const allowlistId = requireAllowlistId(options.allowlist);
|
|
1286
|
-
const memberAddress = toSuiAddress3(member);
|
|
1287
|
-
const capId = await resolveAllowlistCap(ctx.filesReader, ctx.address, allowlistId, options.cap, ctx.signal);
|
|
1288
|
-
const result = await addMember(ctx.adapter, ctx.config, {
|
|
1289
|
-
allowlistId,
|
|
1290
|
-
capId,
|
|
1291
|
-
member: memberAddress,
|
|
1292
|
-
signal: ctx.signal
|
|
1293
|
-
});
|
|
1294
|
-
ctx.output.result(`Added ${memberAddress} to allowlist ${shortId(allowlistId)}. (tx: ${result.digest})`, result);
|
|
1295
|
-
}
|
|
1296
|
-
async function runAllowlistRemoveMember(ctx, member, options) {
|
|
1297
|
-
const allowlistId = requireAllowlistId(options.allowlist);
|
|
1298
|
-
const memberAddress = toSuiAddress3(member);
|
|
1299
|
-
const capId = await resolveAllowlistCap(ctx.filesReader, ctx.address, allowlistId, options.cap, ctx.signal);
|
|
1300
|
-
const result = await removeMember(ctx.adapter, ctx.config, {
|
|
1301
|
-
allowlistId,
|
|
1302
|
-
capId,
|
|
1303
|
-
member: memberAddress,
|
|
1304
|
-
signal: ctx.signal
|
|
1305
|
-
});
|
|
1306
|
-
ctx.output.result(`Removed ${memberAddress} from allowlist ${shortId(allowlistId)}. (tx: ${result.digest})`, result);
|
|
1307
|
-
}
|
|
1308
|
-
async function runAllowlistTransferCap(ctx, recipient, options, gopts) {
|
|
1309
|
-
const allowlistId = requireAllowlistId(options.allowlist);
|
|
1310
|
-
const to = toSuiAddress3(recipient);
|
|
1311
|
-
const proceed = await confirm(`Transfer admin of allowlist ${shortId(allowlistId)} to ${to}? You will lose member-management rights.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
1312
|
-
if (!proceed) {
|
|
1313
|
-
cancelled();
|
|
1314
|
-
}
|
|
1315
|
-
ctx.output.info("Resolving allowlist Cap...");
|
|
1316
|
-
const capId = await resolveAllowlistCap(ctx.filesReader, ctx.address, allowlistId, options.cap, ctx.signal);
|
|
1317
|
-
const result = await transferAllowlistCap(ctx.adapter, ctx.config, {
|
|
1318
|
-
capId,
|
|
1319
|
-
recipient: to,
|
|
1320
|
-
signal: ctx.signal
|
|
1321
|
-
});
|
|
1322
|
-
ctx.output.result(`Transferred allowlist admin to ${to}. (tx: ${result.digest})`, result);
|
|
1323
|
-
}
|
|
1324
|
-
async function runAllowlistDelete(ctx, options, gopts) {
|
|
1325
|
-
const allowlistId = requireAllowlistId(options.allowlist);
|
|
1326
|
-
const proceed = await confirm(`Delete allowlist ${shortId(allowlistId)}? Files gated by it become permanently undecryptable.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
1327
|
-
if (!proceed) {
|
|
1328
|
-
cancelled();
|
|
1329
|
-
}
|
|
1330
|
-
ctx.output.info("Resolving allowlist Cap...");
|
|
1331
|
-
const capId = await resolveAllowlistCap(ctx.filesReader, ctx.address, allowlistId, options.cap, ctx.signal);
|
|
1332
|
-
const result = await deleteAllowlist(ctx.adapter, ctx.config, {
|
|
1333
|
-
allowlistId,
|
|
1334
|
-
capId,
|
|
1335
|
-
signal: ctx.signal
|
|
1336
|
-
});
|
|
1337
|
-
ctx.output.result(`Deleted allowlist ${allowlistId}. (tx: ${result.digest})`, result);
|
|
1338
|
-
}
|
|
1339
|
-
async function runAllowlistGet(ctx, target) {
|
|
1340
|
-
const result = await ctx.filesReader.getAllowlist(toAllowlistId(target), ctx.signal);
|
|
1341
|
-
ctx.output.result(renderAllowlist(result), result);
|
|
1342
|
-
}
|
|
1343
|
-
async function runAllowlistListCaps(ctx, address, options) {
|
|
1344
|
-
const holder = address === undefined ? ctx.ownerAddress : toSuiAddress3(address);
|
|
1345
|
-
if (holder === undefined) {
|
|
1346
|
-
throw new UsageError("No address given and no active account. Pass an address or import an account.");
|
|
1347
|
-
}
|
|
1348
|
-
const page = await ctx.filesReader.listAllowlistCapsOwnedBy(holder, {
|
|
1349
|
-
signal: ctx.signal,
|
|
1350
|
-
...options.limit === undefined ? {} : { limit: parseLimit(options.limit) },
|
|
1351
|
-
...options.cursor === undefined ? {} : { cursor: options.cursor }
|
|
1352
|
-
});
|
|
1353
|
-
if (page.nextCursor !== null) {
|
|
1354
|
-
ctx.output.info(`More results: pass --cursor "${page.nextCursor}"`);
|
|
1355
|
-
}
|
|
1356
|
-
ctx.output.result(renderAllowlistCapList(page.results), {
|
|
1357
|
-
results: page.results,
|
|
1358
|
-
nextCursor: page.nextCursor
|
|
1359
|
-
});
|
|
1360
|
-
}
|
|
1361
|
-
function registerAllowlistCommands(program) {
|
|
1362
|
-
const allowlist = program.command("allowlist").description("Manage allowlists that gate decryption of encrypted files");
|
|
1363
|
-
allowlist.command("create").description("Create an allowlist and transfer its admin cap to you").requiredOption("-n, --name <name>", "Allowlist name").action(async (options, command) => {
|
|
1364
|
-
await runAllowlistCreate(await buildWriteContext(command), options);
|
|
1365
|
-
});
|
|
1366
|
-
const addMemberCmd = allowlist.command("add-member <member>").description("Add a wallet address to an allowlist");
|
|
1367
|
-
allowlistCapOption(allowlistOption(addMemberCmd)).action(async (member, options, command) => {
|
|
1368
|
-
await runAllowlistAddMember(await buildAllowlistWriteContext(command), member, options);
|
|
1369
|
-
});
|
|
1370
|
-
const removeMemberCmd = allowlist.command("remove-member <member>").description("Remove a wallet address from an allowlist");
|
|
1371
|
-
allowlistCapOption(allowlistOption(removeMemberCmd)).action(async (member, options, command) => {
|
|
1372
|
-
await runAllowlistRemoveMember(await buildAllowlistWriteContext(command), member, options);
|
|
1373
|
-
});
|
|
1374
|
-
const transferCmd = allowlist.command("transfer-cap <recipient>").description("Transfer allowlist admin rights to another address");
|
|
1375
|
-
allowlistCapOption(allowlistOption(transferCmd)).action(async (recipient, options, command) => {
|
|
1376
|
-
await runAllowlistTransferCap(await buildAllowlistWriteContext(command), recipient, options, globalOptions(command));
|
|
1377
|
-
});
|
|
1378
|
-
const deleteCmd = allowlist.command("delete").description("Delete an allowlist (dependent files become undecryptable)");
|
|
1379
|
-
allowlistCapOption(allowlistOption(deleteCmd)).action(async (options, command) => {
|
|
1380
|
-
await runAllowlistDelete(await buildAllowlistWriteContext(command), options, globalOptions(command));
|
|
1381
|
-
});
|
|
1382
|
-
allowlist.command("get <allowlist>").description("Fetch an allowlist's name and members").action(async (target, _options, command) => {
|
|
1383
|
-
await runAllowlistGet(await buildFilesReadContext(command), target);
|
|
1384
|
-
});
|
|
1385
|
-
allowlist.command("list-caps [address]").description("List allowlist admin caps held by an address (default: the active account)").option("--limit <n>", "Maximum results per page").option("--cursor <cursor>", "Continue from a previous page cursor").action(async (address, options, command) => {
|
|
1386
|
-
await runAllowlistListCaps(await buildFilesReadContext(command), address, options);
|
|
1387
|
-
});
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
// src/commands/cap.ts
|
|
1391
|
-
import {
|
|
1392
|
-
destroyPublisherCap,
|
|
1393
|
-
issuePublisherCap,
|
|
1394
|
-
revokePublisherCap,
|
|
1395
|
-
toPublisherCapId as toPublisherCapId2,
|
|
1396
|
-
toSuiAddress as toSuiAddress4,
|
|
1397
|
-
transferPublisherCap
|
|
1398
|
-
} from "@arcadiasystems/morse-sdk";
|
|
1399
|
-
|
|
1400
|
-
// src/cli/target.ts
|
|
1401
|
-
import { toPublicationId } from "@arcadiasystems/morse-sdk";
|
|
1402
|
-
var OBJECT_ID = /^0x[0-9a-f]{1,64}$/i;
|
|
1403
|
-
var SLUG_PAGE_LIMIT = 50;
|
|
1404
|
-
async function resolvePublication(ctx, override) {
|
|
1405
|
-
const value = override ?? ctx.settings.publication;
|
|
1406
|
-
if (value === undefined) {
|
|
1407
|
-
throw new UsageError("No publication selected. Pass --publication <slug|id> or run `morse use <slug|id>`.");
|
|
1408
|
-
}
|
|
1409
|
-
if (OBJECT_ID.test(value)) {
|
|
1410
|
-
return toPublicationId(value.toLowerCase());
|
|
1411
|
-
}
|
|
1412
|
-
return resolveSlug(ctx, value);
|
|
1413
|
-
}
|
|
1414
|
-
function resolveCollection(ctx, override) {
|
|
1415
|
-
const value = override ?? ctx.settings.collection;
|
|
1416
|
-
if (value === undefined) {
|
|
1417
|
-
throw new UsageError("No collection selected. Pass --collection <name> or run `morse use <slug|id> <collection>`.");
|
|
1418
|
-
}
|
|
1419
|
-
return value;
|
|
1420
|
-
}
|
|
1421
|
-
async function resolveSlug(ctx, slug) {
|
|
1422
|
-
if (ctx.ownerAddress === undefined) {
|
|
1423
|
-
throw new UsageError(`Cannot resolve the slug "${slug}" without an active account. Pass the publication id, or select an account.`);
|
|
1424
|
-
}
|
|
1425
|
-
ctx.output.info(`Resolving slug "${slug}" among owned publications...`);
|
|
1426
|
-
let cursor;
|
|
1427
|
-
do {
|
|
1428
|
-
const page = await ctx.reader.listPublicationsOwnedBy(ctx.ownerAddress, {
|
|
1429
|
-
limit: SLUG_PAGE_LIMIT,
|
|
1430
|
-
signal: ctx.signal,
|
|
1431
|
-
...cursor === undefined ? {} : { cursor }
|
|
1432
|
-
});
|
|
1433
|
-
for (const owned of page.results) {
|
|
1434
|
-
const publication = await ctx.reader.getPublication(owned.publicationId, ctx.signal);
|
|
1435
|
-
if (publication.slug === slug) {
|
|
1436
|
-
return publication.id;
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
cursor = page.nextCursor ?? undefined;
|
|
1440
|
-
} while (cursor !== undefined);
|
|
1441
|
-
throw new UsageError(`No publication with slug "${slug}" owned by the active account. Pass the publication id instead.`);
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
1297
|
// src/commands/cap.ts
|
|
1445
1298
|
async function runCapList(ctx, address, options) {
|
|
1446
|
-
const holder = address === undefined ? ctx.ownerAddress :
|
|
1299
|
+
const holder = address === undefined ? ctx.ownerAddress : toSuiAddress3(address);
|
|
1447
1300
|
if (holder === undefined) {
|
|
1448
1301
|
throw new UsageError("No address given and no active account. Pass an address or import an account.");
|
|
1449
1302
|
}
|
|
@@ -1462,7 +1315,7 @@ async function runCapList(ctx, address, options) {
|
|
|
1462
1315
|
}
|
|
1463
1316
|
async function runCapIssue(ctx, holder, options) {
|
|
1464
1317
|
const id = await resolvePublication(ctx, options.publication);
|
|
1465
|
-
const holderAddress =
|
|
1318
|
+
const holderAddress = toSuiAddress3(holder);
|
|
1466
1319
|
ctx.output.info("Resolving OwnerCap...");
|
|
1467
1320
|
const ownerCapId = await resolveOwnerCap(ctx.reader, ctx.address, id, options.ownerCap, ctx.signal);
|
|
1468
1321
|
const result = await issuePublisherCap(ctx.adapter, ctx.config, {
|
|
@@ -1506,7 +1359,7 @@ async function runCapDestroy(ctx, publisherCapId, options, gopts) {
|
|
|
1506
1359
|
}
|
|
1507
1360
|
async function runCapTransfer(ctx, publisherCapId, recipient, gopts) {
|
|
1508
1361
|
const capId = toPublisherCapId2(publisherCapId);
|
|
1509
|
-
const to =
|
|
1362
|
+
const to = toSuiAddress3(recipient);
|
|
1510
1363
|
const proceed = await confirm(`Transfer PublisherCap ${capId} to ${to}?`, {
|
|
1511
1364
|
assumeYes: Boolean(gopts.yes),
|
|
1512
1365
|
signal: ctx.signal
|
|
@@ -1993,22 +1846,57 @@ function registerEntryCommands(program) {
|
|
|
1993
1846
|
|
|
1994
1847
|
// src/commands/file.ts
|
|
1995
1848
|
import {
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
1849
|
+
addRecipient,
|
|
1850
|
+
buildRecipientFileSealId,
|
|
1851
|
+
createEncryptedRecipientFile,
|
|
1852
|
+
createRecipientFile,
|
|
1853
|
+
deleteRecipientFile,
|
|
1854
|
+
reconcileRecipientFilesAccessibleBy,
|
|
1855
|
+
reconcileRecipientFilesOwnedBy,
|
|
1856
|
+
removeRecipient,
|
|
2001
1857
|
toBlobObjectId,
|
|
2002
|
-
|
|
2003
|
-
toSuiAddress as
|
|
1858
|
+
toRecipientFileId,
|
|
1859
|
+
toSuiAddress as toSuiAddress4,
|
|
2004
1860
|
toWalrusBlobId,
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
1861
|
+
transferRecipientFileOwnership,
|
|
1862
|
+
updateRecipientFileMetadata,
|
|
1863
|
+
uploadEncryptedRecipientFileFromBytes,
|
|
1864
|
+
uploadRecipientFileFromBytes
|
|
2009
1865
|
} from "@arcadiasystems/morse-sdk";
|
|
2010
1866
|
import { SessionKey as SessionKey2 } from "@mysten/seal";
|
|
2011
1867
|
|
|
1868
|
+
// src/cli/events.ts
|
|
1869
|
+
var PAGE_LIMIT2 = 50;
|
|
1870
|
+
async function fetchEventStream(client, eventType, signal) {
|
|
1871
|
+
const out = [];
|
|
1872
|
+
let cursor;
|
|
1873
|
+
for (;; ) {
|
|
1874
|
+
const page = await client.queryEvents({
|
|
1875
|
+
query: { MoveEventType: eventType },
|
|
1876
|
+
limit: PAGE_LIMIT2,
|
|
1877
|
+
order: "descending",
|
|
1878
|
+
...cursor === undefined ? {} : { cursor },
|
|
1879
|
+
...signal === undefined ? {} : { signal }
|
|
1880
|
+
});
|
|
1881
|
+
for (const event of page.data) {
|
|
1882
|
+
out.push({
|
|
1883
|
+
type: event.type,
|
|
1884
|
+
json: event.parsedJson ?? {},
|
|
1885
|
+
timestampMs: event.timestampMs == null ? 0 : Number(event.timestampMs)
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
if (!page.hasNextPage || page.nextCursor === null || page.nextCursor === undefined) {
|
|
1889
|
+
break;
|
|
1890
|
+
}
|
|
1891
|
+
cursor = page.nextCursor;
|
|
1892
|
+
}
|
|
1893
|
+
return out;
|
|
1894
|
+
}
|
|
1895
|
+
async function fetchEventStreams(client, eventTypes, signal) {
|
|
1896
|
+
const streams = await Promise.all(eventTypes.map((type) => fetchEventStream(client, type, signal)));
|
|
1897
|
+
return streams.flat();
|
|
1898
|
+
}
|
|
1899
|
+
|
|
2012
1900
|
// src/format/hex.ts
|
|
2013
1901
|
var HEX = /^[0-9a-fA-F]+$/;
|
|
2014
1902
|
function encodeHex(bytes) {
|
|
@@ -2030,9 +1918,35 @@ function decodeHex(value) {
|
|
|
2030
1918
|
return bytes;
|
|
2031
1919
|
}
|
|
2032
1920
|
|
|
1921
|
+
// src/format/share.ts
|
|
1922
|
+
var VERSION = "mf1";
|
|
1923
|
+
function b64url(bytes) {
|
|
1924
|
+
return Buffer.from(bytes).toString("base64url");
|
|
1925
|
+
}
|
|
1926
|
+
function encodeShare(fileId, prefix, nonce) {
|
|
1927
|
+
return [
|
|
1928
|
+
VERSION,
|
|
1929
|
+
Buffer.from(fileId, "utf8").toString("base64url"),
|
|
1930
|
+
b64url(prefix),
|
|
1931
|
+
b64url(nonce)
|
|
1932
|
+
].join(".");
|
|
1933
|
+
}
|
|
1934
|
+
function decodeShare(value) {
|
|
1935
|
+
const parts = value.split(".");
|
|
1936
|
+
if (parts.length !== 4 || parts[0] !== VERSION) {
|
|
1937
|
+
throw new UsageError(`Invalid share string: expected "${VERSION}.<fileId>.<prefix>.<nonce>".`);
|
|
1938
|
+
}
|
|
1939
|
+
const fileId = Buffer.from(parts[1] ?? "", "base64url").toString("utf8");
|
|
1940
|
+
const prefix = new Uint8Array(Buffer.from(parts[2] ?? "", "base64url"));
|
|
1941
|
+
const nonce = new Uint8Array(Buffer.from(parts[3] ?? "", "base64url"));
|
|
1942
|
+
if (fileId.length === 0 || prefix.length === 0 || nonce.length === 0) {
|
|
1943
|
+
throw new UsageError("Invalid share string: a bundled field was empty.");
|
|
1944
|
+
}
|
|
1945
|
+
return { fileId, prefix, nonce };
|
|
1946
|
+
}
|
|
1947
|
+
|
|
2033
1948
|
// src/commands/file.ts
|
|
2034
1949
|
var SESSION_KEY_TTL_MIN2 = 10;
|
|
2035
|
-
var SEAL_NONCE_BYTES2 = 16;
|
|
2036
1950
|
function uploadProgress(output) {
|
|
2037
1951
|
const labels = {
|
|
2038
1952
|
encrypting: "Encrypting...",
|
|
@@ -2048,42 +1962,47 @@ function uploadProgress(output) {
|
|
|
2048
1962
|
};
|
|
2049
1963
|
}
|
|
2050
1964
|
async function runFileGet(ctx, target) {
|
|
2051
|
-
const result = await ctx.filesReader.
|
|
2052
|
-
ctx.output.result(
|
|
1965
|
+
const result = await ctx.filesReader.getRecipientFile(toRecipientFileId(target), ctx.signal);
|
|
1966
|
+
ctx.output.result(renderRecipientFile(result), result);
|
|
2053
1967
|
}
|
|
2054
1968
|
async function runFileRegister(ctx, options) {
|
|
2055
|
-
if (options.
|
|
2056
|
-
throw new UsageError("Pass --
|
|
1969
|
+
if (options.public && options.encrypted) {
|
|
1970
|
+
throw new UsageError("Pass --public or --encrypted, not both.");
|
|
2057
1971
|
}
|
|
1972
|
+
const recipients = (options.recipient ?? []).map((r) => toSuiAddress4(r));
|
|
2058
1973
|
const blobId = toWalrusBlobId(options.blobId);
|
|
2059
1974
|
const size = parseByteSize(options.size, "--size");
|
|
2060
1975
|
const blobObjectId = options.blobObjectId === undefined ? undefined : toBlobObjectId(options.blobObjectId);
|
|
2061
|
-
|
|
2062
|
-
const result2 = await createEncryptedFile(ctx.adapter, ctx.config, {
|
|
2063
|
-
allowlistId: toAllowlistId2(options.allowlist),
|
|
2064
|
-
blobId,
|
|
2065
|
-
...blobObjectId === undefined ? {} : { blobObjectId },
|
|
2066
|
-
name: options.name,
|
|
2067
|
-
contentType: options.contentType,
|
|
2068
|
-
size,
|
|
2069
|
-
signal: ctx.signal
|
|
2070
|
-
});
|
|
2071
|
-
ctx.output.result(`Registered encrypted file ${result2.fileId}. (tx: ${result2.digest})`, result2);
|
|
2072
|
-
return;
|
|
2073
|
-
}
|
|
2074
|
-
const result = await createPublicFile(ctx.adapter, ctx.config, {
|
|
1976
|
+
const common = {
|
|
2075
1977
|
blobId,
|
|
2076
1978
|
...blobObjectId === undefined ? {} : { blobObjectId },
|
|
2077
1979
|
name: options.name,
|
|
2078
1980
|
contentType: options.contentType,
|
|
2079
1981
|
size,
|
|
1982
|
+
recipients,
|
|
2080
1983
|
signal: ctx.signal
|
|
2081
|
-
}
|
|
2082
|
-
|
|
1984
|
+
};
|
|
1985
|
+
if (options.encrypted) {
|
|
1986
|
+
if (options.sealPrefix === undefined) {
|
|
1987
|
+
throw new UsageError("--encrypted requires --seal-prefix <hex> (the prefix the blob was encrypted under).");
|
|
1988
|
+
}
|
|
1989
|
+
const result = await createEncryptedRecipientFile(ctx.adapter, ctx.config, {
|
|
1990
|
+
...common,
|
|
1991
|
+
sealIdPrefix: decodeHex(options.sealPrefix)
|
|
1992
|
+
});
|
|
1993
|
+
ctx.output.result(`Registered encrypted file ${result.fileId}. (tx: ${result.digest})`, result);
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
if (options.public) {
|
|
1997
|
+
const result = await createRecipientFile(ctx.adapter, ctx.config, common);
|
|
1998
|
+
ctx.output.result(`Registered public file ${result.fileId}. (tx: ${result.digest})`, result);
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
throw new UsageError("Pass --public for a world-readable file, or --encrypted --seal-prefix <hex>.");
|
|
2083
2002
|
}
|
|
2084
2003
|
async function runFileUpdate(ctx, target, options) {
|
|
2085
|
-
const result = await
|
|
2086
|
-
fileId:
|
|
2004
|
+
const result = await updateRecipientFileMetadata(ctx.adapter, ctx.config, {
|
|
2005
|
+
fileId: toRecipientFileId(target),
|
|
2087
2006
|
name: options.name,
|
|
2088
2007
|
contentType: options.contentType,
|
|
2089
2008
|
signal: ctx.signal
|
|
@@ -2091,13 +2010,13 @@ async function runFileUpdate(ctx, target, options) {
|
|
|
2091
2010
|
ctx.output.result(`Updated file ${shortId(target)} metadata. (tx: ${result.digest})`, result);
|
|
2092
2011
|
}
|
|
2093
2012
|
async function runFileTransferOwnership(ctx, target, newOwner, gopts) {
|
|
2094
|
-
const fileId =
|
|
2095
|
-
const to =
|
|
2096
|
-
const proceed = await confirm(`Transfer
|
|
2013
|
+
const fileId = toRecipientFileId(target);
|
|
2014
|
+
const to = toSuiAddress4(newOwner);
|
|
2015
|
+
const proceed = await confirm(`Transfer ownership of file ${shortId(fileId)} to ${to}? Recipient access is governed separately by the recipient list.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
2097
2016
|
if (!proceed) {
|
|
2098
2017
|
cancelled();
|
|
2099
2018
|
}
|
|
2100
|
-
const result = await
|
|
2019
|
+
const result = await transferRecipientFileOwnership(ctx.adapter, ctx.config, {
|
|
2101
2020
|
fileId,
|
|
2102
2021
|
newOwner: to,
|
|
2103
2022
|
signal: ctx.signal
|
|
@@ -2105,76 +2024,140 @@ async function runFileTransferOwnership(ctx, target, newOwner, gopts) {
|
|
|
2105
2024
|
ctx.output.result(`Transferred file ownership to ${to}. (tx: ${result.digest})`, result);
|
|
2106
2025
|
}
|
|
2107
2026
|
async function runFileDelete(ctx, target, gopts) {
|
|
2108
|
-
const fileId =
|
|
2109
|
-
const proceed = await confirm(`Delete file
|
|
2027
|
+
const fileId = toRecipientFileId(target);
|
|
2028
|
+
const proceed = await confirm(`Delete file ${shortId(fileId)}? The Walrus blob is not deleted; it expires on its own lease.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
2110
2029
|
if (!proceed) {
|
|
2111
2030
|
cancelled();
|
|
2112
2031
|
}
|
|
2113
|
-
const result = await
|
|
2032
|
+
const result = await deleteRecipientFile(ctx.adapter, ctx.config, {
|
|
2114
2033
|
fileId,
|
|
2115
2034
|
signal: ctx.signal
|
|
2116
2035
|
});
|
|
2117
2036
|
ctx.output.result(`Deleted file ${fileId}. (tx: ${result.digest})`, result);
|
|
2118
2037
|
}
|
|
2038
|
+
async function runFileRecipientAdd(ctx, target, recipient) {
|
|
2039
|
+
const fileId = toRecipientFileId(target);
|
|
2040
|
+
const member = toSuiAddress4(recipient);
|
|
2041
|
+
const result = await addRecipient(ctx.adapter, ctx.config, {
|
|
2042
|
+
fileId,
|
|
2043
|
+
recipient: member,
|
|
2044
|
+
signal: ctx.signal
|
|
2045
|
+
});
|
|
2046
|
+
ctx.output.result(`Added ${member} to file ${shortId(fileId)}. (tx: ${result.digest})`, result);
|
|
2047
|
+
}
|
|
2048
|
+
async function runFileRecipientRemove(ctx, target, recipient) {
|
|
2049
|
+
const fileId = toRecipientFileId(target);
|
|
2050
|
+
const member = toSuiAddress4(recipient);
|
|
2051
|
+
const result = await removeRecipient(ctx.adapter, ctx.config, {
|
|
2052
|
+
fileId,
|
|
2053
|
+
recipient: member,
|
|
2054
|
+
signal: ctx.signal
|
|
2055
|
+
});
|
|
2056
|
+
ctx.output.result(`Removed ${member} from file ${shortId(fileId)}. (tx: ${result.digest})`, result);
|
|
2057
|
+
}
|
|
2058
|
+
async function runFileRecipientList(ctx, target) {
|
|
2059
|
+
const file = await ctx.filesReader.getRecipientFile(toRecipientFileId(target), ctx.signal);
|
|
2060
|
+
const human = file.members.length === 0 ? "No recipients." : file.members.join(`
|
|
2061
|
+
`);
|
|
2062
|
+
ctx.output.result(human, { fileId: file.id, recipients: file.members });
|
|
2063
|
+
}
|
|
2119
2064
|
async function runFileUpload(ctx, path, options) {
|
|
2120
|
-
if (options.
|
|
2121
|
-
throw new UsageError("Pass --
|
|
2065
|
+
if (options.public && options.encrypt) {
|
|
2066
|
+
throw new UsageError("Pass --public or --encrypt, not both.");
|
|
2067
|
+
}
|
|
2068
|
+
const recipients = (options.recipient ?? []).map((r) => toSuiAddress4(r));
|
|
2069
|
+
if (options.public && recipients.length > 0) {
|
|
2070
|
+
throw new UsageError("--recipient applies to encrypted uploads; a public file is readable by anyone.");
|
|
2071
|
+
}
|
|
2072
|
+
if (!options.public && !options.encrypt && recipients.length === 0) {
|
|
2073
|
+
throw new UsageError("Pass --public for a world-readable file, or --encrypt (optionally with --recipient <addr>).");
|
|
2122
2074
|
}
|
|
2123
2075
|
const epochs = parsePositiveInt(options.epochs ?? "3", "--epochs");
|
|
2124
2076
|
const bytes = await readContentBytes({ file: path });
|
|
2125
2077
|
const contentType = resolveContentType(options.contentType, path);
|
|
2126
2078
|
const upload = { epochs, deletable: true };
|
|
2127
2079
|
const onProgress = uploadProgress(ctx.output);
|
|
2128
|
-
if (options.
|
|
2129
|
-
const
|
|
2130
|
-
const sealId = buildAllowlistSealId(allowlistId, crypto.getRandomValues(new Uint8Array(SEAL_NONCE_BYTES2)));
|
|
2131
|
-
const result2 = await uploadEncryptedFileFromBytes(ctx.adapter, ctx.config, {
|
|
2080
|
+
if (options.public) {
|
|
2081
|
+
const result2 = await uploadRecipientFileFromBytes(ctx.adapter, ctx.config, {
|
|
2132
2082
|
walrus: ctx.walrus,
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
sealId,
|
|
2136
|
-
plaintext: bytes,
|
|
2083
|
+
bytes,
|
|
2084
|
+
recipients: [],
|
|
2137
2085
|
name: options.name,
|
|
2138
2086
|
contentType,
|
|
2139
2087
|
upload,
|
|
2140
2088
|
signal: ctx.signal,
|
|
2141
2089
|
onProgress
|
|
2142
2090
|
});
|
|
2143
|
-
const
|
|
2144
|
-
const
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
` tx: ${result2.digest}`,
|
|
2149
|
-
"Save the seal id: decrypting later needs it plus allowlist membership."
|
|
2150
|
-
].join(`
|
|
2151
|
-
`);
|
|
2152
|
-
ctx.output.result(human2, { ...result2, sealId: sealIdHex });
|
|
2091
|
+
const aggregator = ctx.config.walrusEndpoints.aggregator;
|
|
2092
|
+
const viewUrl = aggregator.length > 0 ? `${aggregator}/v1/blobs/${result2.blobId}` : undefined;
|
|
2093
|
+
const human2 = viewUrl === undefined ? `Uploaded public file ${result2.fileId}. (tx: ${result2.digest})` : `Uploaded public file ${result2.fileId}. (tx: ${result2.digest})
|
|
2094
|
+
view: ${viewUrl}`;
|
|
2095
|
+
ctx.output.result(human2, { ...result2, viewUrl: viewUrl ?? null });
|
|
2153
2096
|
return;
|
|
2154
2097
|
}
|
|
2155
|
-
const result = await
|
|
2098
|
+
const result = await uploadEncryptedRecipientFileFromBytes(ctx.adapter, ctx.config, {
|
|
2156
2099
|
walrus: ctx.walrus,
|
|
2157
|
-
|
|
2100
|
+
seal: ctx.seal,
|
|
2101
|
+
plaintext: bytes,
|
|
2102
|
+
recipients,
|
|
2158
2103
|
name: options.name,
|
|
2159
2104
|
contentType,
|
|
2160
2105
|
upload,
|
|
2161
2106
|
signal: ctx.signal,
|
|
2162
2107
|
onProgress
|
|
2163
2108
|
});
|
|
2164
|
-
const
|
|
2165
|
-
const
|
|
2166
|
-
const
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
}
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2109
|
+
const prefixHex = encodeHex(result.sealIdPrefix);
|
|
2110
|
+
const nonceHex = encodeHex(result.sealNonce);
|
|
2111
|
+
const share = encodeShare(result.fileId, result.sealIdPrefix, result.sealNonce);
|
|
2112
|
+
const human = [
|
|
2113
|
+
`Uploaded encrypted file ${result.fileId}`,
|
|
2114
|
+
` blobId: ${result.blobId}`,
|
|
2115
|
+
` share: ${share}`,
|
|
2116
|
+
` tx: ${result.digest}`,
|
|
2117
|
+
"Give the share string to recipients; they need it (plus membership) to decrypt."
|
|
2118
|
+
].join(`
|
|
2119
|
+
`);
|
|
2120
|
+
ctx.output.result(human, {
|
|
2121
|
+
fileId: result.fileId,
|
|
2122
|
+
blobId: result.blobId,
|
|
2123
|
+
blobObjectId: result.blobObjectId,
|
|
2124
|
+
digest: result.digest,
|
|
2125
|
+
gasUsedMist: result.gasUsedMist,
|
|
2126
|
+
sealIdPrefix: prefixHex,
|
|
2127
|
+
sealNonce: nonceHex,
|
|
2128
|
+
share
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
function resolveDownloadTarget(positional, options) {
|
|
2132
|
+
if (options.share !== undefined) {
|
|
2133
|
+
const decoded = decodeShare(options.share);
|
|
2134
|
+
if (positional !== undefined && String(toRecipientFileId(positional)) !== String(toRecipientFileId(decoded.fileId))) {
|
|
2135
|
+
throw new UsageError("The <file> argument does not match the file id in --share.");
|
|
2136
|
+
}
|
|
2137
|
+
return {
|
|
2138
|
+
fileId: decoded.fileId,
|
|
2139
|
+
decrypt: { prefix: decoded.prefix, nonce: decoded.nonce }
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
if (positional === undefined) {
|
|
2143
|
+
throw new UsageError("Pass a <file> id, or --share <string>.");
|
|
2173
2144
|
}
|
|
2174
|
-
if (
|
|
2175
|
-
|
|
2145
|
+
if (options.prefix !== undefined || options.nonce !== undefined) {
|
|
2146
|
+
if (options.prefix === undefined || options.nonce === undefined) {
|
|
2147
|
+
throw new UsageError("Decrypting needs both --prefix and --nonce (or use --share).");
|
|
2148
|
+
}
|
|
2149
|
+
return {
|
|
2150
|
+
fileId: positional,
|
|
2151
|
+
decrypt: {
|
|
2152
|
+
prefix: decodeHex(options.prefix),
|
|
2153
|
+
nonce: decodeHex(options.nonce)
|
|
2154
|
+
}
|
|
2155
|
+
};
|
|
2176
2156
|
}
|
|
2177
|
-
|
|
2157
|
+
return { fileId: positional };
|
|
2158
|
+
}
|
|
2159
|
+
async function decryptRecipientFile(ctx, fileId, ciphertext, prefix, nonce) {
|
|
2160
|
+
const sealId = buildRecipientFileSealId(prefix, nonce);
|
|
2178
2161
|
const { keypair, address } = await ctx.unlockSigner();
|
|
2179
2162
|
ctx.output.info("Signing a SessionKey with the active account...");
|
|
2180
2163
|
const sessionKey = await SessionKey2.create({
|
|
@@ -2184,24 +2167,29 @@ async function decryptFile(ctx, file, ciphertext, sealIdHex) {
|
|
|
2184
2167
|
signer: keypair,
|
|
2185
2168
|
suiClient: ctx.client
|
|
2186
2169
|
});
|
|
2187
|
-
return ctx.seal.
|
|
2170
|
+
return ctx.seal.decryptUnderRecipientFile(ciphertext, {
|
|
2171
|
+
sessionKey,
|
|
2188
2172
|
sealId,
|
|
2189
|
-
|
|
2190
|
-
sessionKey
|
|
2173
|
+
fileId
|
|
2191
2174
|
});
|
|
2192
2175
|
}
|
|
2193
|
-
async function runFileDownload(ctx,
|
|
2176
|
+
async function runFileDownload(ctx, positional, options) {
|
|
2194
2177
|
if (ctx.output.isJson && options.out === undefined) {
|
|
2195
2178
|
throw new UsageError("Downloading content to stdout is not supported in --json mode; pass --out <path>.");
|
|
2196
2179
|
}
|
|
2197
|
-
const
|
|
2180
|
+
const target = resolveDownloadTarget(positional, options);
|
|
2181
|
+
const fileId = toRecipientFileId(target.fileId);
|
|
2182
|
+
const file = await ctx.filesReader.getRecipientFile(fileId, ctx.signal);
|
|
2183
|
+
if (target.decrypt === undefined && file.members.length > 0) {
|
|
2184
|
+
ctx.output.warn("This file has a recipient list, so its bytes are likely encrypted. Pass --share (or --prefix and --nonce) to decrypt; writing the raw bytes as-is.");
|
|
2185
|
+
}
|
|
2198
2186
|
ctx.output.info("Fetching content from Walrus...");
|
|
2199
2187
|
const bytes = await ctx.walrusRead.readBlob(file.blobId, {
|
|
2200
2188
|
signal: ctx.signal
|
|
2201
2189
|
});
|
|
2202
2190
|
let content = bytes;
|
|
2203
|
-
if (
|
|
2204
|
-
content = await
|
|
2191
|
+
if (target.decrypt !== undefined) {
|
|
2192
|
+
content = await decryptRecipientFile(ctx, fileId, bytes, target.decrypt.prefix, target.decrypt.nonce);
|
|
2205
2193
|
}
|
|
2206
2194
|
if (options.out !== undefined) {
|
|
2207
2195
|
await writeFileContents(options.out, content);
|
|
@@ -2216,13 +2204,57 @@ async function runFileDownload(ctx, target, options) {
|
|
|
2216
2204
|
}
|
|
2217
2205
|
process.stdout.write(content);
|
|
2218
2206
|
}
|
|
2207
|
+
async function runFileList(ctx, options) {
|
|
2208
|
+
const limit = options.limit === undefined ? undefined : parseLimit(options.limit);
|
|
2209
|
+
const address = options.address === undefined ? ctx.ownerAddress : toSuiAddress4(options.address);
|
|
2210
|
+
if (address === undefined) {
|
|
2211
|
+
throw new UsageError("No address given and no active account. Pass --address or import an account.");
|
|
2212
|
+
}
|
|
2213
|
+
const types = ctx.eventTypes;
|
|
2214
|
+
let summaries;
|
|
2215
|
+
if (options.accessible) {
|
|
2216
|
+
ctx.output.info("Fetching recipient and file events...");
|
|
2217
|
+
const events = await fetchEventStreams(ctx.events, [
|
|
2218
|
+
types.RecipientFileCreated,
|
|
2219
|
+
types.RecipientAdded,
|
|
2220
|
+
types.RecipientRemoved,
|
|
2221
|
+
types.RecipientFileDeleted
|
|
2222
|
+
], ctx.signal);
|
|
2223
|
+
summaries = reconcileRecipientFilesAccessibleBy(events, address, types);
|
|
2224
|
+
} else {
|
|
2225
|
+
ctx.output.info("Fetching file events...");
|
|
2226
|
+
const events = await fetchEventStreams(ctx.events, [
|
|
2227
|
+
types.RecipientFileCreated,
|
|
2228
|
+
types.RecipientFileOwnershipTransferred,
|
|
2229
|
+
types.RecipientFileDeleted
|
|
2230
|
+
], ctx.signal);
|
|
2231
|
+
summaries = reconcileRecipientFilesOwnedBy(events, address, types);
|
|
2232
|
+
}
|
|
2233
|
+
const limited = limit === undefined ? summaries : summaries.slice(0, limit);
|
|
2234
|
+
let items = limited;
|
|
2235
|
+
if (options.hydrate) {
|
|
2236
|
+
ctx.output.info(`Hydrating ${limited.length} file(s)...`);
|
|
2237
|
+
const full = [];
|
|
2238
|
+
for (const summary of limited) {
|
|
2239
|
+
const record = await ctx.filesReader.getRecipientFile(summary.id, ctx.signal);
|
|
2240
|
+
full.push({ ...record, kind: "full" });
|
|
2241
|
+
}
|
|
2242
|
+
items = full;
|
|
2243
|
+
}
|
|
2244
|
+
ctx.output.result(renderFileList(items), items);
|
|
2245
|
+
}
|
|
2219
2246
|
function registerFileCommands(program) {
|
|
2220
|
-
const file = program.command("file").description("Upload, register, read, and manage files on Walrus");
|
|
2221
|
-
const upload = file.command("upload <path>").description("Encrypt (or not), upload to Walrus, and register a file").requiredOption("-n, --name <name>", "File name").option("--public", "Upload a world-readable (unencrypted) file").option("--content-type <mime>", "MIME content type (inferred from the path if omitted)").option("--epochs <n>", "Walrus storage epochs", "3");
|
|
2222
|
-
|
|
2247
|
+
const file = program.command("file").description("Upload, register, read, list, and manage files on Walrus");
|
|
2248
|
+
const upload = file.command("upload <path>").description("Encrypt (or not), upload to Walrus, and register a file").requiredOption("-n, --name <name>", "File name").option("--public", "Upload a world-readable (unencrypted) file").option("--encrypt", "Encrypt the file (implied when --recipient is given)").option("--content-type <mime>", "MIME content type (inferred from the path if omitted)").option("--epochs <n>", "Walrus storage epochs", "3");
|
|
2249
|
+
recipientOption(upload).action(async (path, options, command) => {
|
|
2223
2250
|
await runFileUpload(await buildEncryptContext(command), path, options);
|
|
2224
2251
|
});
|
|
2225
|
-
|
|
2252
|
+
file.command("list").description("List files owned by (or accessible to) an address, via event indexing").option("--address <addr>", "Address to query (default: the active account)").option("--accessible", "List files you can decrypt as a recipient, not just owned").option("--hydrate", "Fetch full records (one read per file)").option("--limit <n>", "Cap the number of results").option("--indexer-url <url>", "Event source URL (default: the RPC; uses suix_queryEvents, a deprecated Sui endpoint)").action(async (options, command) => {
|
|
2253
|
+
await runFileList(await buildFileListContext(command, {
|
|
2254
|
+
indexerUrl: options.indexerUrl
|
|
2255
|
+
}), options);
|
|
2256
|
+
});
|
|
2257
|
+
const download = file.command("download [file]").description("Download a file's content; decrypts in place when encrypted").option("--out <path>", "Write content to a file instead of stdout").option("--share <string>", "Share string from upload (bundles file id, prefix, nonce)").option("--prefix <hex>", "Seal prefix (with --nonce) to decrypt").option("--nonce <hex>", "Seal nonce (with --prefix) to decrypt");
|
|
2226
2258
|
viaAggregatorOption(download).action(async (target, options, command) => {
|
|
2227
2259
|
await runFileDownload(await buildFileDownloadContext(command, {
|
|
2228
2260
|
viaAggregator: options.viaAggregator
|
|
@@ -2231,26 +2263,36 @@ function registerFileCommands(program) {
|
|
|
2231
2263
|
file.command("get <file>").description("Fetch a file's on-chain metadata").action(async (target, _options, command) => {
|
|
2232
2264
|
await runFileGet(await buildFilesReadContext(command), target);
|
|
2233
2265
|
});
|
|
2234
|
-
const register = file.command("register").description("Register on-chain metadata for a blob already on Walrus").requiredOption("--blob-id <id>", "Walrus content id").requiredOption("-n, --name <name>", "File name").requiredOption("--content-type <mime>", "MIME content type").requiredOption("--size <bytes>", "Plaintext byte length").option("--public", "Register a world-readable (unencrypted) file").option("--blob-object-id <id>", "On-chain Walrus Blob object id, if known");
|
|
2235
|
-
|
|
2266
|
+
const register = file.command("register").description("Register on-chain metadata for a blob already on Walrus").requiredOption("--blob-id <id>", "Walrus content id").requiredOption("-n, --name <name>", "File name").requiredOption("--content-type <mime>", "MIME content type").requiredOption("--size <bytes>", "Plaintext byte length").option("--public", "Register a world-readable (unencrypted) file").option("--encrypted", "Register an encrypted file (needs --seal-prefix)").option("--seal-prefix <hex>", "Seal prefix the blob was encrypted under").option("--blob-object-id <id>", "On-chain Walrus Blob object id, if known");
|
|
2267
|
+
recipientOption(register).action(async (options, command) => {
|
|
2236
2268
|
await runFileRegister(await buildWriteContext(command), options);
|
|
2237
2269
|
});
|
|
2238
2270
|
file.command("update <file>").description("Update a file's name and content type (owner only)").requiredOption("-n, --name <name>", "New file name").requiredOption("--content-type <mime>", "New MIME content type").action(async (target, options, command) => {
|
|
2239
2271
|
await runFileUpdate(await buildWriteContext(command), target, options);
|
|
2240
2272
|
});
|
|
2241
|
-
file.command("transfer-ownership <file> <newOwner>").description("Transfer a file's
|
|
2273
|
+
file.command("transfer-ownership <file> <newOwner>").description("Transfer a file's ownership (not recipient access)").action(async (target, newOwner, _options, command) => {
|
|
2242
2274
|
await runFileTransferOwnership(await buildWriteContext(command), target, newOwner, globalOptions(command));
|
|
2243
2275
|
});
|
|
2244
2276
|
file.command("delete <file>").description("Delete a file's metadata record (does not delete the blob)").action(async (target, _options, command) => {
|
|
2245
2277
|
await runFileDelete(await buildWriteContext(command), target, globalOptions(command));
|
|
2246
2278
|
});
|
|
2279
|
+
const recipient = file.command("recipient").description("Manage a file's recipient list");
|
|
2280
|
+
recipient.command("add <file> <address>").description("Allow an address to decrypt the file").action(async (target, address, _options, command) => {
|
|
2281
|
+
await runFileRecipientAdd(await buildWriteContext(command), target, address);
|
|
2282
|
+
});
|
|
2283
|
+
recipient.command("remove <file> <address>").description("Revoke an address's access to the file").action(async (target, address, _options, command) => {
|
|
2284
|
+
await runFileRecipientRemove(await buildWriteContext(command), target, address);
|
|
2285
|
+
});
|
|
2286
|
+
recipient.command("list <file>").description("List the file's recipients").action(async (target, _options, command) => {
|
|
2287
|
+
await runFileRecipientList(await buildFilesReadContext(command), target);
|
|
2288
|
+
});
|
|
2247
2289
|
}
|
|
2248
2290
|
|
|
2249
2291
|
// src/commands/publication.ts
|
|
2250
2292
|
import {
|
|
2251
2293
|
createPublication,
|
|
2252
2294
|
deletePublication,
|
|
2253
|
-
toSuiAddress as
|
|
2295
|
+
toSuiAddress as toSuiAddress5,
|
|
2254
2296
|
transferOwnership
|
|
2255
2297
|
} from "@arcadiasystems/morse-sdk";
|
|
2256
2298
|
async function runPublicationGet(ctx, target) {
|
|
@@ -2259,7 +2301,7 @@ async function runPublicationGet(ctx, target) {
|
|
|
2259
2301
|
ctx.output.result(renderPublication(result), result);
|
|
2260
2302
|
}
|
|
2261
2303
|
async function runPublicationList(ctx, address, options) {
|
|
2262
|
-
const owner = address === undefined ? ctx.ownerAddress :
|
|
2304
|
+
const owner = address === undefined ? ctx.ownerAddress : toSuiAddress5(address);
|
|
2263
2305
|
if (owner === undefined) {
|
|
2264
2306
|
throw new UsageError("No address given and no active account. Pass an address or import an account.");
|
|
2265
2307
|
}
|
|
@@ -2338,7 +2380,7 @@ async function runPublicationDelete(ctx, target, options, gopts) {
|
|
|
2338
2380
|
}
|
|
2339
2381
|
async function runPublicationTransferOwnership(ctx, recipient, options, gopts) {
|
|
2340
2382
|
const id = await resolvePublication(ctx, options.publication);
|
|
2341
|
-
const to =
|
|
2383
|
+
const to = toSuiAddress5(recipient);
|
|
2342
2384
|
const proceed = await confirm(`Transfer ownership of ${shortId(id)} to ${to}? You will lose owner control.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
2343
2385
|
if (!proceed) {
|
|
2344
2386
|
cancelled();
|
|
@@ -2530,7 +2572,6 @@ function registerCommands(program) {
|
|
|
2530
2572
|
registerEntryCommands(program);
|
|
2531
2573
|
registerRevisionCommands(program);
|
|
2532
2574
|
registerCapCommands(program);
|
|
2533
|
-
registerAllowlistCommands(program);
|
|
2534
2575
|
registerFileCommands(program);
|
|
2535
2576
|
}
|
|
2536
2577
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcadiasystems/morse-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Command-line interface for the Morse decentralized CMS on Sui.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"prepublishOnly": "bun run check && bun run build"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@arcadiasystems/morse-sdk": "^0.
|
|
58
|
+
"@arcadiasystems/morse-sdk": "^0.4.0",
|
|
59
59
|
"@mysten/seal": "1.1.3",
|
|
60
60
|
"@mysten/sui": "2.16.2",
|
|
61
61
|
"@mysten/walrus": "1.1.6",
|