@arcadiasystems/morse-cli 0.3.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 +38 -0
- package/README.md +34 -38
- package/dist/index.js +307 -374
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,44 @@ 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
|
+
|
|
7
45
|
## [0.3.0] - 2026-06-04
|
|
8
46
|
|
|
9
47
|
Event-based file listing, wrapping the SDK 0.3.0 reconcile helpers. Purely
|
package/README.md
CHANGED
|
@@ -228,56 +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
|
|
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). |
|
|
253
242
|
| `file list [--address <addr>] [--accessible] [--hydrate] [--limit <n>] [--indexer-url <url>]` | List files owned by (or, with `--accessible`, decryptable by) an address. |
|
|
254
|
-
| `file get <file>` | Fetch a file's on-chain metadata. |
|
|
243
|
+
| `file get <file>` | Fetch a file's on-chain metadata and recipient list. |
|
|
255
244
|
| `file update <file> --name <n> --content-type <m>` | Update name and MIME (owner only). |
|
|
256
|
-
| `file transfer-ownership <file> <newOwner> [-y]` | Transfer
|
|
245
|
+
| `file transfer-ownership <file> <newOwner> [-y]` | Transfer ownership (not recipient access, which the list governs). |
|
|
257
246
|
| `file delete <file> [-y]` | Delete the metadata record (the Walrus blob expires on its own lease). |
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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.
|
|
273
268
|
|
|
274
269
|
## Output and scripting
|
|
275
270
|
|
|
276
271
|
- Human-readable output goes to stdout; progress, warnings, and prompts go to
|
|
277
272
|
stderr. Redirecting stdout yields only the result.
|
|
278
273
|
- `--json` emits a single JSON document on stdout (nothing else). `bigint` values
|
|
279
|
-
(e.g. `gasUsedMist`) are encoded as decimal strings and byte arrays (
|
|
280
|
-
|
|
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.
|
|
281
277
|
- Color is emitted only to a TTY and honors `NO_COLOR` and `FORCE_COLOR`.
|
|
282
278
|
|
|
283
279
|
## Exit codes
|
|
@@ -300,7 +296,7 @@ file (one read each) when you need them.
|
|
|
300
296
|
- [`encrypt-decrypt.sh`](./examples/encrypt-decrypt.sh): encrypt with Seal and decrypt back.
|
|
301
297
|
- [`delegation.sh`](./examples/delegation.sh): issue a PublisherCap to a delegate, then revoke it.
|
|
302
298
|
- [`ci-noninteractive.sh`](./examples/ci-noninteractive.sh): env-var auth, `--yes`, and `--json` parsing.
|
|
303
|
-
- [`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).
|
|
304
300
|
|
|
305
301
|
## Limitations
|
|
306
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,28 +889,27 @@ 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 {
|
|
905
|
-
|
|
904
|
+
buildRecipientFileEventTypes,
|
|
906
905
|
DefaultSealAdapter,
|
|
907
906
|
DefaultWalrusReadAdapter,
|
|
908
907
|
DefaultWalrusWriteAdapter,
|
|
909
908
|
HttpAggregatorReadAdapter,
|
|
910
909
|
KeypairAdapter,
|
|
911
910
|
morseConfig,
|
|
912
|
-
|
|
913
|
-
|
|
911
|
+
RpcPublicationReader,
|
|
912
|
+
RpcRecipientFilesReader
|
|
914
913
|
} from "@arcadiasystems/morse-sdk";
|
|
915
914
|
import { SuiGrpcClient } from "@mysten/sui/grpc";
|
|
916
915
|
import { SuiJsonRpcClient } from "@mysten/sui/jsonRpc";
|
|
@@ -954,14 +953,9 @@ async function buildFilesReadContext(command) {
|
|
|
954
953
|
const base = await buildReadContext(command);
|
|
955
954
|
return {
|
|
956
955
|
...base,
|
|
957
|
-
filesReader:
|
|
958
|
-
|
|
959
|
-
}
|
|
960
|
-
async function buildAllowlistWriteContext(command) {
|
|
961
|
-
const base = await buildWriteContext(command);
|
|
962
|
-
return {
|
|
963
|
-
...base,
|
|
964
|
-
filesReader: RpcFilesReader.fromMorseConfig(base.config, base.client)
|
|
956
|
+
filesReader: RpcRecipientFilesReader.fromConfig(base.client, {
|
|
957
|
+
packageId: base.config.packageId
|
|
958
|
+
})
|
|
965
959
|
};
|
|
966
960
|
}
|
|
967
961
|
async function buildContentContext(command) {
|
|
@@ -1022,7 +1016,9 @@ async function buildFileDownloadContext(command, opts = {}) {
|
|
|
1022
1016
|
}
|
|
1023
1017
|
return {
|
|
1024
1018
|
...base,
|
|
1025
|
-
filesReader:
|
|
1019
|
+
filesReader: RpcRecipientFilesReader.fromConfig(base.client, {
|
|
1020
|
+
packageId: base.config.packageId
|
|
1021
|
+
}),
|
|
1026
1022
|
walrusRead: walrusReadAdapter(base, network, Boolean(opts.viaAggregator)),
|
|
1027
1023
|
seal: DefaultSealAdapter.fromMorseConfig(base.config, {}, base.client),
|
|
1028
1024
|
unlockSigner: () => resolveSigner(base.settings.account, process.env, base.signal)
|
|
@@ -1030,9 +1026,9 @@ async function buildFileDownloadContext(command, opts = {}) {
|
|
|
1030
1026
|
}
|
|
1031
1027
|
async function buildFileListContext(command, opts = {}) {
|
|
1032
1028
|
const base = await buildReadContext(command);
|
|
1033
|
-
const originPackageId = base.config.
|
|
1029
|
+
const originPackageId = base.config.recipientFileEventOriginPackageId;
|
|
1034
1030
|
if (originPackageId === undefined) {
|
|
1035
|
-
throw new CliError("File listing is unavailable on this network: no
|
|
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);
|
|
1036
1032
|
}
|
|
1037
1033
|
const events = new SuiJsonRpcClient({
|
|
1038
1034
|
network: base.settings.network,
|
|
@@ -1040,12 +1036,58 @@ async function buildFileListContext(command, opts = {}) {
|
|
|
1040
1036
|
});
|
|
1041
1037
|
return {
|
|
1042
1038
|
...base,
|
|
1043
|
-
filesReader:
|
|
1039
|
+
filesReader: RpcRecipientFilesReader.fromConfig(base.client, {
|
|
1040
|
+
packageId: base.config.packageId
|
|
1041
|
+
}),
|
|
1044
1042
|
events,
|
|
1045
|
-
eventTypes:
|
|
1043
|
+
eventTypes: buildRecipientFileEventTypes(originPackageId)
|
|
1046
1044
|
};
|
|
1047
1045
|
}
|
|
1048
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
|
+
}
|
|
1090
|
+
|
|
1049
1091
|
// src/format/ids.ts
|
|
1050
1092
|
var HEAD = 6;
|
|
1051
1093
|
var TAIL = 4;
|
|
@@ -1113,50 +1155,30 @@ function renderEntryList(entries) {
|
|
|
1113
1155
|
return entries.map((entry) => `#${entry.id} ${entry.name} (${entry.revisions.length} revisions)`).join(`
|
|
1114
1156
|
`);
|
|
1115
1157
|
}
|
|
1116
|
-
function renderAllowlist(allowlist) {
|
|
1117
|
-
const members = allowlist.members.length === 0 ? "(none)" : allowlist.members.map((m) => ` ${m}`).join(`
|
|
1118
|
-
`);
|
|
1119
|
-
return [
|
|
1120
|
-
`${allowlist.name} (${shortId(allowlist.id)})`,
|
|
1121
|
-
`members (${allowlist.members.length}):`,
|
|
1122
|
-
members
|
|
1123
|
-
].join(`
|
|
1124
|
-
`);
|
|
1125
|
-
}
|
|
1126
|
-
function renderAllowlistCapList(caps) {
|
|
1127
|
-
if (caps.length === 0) {
|
|
1128
|
-
return "No allowlist caps held by this address.";
|
|
1129
|
-
}
|
|
1130
|
-
return caps.map((cap) => `${cap.id} allowlist ${cap.allowlistId}`).join(`
|
|
1131
|
-
`);
|
|
1132
|
-
}
|
|
1133
1158
|
function renderFileList(items) {
|
|
1134
1159
|
if (items.length === 0) {
|
|
1135
1160
|
return "No files.";
|
|
1136
1161
|
}
|
|
1137
|
-
const header = "id name size
|
|
1162
|
+
const header = "id name size recipients created";
|
|
1138
1163
|
const rows = items.map((file) => {
|
|
1139
|
-
const allowlist = file.allowlistId === null ? "public" : shortId(file.allowlistId);
|
|
1140
1164
|
const created = new Date(file.createdAtMs).toISOString().slice(0, 10);
|
|
1141
|
-
|
|
1142
|
-
return `${shortId(file.id)} ${file.name} ${file.size} ${encrypted} ${allowlist} ${created}`;
|
|
1165
|
+
return `${shortId(file.id)} ${file.name} ${file.size} ${file.members.length} ${created}`;
|
|
1143
1166
|
});
|
|
1144
1167
|
return [header, ...rows].join(`
|
|
1145
1168
|
`);
|
|
1146
1169
|
}
|
|
1147
|
-
function
|
|
1148
|
-
const
|
|
1170
|
+
function renderRecipientFile(file) {
|
|
1171
|
+
const recipients = file.members.length === 0 ? "(none)" : file.members.map((m) => ` ${m}`).join(`
|
|
1172
|
+
`);
|
|
1173
|
+
return [
|
|
1149
1174
|
`${file.name} (${shortId(file.id)})`,
|
|
1150
1175
|
`contentType: ${file.contentType}`,
|
|
1151
1176
|
`size: ${file.size}`,
|
|
1152
|
-
`encrypted: ${file.encrypted}`,
|
|
1153
1177
|
`blobId: ${file.blobId}`,
|
|
1154
|
-
`owner: ${file.owner}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
}
|
|
1159
|
-
return lines.join(`
|
|
1178
|
+
`owner: ${file.owner}`,
|
|
1179
|
+
`recipients (${file.members.length}):`,
|
|
1180
|
+
recipients
|
|
1181
|
+
].join(`
|
|
1160
1182
|
`);
|
|
1161
1183
|
}
|
|
1162
1184
|
function headLabel(value) {
|
|
@@ -1181,11 +1203,11 @@ function publisherCapOption(command) {
|
|
|
1181
1203
|
function ownerCapOption(command) {
|
|
1182
1204
|
return command.option("--owner-cap <id>", "OwnerCap ID (auto-resolved if omitted)");
|
|
1183
1205
|
}
|
|
1184
|
-
function
|
|
1185
|
-
return
|
|
1206
|
+
function collect(value, previous) {
|
|
1207
|
+
return [...previous, value];
|
|
1186
1208
|
}
|
|
1187
|
-
function
|
|
1188
|
-
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, []);
|
|
1189
1211
|
}
|
|
1190
1212
|
function viaAggregatorOption(command) {
|
|
1191
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.");
|
|
@@ -1196,7 +1218,6 @@ function contentOptions(command) {
|
|
|
1196
1218
|
|
|
1197
1219
|
// src/commands/resolve.ts
|
|
1198
1220
|
import {
|
|
1199
|
-
toAllowlistCapId,
|
|
1200
1221
|
toOwnerCapId,
|
|
1201
1222
|
toPublisherCapId
|
|
1202
1223
|
} from "@arcadiasystems/morse-sdk";
|
|
@@ -1239,25 +1260,6 @@ async function resolvePublisherCap(reader, address, publicationId, override, sig
|
|
|
1239
1260
|
} while (cursor !== undefined);
|
|
1240
1261
|
throw new CliError(`No PublisherCap for ${publicationId} held by ${address}. Pass --publisher-cap, or check that the active account holds one.`, ExitCode.NotFound);
|
|
1241
1262
|
}
|
|
1242
|
-
async function resolveAllowlistCap(filesReader, address, allowlistId, override, signal) {
|
|
1243
|
-
if (override !== undefined) {
|
|
1244
|
-
return toAllowlistCapId(override);
|
|
1245
|
-
}
|
|
1246
|
-
let cursor;
|
|
1247
|
-
do {
|
|
1248
|
-
const page = await filesReader.listAllowlistCapsOwnedBy(address, {
|
|
1249
|
-
limit: PAGE_LIMIT,
|
|
1250
|
-
signal,
|
|
1251
|
-
...cursor === undefined ? {} : { cursor }
|
|
1252
|
-
});
|
|
1253
|
-
const match = page.results.find((c) => c.allowlistId === allowlistId);
|
|
1254
|
-
if (match !== undefined) {
|
|
1255
|
-
return match.id;
|
|
1256
|
-
}
|
|
1257
|
-
cursor = page.nextCursor ?? undefined;
|
|
1258
|
-
} while (cursor !== undefined);
|
|
1259
|
-
throw new CliError(`No allowlist Cap for ${allowlistId} held by ${address}. Pass --cap, or check that the active account administers it.`, ExitCode.NotFound);
|
|
1260
|
-
}
|
|
1261
1263
|
|
|
1262
1264
|
// src/commands/shared.ts
|
|
1263
1265
|
import { StorageMode } from "@arcadiasystems/morse-sdk";
|
|
@@ -1282,7 +1284,7 @@ function parseByteSize(value, name) {
|
|
|
1282
1284
|
if (!/^\d+$/.test(value)) {
|
|
1283
1285
|
throw new UsageError(`${name} must be a non-negative integer, got "${value}".`);
|
|
1284
1286
|
}
|
|
1285
|
-
return
|
|
1287
|
+
return Number(value);
|
|
1286
1288
|
}
|
|
1287
1289
|
function parseId(value, name) {
|
|
1288
1290
|
const id = Number(value);
|
|
@@ -1292,191 +1294,9 @@ function parseId(value, name) {
|
|
|
1292
1294
|
return id;
|
|
1293
1295
|
}
|
|
1294
1296
|
|
|
1295
|
-
// src/commands/allowlist.ts
|
|
1296
|
-
function requireAllowlistId(value) {
|
|
1297
|
-
if (value === undefined) {
|
|
1298
|
-
throw new UsageError("Pass --allowlist <id>.");
|
|
1299
|
-
}
|
|
1300
|
-
return toAllowlistId(value);
|
|
1301
|
-
}
|
|
1302
|
-
async function runAllowlistCreate(ctx, options) {
|
|
1303
|
-
ctx.output.info(`Creating allowlist "${options.name}"...`);
|
|
1304
|
-
const result = await createAllowlist(ctx.adapter, ctx.config, {
|
|
1305
|
-
name: options.name,
|
|
1306
|
-
signal: ctx.signal
|
|
1307
|
-
});
|
|
1308
|
-
const human = [
|
|
1309
|
-
`Created allowlist "${options.name}" (${result.allowlistId})`,
|
|
1310
|
-
` cap: ${result.capId}`,
|
|
1311
|
-
` tx: ${result.digest}`,
|
|
1312
|
-
"Save the cap id: it is the admin token for managing members."
|
|
1313
|
-
].join(`
|
|
1314
|
-
`);
|
|
1315
|
-
ctx.output.result(human, result);
|
|
1316
|
-
}
|
|
1317
|
-
async function runAllowlistAddMember(ctx, member, options) {
|
|
1318
|
-
const allowlistId = requireAllowlistId(options.allowlist);
|
|
1319
|
-
const memberAddress = toSuiAddress3(member);
|
|
1320
|
-
const capId = await resolveAllowlistCap(ctx.filesReader, ctx.address, allowlistId, options.cap, ctx.signal);
|
|
1321
|
-
const result = await addMember(ctx.adapter, ctx.config, {
|
|
1322
|
-
allowlistId,
|
|
1323
|
-
capId,
|
|
1324
|
-
member: memberAddress,
|
|
1325
|
-
signal: ctx.signal
|
|
1326
|
-
});
|
|
1327
|
-
ctx.output.result(`Added ${memberAddress} to allowlist ${shortId(allowlistId)}. (tx: ${result.digest})`, result);
|
|
1328
|
-
}
|
|
1329
|
-
async function runAllowlistRemoveMember(ctx, member, options) {
|
|
1330
|
-
const allowlistId = requireAllowlistId(options.allowlist);
|
|
1331
|
-
const memberAddress = toSuiAddress3(member);
|
|
1332
|
-
const capId = await resolveAllowlistCap(ctx.filesReader, ctx.address, allowlistId, options.cap, ctx.signal);
|
|
1333
|
-
const result = await removeMember(ctx.adapter, ctx.config, {
|
|
1334
|
-
allowlistId,
|
|
1335
|
-
capId,
|
|
1336
|
-
member: memberAddress,
|
|
1337
|
-
signal: ctx.signal
|
|
1338
|
-
});
|
|
1339
|
-
ctx.output.result(`Removed ${memberAddress} from allowlist ${shortId(allowlistId)}. (tx: ${result.digest})`, result);
|
|
1340
|
-
}
|
|
1341
|
-
async function runAllowlistTransferCap(ctx, recipient, options, gopts) {
|
|
1342
|
-
const allowlistId = requireAllowlistId(options.allowlist);
|
|
1343
|
-
const to = toSuiAddress3(recipient);
|
|
1344
|
-
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 });
|
|
1345
|
-
if (!proceed) {
|
|
1346
|
-
cancelled();
|
|
1347
|
-
}
|
|
1348
|
-
ctx.output.info("Resolving allowlist Cap...");
|
|
1349
|
-
const capId = await resolveAllowlistCap(ctx.filesReader, ctx.address, allowlistId, options.cap, ctx.signal);
|
|
1350
|
-
const result = await transferAllowlistCap(ctx.adapter, ctx.config, {
|
|
1351
|
-
capId,
|
|
1352
|
-
recipient: to,
|
|
1353
|
-
signal: ctx.signal
|
|
1354
|
-
});
|
|
1355
|
-
ctx.output.result(`Transferred allowlist admin to ${to}. (tx: ${result.digest})`, result);
|
|
1356
|
-
}
|
|
1357
|
-
async function runAllowlistDelete(ctx, options, gopts) {
|
|
1358
|
-
const allowlistId = requireAllowlistId(options.allowlist);
|
|
1359
|
-
const proceed = await confirm(`Delete allowlist ${shortId(allowlistId)}? Files gated by it become permanently undecryptable.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
1360
|
-
if (!proceed) {
|
|
1361
|
-
cancelled();
|
|
1362
|
-
}
|
|
1363
|
-
ctx.output.info("Resolving allowlist Cap...");
|
|
1364
|
-
const capId = await resolveAllowlistCap(ctx.filesReader, ctx.address, allowlistId, options.cap, ctx.signal);
|
|
1365
|
-
const result = await deleteAllowlist(ctx.adapter, ctx.config, {
|
|
1366
|
-
allowlistId,
|
|
1367
|
-
capId,
|
|
1368
|
-
signal: ctx.signal
|
|
1369
|
-
});
|
|
1370
|
-
ctx.output.result(`Deleted allowlist ${allowlistId}. (tx: ${result.digest})`, result);
|
|
1371
|
-
}
|
|
1372
|
-
async function runAllowlistGet(ctx, target) {
|
|
1373
|
-
const result = await ctx.filesReader.getAllowlist(toAllowlistId(target), ctx.signal);
|
|
1374
|
-
ctx.output.result(renderAllowlist(result), result);
|
|
1375
|
-
}
|
|
1376
|
-
async function runAllowlistListCaps(ctx, address, options) {
|
|
1377
|
-
const holder = address === undefined ? ctx.ownerAddress : toSuiAddress3(address);
|
|
1378
|
-
if (holder === undefined) {
|
|
1379
|
-
throw new UsageError("No address given and no active account. Pass an address or import an account.");
|
|
1380
|
-
}
|
|
1381
|
-
const page = await ctx.filesReader.listAllowlistCapsOwnedBy(holder, {
|
|
1382
|
-
signal: ctx.signal,
|
|
1383
|
-
...options.limit === undefined ? {} : { limit: parseLimit(options.limit) },
|
|
1384
|
-
...options.cursor === undefined ? {} : { cursor: options.cursor }
|
|
1385
|
-
});
|
|
1386
|
-
if (page.nextCursor !== null) {
|
|
1387
|
-
ctx.output.info(`More results: pass --cursor "${page.nextCursor}"`);
|
|
1388
|
-
}
|
|
1389
|
-
ctx.output.result(renderAllowlistCapList(page.results), {
|
|
1390
|
-
results: page.results,
|
|
1391
|
-
nextCursor: page.nextCursor
|
|
1392
|
-
});
|
|
1393
|
-
}
|
|
1394
|
-
function registerAllowlistCommands(program) {
|
|
1395
|
-
const allowlist = program.command("allowlist").description("Manage allowlists that gate decryption of encrypted files");
|
|
1396
|
-
allowlist.command("create").description("Create an allowlist and transfer its admin cap to you").requiredOption("-n, --name <name>", "Allowlist name").action(async (options, command) => {
|
|
1397
|
-
await runAllowlistCreate(await buildWriteContext(command), options);
|
|
1398
|
-
});
|
|
1399
|
-
const addMemberCmd = allowlist.command("add-member <member>").description("Add a wallet address to an allowlist");
|
|
1400
|
-
allowlistCapOption(allowlistOption(addMemberCmd)).action(async (member, options, command) => {
|
|
1401
|
-
await runAllowlistAddMember(await buildAllowlistWriteContext(command), member, options);
|
|
1402
|
-
});
|
|
1403
|
-
const removeMemberCmd = allowlist.command("remove-member <member>").description("Remove a wallet address from an allowlist");
|
|
1404
|
-
allowlistCapOption(allowlistOption(removeMemberCmd)).action(async (member, options, command) => {
|
|
1405
|
-
await runAllowlistRemoveMember(await buildAllowlistWriteContext(command), member, options);
|
|
1406
|
-
});
|
|
1407
|
-
const transferCmd = allowlist.command("transfer-cap <recipient>").description("Transfer allowlist admin rights to another address");
|
|
1408
|
-
allowlistCapOption(allowlistOption(transferCmd)).action(async (recipient, options, command) => {
|
|
1409
|
-
await runAllowlistTransferCap(await buildAllowlistWriteContext(command), recipient, options, globalOptions(command));
|
|
1410
|
-
});
|
|
1411
|
-
const deleteCmd = allowlist.command("delete").description("Delete an allowlist (dependent files become undecryptable)");
|
|
1412
|
-
allowlistCapOption(allowlistOption(deleteCmd)).action(async (options, command) => {
|
|
1413
|
-
await runAllowlistDelete(await buildAllowlistWriteContext(command), options, globalOptions(command));
|
|
1414
|
-
});
|
|
1415
|
-
allowlist.command("get <allowlist>").description("Fetch an allowlist's name and members").action(async (target, _options, command) => {
|
|
1416
|
-
await runAllowlistGet(await buildFilesReadContext(command), target);
|
|
1417
|
-
});
|
|
1418
|
-
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) => {
|
|
1419
|
-
await runAllowlistListCaps(await buildFilesReadContext(command), address, options);
|
|
1420
|
-
});
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
// src/commands/cap.ts
|
|
1424
|
-
import {
|
|
1425
|
-
destroyPublisherCap,
|
|
1426
|
-
issuePublisherCap,
|
|
1427
|
-
revokePublisherCap,
|
|
1428
|
-
toPublisherCapId as toPublisherCapId2,
|
|
1429
|
-
toSuiAddress as toSuiAddress4,
|
|
1430
|
-
transferPublisherCap
|
|
1431
|
-
} from "@arcadiasystems/morse-sdk";
|
|
1432
|
-
|
|
1433
|
-
// src/cli/target.ts
|
|
1434
|
-
import { toPublicationId } from "@arcadiasystems/morse-sdk";
|
|
1435
|
-
var OBJECT_ID = /^0x[0-9a-f]{1,64}$/i;
|
|
1436
|
-
var SLUG_PAGE_LIMIT = 50;
|
|
1437
|
-
async function resolvePublication(ctx, override) {
|
|
1438
|
-
const value = override ?? ctx.settings.publication;
|
|
1439
|
-
if (value === undefined) {
|
|
1440
|
-
throw new UsageError("No publication selected. Pass --publication <slug|id> or run `morse use <slug|id>`.");
|
|
1441
|
-
}
|
|
1442
|
-
if (OBJECT_ID.test(value)) {
|
|
1443
|
-
return toPublicationId(value.toLowerCase());
|
|
1444
|
-
}
|
|
1445
|
-
return resolveSlug(ctx, value);
|
|
1446
|
-
}
|
|
1447
|
-
function resolveCollection(ctx, override) {
|
|
1448
|
-
const value = override ?? ctx.settings.collection;
|
|
1449
|
-
if (value === undefined) {
|
|
1450
|
-
throw new UsageError("No collection selected. Pass --collection <name> or run `morse use <slug|id> <collection>`.");
|
|
1451
|
-
}
|
|
1452
|
-
return value;
|
|
1453
|
-
}
|
|
1454
|
-
async function resolveSlug(ctx, slug) {
|
|
1455
|
-
if (ctx.ownerAddress === undefined) {
|
|
1456
|
-
throw new UsageError(`Cannot resolve the slug "${slug}" without an active account. Pass the publication id, or select an account.`);
|
|
1457
|
-
}
|
|
1458
|
-
ctx.output.info(`Resolving slug "${slug}" among owned publications...`);
|
|
1459
|
-
let cursor;
|
|
1460
|
-
do {
|
|
1461
|
-
const page = await ctx.reader.listPublicationsOwnedBy(ctx.ownerAddress, {
|
|
1462
|
-
limit: SLUG_PAGE_LIMIT,
|
|
1463
|
-
signal: ctx.signal,
|
|
1464
|
-
...cursor === undefined ? {} : { cursor }
|
|
1465
|
-
});
|
|
1466
|
-
for (const owned of page.results) {
|
|
1467
|
-
const publication = await ctx.reader.getPublication(owned.publicationId, ctx.signal);
|
|
1468
|
-
if (publication.slug === slug) {
|
|
1469
|
-
return publication.id;
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
cursor = page.nextCursor ?? undefined;
|
|
1473
|
-
} while (cursor !== undefined);
|
|
1474
|
-
throw new UsageError(`No publication with slug "${slug}" owned by the active account. Pass the publication id instead.`);
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
1297
|
// src/commands/cap.ts
|
|
1478
1298
|
async function runCapList(ctx, address, options) {
|
|
1479
|
-
const holder = address === undefined ? ctx.ownerAddress :
|
|
1299
|
+
const holder = address === undefined ? ctx.ownerAddress : toSuiAddress3(address);
|
|
1480
1300
|
if (holder === undefined) {
|
|
1481
1301
|
throw new UsageError("No address given and no active account. Pass an address or import an account.");
|
|
1482
1302
|
}
|
|
@@ -1495,7 +1315,7 @@ async function runCapList(ctx, address, options) {
|
|
|
1495
1315
|
}
|
|
1496
1316
|
async function runCapIssue(ctx, holder, options) {
|
|
1497
1317
|
const id = await resolvePublication(ctx, options.publication);
|
|
1498
|
-
const holderAddress =
|
|
1318
|
+
const holderAddress = toSuiAddress3(holder);
|
|
1499
1319
|
ctx.output.info("Resolving OwnerCap...");
|
|
1500
1320
|
const ownerCapId = await resolveOwnerCap(ctx.reader, ctx.address, id, options.ownerCap, ctx.signal);
|
|
1501
1321
|
const result = await issuePublisherCap(ctx.adapter, ctx.config, {
|
|
@@ -1539,7 +1359,7 @@ async function runCapDestroy(ctx, publisherCapId, options, gopts) {
|
|
|
1539
1359
|
}
|
|
1540
1360
|
async function runCapTransfer(ctx, publisherCapId, recipient, gopts) {
|
|
1541
1361
|
const capId = toPublisherCapId2(publisherCapId);
|
|
1542
|
-
const to =
|
|
1362
|
+
const to = toSuiAddress3(recipient);
|
|
1543
1363
|
const proceed = await confirm(`Transfer PublisherCap ${capId} to ${to}?`, {
|
|
1544
1364
|
assumeYes: Boolean(gopts.yes),
|
|
1545
1365
|
signal: ctx.signal
|
|
@@ -2026,21 +1846,22 @@ function registerEntryCommands(program) {
|
|
|
2026
1846
|
|
|
2027
1847
|
// src/commands/file.ts
|
|
2028
1848
|
import {
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
1849
|
+
addRecipient,
|
|
1850
|
+
buildRecipientFileSealId,
|
|
1851
|
+
createEncryptedRecipientFile,
|
|
1852
|
+
createRecipientFile,
|
|
1853
|
+
deleteRecipientFile,
|
|
1854
|
+
reconcileRecipientFilesAccessibleBy,
|
|
1855
|
+
reconcileRecipientFilesOwnedBy,
|
|
1856
|
+
removeRecipient,
|
|
2036
1857
|
toBlobObjectId,
|
|
2037
|
-
|
|
2038
|
-
toSuiAddress as
|
|
1858
|
+
toRecipientFileId,
|
|
1859
|
+
toSuiAddress as toSuiAddress4,
|
|
2039
1860
|
toWalrusBlobId,
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
1861
|
+
transferRecipientFileOwnership,
|
|
1862
|
+
updateRecipientFileMetadata,
|
|
1863
|
+
uploadEncryptedRecipientFileFromBytes,
|
|
1864
|
+
uploadRecipientFileFromBytes
|
|
2044
1865
|
} from "@arcadiasystems/morse-sdk";
|
|
2045
1866
|
import { SessionKey as SessionKey2 } from "@mysten/seal";
|
|
2046
1867
|
|
|
@@ -2060,8 +1881,8 @@ async function fetchEventStream(client, eventType, signal) {
|
|
|
2060
1881
|
for (const event of page.data) {
|
|
2061
1882
|
out.push({
|
|
2062
1883
|
type: event.type,
|
|
2063
|
-
|
|
2064
|
-
timestampMs: event.timestampMs
|
|
1884
|
+
json: event.parsedJson ?? {},
|
|
1885
|
+
timestampMs: event.timestampMs == null ? 0 : Number(event.timestampMs)
|
|
2065
1886
|
});
|
|
2066
1887
|
}
|
|
2067
1888
|
if (!page.hasNextPage || page.nextCursor === null || page.nextCursor === undefined) {
|
|
@@ -2097,9 +1918,35 @@ function decodeHex(value) {
|
|
|
2097
1918
|
return bytes;
|
|
2098
1919
|
}
|
|
2099
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
|
+
|
|
2100
1948
|
// src/commands/file.ts
|
|
2101
1949
|
var SESSION_KEY_TTL_MIN2 = 10;
|
|
2102
|
-
var SEAL_NONCE_BYTES2 = 16;
|
|
2103
1950
|
function uploadProgress(output) {
|
|
2104
1951
|
const labels = {
|
|
2105
1952
|
encrypting: "Encrypting...",
|
|
@@ -2115,42 +1962,47 @@ function uploadProgress(output) {
|
|
|
2115
1962
|
};
|
|
2116
1963
|
}
|
|
2117
1964
|
async function runFileGet(ctx, target) {
|
|
2118
|
-
const result = await ctx.filesReader.
|
|
2119
|
-
ctx.output.result(
|
|
1965
|
+
const result = await ctx.filesReader.getRecipientFile(toRecipientFileId(target), ctx.signal);
|
|
1966
|
+
ctx.output.result(renderRecipientFile(result), result);
|
|
2120
1967
|
}
|
|
2121
1968
|
async function runFileRegister(ctx, options) {
|
|
2122
|
-
if (options.
|
|
2123
|
-
throw new UsageError("Pass --
|
|
1969
|
+
if (options.public && options.encrypted) {
|
|
1970
|
+
throw new UsageError("Pass --public or --encrypted, not both.");
|
|
2124
1971
|
}
|
|
1972
|
+
const recipients = (options.recipient ?? []).map((r) => toSuiAddress4(r));
|
|
2125
1973
|
const blobId = toWalrusBlobId(options.blobId);
|
|
2126
1974
|
const size = parseByteSize(options.size, "--size");
|
|
2127
1975
|
const blobObjectId = options.blobObjectId === undefined ? undefined : toBlobObjectId(options.blobObjectId);
|
|
2128
|
-
|
|
2129
|
-
const result2 = await createEncryptedFile(ctx.adapter, ctx.config, {
|
|
2130
|
-
allowlistId: toAllowlistId2(options.allowlist),
|
|
2131
|
-
blobId,
|
|
2132
|
-
...blobObjectId === undefined ? {} : { blobObjectId },
|
|
2133
|
-
name: options.name,
|
|
2134
|
-
contentType: options.contentType,
|
|
2135
|
-
size,
|
|
2136
|
-
signal: ctx.signal
|
|
2137
|
-
});
|
|
2138
|
-
ctx.output.result(`Registered encrypted file ${result2.fileId}. (tx: ${result2.digest})`, result2);
|
|
2139
|
-
return;
|
|
2140
|
-
}
|
|
2141
|
-
const result = await createPublicFile(ctx.adapter, ctx.config, {
|
|
1976
|
+
const common = {
|
|
2142
1977
|
blobId,
|
|
2143
1978
|
...blobObjectId === undefined ? {} : { blobObjectId },
|
|
2144
1979
|
name: options.name,
|
|
2145
1980
|
contentType: options.contentType,
|
|
2146
1981
|
size,
|
|
1982
|
+
recipients,
|
|
2147
1983
|
signal: ctx.signal
|
|
2148
|
-
}
|
|
2149
|
-
|
|
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>.");
|
|
2150
2002
|
}
|
|
2151
2003
|
async function runFileUpdate(ctx, target, options) {
|
|
2152
|
-
const result = await
|
|
2153
|
-
fileId:
|
|
2004
|
+
const result = await updateRecipientFileMetadata(ctx.adapter, ctx.config, {
|
|
2005
|
+
fileId: toRecipientFileId(target),
|
|
2154
2006
|
name: options.name,
|
|
2155
2007
|
contentType: options.contentType,
|
|
2156
2008
|
signal: ctx.signal
|
|
@@ -2158,13 +2010,13 @@ async function runFileUpdate(ctx, target, options) {
|
|
|
2158
2010
|
ctx.output.result(`Updated file ${shortId(target)} metadata. (tx: ${result.digest})`, result);
|
|
2159
2011
|
}
|
|
2160
2012
|
async function runFileTransferOwnership(ctx, target, newOwner, gopts) {
|
|
2161
|
-
const fileId =
|
|
2162
|
-
const to =
|
|
2163
|
-
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 });
|
|
2164
2016
|
if (!proceed) {
|
|
2165
2017
|
cancelled();
|
|
2166
2018
|
}
|
|
2167
|
-
const result = await
|
|
2019
|
+
const result = await transferRecipientFileOwnership(ctx.adapter, ctx.config, {
|
|
2168
2020
|
fileId,
|
|
2169
2021
|
newOwner: to,
|
|
2170
2022
|
signal: ctx.signal
|
|
@@ -2172,76 +2024,140 @@ async function runFileTransferOwnership(ctx, target, newOwner, gopts) {
|
|
|
2172
2024
|
ctx.output.result(`Transferred file ownership to ${to}. (tx: ${result.digest})`, result);
|
|
2173
2025
|
}
|
|
2174
2026
|
async function runFileDelete(ctx, target, gopts) {
|
|
2175
|
-
const fileId =
|
|
2176
|
-
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 });
|
|
2177
2029
|
if (!proceed) {
|
|
2178
2030
|
cancelled();
|
|
2179
2031
|
}
|
|
2180
|
-
const result = await
|
|
2032
|
+
const result = await deleteRecipientFile(ctx.adapter, ctx.config, {
|
|
2181
2033
|
fileId,
|
|
2182
2034
|
signal: ctx.signal
|
|
2183
2035
|
});
|
|
2184
2036
|
ctx.output.result(`Deleted file ${fileId}. (tx: ${result.digest})`, result);
|
|
2185
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
|
+
}
|
|
2186
2064
|
async function runFileUpload(ctx, path, options) {
|
|
2187
|
-
if (options.
|
|
2188
|
-
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>).");
|
|
2189
2074
|
}
|
|
2190
2075
|
const epochs = parsePositiveInt(options.epochs ?? "3", "--epochs");
|
|
2191
2076
|
const bytes = await readContentBytes({ file: path });
|
|
2192
2077
|
const contentType = resolveContentType(options.contentType, path);
|
|
2193
2078
|
const upload = { epochs, deletable: true };
|
|
2194
2079
|
const onProgress = uploadProgress(ctx.output);
|
|
2195
|
-
if (options.
|
|
2196
|
-
const
|
|
2197
|
-
const sealId = buildAllowlistSealId(allowlistId, crypto.getRandomValues(new Uint8Array(SEAL_NONCE_BYTES2)));
|
|
2198
|
-
const result2 = await uploadEncryptedFileFromBytes(ctx.adapter, ctx.config, {
|
|
2080
|
+
if (options.public) {
|
|
2081
|
+
const result2 = await uploadRecipientFileFromBytes(ctx.adapter, ctx.config, {
|
|
2199
2082
|
walrus: ctx.walrus,
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
sealId,
|
|
2203
|
-
plaintext: bytes,
|
|
2083
|
+
bytes,
|
|
2084
|
+
recipients: [],
|
|
2204
2085
|
name: options.name,
|
|
2205
2086
|
contentType,
|
|
2206
2087
|
upload,
|
|
2207
2088
|
signal: ctx.signal,
|
|
2208
2089
|
onProgress
|
|
2209
2090
|
});
|
|
2210
|
-
const
|
|
2211
|
-
const
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
` tx: ${result2.digest}`,
|
|
2216
|
-
"Save the seal id: decrypting later needs it plus allowlist membership."
|
|
2217
|
-
].join(`
|
|
2218
|
-
`);
|
|
2219
|
-
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 });
|
|
2220
2096
|
return;
|
|
2221
2097
|
}
|
|
2222
|
-
const result = await
|
|
2098
|
+
const result = await uploadEncryptedRecipientFileFromBytes(ctx.adapter, ctx.config, {
|
|
2223
2099
|
walrus: ctx.walrus,
|
|
2224
|
-
|
|
2100
|
+
seal: ctx.seal,
|
|
2101
|
+
plaintext: bytes,
|
|
2102
|
+
recipients,
|
|
2225
2103
|
name: options.name,
|
|
2226
2104
|
contentType,
|
|
2227
2105
|
upload,
|
|
2228
2106
|
signal: ctx.signal,
|
|
2229
2107
|
onProgress
|
|
2230
2108
|
});
|
|
2231
|
-
const
|
|
2232
|
-
const
|
|
2233
|
-
const
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
}
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
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>.");
|
|
2240
2144
|
}
|
|
2241
|
-
if (
|
|
2242
|
-
|
|
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
|
+
};
|
|
2243
2156
|
}
|
|
2244
|
-
|
|
2157
|
+
return { fileId: positional };
|
|
2158
|
+
}
|
|
2159
|
+
async function decryptRecipientFile(ctx, fileId, ciphertext, prefix, nonce) {
|
|
2160
|
+
const sealId = buildRecipientFileSealId(prefix, nonce);
|
|
2245
2161
|
const { keypair, address } = await ctx.unlockSigner();
|
|
2246
2162
|
ctx.output.info("Signing a SessionKey with the active account...");
|
|
2247
2163
|
const sessionKey = await SessionKey2.create({
|
|
@@ -2251,24 +2167,29 @@ async function decryptFile(ctx, file, ciphertext, sealIdHex) {
|
|
|
2251
2167
|
signer: keypair,
|
|
2252
2168
|
suiClient: ctx.client
|
|
2253
2169
|
});
|
|
2254
|
-
return ctx.seal.
|
|
2170
|
+
return ctx.seal.decryptUnderRecipientFile(ciphertext, {
|
|
2171
|
+
sessionKey,
|
|
2255
2172
|
sealId,
|
|
2256
|
-
|
|
2257
|
-
sessionKey
|
|
2173
|
+
fileId
|
|
2258
2174
|
});
|
|
2259
2175
|
}
|
|
2260
|
-
async function runFileDownload(ctx,
|
|
2176
|
+
async function runFileDownload(ctx, positional, options) {
|
|
2261
2177
|
if (ctx.output.isJson && options.out === undefined) {
|
|
2262
2178
|
throw new UsageError("Downloading content to stdout is not supported in --json mode; pass --out <path>.");
|
|
2263
2179
|
}
|
|
2264
|
-
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
|
+
}
|
|
2265
2186
|
ctx.output.info("Fetching content from Walrus...");
|
|
2266
2187
|
const bytes = await ctx.walrusRead.readBlob(file.blobId, {
|
|
2267
2188
|
signal: ctx.signal
|
|
2268
2189
|
});
|
|
2269
2190
|
let content = bytes;
|
|
2270
|
-
if (
|
|
2271
|
-
content = await
|
|
2191
|
+
if (target.decrypt !== undefined) {
|
|
2192
|
+
content = await decryptRecipientFile(ctx, fileId, bytes, target.decrypt.prefix, target.decrypt.nonce);
|
|
2272
2193
|
}
|
|
2273
2194
|
if (options.out !== undefined) {
|
|
2274
2195
|
await writeFileContents(options.out, content);
|
|
@@ -2285,26 +2206,29 @@ async function runFileDownload(ctx, target, options) {
|
|
|
2285
2206
|
}
|
|
2286
2207
|
async function runFileList(ctx, options) {
|
|
2287
2208
|
const limit = options.limit === undefined ? undefined : parseLimit(options.limit);
|
|
2288
|
-
const address = options.address === undefined ? ctx.ownerAddress :
|
|
2209
|
+
const address = options.address === undefined ? ctx.ownerAddress : toSuiAddress4(options.address);
|
|
2289
2210
|
if (address === undefined) {
|
|
2290
2211
|
throw new UsageError("No address given and no active account. Pass --address or import an account.");
|
|
2291
2212
|
}
|
|
2292
2213
|
const types = ctx.eventTypes;
|
|
2293
2214
|
let summaries;
|
|
2294
2215
|
if (options.accessible) {
|
|
2295
|
-
ctx.output.info("Fetching
|
|
2216
|
+
ctx.output.info("Fetching recipient and file events...");
|
|
2296
2217
|
const events = await fetchEventStreams(ctx.events, [
|
|
2297
|
-
types.
|
|
2298
|
-
types.
|
|
2299
|
-
types.
|
|
2300
|
-
types.
|
|
2301
|
-
types.FileDeleted
|
|
2218
|
+
types.RecipientFileCreated,
|
|
2219
|
+
types.RecipientAdded,
|
|
2220
|
+
types.RecipientRemoved,
|
|
2221
|
+
types.RecipientFileDeleted
|
|
2302
2222
|
], ctx.signal);
|
|
2303
|
-
summaries =
|
|
2223
|
+
summaries = reconcileRecipientFilesAccessibleBy(events, address, types);
|
|
2304
2224
|
} else {
|
|
2305
2225
|
ctx.output.info("Fetching file events...");
|
|
2306
|
-
const events = await fetchEventStreams(ctx.events, [
|
|
2307
|
-
|
|
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);
|
|
2308
2232
|
}
|
|
2309
2233
|
const limited = limit === undefined ? summaries : summaries.slice(0, limit);
|
|
2310
2234
|
let items = limited;
|
|
@@ -2312,7 +2236,7 @@ async function runFileList(ctx, options) {
|
|
|
2312
2236
|
ctx.output.info(`Hydrating ${limited.length} file(s)...`);
|
|
2313
2237
|
const full = [];
|
|
2314
2238
|
for (const summary of limited) {
|
|
2315
|
-
const record = await ctx.filesReader.
|
|
2239
|
+
const record = await ctx.filesReader.getRecipientFile(summary.id, ctx.signal);
|
|
2316
2240
|
full.push({ ...record, kind: "full" });
|
|
2317
2241
|
}
|
|
2318
2242
|
items = full;
|
|
@@ -2320,17 +2244,17 @@ async function runFileList(ctx, options) {
|
|
|
2320
2244
|
ctx.output.result(renderFileList(items), items);
|
|
2321
2245
|
}
|
|
2322
2246
|
function registerFileCommands(program) {
|
|
2323
|
-
const file = program.command("file").description("Upload, register, read, and manage files on Walrus");
|
|
2324
|
-
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");
|
|
2325
|
-
|
|
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) => {
|
|
2326
2250
|
await runFileUpload(await buildEncryptContext(command), path, options);
|
|
2327
2251
|
});
|
|
2328
|
-
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
|
|
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) => {
|
|
2329
2253
|
await runFileList(await buildFileListContext(command, {
|
|
2330
2254
|
indexerUrl: options.indexerUrl
|
|
2331
2255
|
}), options);
|
|
2332
2256
|
});
|
|
2333
|
-
const download = file.command("download
|
|
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");
|
|
2334
2258
|
viaAggregatorOption(download).action(async (target, options, command) => {
|
|
2335
2259
|
await runFileDownload(await buildFileDownloadContext(command, {
|
|
2336
2260
|
viaAggregator: options.viaAggregator
|
|
@@ -2339,26 +2263,36 @@ function registerFileCommands(program) {
|
|
|
2339
2263
|
file.command("get <file>").description("Fetch a file's on-chain metadata").action(async (target, _options, command) => {
|
|
2340
2264
|
await runFileGet(await buildFilesReadContext(command), target);
|
|
2341
2265
|
});
|
|
2342
|
-
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");
|
|
2343
|
-
|
|
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) => {
|
|
2344
2268
|
await runFileRegister(await buildWriteContext(command), options);
|
|
2345
2269
|
});
|
|
2346
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) => {
|
|
2347
2271
|
await runFileUpdate(await buildWriteContext(command), target, options);
|
|
2348
2272
|
});
|
|
2349
|
-
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) => {
|
|
2350
2274
|
await runFileTransferOwnership(await buildWriteContext(command), target, newOwner, globalOptions(command));
|
|
2351
2275
|
});
|
|
2352
2276
|
file.command("delete <file>").description("Delete a file's metadata record (does not delete the blob)").action(async (target, _options, command) => {
|
|
2353
2277
|
await runFileDelete(await buildWriteContext(command), target, globalOptions(command));
|
|
2354
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
|
+
});
|
|
2355
2289
|
}
|
|
2356
2290
|
|
|
2357
2291
|
// src/commands/publication.ts
|
|
2358
2292
|
import {
|
|
2359
2293
|
createPublication,
|
|
2360
2294
|
deletePublication,
|
|
2361
|
-
toSuiAddress as
|
|
2295
|
+
toSuiAddress as toSuiAddress5,
|
|
2362
2296
|
transferOwnership
|
|
2363
2297
|
} from "@arcadiasystems/morse-sdk";
|
|
2364
2298
|
async function runPublicationGet(ctx, target) {
|
|
@@ -2367,7 +2301,7 @@ async function runPublicationGet(ctx, target) {
|
|
|
2367
2301
|
ctx.output.result(renderPublication(result), result);
|
|
2368
2302
|
}
|
|
2369
2303
|
async function runPublicationList(ctx, address, options) {
|
|
2370
|
-
const owner = address === undefined ? ctx.ownerAddress :
|
|
2304
|
+
const owner = address === undefined ? ctx.ownerAddress : toSuiAddress5(address);
|
|
2371
2305
|
if (owner === undefined) {
|
|
2372
2306
|
throw new UsageError("No address given and no active account. Pass an address or import an account.");
|
|
2373
2307
|
}
|
|
@@ -2446,7 +2380,7 @@ async function runPublicationDelete(ctx, target, options, gopts) {
|
|
|
2446
2380
|
}
|
|
2447
2381
|
async function runPublicationTransferOwnership(ctx, recipient, options, gopts) {
|
|
2448
2382
|
const id = await resolvePublication(ctx, options.publication);
|
|
2449
|
-
const to =
|
|
2383
|
+
const to = toSuiAddress5(recipient);
|
|
2450
2384
|
const proceed = await confirm(`Transfer ownership of ${shortId(id)} to ${to}? You will lose owner control.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
2451
2385
|
if (!proceed) {
|
|
2452
2386
|
cancelled();
|
|
@@ -2638,7 +2572,6 @@ function registerCommands(program) {
|
|
|
2638
2572
|
registerEntryCommands(program);
|
|
2639
2573
|
registerRevisionCommands(program);
|
|
2640
2574
|
registerCapCommands(program);
|
|
2641
|
-
registerAllowlistCommands(program);
|
|
2642
2575
|
registerFileCommands(program);
|
|
2643
2576
|
}
|
|
2644
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",
|