@arcadiasystems/morse-cli 0.1.0 → 0.2.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 +34 -1
- package/README.md +59 -3
- package/dist/index.js +1304 -713
- package/package.json +8 -3
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.2.0",
|
|
152
152
|
description: "Command-line interface for the Morse decentralized CMS on Sui.",
|
|
153
153
|
license: "MIT",
|
|
154
154
|
type: "module",
|
|
@@ -194,11 +194,16 @@ var package_default = {
|
|
|
194
194
|
lint: "biome check .",
|
|
195
195
|
"lint:fix": "biome check --write .",
|
|
196
196
|
test: "bun test",
|
|
197
|
+
"test:unit": "bun test test/*.test.ts",
|
|
198
|
+
"test:cli": "bun test test/cli/",
|
|
197
199
|
"test:coverage": "bun test --coverage",
|
|
198
|
-
|
|
200
|
+
"test:e2e": "MORSE_E2E=1 bun test test/e2e/",
|
|
201
|
+
coverage: "bun scripts/coverage-gate.ts",
|
|
202
|
+
check: "tsc --noEmit && biome check . && bun scripts/coverage-gate.ts",
|
|
203
|
+
prepublishOnly: "bun run check && bun run build"
|
|
199
204
|
},
|
|
200
205
|
dependencies: {
|
|
201
|
-
"@arcadiasystems/morse-sdk": "^0.
|
|
206
|
+
"@arcadiasystems/morse-sdk": "^0.2.0",
|
|
202
207
|
"@mysten/seal": "1.1.3",
|
|
203
208
|
"@mysten/sui": "2.16.2",
|
|
204
209
|
"@mysten/walrus": "1.1.6",
|
|
@@ -313,27 +318,41 @@ class Output {
|
|
|
313
318
|
}
|
|
314
319
|
result(human, data) {
|
|
315
320
|
if (this.options.json) {
|
|
316
|
-
|
|
321
|
+
this.out(`${toJson(data)}
|
|
317
322
|
`);
|
|
318
323
|
return;
|
|
319
324
|
}
|
|
320
|
-
|
|
325
|
+
this.out(`${human}
|
|
321
326
|
`);
|
|
322
327
|
}
|
|
323
328
|
info(message) {
|
|
324
329
|
if (this.options.quiet || this.options.json) {
|
|
325
330
|
return;
|
|
326
331
|
}
|
|
327
|
-
|
|
332
|
+
this.err(`${this.paint(message, DIM)}
|
|
328
333
|
`);
|
|
329
334
|
}
|
|
330
335
|
warn(message) {
|
|
331
336
|
if (this.options.quiet || this.options.json) {
|
|
332
337
|
return;
|
|
333
338
|
}
|
|
334
|
-
|
|
339
|
+
this.err(`${this.paint(message, YELLOW)}
|
|
335
340
|
`);
|
|
336
341
|
}
|
|
342
|
+
out(text) {
|
|
343
|
+
if (this.options.writeOut !== undefined) {
|
|
344
|
+
this.options.writeOut(text);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
process.stdout.write(text);
|
|
348
|
+
}
|
|
349
|
+
err(text) {
|
|
350
|
+
if (this.options.writeErr !== undefined) {
|
|
351
|
+
this.options.writeErr(text);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
process.stderr.write(text);
|
|
355
|
+
}
|
|
337
356
|
paint(text, code) {
|
|
338
357
|
return this.options.color ? `\x1B[${code}m${text}${RESET}` : text;
|
|
339
358
|
}
|
|
@@ -766,79 +785,85 @@ function accountAddress(account, env = process.env) {
|
|
|
766
785
|
}
|
|
767
786
|
|
|
768
787
|
// src/commands/account.ts
|
|
788
|
+
async function runAccountImport(output, gopts, env, signal) {
|
|
789
|
+
const secret = await readSecretToImport(env, signal);
|
|
790
|
+
const password = await resolvePassword("create", env, signal);
|
|
791
|
+
const address = await importKey(secret, password);
|
|
792
|
+
const profileName = await associateAccount(gopts, address);
|
|
793
|
+
output.info(`Imported ${address} into keystore.`);
|
|
794
|
+
output.result(`Imported account ${address} (profile "${profileName}").`, {
|
|
795
|
+
address,
|
|
796
|
+
profile: profileName
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
async function runAccountList(output, gopts) {
|
|
800
|
+
const addresses = await listAddresses();
|
|
801
|
+
const active = await resolveActiveAccount(gopts);
|
|
802
|
+
if (addresses.length === 0) {
|
|
803
|
+
output.result("No accounts. Import one with: morse account import", {
|
|
804
|
+
active,
|
|
805
|
+
accounts: []
|
|
806
|
+
});
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
const human = addresses.map((address) => `${address === active ? "*" : " "} ${address}`).join(`
|
|
810
|
+
`);
|
|
811
|
+
output.result(human, { active, accounts: addresses });
|
|
812
|
+
}
|
|
813
|
+
async function runAccountShow(output, gopts) {
|
|
814
|
+
const active = await resolveActiveAccount(gopts);
|
|
815
|
+
if (active === undefined) {
|
|
816
|
+
throw new UsageError("No active account. Import one with `morse account import` or set MORSE_ADDRESS.");
|
|
817
|
+
}
|
|
818
|
+
output.result(active, { address: active });
|
|
819
|
+
}
|
|
820
|
+
async function runAccountUse(output, gopts, address) {
|
|
821
|
+
if (!await hasKeystore(address)) {
|
|
822
|
+
throw new UsageError(`No keystore for ${address}. Import it first with: morse account import`);
|
|
823
|
+
}
|
|
824
|
+
const profileName = await associateAccount(gopts, address);
|
|
825
|
+
output.result(`Active account for "${profileName}" set to ${address}.`, {
|
|
826
|
+
address,
|
|
827
|
+
profile: profileName
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
async function runAccountExport(output, gopts, address, env, signal) {
|
|
831
|
+
if (output.isJson) {
|
|
832
|
+
throw new UsageError("account export is not available in --json mode.");
|
|
833
|
+
}
|
|
834
|
+
if (!isInteractive()) {
|
|
835
|
+
throw new UsageError("account export requires an interactive terminal.");
|
|
836
|
+
}
|
|
837
|
+
if (gopts.yes) {
|
|
838
|
+
throw new UsageError("account export does not accept --yes. Confirm interactively.");
|
|
839
|
+
}
|
|
840
|
+
const proceed = await confirm(`Reveal the secret key for ${address}? Anyone who sees it controls the account.`, { signal });
|
|
841
|
+
if (!proceed) {
|
|
842
|
+
cancelled();
|
|
843
|
+
}
|
|
844
|
+
const password = await resolvePassword("unlock", env, signal);
|
|
845
|
+
const secret = await unlockSecret(address, password);
|
|
846
|
+
process.stderr.write(`Warning: secret key follows. Handle it with care.
|
|
847
|
+
`);
|
|
848
|
+
process.stdout.write(`${secret}
|
|
849
|
+
`);
|
|
850
|
+
}
|
|
769
851
|
function registerAccountCommands(program) {
|
|
770
852
|
const account = program.command("account").description("Import and manage encrypted signing keys");
|
|
771
853
|
account.command("import").description("Import a private key into an encrypted keystore").action(async (_options, command) => {
|
|
772
|
-
|
|
773
|
-
const output = outputFor(command);
|
|
774
|
-
const signal = sigintSignal();
|
|
775
|
-
const secret = await readSecretToImport(process.env, signal);
|
|
776
|
-
const password = await resolvePassword("create", process.env, signal);
|
|
777
|
-
const address = await importKey(secret, password);
|
|
778
|
-
const profileName = await associateAccount(opts, address);
|
|
779
|
-
output.info(`Imported ${address} into keystore.`);
|
|
780
|
-
output.result(`Imported account ${address} (profile "${profileName}").`, {
|
|
781
|
-
address,
|
|
782
|
-
profile: profileName
|
|
783
|
-
});
|
|
854
|
+
await runAccountImport(outputFor(command), globalOptions(command), process.env, sigintSignal());
|
|
784
855
|
});
|
|
785
856
|
account.command("list").description("List imported accounts").action(async (_options, command) => {
|
|
786
|
-
|
|
787
|
-
const addresses = await listAddresses();
|
|
788
|
-
const active = await resolveActiveAccount(globalOptions(command));
|
|
789
|
-
if (addresses.length === 0) {
|
|
790
|
-
output.result("No accounts. Import one with: morse account import", {
|
|
791
|
-
active,
|
|
792
|
-
accounts: []
|
|
793
|
-
});
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
const human = addresses.map((address) => `${address === active ? "*" : " "} ${address}`).join(`
|
|
797
|
-
`);
|
|
798
|
-
output.result(human, { active, accounts: addresses });
|
|
857
|
+
await runAccountList(outputFor(command), globalOptions(command));
|
|
799
858
|
});
|
|
800
859
|
account.command("show").description("Print the active account address").action(async (_options, command) => {
|
|
801
|
-
|
|
802
|
-
const active = await resolveActiveAccount(globalOptions(command));
|
|
803
|
-
if (active === undefined) {
|
|
804
|
-
throw new UsageError("No active account. Import one with `morse account import` or set MORSE_ADDRESS.");
|
|
805
|
-
}
|
|
806
|
-
output.result(active, { address: active });
|
|
860
|
+
await runAccountShow(outputFor(command), globalOptions(command));
|
|
807
861
|
});
|
|
808
862
|
account.command("use <address>").description("Set the active account for the current profile").action(async (address, _options, command) => {
|
|
809
|
-
|
|
810
|
-
const output = outputFor(command);
|
|
811
|
-
if (!await hasKeystore(address)) {
|
|
812
|
-
throw new UsageError(`No keystore for ${address}. Import it first with: morse account import`);
|
|
813
|
-
}
|
|
814
|
-
const profileName = await associateAccount(opts, address);
|
|
815
|
-
output.result(`Active account for "${profileName}" set to ${address}.`, {
|
|
816
|
-
address,
|
|
817
|
-
profile: profileName
|
|
818
|
-
});
|
|
863
|
+
await runAccountUse(outputFor(command), globalOptions(command), address);
|
|
819
864
|
});
|
|
820
865
|
account.command("export <address>").description("Print a decrypted secret key (dangerous)").action(async (address, _options, command) => {
|
|
821
|
-
|
|
822
|
-
if (output.isJson) {
|
|
823
|
-
throw new UsageError("account export is not available in --json mode.");
|
|
824
|
-
}
|
|
825
|
-
if (!isInteractive()) {
|
|
826
|
-
throw new UsageError("account export requires an interactive terminal.");
|
|
827
|
-
}
|
|
828
|
-
if (globalOptions(command).yes) {
|
|
829
|
-
throw new UsageError("account export does not accept --yes. Confirm interactively.");
|
|
830
|
-
}
|
|
831
|
-
const signal = sigintSignal();
|
|
832
|
-
const proceed = await confirm(`Reveal the secret key for ${address}? Anyone who sees it controls the account.`, { signal });
|
|
833
|
-
if (!proceed) {
|
|
834
|
-
cancelled();
|
|
835
|
-
}
|
|
836
|
-
const password = await resolvePassword("unlock", process.env, signal);
|
|
837
|
-
const secret = await unlockSecret(address, password);
|
|
838
|
-
process.stderr.write(`Warning: secret key follows. Handle it with care.
|
|
839
|
-
`);
|
|
840
|
-
process.stdout.write(`${secret}
|
|
841
|
-
`);
|
|
866
|
+
await runAccountExport(outputFor(command), globalOptions(command), address, process.env, sigintSignal());
|
|
842
867
|
});
|
|
843
868
|
}
|
|
844
869
|
async function readSecretToImport(env, signal) {
|
|
@@ -857,21 +882,22 @@ async function readSecretToImport(env, signal) {
|
|
|
857
882
|
}
|
|
858
883
|
return secret;
|
|
859
884
|
}
|
|
860
|
-
async function associateAccount(
|
|
861
|
-
return updateActiveProfile(
|
|
885
|
+
async function associateAccount(gopts, address) {
|
|
886
|
+
return updateActiveProfile(gopts, { account: address });
|
|
862
887
|
}
|
|
863
|
-
async function resolveActiveAccount(
|
|
864
|
-
return accountAddress(resolveSettings(
|
|
888
|
+
async function resolveActiveAccount(gopts) {
|
|
889
|
+
return accountAddress(resolveSettings(gopts, await loadConfig()).account);
|
|
865
890
|
}
|
|
866
891
|
|
|
867
|
-
// src/commands/
|
|
892
|
+
// src/commands/allowlist.ts
|
|
868
893
|
import {
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
894
|
+
addMember,
|
|
895
|
+
createAllowlist,
|
|
896
|
+
deleteAllowlist,
|
|
897
|
+
removeMember,
|
|
898
|
+
toAllowlistId,
|
|
873
899
|
toSuiAddress as toSuiAddress3,
|
|
874
|
-
|
|
900
|
+
transferAllowlistCap
|
|
875
901
|
} from "@arcadiasystems/morse-sdk";
|
|
876
902
|
|
|
877
903
|
// src/cli/context.ts
|
|
@@ -879,8 +905,10 @@ import {
|
|
|
879
905
|
DefaultSealAdapter,
|
|
880
906
|
DefaultWalrusReadAdapter,
|
|
881
907
|
DefaultWalrusWriteAdapter,
|
|
908
|
+
HttpAggregatorReadAdapter,
|
|
882
909
|
KeypairAdapter,
|
|
883
910
|
morseConfig,
|
|
911
|
+
RpcFilesReader,
|
|
884
912
|
RpcPublicationReader
|
|
885
913
|
} from "@arcadiasystems/morse-sdk";
|
|
886
914
|
import { SuiGrpcClient } from "@mysten/sui/grpc";
|
|
@@ -920,6 +948,20 @@ async function buildWriteContext(command) {
|
|
|
920
948
|
address
|
|
921
949
|
};
|
|
922
950
|
}
|
|
951
|
+
async function buildFilesReadContext(command) {
|
|
952
|
+
const base = await buildReadContext(command);
|
|
953
|
+
return {
|
|
954
|
+
...base,
|
|
955
|
+
filesReader: RpcFilesReader.fromMorseConfig(base.config, base.client)
|
|
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)
|
|
963
|
+
};
|
|
964
|
+
}
|
|
923
965
|
async function buildContentContext(command) {
|
|
924
966
|
const { base, keypair, address } = await buildSignedBase(command);
|
|
925
967
|
const network = base.settings.network;
|
|
@@ -935,74 +977,54 @@ async function buildEncryptContext(command) {
|
|
|
935
977
|
const seal = DefaultSealAdapter.fromMorseConfig(ctx.config, {}, ctx.client);
|
|
936
978
|
return { ...ctx, seal };
|
|
937
979
|
}
|
|
938
|
-
|
|
980
|
+
function walrusReadAdapter(base, network, viaAggregator) {
|
|
981
|
+
if (viaAggregator) {
|
|
982
|
+
return HttpAggregatorReadAdapter.fromMorseConfig(base.config, base.client);
|
|
983
|
+
}
|
|
984
|
+
return DefaultWalrusReadAdapter.fromConfig({
|
|
985
|
+
network,
|
|
986
|
+
suiClient: base.client
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
async function buildReadContentContext(command, opts = {}) {
|
|
939
990
|
const base = await buildReadContext(command);
|
|
940
991
|
const network = base.settings.network;
|
|
941
992
|
if (network === "localnet") {
|
|
942
993
|
throw new CliError("Walrus reads are not available on localnet. Use testnet or mainnet.", ExitCode.Usage);
|
|
943
994
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
}
|
|
948
|
-
return { ...base, walrusRead };
|
|
995
|
+
return {
|
|
996
|
+
...base,
|
|
997
|
+
walrusRead: walrusReadAdapter(base, network, Boolean(opts.viaAggregator))
|
|
998
|
+
};
|
|
949
999
|
}
|
|
950
|
-
async function buildDecryptContext(command) {
|
|
1000
|
+
async function buildDecryptContext(command, opts = {}) {
|
|
951
1001
|
const { base, keypair, address } = await buildSignedBase(command);
|
|
952
1002
|
const network = base.settings.network;
|
|
953
1003
|
if (network === "localnet") {
|
|
954
1004
|
throw new CliError("Seal decryption is not available on localnet. Use testnet or mainnet.", ExitCode.Usage);
|
|
955
1005
|
}
|
|
956
1006
|
const seal = DefaultSealAdapter.fromMorseConfig(base.config, {}, base.client);
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
// src/cli/target.ts
|
|
965
|
-
import { toPublicationId } from "@arcadiasystems/morse-sdk";
|
|
966
|
-
var OBJECT_ID = /^0x[0-9a-f]{1,64}$/i;
|
|
967
|
-
var SLUG_PAGE_LIMIT = 50;
|
|
968
|
-
async function resolvePublication(ctx, override) {
|
|
969
|
-
const value = override ?? ctx.settings.publication;
|
|
970
|
-
if (value === undefined) {
|
|
971
|
-
throw new UsageError("No publication selected. Pass --publication <slug|id> or run `morse use <slug|id>`.");
|
|
972
|
-
}
|
|
973
|
-
if (OBJECT_ID.test(value)) {
|
|
974
|
-
return toPublicationId(value.toLowerCase());
|
|
975
|
-
}
|
|
976
|
-
return resolveSlug(ctx, value);
|
|
977
|
-
}
|
|
978
|
-
function resolveCollection(ctx, override) {
|
|
979
|
-
const value = override ?? ctx.settings.collection;
|
|
980
|
-
if (value === undefined) {
|
|
981
|
-
throw new UsageError("No collection selected. Pass --collection <name> or run `morse use <slug|id> <collection>`.");
|
|
982
|
-
}
|
|
983
|
-
return value;
|
|
1007
|
+
return {
|
|
1008
|
+
...base,
|
|
1009
|
+
keypair,
|
|
1010
|
+
address,
|
|
1011
|
+
seal,
|
|
1012
|
+
walrusRead: walrusReadAdapter(base, network, Boolean(opts.viaAggregator))
|
|
1013
|
+
};
|
|
984
1014
|
}
|
|
985
|
-
async function
|
|
986
|
-
|
|
987
|
-
|
|
1015
|
+
async function buildFileDownloadContext(command, opts = {}) {
|
|
1016
|
+
const base = await buildReadContext(command);
|
|
1017
|
+
const network = base.settings.network;
|
|
1018
|
+
if (network === "localnet") {
|
|
1019
|
+
throw new CliError("Walrus reads are not available on localnet. Use testnet or mainnet.", ExitCode.Usage);
|
|
988
1020
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
});
|
|
997
|
-
for (const owned of page.results) {
|
|
998
|
-
const publication = await ctx.reader.getPublication(owned.publicationId, ctx.signal);
|
|
999
|
-
if (publication.slug === slug) {
|
|
1000
|
-
return publication.id;
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
cursor = page.nextCursor ?? undefined;
|
|
1004
|
-
} while (cursor !== undefined);
|
|
1005
|
-
throw new UsageError(`No publication with slug "${slug}" owned by the active account. Pass the publication id instead.`);
|
|
1021
|
+
return {
|
|
1022
|
+
...base,
|
|
1023
|
+
filesReader: RpcFilesReader.fromMorseConfig(base.config, base.client),
|
|
1024
|
+
walrusRead: walrusReadAdapter(base, network, Boolean(opts.viaAggregator)),
|
|
1025
|
+
seal: DefaultSealAdapter.fromMorseConfig(base.config, {}, base.client),
|
|
1026
|
+
unlockSigner: () => resolveSigner(base.settings.account, process.env, base.signal)
|
|
1027
|
+
};
|
|
1006
1028
|
}
|
|
1007
1029
|
|
|
1008
1030
|
// src/format/ids.ts
|
|
@@ -1072,6 +1094,38 @@ function renderEntryList(entries) {
|
|
|
1072
1094
|
return entries.map((entry) => `#${entry.id} ${entry.name} (${entry.revisions.length} revisions)`).join(`
|
|
1073
1095
|
`);
|
|
1074
1096
|
}
|
|
1097
|
+
function renderAllowlist(allowlist) {
|
|
1098
|
+
const members = allowlist.members.length === 0 ? "(none)" : allowlist.members.map((m) => ` ${m}`).join(`
|
|
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.";
|
|
1110
|
+
}
|
|
1111
|
+
return caps.map((cap) => `${cap.id} allowlist ${cap.allowlistId}`).join(`
|
|
1112
|
+
`);
|
|
1113
|
+
}
|
|
1114
|
+
function renderEncryptedFile(file) {
|
|
1115
|
+
const lines = [
|
|
1116
|
+
`${file.name} (${shortId(file.id)})`,
|
|
1117
|
+
`contentType: ${file.contentType}`,
|
|
1118
|
+
`size: ${file.size}`,
|
|
1119
|
+
`encrypted: ${file.encrypted}`,
|
|
1120
|
+
`blobId: ${file.blobId}`,
|
|
1121
|
+
`owner: ${file.owner}`
|
|
1122
|
+
];
|
|
1123
|
+
if (file.allowlistId !== null) {
|
|
1124
|
+
lines.push(`allowlist: ${file.allowlistId}`);
|
|
1125
|
+
}
|
|
1126
|
+
return lines.join(`
|
|
1127
|
+
`);
|
|
1128
|
+
}
|
|
1075
1129
|
function headLabel(value) {
|
|
1076
1130
|
return value === null ? "none" : String(value);
|
|
1077
1131
|
}
|
|
@@ -1094,12 +1148,22 @@ function publisherCapOption(command) {
|
|
|
1094
1148
|
function ownerCapOption(command) {
|
|
1095
1149
|
return command.option("--owner-cap <id>", "OwnerCap ID (auto-resolved if omitted)");
|
|
1096
1150
|
}
|
|
1151
|
+
function allowlistOption(command) {
|
|
1152
|
+
return command.option("-a, --allowlist <id>", "Allowlist object id");
|
|
1153
|
+
}
|
|
1154
|
+
function allowlistCapOption(command) {
|
|
1155
|
+
return command.option("--cap <id>", "Allowlist admin Cap id (auto-resolved from owned caps if omitted)");
|
|
1156
|
+
}
|
|
1157
|
+
function viaAggregatorOption(command) {
|
|
1158
|
+
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.");
|
|
1159
|
+
}
|
|
1097
1160
|
function contentOptions(command) {
|
|
1098
1161
|
return command.option("-f, --file <path>", "File to upload (or - for stdin)").option("--stdin", "Read content from stdin").option("--content-type <type>", "MIME content type (inferred from --file if omitted)").option("--epochs <n>", "Walrus storage epochs", "3");
|
|
1099
1162
|
}
|
|
1100
1163
|
|
|
1101
1164
|
// src/commands/resolve.ts
|
|
1102
1165
|
import {
|
|
1166
|
+
toAllowlistCapId,
|
|
1103
1167
|
toOwnerCapId,
|
|
1104
1168
|
toPublisherCapId
|
|
1105
1169
|
} from "@arcadiasystems/morse-sdk";
|
|
@@ -1142,6 +1206,25 @@ async function resolvePublisherCap(reader, address, publicationId, override, sig
|
|
|
1142
1206
|
} while (cursor !== undefined);
|
|
1143
1207
|
throw new CliError(`No PublisherCap for ${publicationId} held by ${address}. Pass --publisher-cap, or check that the active account holds one.`, ExitCode.NotFound);
|
|
1144
1208
|
}
|
|
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
|
+
}
|
|
1145
1228
|
|
|
1146
1229
|
// src/commands/shared.ts
|
|
1147
1230
|
import { StorageMode } from "@arcadiasystems/morse-sdk";
|
|
@@ -1162,6 +1245,12 @@ function parsePositiveInt(value, name) {
|
|
|
1162
1245
|
function parseLimit(value) {
|
|
1163
1246
|
return parsePositiveInt(value, "--limit");
|
|
1164
1247
|
}
|
|
1248
|
+
function parseByteSize(value, name) {
|
|
1249
|
+
if (!/^\d+$/.test(value)) {
|
|
1250
|
+
throw new UsageError(`${name} must be a non-negative integer, got "${value}".`);
|
|
1251
|
+
}
|
|
1252
|
+
return BigInt(value);
|
|
1253
|
+
}
|
|
1165
1254
|
function parseId(value, name) {
|
|
1166
1255
|
const id = Number(value);
|
|
1167
1256
|
if (!Number.isInteger(id) || id < 0) {
|
|
@@ -1170,228 +1259,437 @@ function parseId(value, name) {
|
|
|
1170
1259
|
return id;
|
|
1171
1260
|
}
|
|
1172
1261
|
|
|
1173
|
-
// src/commands/
|
|
1174
|
-
function
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
...options.cursor === undefined ? {} : { cursor: options.cursor }
|
|
1186
|
-
});
|
|
1187
|
-
if (page.nextCursor !== null) {
|
|
1188
|
-
ctx.output.info(`More results: pass --cursor "${page.nextCursor}"`);
|
|
1189
|
-
}
|
|
1190
|
-
ctx.output.result(renderPublisherCapList(page.results), {
|
|
1191
|
-
results: page.results,
|
|
1192
|
-
nextCursor: page.nextCursor
|
|
1193
|
-
});
|
|
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
|
|
1194
1274
|
});
|
|
1195
|
-
const
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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
|
|
1209
1293
|
});
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
const result = await revokePublisherCap(ctx.adapter, ctx.config, {
|
|
1222
|
-
publicationId: id,
|
|
1223
|
-
ownerCapId,
|
|
1224
|
-
publisherCapId: capId,
|
|
1225
|
-
signal: ctx.signal
|
|
1226
|
-
});
|
|
1227
|
-
ctx.output.result(`Revoked PublisherCap ${capId}. (tx: ${result.digest})`, result);
|
|
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
|
|
1228
1305
|
});
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
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
|
|
1244
1321
|
});
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
signal: ctx.signal
|
|
1260
|
-
});
|
|
1261
|
-
ctx.output.result(`Transferred PublisherCap ${capId} to ${to}. (tx: ${result.digest})`, result);
|
|
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
|
|
1262
1336
|
});
|
|
1337
|
+
ctx.output.result(`Deleted allowlist ${allowlistId}. (tx: ${result.digest})`, result);
|
|
1263
1338
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
ctx.
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
});
|
|
1278
|
-
});
|
|
1279
|
-
const create = collection.command("create <name>").description("Create a collection and select it as the active collection").option("--mode <mode>", "Storage mode: blob or quilt", "blob");
|
|
1280
|
-
publicationOption(publisherCapOption(create)).action(async (name, options, command) => {
|
|
1281
|
-
const ctx = await buildWriteContext(command);
|
|
1282
|
-
const id = await resolvePublication(ctx, options.publication);
|
|
1283
|
-
const storageMode = coerceStorageMode(options.mode);
|
|
1284
|
-
const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
|
|
1285
|
-
ctx.output.info(`Creating collection "${name}" (${storageMode})...`);
|
|
1286
|
-
const result = await createCollection(ctx.adapter, ctx.config, {
|
|
1287
|
-
publicationId: id,
|
|
1288
|
-
publisherCapId,
|
|
1289
|
-
name,
|
|
1290
|
-
storageMode,
|
|
1291
|
-
signal: ctx.signal
|
|
1292
|
-
});
|
|
1293
|
-
await updateActiveProfile(globalOptions(command), { collection: name });
|
|
1294
|
-
ctx.output.result(`Created collection "${name}". Selected as the active collection. (tx: ${result.digest})`, result);
|
|
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 }
|
|
1295
1352
|
});
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
cancelled();
|
|
1303
|
-
}
|
|
1304
|
-
const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
|
|
1305
|
-
const result = await deleteCollection(ctx.adapter, ctx.config, {
|
|
1306
|
-
publicationId: id,
|
|
1307
|
-
publisherCapId,
|
|
1308
|
-
name,
|
|
1309
|
-
signal: ctx.signal
|
|
1310
|
-
});
|
|
1311
|
-
if (ctx.settings.collection === name) {
|
|
1312
|
-
await updateActiveProfile(globalOptions(command), {
|
|
1313
|
-
collection: undefined
|
|
1314
|
-
});
|
|
1315
|
-
}
|
|
1316
|
-
ctx.output.result(`Deleted collection "${name}". (tx: ${result.digest})`, result);
|
|
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
|
|
1317
1359
|
});
|
|
1318
1360
|
}
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
config.command("path").description("Print the config file path").action((_options, command) => {
|
|
1324
|
-
const path = configFilePath();
|
|
1325
|
-
outputFor(command).result(path, { path });
|
|
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);
|
|
1326
1365
|
});
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
defaultProfile: cfg.defaultProfile,
|
|
1331
|
-
profiles: cfg.profiles
|
|
1332
|
-
});
|
|
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);
|
|
1333
1369
|
});
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
const profiles = {
|
|
1338
|
-
...cfg.profiles,
|
|
1339
|
-
[name]: {
|
|
1340
|
-
network,
|
|
1341
|
-
...options.rpc === undefined ? {} : { rpc: options.rpc }
|
|
1342
|
-
}
|
|
1343
|
-
};
|
|
1344
|
-
const defaultProfile = Object.keys(cfg.profiles).length === 0 ? name : cfg.defaultProfile;
|
|
1345
|
-
await saveConfig({ ...cfg, profiles, defaultProfile });
|
|
1346
|
-
outputFor(command).result(`Saved profile "${name}" (${network}).`, {
|
|
1347
|
-
profile: name,
|
|
1348
|
-
network,
|
|
1349
|
-
rpc: options.rpc,
|
|
1350
|
-
default: defaultProfile === name
|
|
1351
|
-
});
|
|
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);
|
|
1352
1373
|
});
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
await saveConfig({ ...cfg, defaultProfile: name });
|
|
1357
|
-
outputFor(command).result(`Default profile set to "${name}".`, {
|
|
1358
|
-
defaultProfile: name
|
|
1359
|
-
});
|
|
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));
|
|
1360
1377
|
});
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
await
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
profiles: Object.keys(rest)
|
|
1371
|
-
});
|
|
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);
|
|
1372
1387
|
});
|
|
1373
1388
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
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
|
+
// src/commands/cap.ts
|
|
1445
|
+
async function runCapList(ctx, address, options) {
|
|
1446
|
+
const holder = address === undefined ? ctx.ownerAddress : toSuiAddress4(address);
|
|
1447
|
+
if (holder === undefined) {
|
|
1448
|
+
throw new UsageError("No address given and no active account. Pass an address or import an account.");
|
|
1449
|
+
}
|
|
1450
|
+
const page = await ctx.reader.listPublisherCapsOwnedBy(holder, {
|
|
1451
|
+
signal: ctx.signal,
|
|
1452
|
+
...options.limit === undefined ? {} : { limit: parseLimit(options.limit) },
|
|
1453
|
+
...options.cursor === undefined ? {} : { cursor: options.cursor }
|
|
1454
|
+
});
|
|
1455
|
+
if (page.nextCursor !== null) {
|
|
1456
|
+
ctx.output.info(`More results: pass --cursor "${page.nextCursor}"`);
|
|
1457
|
+
}
|
|
1458
|
+
ctx.output.result(renderPublisherCapList(page.results), {
|
|
1459
|
+
results: page.results,
|
|
1460
|
+
nextCursor: page.nextCursor
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
async function runCapIssue(ctx, holder, options) {
|
|
1464
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1465
|
+
const holderAddress = toSuiAddress4(holder);
|
|
1466
|
+
ctx.output.info("Resolving OwnerCap...");
|
|
1467
|
+
const ownerCapId = await resolveOwnerCap(ctx.reader, ctx.address, id, options.ownerCap, ctx.signal);
|
|
1468
|
+
const result = await issuePublisherCap(ctx.adapter, ctx.config, {
|
|
1469
|
+
publicationId: id,
|
|
1470
|
+
ownerCapId,
|
|
1471
|
+
holder: holderAddress,
|
|
1472
|
+
signal: ctx.signal
|
|
1473
|
+
});
|
|
1474
|
+
ctx.output.result(`Issued PublisherCap ${result.publisherCapId} to ${holderAddress}. (tx: ${result.digest})`, result);
|
|
1475
|
+
}
|
|
1476
|
+
async function runCapRevoke(ctx, publisherCapId, options, gopts) {
|
|
1477
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1478
|
+
const capId = toPublisherCapId2(publisherCapId);
|
|
1479
|
+
const proceed = await confirm(`Revoke PublisherCap ${capId}? It can no longer be used to write.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
1480
|
+
if (!proceed) {
|
|
1481
|
+
cancelled();
|
|
1482
|
+
}
|
|
1483
|
+
ctx.output.info("Resolving OwnerCap...");
|
|
1484
|
+
const ownerCapId = await resolveOwnerCap(ctx.reader, ctx.address, id, options.ownerCap, ctx.signal);
|
|
1485
|
+
const result = await revokePublisherCap(ctx.adapter, ctx.config, {
|
|
1486
|
+
publicationId: id,
|
|
1487
|
+
ownerCapId,
|
|
1488
|
+
publisherCapId: capId,
|
|
1489
|
+
signal: ctx.signal
|
|
1490
|
+
});
|
|
1491
|
+
ctx.output.result(`Revoked PublisherCap ${capId}. (tx: ${result.digest})`, result);
|
|
1492
|
+
}
|
|
1493
|
+
async function runCapDestroy(ctx, publisherCapId, options, gopts) {
|
|
1494
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1495
|
+
const capId = toPublisherCapId2(publisherCapId);
|
|
1496
|
+
const proceed = await confirm(`Destroy PublisherCap ${capId}? This is permanent.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
1497
|
+
if (!proceed) {
|
|
1498
|
+
cancelled();
|
|
1499
|
+
}
|
|
1500
|
+
const result = await destroyPublisherCap(ctx.adapter, ctx.config, {
|
|
1501
|
+
publicationId: id,
|
|
1502
|
+
publisherCapId: capId,
|
|
1503
|
+
signal: ctx.signal
|
|
1504
|
+
});
|
|
1505
|
+
ctx.output.result(`Destroyed PublisherCap ${capId}. (tx: ${result.digest})`, result);
|
|
1506
|
+
}
|
|
1507
|
+
async function runCapTransfer(ctx, publisherCapId, recipient, gopts) {
|
|
1508
|
+
const capId = toPublisherCapId2(publisherCapId);
|
|
1509
|
+
const to = toSuiAddress4(recipient);
|
|
1510
|
+
const proceed = await confirm(`Transfer PublisherCap ${capId} to ${to}?`, {
|
|
1511
|
+
assumeYes: Boolean(gopts.yes),
|
|
1512
|
+
signal: ctx.signal
|
|
1513
|
+
});
|
|
1514
|
+
if (!proceed) {
|
|
1515
|
+
cancelled();
|
|
1516
|
+
}
|
|
1517
|
+
const result = await transferPublisherCap(ctx.adapter, ctx.config, {
|
|
1518
|
+
publisherCapId: capId,
|
|
1519
|
+
recipient: to,
|
|
1520
|
+
signal: ctx.signal
|
|
1521
|
+
});
|
|
1522
|
+
ctx.output.result(`Transferred PublisherCap ${capId} to ${to}. (tx: ${result.digest})`, result);
|
|
1523
|
+
}
|
|
1524
|
+
function registerCapCommands(program) {
|
|
1525
|
+
const cap = program.command("cap").description("Manage PublisherCaps (write-access capabilities)");
|
|
1526
|
+
cap.command("list [address]").description("List publisher 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) => {
|
|
1527
|
+
await runCapList(await buildReadContext(command), address, options);
|
|
1528
|
+
});
|
|
1529
|
+
const issue = cap.command("issue <holder>").description("Issue a PublisherCap bound to an address");
|
|
1530
|
+
publicationOption(ownerCapOption(issue)).action(async (holder, options, command) => {
|
|
1531
|
+
await runCapIssue(await buildWriteContext(command), holder, options);
|
|
1532
|
+
});
|
|
1533
|
+
const revoke = cap.command("revoke <publisherCapId>").description("Revoke a PublisherCap so it can no longer write");
|
|
1534
|
+
publicationOption(ownerCapOption(revoke)).action(async (publisherCapId, options, command) => {
|
|
1535
|
+
await runCapRevoke(await buildWriteContext(command), publisherCapId, options, globalOptions(command));
|
|
1536
|
+
});
|
|
1537
|
+
const destroy = cap.command("destroy <publisherCapId>").description("Destroy a PublisherCap held by the active account");
|
|
1538
|
+
publicationOption(destroy).action(async (publisherCapId, options, command) => {
|
|
1539
|
+
await runCapDestroy(await buildWriteContext(command), publisherCapId, options, globalOptions(command));
|
|
1540
|
+
});
|
|
1541
|
+
cap.command("transfer <publisherCapId> <recipient>").description("Transfer a PublisherCap object to another address").action(async (publisherCapId, recipient, _options, command) => {
|
|
1542
|
+
await runCapTransfer(await buildWriteContext(command), publisherCapId, recipient, globalOptions(command));
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// src/commands/collection.ts
|
|
1547
|
+
import { createCollection, deleteCollection } from "@arcadiasystems/morse-sdk";
|
|
1548
|
+
async function runCollectionList(ctx, options) {
|
|
1549
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1550
|
+
const publication = await ctx.reader.getPublication(id, ctx.signal);
|
|
1551
|
+
ctx.output.result(renderCollectionList(publication.collections), {
|
|
1552
|
+
publication: id,
|
|
1553
|
+
collections: publication.collections
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
async function runCollectionCreate(ctx, name, options, gopts) {
|
|
1557
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1558
|
+
const storageMode = coerceStorageMode(options.mode);
|
|
1559
|
+
const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
|
|
1560
|
+
ctx.output.info(`Creating collection "${name}" (${storageMode})...`);
|
|
1561
|
+
const result = await createCollection(ctx.adapter, ctx.config, {
|
|
1562
|
+
publicationId: id,
|
|
1563
|
+
publisherCapId,
|
|
1564
|
+
name,
|
|
1565
|
+
storageMode,
|
|
1566
|
+
signal: ctx.signal
|
|
1567
|
+
});
|
|
1568
|
+
await updateActiveProfile(gopts, { collection: name });
|
|
1569
|
+
ctx.output.result(`Created collection "${name}". Selected as the active collection. (tx: ${result.digest})`, result);
|
|
1570
|
+
}
|
|
1571
|
+
async function runCollectionDelete(ctx, name, options, gopts) {
|
|
1572
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1573
|
+
const proceed = await confirm(`Delete collection "${name}" from ${shortId(id)}? It must be empty.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
1574
|
+
if (!proceed) {
|
|
1575
|
+
cancelled();
|
|
1576
|
+
}
|
|
1577
|
+
const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
|
|
1578
|
+
const result = await deleteCollection(ctx.adapter, ctx.config, {
|
|
1579
|
+
publicationId: id,
|
|
1580
|
+
publisherCapId,
|
|
1581
|
+
name,
|
|
1582
|
+
signal: ctx.signal
|
|
1583
|
+
});
|
|
1584
|
+
if (ctx.settings.collection === name) {
|
|
1585
|
+
await updateActiveProfile(gopts, { collection: undefined });
|
|
1586
|
+
}
|
|
1587
|
+
ctx.output.result(`Deleted collection "${name}". (tx: ${result.digest})`, result);
|
|
1588
|
+
}
|
|
1589
|
+
function registerCollectionCommands(program) {
|
|
1590
|
+
const collection = program.command("collection").description("Manage collections within a publication");
|
|
1591
|
+
const list = collection.command("list").description("List the collections in a publication");
|
|
1592
|
+
publicationOption(list).action(async (options, command) => {
|
|
1593
|
+
await runCollectionList(await buildReadContext(command), options);
|
|
1594
|
+
});
|
|
1595
|
+
const create = collection.command("create <name>").description("Create a collection and select it as the active collection").option("--mode <mode>", "Storage mode: blob or quilt", "blob");
|
|
1596
|
+
publicationOption(publisherCapOption(create)).action(async (name, options, command) => {
|
|
1597
|
+
await runCollectionCreate(await buildWriteContext(command), name, options, globalOptions(command));
|
|
1598
|
+
});
|
|
1599
|
+
const remove = collection.command("delete <name>").description("Delete an empty collection");
|
|
1600
|
+
publicationOption(publisherCapOption(remove)).action(async (name, options, command) => {
|
|
1601
|
+
await runCollectionDelete(await buildWriteContext(command), name, options, globalOptions(command));
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// src/commands/config.ts
|
|
1606
|
+
function runConfigPath(output) {
|
|
1607
|
+
const path = configFilePath();
|
|
1608
|
+
output.result(path, { path });
|
|
1609
|
+
}
|
|
1610
|
+
async function runConfigList(output) {
|
|
1611
|
+
const cfg = await loadConfig();
|
|
1612
|
+
output.result(renderProfiles(cfg), {
|
|
1613
|
+
defaultProfile: cfg.defaultProfile,
|
|
1614
|
+
profiles: cfg.profiles
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
async function runConfigAdd(output, name, options) {
|
|
1618
|
+
const network = coerceNetwork(options.network);
|
|
1619
|
+
const cfg = await loadConfig();
|
|
1620
|
+
const profiles = {
|
|
1621
|
+
...cfg.profiles,
|
|
1622
|
+
[name]: {
|
|
1623
|
+
network,
|
|
1624
|
+
...options.rpc === undefined ? {} : { rpc: options.rpc }
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
const defaultProfile = Object.keys(cfg.profiles).length === 0 ? name : cfg.defaultProfile;
|
|
1628
|
+
await saveConfig({ ...cfg, profiles, defaultProfile });
|
|
1629
|
+
output.result(`Saved profile "${name}" (${network}).`, {
|
|
1630
|
+
profile: name,
|
|
1631
|
+
network,
|
|
1632
|
+
rpc: options.rpc,
|
|
1633
|
+
default: defaultProfile === name
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
async function runConfigUse(output, name) {
|
|
1637
|
+
const cfg = await loadConfig();
|
|
1638
|
+
requireProfile(cfg, name);
|
|
1639
|
+
await saveConfig({ ...cfg, defaultProfile: name });
|
|
1640
|
+
output.result(`Default profile set to "${name}".`, { defaultProfile: name });
|
|
1641
|
+
}
|
|
1642
|
+
async function runConfigRemove(output, name) {
|
|
1643
|
+
const cfg = await loadConfig();
|
|
1644
|
+
requireProfile(cfg, name);
|
|
1645
|
+
const { [name]: _removed, ...rest } = cfg.profiles;
|
|
1646
|
+
const defaultProfile = cfg.defaultProfile === name ? Object.keys(rest)[0] ?? "default" : cfg.defaultProfile;
|
|
1647
|
+
await saveConfig({ ...cfg, profiles: rest, defaultProfile });
|
|
1648
|
+
output.result(`Removed profile "${name}".`, {
|
|
1649
|
+
removed: name,
|
|
1650
|
+
defaultProfile,
|
|
1651
|
+
profiles: Object.keys(rest)
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
function registerConfigCommands(program) {
|
|
1655
|
+
const config = program.command("config").description("Manage profiles and CLI configuration");
|
|
1656
|
+
config.command("path").description("Print the config file path").action((_options, command) => {
|
|
1657
|
+
runConfigPath(outputFor(command));
|
|
1658
|
+
});
|
|
1659
|
+
config.command("list").description("List profiles and show the default").action(async (_options, command) => {
|
|
1660
|
+
await runConfigList(outputFor(command));
|
|
1661
|
+
});
|
|
1662
|
+
config.command("add <name>").description("Create or update a profile").requiredOption("--network <network>", "Sui network: testnet or localnet").option("--rpc <url>", "RPC URL override for this profile").action(async (name, options, command) => {
|
|
1663
|
+
await runConfigAdd(outputFor(command), name, options);
|
|
1664
|
+
});
|
|
1665
|
+
config.command("use <name>").description("Set the default profile").action(async (name, _options, command) => {
|
|
1666
|
+
await runConfigUse(outputFor(command), name);
|
|
1667
|
+
});
|
|
1668
|
+
config.command("remove <name>").description("Delete a profile").action(async (name, _options, command) => {
|
|
1669
|
+
await runConfigRemove(outputFor(command), name);
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
function requireProfile(config, name) {
|
|
1673
|
+
if (!(name in config.profiles)) {
|
|
1674
|
+
throw new UsageError(`No profile named "${name}". Create it with: morse config add ${name} --network testnet`);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
function renderProfiles(config) {
|
|
1678
|
+
const names = Object.keys(config.profiles);
|
|
1679
|
+
if (names.length === 0) {
|
|
1680
|
+
return "No profiles configured. Add one with: morse config add <name> --network testnet";
|
|
1681
|
+
}
|
|
1682
|
+
return names.map((name) => {
|
|
1683
|
+
const profile = config.profiles[name];
|
|
1684
|
+
if (profile === undefined) {
|
|
1685
|
+
return name;
|
|
1686
|
+
}
|
|
1687
|
+
const marker = name === config.defaultProfile ? "*" : " ";
|
|
1688
|
+
const parts = [profile.network];
|
|
1689
|
+
if (profile.rpc !== undefined) {
|
|
1690
|
+
parts.push(profile.rpc);
|
|
1691
|
+
}
|
|
1692
|
+
if (profile.account !== undefined) {
|
|
1395
1693
|
parts.push(profile.account);
|
|
1396
1694
|
}
|
|
1397
1695
|
return `${marker} ${name} ${parts.join(" ")}`;
|
|
@@ -1462,333 +1760,615 @@ import {
|
|
|
1462
1760
|
import { SessionKey } from "@mysten/seal";
|
|
1463
1761
|
var SESSION_KEY_TTL_MIN = 10;
|
|
1464
1762
|
var SEAL_NONCE_BYTES = 16;
|
|
1763
|
+
async function runEntryAddEncrypted(ctx, name, options) {
|
|
1764
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1765
|
+
const collection = resolveCollection(ctx, options.collection);
|
|
1766
|
+
const epochs = parsePositiveInt(options.epochs, "--epochs");
|
|
1767
|
+
const plaintext = await readContentBytes(options);
|
|
1768
|
+
const contentType = resolveContentType(options.contentType, options.stdin ? undefined : options.file);
|
|
1769
|
+
const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
|
|
1770
|
+
const sealId = buildPublisherSealId(id, crypto.getRandomValues(new Uint8Array(SEAL_NONCE_BYTES)));
|
|
1771
|
+
ctx.output.info(`Encrypting and uploading ${plaintext.length} bytes...`);
|
|
1772
|
+
const result = await addEncryptedEntryFromBytes(ctx.adapter, ctx.config, {
|
|
1773
|
+
walrus: ctx.walrus,
|
|
1774
|
+
seal: ctx.seal,
|
|
1775
|
+
publicationId: id,
|
|
1776
|
+
publisherCapId,
|
|
1777
|
+
collectionName: collection,
|
|
1778
|
+
name,
|
|
1779
|
+
plaintext,
|
|
1780
|
+
contentType,
|
|
1781
|
+
sealId,
|
|
1782
|
+
upload: { epochs, deletable: true },
|
|
1783
|
+
signal: ctx.signal
|
|
1784
|
+
});
|
|
1785
|
+
ctx.output.result(`Added encrypted entry #${result.entryId} "${name}". (tx: ${result.digest})`, { ...result, sealId });
|
|
1786
|
+
}
|
|
1787
|
+
async function runEntryDecrypt(ctx, entryId, revisionId, options) {
|
|
1788
|
+
if (ctx.output.isJson && options.out === undefined) {
|
|
1789
|
+
throw new UsageError("Decrypting to stdout is not supported in --json mode; pass --out <path>.");
|
|
1790
|
+
}
|
|
1791
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1792
|
+
const collection = resolveCollection(ctx, options.collection);
|
|
1793
|
+
const numericEntryId = parseId(entryId, "entryId");
|
|
1794
|
+
const entryData = await ctx.reader.getEntry(id, collection, numericEntryId, ctx.signal);
|
|
1795
|
+
if (revisionId === undefined && entryData.revisions.length === 0) {
|
|
1796
|
+
throw new UsageError(`Entry #${numericEntryId} has no revisions.`);
|
|
1797
|
+
}
|
|
1798
|
+
const revisionIndex = revisionId === undefined ? entryData.revisions.length - 1 : parseId(revisionId, "revision");
|
|
1799
|
+
const revision = entryData.revisions[revisionIndex];
|
|
1800
|
+
if (revision === undefined) {
|
|
1801
|
+
throw new UsageError(`Entry #${numericEntryId} has no revision at index ${revisionIndex}.`);
|
|
1802
|
+
}
|
|
1803
|
+
if (!revision.encrypted || revision.sealId === null) {
|
|
1804
|
+
throw new UsageError(`Revision #${revisionIndex} of entry #${numericEntryId} is not encrypted.`);
|
|
1805
|
+
}
|
|
1806
|
+
const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
|
|
1807
|
+
ctx.output.info("Fetching ciphertext from Walrus...");
|
|
1808
|
+
const ciphertext = await ctx.walrusRead.readBlobRef(revision.blobRef, {
|
|
1809
|
+
signal: ctx.signal
|
|
1810
|
+
});
|
|
1811
|
+
ctx.output.info("Signing a SessionKey with the active account...");
|
|
1812
|
+
const sessionKey = await SessionKey.create({
|
|
1813
|
+
address: ctx.address,
|
|
1814
|
+
packageId: ctx.config.originalPackageId ?? ctx.config.packageId,
|
|
1815
|
+
ttlMin: SESSION_KEY_TTL_MIN,
|
|
1816
|
+
signer: ctx.keypair,
|
|
1817
|
+
suiClient: ctx.client
|
|
1818
|
+
});
|
|
1819
|
+
const plaintext = await ctx.seal.decrypt(ciphertext, {
|
|
1820
|
+
sessionKey,
|
|
1821
|
+
sealId: revision.sealId,
|
|
1822
|
+
publisherCapId
|
|
1823
|
+
});
|
|
1824
|
+
if (options.out !== undefined) {
|
|
1825
|
+
await writeFileContents(options.out, plaintext);
|
|
1826
|
+
ctx.output.result(`Wrote ${plaintext.length} bytes to ${options.out}.`, {
|
|
1827
|
+
entryId: numericEntryId,
|
|
1828
|
+
revisionId: revisionIndex,
|
|
1829
|
+
bytes: plaintext.length,
|
|
1830
|
+
contentType: revision.contentType,
|
|
1831
|
+
out: options.out
|
|
1832
|
+
});
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
process.stdout.write(plaintext);
|
|
1836
|
+
}
|
|
1465
1837
|
function registerEncryptedEntryCommands(entry) {
|
|
1466
1838
|
const add = entry.command("add-encrypted <name>").description("Encrypt a file or stdin with Seal and add it as a new entry");
|
|
1467
1839
|
collectionOption(publicationOption(publisherCapOption(contentOptions(add)))).action(async (name, options, command) => {
|
|
1468
|
-
|
|
1469
|
-
const id = await resolvePublication(ctx, options.publication);
|
|
1470
|
-
const collection = resolveCollection(ctx, options.collection);
|
|
1471
|
-
const epochs = parsePositiveInt(options.epochs, "--epochs");
|
|
1472
|
-
const plaintext = await readContentBytes(options);
|
|
1473
|
-
const contentType = resolveContentType(options.contentType, options.stdin ? undefined : options.file);
|
|
1474
|
-
const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
|
|
1475
|
-
const sealId = buildPublisherSealId(id, crypto.getRandomValues(new Uint8Array(SEAL_NONCE_BYTES)));
|
|
1476
|
-
ctx.output.info(`Encrypting and uploading ${plaintext.length} bytes...`);
|
|
1477
|
-
const result = await addEncryptedEntryFromBytes(ctx.adapter, ctx.config, {
|
|
1478
|
-
walrus: ctx.walrus,
|
|
1479
|
-
seal: ctx.seal,
|
|
1480
|
-
publicationId: id,
|
|
1481
|
-
publisherCapId,
|
|
1482
|
-
collectionName: collection,
|
|
1483
|
-
name,
|
|
1484
|
-
plaintext,
|
|
1485
|
-
contentType,
|
|
1486
|
-
sealId,
|
|
1487
|
-
upload: { epochs, deletable: true },
|
|
1488
|
-
signal: ctx.signal
|
|
1489
|
-
});
|
|
1490
|
-
ctx.output.result(`Added encrypted entry #${result.entryId} "${name}". (tx: ${result.digest})`, { ...result, sealId });
|
|
1840
|
+
await runEntryAddEncrypted(await buildEncryptContext(command), name, options);
|
|
1491
1841
|
});
|
|
1492
1842
|
const decrypt = entry.command("decrypt <entryId> [revisionIndex]").description("Decrypt an encrypted revision by zero-based index (default: latest); signs a SessionKey with the active account").option("--out <path>", "Write plaintext to a file instead of stdout");
|
|
1493
|
-
collectionOption(publicationOption(publisherCapOption(decrypt))).action(async (entryId, revisionId, options, command) => {
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
}
|
|
1498
|
-
const id = await resolvePublication(ctx, options.publication);
|
|
1499
|
-
const collection = resolveCollection(ctx, options.collection);
|
|
1500
|
-
const numericEntryId = parseId(entryId, "entryId");
|
|
1501
|
-
const entryData = await ctx.reader.getEntry(id, collection, numericEntryId, ctx.signal);
|
|
1502
|
-
if (revisionId === undefined && entryData.revisions.length === 0) {
|
|
1503
|
-
throw new UsageError(`Entry #${numericEntryId} has no revisions.`);
|
|
1504
|
-
}
|
|
1505
|
-
const revisionIndex = revisionId === undefined ? entryData.revisions.length - 1 : parseId(revisionId, "revision");
|
|
1506
|
-
const revision = entryData.revisions[revisionIndex];
|
|
1507
|
-
if (revision === undefined) {
|
|
1508
|
-
throw new UsageError(`Entry #${numericEntryId} has no revision at index ${revisionIndex}.`);
|
|
1509
|
-
}
|
|
1510
|
-
if (!revision.encrypted || revision.sealId === null) {
|
|
1511
|
-
throw new UsageError(`Revision #${revisionIndex} of entry #${numericEntryId} is not encrypted.`);
|
|
1512
|
-
}
|
|
1513
|
-
const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
|
|
1514
|
-
ctx.output.info("Fetching ciphertext from Walrus...");
|
|
1515
|
-
const ciphertext = await ctx.walrusRead.readBlobRef(revision.blobRef, {
|
|
1516
|
-
signal: ctx.signal
|
|
1517
|
-
});
|
|
1518
|
-
ctx.output.info("Signing a SessionKey with the active account...");
|
|
1519
|
-
const sessionKey = await SessionKey.create({
|
|
1520
|
-
address: ctx.address,
|
|
1521
|
-
packageId: ctx.config.originalPackageId ?? ctx.config.packageId,
|
|
1522
|
-
ttlMin: SESSION_KEY_TTL_MIN,
|
|
1523
|
-
signer: ctx.keypair,
|
|
1524
|
-
suiClient: ctx.client
|
|
1525
|
-
});
|
|
1526
|
-
const plaintext = await ctx.seal.decrypt(ciphertext, {
|
|
1527
|
-
sessionKey,
|
|
1528
|
-
sealId: revision.sealId,
|
|
1529
|
-
publisherCapId
|
|
1530
|
-
});
|
|
1531
|
-
if (options.out !== undefined) {
|
|
1532
|
-
await writeFileContents(options.out, plaintext);
|
|
1533
|
-
ctx.output.result(`Wrote ${plaintext.length} bytes to ${options.out}.`, {
|
|
1534
|
-
entryId: numericEntryId,
|
|
1535
|
-
revisionId: revisionIndex,
|
|
1536
|
-
bytes: plaintext.length,
|
|
1537
|
-
contentType: revision.contentType,
|
|
1538
|
-
out: options.out
|
|
1539
|
-
});
|
|
1540
|
-
return;
|
|
1541
|
-
}
|
|
1542
|
-
process.stdout.write(plaintext);
|
|
1843
|
+
collectionOption(publicationOption(publisherCapOption(viaAggregatorOption(decrypt)))).action(async (entryId, revisionId, options, command) => {
|
|
1844
|
+
await runEntryDecrypt(await buildDecryptContext(command, {
|
|
1845
|
+
viaAggregator: options.viaAggregator
|
|
1846
|
+
}), entryId, revisionId, options);
|
|
1543
1847
|
});
|
|
1544
1848
|
}
|
|
1545
1849
|
|
|
1546
1850
|
// src/commands/entry.ts
|
|
1851
|
+
async function runEntryGet(ctx, entryId, options) {
|
|
1852
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1853
|
+
const collection = resolveCollection(ctx, options.collection);
|
|
1854
|
+
const result = await ctx.reader.getEntry(id, collection, parseId(entryId, "entryId"), ctx.signal);
|
|
1855
|
+
ctx.output.result(renderEntry(result), result);
|
|
1856
|
+
}
|
|
1857
|
+
async function runEntryList(ctx, options) {
|
|
1858
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1859
|
+
const collection = resolveCollection(ctx, options.collection);
|
|
1860
|
+
const page = await ctx.reader.listEntries(id, collection, {
|
|
1861
|
+
signal: ctx.signal,
|
|
1862
|
+
...options.limit === undefined ? {} : { limit: parseLimit(options.limit) },
|
|
1863
|
+
...options.cursor === undefined ? {} : { cursor: options.cursor }
|
|
1864
|
+
});
|
|
1865
|
+
if (page.nextCursor !== null) {
|
|
1866
|
+
ctx.output.info(`More results: pass --cursor "${page.nextCursor}"`);
|
|
1867
|
+
}
|
|
1868
|
+
ctx.output.result(renderEntryList(page.results), {
|
|
1869
|
+
results: page.results,
|
|
1870
|
+
nextCursor: page.nextCursor
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
async function runEntryScan(ctx, options) {
|
|
1874
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1875
|
+
const collection = resolveCollection(ctx, options.collection);
|
|
1876
|
+
const entries = [];
|
|
1877
|
+
for await (const item of ctx.reader.scanEntries(id, collection, {
|
|
1878
|
+
signal: ctx.signal
|
|
1879
|
+
})) {
|
|
1880
|
+
entries.push(item);
|
|
1881
|
+
}
|
|
1882
|
+
ctx.output.result(renderEntryList(entries), { results: entries });
|
|
1883
|
+
}
|
|
1884
|
+
async function runEntryAdd(ctx, name, options) {
|
|
1885
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1886
|
+
const collection = resolveCollection(ctx, options.collection);
|
|
1887
|
+
const epochs = parsePositiveInt(options.epochs, "--epochs");
|
|
1888
|
+
const bytes = await readContentBytes(options);
|
|
1889
|
+
const contentType = resolveContentType(options.contentType, options.stdin ? undefined : options.file);
|
|
1890
|
+
const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
|
|
1891
|
+
ctx.output.info(`Uploading ${bytes.length} bytes to Walrus...`);
|
|
1892
|
+
const result = await addEntryFromBytes(ctx.adapter, ctx.config, {
|
|
1893
|
+
walrus: ctx.walrus,
|
|
1894
|
+
publicationId: id,
|
|
1895
|
+
publisherCapId,
|
|
1896
|
+
collectionName: collection,
|
|
1897
|
+
name,
|
|
1898
|
+
bytes,
|
|
1899
|
+
contentType,
|
|
1900
|
+
upload: { epochs, deletable: true },
|
|
1901
|
+
signal: ctx.signal
|
|
1902
|
+
});
|
|
1903
|
+
const aggregator = ctx.config.walrusEndpoints.aggregator;
|
|
1904
|
+
const viewUrl = aggregator.length > 0 ? `${aggregator}/v1/blobs/${result.blobId}` : undefined;
|
|
1905
|
+
const human = viewUrl === undefined ? `Added entry #${result.entryId} "${name}". (tx: ${result.digest})` : `Added entry #${result.entryId} "${name}". (tx: ${result.digest})
|
|
1906
|
+
view: ${viewUrl}`;
|
|
1907
|
+
ctx.output.result(human, { ...result, viewUrl: viewUrl ?? null });
|
|
1908
|
+
}
|
|
1909
|
+
async function runEntryDelete(ctx, entryId, options, gopts) {
|
|
1910
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1911
|
+
const collection = resolveCollection(ctx, options.collection);
|
|
1912
|
+
const numericEntryId = parseId(entryId, "entryId");
|
|
1913
|
+
const proceed = await confirm(`Delete entry #${numericEntryId} from ${shortId(id)}/${collection}? This cannot be undone.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
1914
|
+
if (!proceed) {
|
|
1915
|
+
cancelled();
|
|
1916
|
+
}
|
|
1917
|
+
const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
|
|
1918
|
+
const result = await deleteEntry(ctx.adapter, ctx.config, {
|
|
1919
|
+
publicationId: id,
|
|
1920
|
+
publisherCapId,
|
|
1921
|
+
collectionName: collection,
|
|
1922
|
+
entryId: numericEntryId,
|
|
1923
|
+
signal: ctx.signal
|
|
1924
|
+
});
|
|
1925
|
+
ctx.output.result(`Deleted entry #${numericEntryId}. (tx: ${result.digest})`, result);
|
|
1926
|
+
}
|
|
1927
|
+
async function runEntryRead(ctx, entryId, revisionId, options) {
|
|
1928
|
+
if (ctx.output.isJson && options.out === undefined) {
|
|
1929
|
+
throw new UsageError("Reading content to stdout is not supported in --json mode; pass --out <path>.");
|
|
1930
|
+
}
|
|
1931
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1932
|
+
const collection = resolveCollection(ctx, options.collection);
|
|
1933
|
+
const numericEntryId = parseId(entryId, "entryId");
|
|
1934
|
+
const entryData = await ctx.reader.getEntry(id, collection, numericEntryId, ctx.signal);
|
|
1935
|
+
if (revisionId === undefined && entryData.revisions.length === 0) {
|
|
1936
|
+
throw new UsageError(`Entry #${numericEntryId} has no revisions.`);
|
|
1937
|
+
}
|
|
1938
|
+
const revisionIndex = revisionId === undefined ? entryData.revisions.length - 1 : parseId(revisionId, "revision");
|
|
1939
|
+
const revision = entryData.revisions[revisionIndex];
|
|
1940
|
+
if (revision === undefined) {
|
|
1941
|
+
throw new UsageError(`Entry #${numericEntryId} has no revision at index ${revisionIndex}.`);
|
|
1942
|
+
}
|
|
1943
|
+
if (revision.encrypted) {
|
|
1944
|
+
throw new UsageError(`Revision #${revisionIndex} of entry #${numericEntryId} is encrypted; use \`morse entry decrypt\`.`);
|
|
1945
|
+
}
|
|
1946
|
+
ctx.output.info("Fetching content from Walrus...");
|
|
1947
|
+
const bytes = await ctx.walrusRead.readBlobRef(revision.blobRef, {
|
|
1948
|
+
signal: ctx.signal
|
|
1949
|
+
});
|
|
1950
|
+
if (options.out !== undefined) {
|
|
1951
|
+
await writeFileContents(options.out, bytes);
|
|
1952
|
+
ctx.output.result(`Wrote ${bytes.length} bytes to ${options.out}.`, {
|
|
1953
|
+
entryId: numericEntryId,
|
|
1954
|
+
revisionId: revisionIndex,
|
|
1955
|
+
bytes: bytes.length,
|
|
1956
|
+
contentType: revision.contentType,
|
|
1957
|
+
out: options.out
|
|
1958
|
+
});
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
process.stdout.write(bytes);
|
|
1962
|
+
}
|
|
1547
1963
|
function registerEntryCommands(program) {
|
|
1548
1964
|
const entry = program.command("entry").description("Read, add, and delete entries in a collection");
|
|
1549
1965
|
const get = entry.command("get <entryId>").description("Fetch a single entry");
|
|
1550
1966
|
collectionOption(publicationOption(get)).action(async (entryId, options, command) => {
|
|
1551
|
-
|
|
1552
|
-
const id = await resolvePublication(ctx, options.publication);
|
|
1553
|
-
const collection = resolveCollection(ctx, options.collection);
|
|
1554
|
-
const result = await ctx.reader.getEntry(id, collection, parseId(entryId, "entryId"), ctx.signal);
|
|
1555
|
-
ctx.output.result(renderEntry(result), result);
|
|
1967
|
+
await runEntryGet(await buildReadContext(command), entryId, options);
|
|
1556
1968
|
});
|
|
1557
1969
|
const list = entry.command("list").description("List entries in a collection").option("--limit <n>", "Maximum results per page").option("--cursor <cursor>", "Continue from a previous page cursor");
|
|
1558
1970
|
collectionOption(publicationOption(list)).action(async (options, command) => {
|
|
1559
|
-
|
|
1560
|
-
const id = await resolvePublication(ctx, options.publication);
|
|
1561
|
-
const collection = resolveCollection(ctx, options.collection);
|
|
1562
|
-
const page = await ctx.reader.listEntries(id, collection, {
|
|
1563
|
-
signal: ctx.signal,
|
|
1564
|
-
...options.limit === undefined ? {} : { limit: parseLimit(options.limit) },
|
|
1565
|
-
...options.cursor === undefined ? {} : { cursor: options.cursor }
|
|
1566
|
-
});
|
|
1567
|
-
if (page.nextCursor !== null) {
|
|
1568
|
-
ctx.output.info(`More results: pass --cursor "${page.nextCursor}"`);
|
|
1569
|
-
}
|
|
1570
|
-
ctx.output.result(renderEntryList(page.results), {
|
|
1571
|
-
results: page.results,
|
|
1572
|
-
nextCursor: page.nextCursor
|
|
1573
|
-
});
|
|
1971
|
+
await runEntryList(await buildReadContext(command), options);
|
|
1574
1972
|
});
|
|
1575
1973
|
const scan = entry.command("scan").description("List every entry in a collection (auto-paginated)");
|
|
1576
1974
|
collectionOption(publicationOption(scan)).action(async (options, command) => {
|
|
1577
|
-
|
|
1578
|
-
const id = await resolvePublication(ctx, options.publication);
|
|
1579
|
-
const collection = resolveCollection(ctx, options.collection);
|
|
1580
|
-
const entries = [];
|
|
1581
|
-
for await (const item of ctx.reader.scanEntries(id, collection, {
|
|
1582
|
-
signal: ctx.signal
|
|
1583
|
-
})) {
|
|
1584
|
-
entries.push(item);
|
|
1585
|
-
}
|
|
1586
|
-
ctx.output.result(renderEntryList(entries), { results: entries });
|
|
1975
|
+
await runEntryScan(await buildReadContext(command), options);
|
|
1587
1976
|
});
|
|
1588
1977
|
const add = entry.command("add <name>").description("Upload content from a file or stdin and add it as a new entry");
|
|
1589
1978
|
collectionOption(publicationOption(publisherCapOption(contentOptions(add)))).action(async (name, options, command) => {
|
|
1590
|
-
|
|
1591
|
-
const id = await resolvePublication(ctx, options.publication);
|
|
1592
|
-
const collection = resolveCollection(ctx, options.collection);
|
|
1593
|
-
const epochs = parsePositiveInt(options.epochs, "--epochs");
|
|
1594
|
-
const bytes = await readContentBytes(options);
|
|
1595
|
-
const contentType = resolveContentType(options.contentType, options.stdin ? undefined : options.file);
|
|
1596
|
-
const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
|
|
1597
|
-
ctx.output.info(`Uploading ${bytes.length} bytes to Walrus...`);
|
|
1598
|
-
const result = await addEntryFromBytes(ctx.adapter, ctx.config, {
|
|
1599
|
-
walrus: ctx.walrus,
|
|
1600
|
-
publicationId: id,
|
|
1601
|
-
publisherCapId,
|
|
1602
|
-
collectionName: collection,
|
|
1603
|
-
name,
|
|
1604
|
-
bytes,
|
|
1605
|
-
contentType,
|
|
1606
|
-
upload: { epochs, deletable: true },
|
|
1607
|
-
signal: ctx.signal
|
|
1608
|
-
});
|
|
1609
|
-
const aggregator = ctx.config.walrusEndpoints.aggregator;
|
|
1610
|
-
const viewUrl = aggregator.length > 0 ? `${aggregator}/v1/blobs/${result.blobId}` : undefined;
|
|
1611
|
-
const human = viewUrl === undefined ? `Added entry #${result.entryId} "${name}". (tx: ${result.digest})` : `Added entry #${result.entryId} "${name}". (tx: ${result.digest})
|
|
1612
|
-
view: ${viewUrl}`;
|
|
1613
|
-
ctx.output.result(human, { ...result, viewUrl: viewUrl ?? null });
|
|
1979
|
+
await runEntryAdd(await buildContentContext(command), name, options);
|
|
1614
1980
|
});
|
|
1615
1981
|
const remove = entry.command("delete <entryId>").description("Delete an entry and its revisions");
|
|
1616
1982
|
collectionOption(publicationOption(publisherCapOption(remove))).action(async (entryId, options, command) => {
|
|
1617
|
-
|
|
1618
|
-
const id = await resolvePublication(ctx, options.publication);
|
|
1619
|
-
const collection = resolveCollection(ctx, options.collection);
|
|
1620
|
-
const numericEntryId = parseId(entryId, "entryId");
|
|
1621
|
-
const proceed = await confirm(`Delete entry #${numericEntryId} from ${shortId(id)}/${collection}? This cannot be undone.`, { assumeYes: Boolean(globalOptions(command).yes), signal: ctx.signal });
|
|
1622
|
-
if (!proceed) {
|
|
1623
|
-
cancelled();
|
|
1624
|
-
}
|
|
1625
|
-
const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
|
|
1626
|
-
const result = await deleteEntry(ctx.adapter, ctx.config, {
|
|
1627
|
-
publicationId: id,
|
|
1628
|
-
publisherCapId,
|
|
1629
|
-
collectionName: collection,
|
|
1630
|
-
entryId: numericEntryId,
|
|
1631
|
-
signal: ctx.signal
|
|
1632
|
-
});
|
|
1633
|
-
ctx.output.result(`Deleted entry #${numericEntryId}. (tx: ${result.digest})`, result);
|
|
1983
|
+
await runEntryDelete(await buildWriteContext(command), entryId, options, globalOptions(command));
|
|
1634
1984
|
});
|
|
1635
1985
|
const read = entry.command("read <entryId> [revisionIndex]").description("Fetch a public entry's content to stdout or a file").option("--out <path>", "Write content to a file instead of stdout");
|
|
1636
|
-
collectionOption(publicationOption(read)).action(async (entryId, revisionId, options, command) => {
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1986
|
+
collectionOption(publicationOption(viaAggregatorOption(read))).action(async (entryId, revisionId, options, command) => {
|
|
1987
|
+
await runEntryRead(await buildReadContentContext(command, {
|
|
1988
|
+
viaAggregator: options.viaAggregator
|
|
1989
|
+
}), entryId, revisionId, options);
|
|
1990
|
+
});
|
|
1991
|
+
registerEncryptedEntryCommands(entry);
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// src/commands/file.ts
|
|
1995
|
+
import {
|
|
1996
|
+
buildAllowlistSealId,
|
|
1997
|
+
createEncryptedFile,
|
|
1998
|
+
createPublicFile,
|
|
1999
|
+
deleteFile,
|
|
2000
|
+
toAllowlistId as toAllowlistId2,
|
|
2001
|
+
toBlobObjectId,
|
|
2002
|
+
toEncryptedFileId,
|
|
2003
|
+
toSuiAddress as toSuiAddress5,
|
|
2004
|
+
toWalrusBlobId,
|
|
2005
|
+
transferFileOwnership,
|
|
2006
|
+
updateFileMetadata,
|
|
2007
|
+
uploadEncryptedFileFromBytes,
|
|
2008
|
+
uploadPublicFileFromBytes
|
|
2009
|
+
} from "@arcadiasystems/morse-sdk";
|
|
2010
|
+
import { SessionKey as SessionKey2 } from "@mysten/seal";
|
|
2011
|
+
|
|
2012
|
+
// src/format/hex.ts
|
|
2013
|
+
var HEX = /^[0-9a-fA-F]+$/;
|
|
2014
|
+
function encodeHex(bytes) {
|
|
2015
|
+
let out = "0x";
|
|
2016
|
+
for (const byte of bytes) {
|
|
2017
|
+
out += byte.toString(16).padStart(2, "0");
|
|
2018
|
+
}
|
|
2019
|
+
return out;
|
|
2020
|
+
}
|
|
2021
|
+
function decodeHex(value) {
|
|
2022
|
+
const body = value.startsWith("0x") ? value.slice(2) : value;
|
|
2023
|
+
if (body.length === 0 || body.length % 2 !== 0 || !HEX.test(body)) {
|
|
2024
|
+
throw new UsageError(`Invalid hex value: expected an even number of hex digits, got ${JSON.stringify(value)}.`);
|
|
2025
|
+
}
|
|
2026
|
+
const bytes = new Uint8Array(body.length / 2);
|
|
2027
|
+
for (let i = 0;i < bytes.length; i += 1) {
|
|
2028
|
+
bytes[i] = Number.parseInt(body.slice(i * 2, i * 2 + 2), 16);
|
|
2029
|
+
}
|
|
2030
|
+
return bytes;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
// src/commands/file.ts
|
|
2034
|
+
var SESSION_KEY_TTL_MIN2 = 10;
|
|
2035
|
+
var SEAL_NONCE_BYTES2 = 16;
|
|
2036
|
+
function uploadProgress(output) {
|
|
2037
|
+
const labels = {
|
|
2038
|
+
encrypting: "Encrypting...",
|
|
2039
|
+
uploading: "Uploading to Walrus...",
|
|
2040
|
+
submitting: "Submitting transaction...",
|
|
2041
|
+
complete: ""
|
|
2042
|
+
};
|
|
2043
|
+
return (event) => {
|
|
2044
|
+
const label = labels[event.phase];
|
|
2045
|
+
if (label.length > 0) {
|
|
2046
|
+
output.info(label);
|
|
1655
2047
|
}
|
|
1656
|
-
|
|
1657
|
-
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
async function runFileGet(ctx, target) {
|
|
2051
|
+
const result = await ctx.filesReader.getEncryptedFile(toEncryptedFileId(target), ctx.signal);
|
|
2052
|
+
ctx.output.result(renderEncryptedFile(result), result);
|
|
2053
|
+
}
|
|
2054
|
+
async function runFileRegister(ctx, options) {
|
|
2055
|
+
if (options.allowlist === undefined && options.public !== true) {
|
|
2056
|
+
throw new UsageError("Pass --allowlist <id> to register an encrypted file, or --public for a world-readable one.");
|
|
2057
|
+
}
|
|
2058
|
+
const blobId = toWalrusBlobId(options.blobId);
|
|
2059
|
+
const size = parseByteSize(options.size, "--size");
|
|
2060
|
+
const blobObjectId = options.blobObjectId === undefined ? undefined : toBlobObjectId(options.blobObjectId);
|
|
2061
|
+
if (options.allowlist !== undefined) {
|
|
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,
|
|
1658
2069
|
signal: ctx.signal
|
|
1659
2070
|
});
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
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, {
|
|
2075
|
+
blobId,
|
|
2076
|
+
...blobObjectId === undefined ? {} : { blobObjectId },
|
|
2077
|
+
name: options.name,
|
|
2078
|
+
contentType: options.contentType,
|
|
2079
|
+
size,
|
|
2080
|
+
signal: ctx.signal
|
|
2081
|
+
});
|
|
2082
|
+
ctx.output.result(`Registered public file ${result.fileId}. (tx: ${result.digest})`, result);
|
|
2083
|
+
}
|
|
2084
|
+
async function runFileUpdate(ctx, target, options) {
|
|
2085
|
+
const result = await updateFileMetadata(ctx.adapter, ctx.config, {
|
|
2086
|
+
fileId: toEncryptedFileId(target),
|
|
2087
|
+
name: options.name,
|
|
2088
|
+
contentType: options.contentType,
|
|
2089
|
+
signal: ctx.signal
|
|
2090
|
+
});
|
|
2091
|
+
ctx.output.result(`Updated file ${shortId(target)} metadata. (tx: ${result.digest})`, result);
|
|
2092
|
+
}
|
|
2093
|
+
async function runFileTransferOwnership(ctx, target, newOwner, gopts) {
|
|
2094
|
+
const fileId = toEncryptedFileId(target);
|
|
2095
|
+
const to = toSuiAddress5(newOwner);
|
|
2096
|
+
const proceed = await confirm(`Transfer metadata ownership of file ${shortId(fileId)} to ${to}? Decryption access is governed separately by the allowlist.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
2097
|
+
if (!proceed) {
|
|
2098
|
+
cancelled();
|
|
2099
|
+
}
|
|
2100
|
+
const result = await transferFileOwnership(ctx.adapter, ctx.config, {
|
|
2101
|
+
fileId,
|
|
2102
|
+
newOwner: to,
|
|
2103
|
+
signal: ctx.signal
|
|
2104
|
+
});
|
|
2105
|
+
ctx.output.result(`Transferred file ownership to ${to}. (tx: ${result.digest})`, result);
|
|
2106
|
+
}
|
|
2107
|
+
async function runFileDelete(ctx, target, gopts) {
|
|
2108
|
+
const fileId = toEncryptedFileId(target);
|
|
2109
|
+
const proceed = await confirm(`Delete file metadata ${shortId(fileId)}? The Walrus blob is not deleted; it expires on its own lease.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
2110
|
+
if (!proceed) {
|
|
2111
|
+
cancelled();
|
|
2112
|
+
}
|
|
2113
|
+
const result = await deleteFile(ctx.adapter, ctx.config, {
|
|
2114
|
+
fileId,
|
|
2115
|
+
signal: ctx.signal
|
|
2116
|
+
});
|
|
2117
|
+
ctx.output.result(`Deleted file ${fileId}. (tx: ${result.digest})`, result);
|
|
2118
|
+
}
|
|
2119
|
+
async function runFileUpload(ctx, path, options) {
|
|
2120
|
+
if (options.allowlist === undefined && options.public !== true) {
|
|
2121
|
+
throw new UsageError("Pass --allowlist <id> to upload an encrypted file, or --public for a world-readable one.");
|
|
2122
|
+
}
|
|
2123
|
+
const epochs = parsePositiveInt(options.epochs ?? "3", "--epochs");
|
|
2124
|
+
const bytes = await readContentBytes({ file: path });
|
|
2125
|
+
const contentType = resolveContentType(options.contentType, path);
|
|
2126
|
+
const upload = { epochs, deletable: true };
|
|
2127
|
+
const onProgress = uploadProgress(ctx.output);
|
|
2128
|
+
if (options.allowlist !== undefined) {
|
|
2129
|
+
const allowlistId = toAllowlistId2(options.allowlist);
|
|
2130
|
+
const sealId = buildAllowlistSealId(allowlistId, crypto.getRandomValues(new Uint8Array(SEAL_NONCE_BYTES2)));
|
|
2131
|
+
const result2 = await uploadEncryptedFileFromBytes(ctx.adapter, ctx.config, {
|
|
2132
|
+
walrus: ctx.walrus,
|
|
2133
|
+
seal: ctx.seal,
|
|
2134
|
+
allowlistId,
|
|
2135
|
+
sealId,
|
|
2136
|
+
plaintext: bytes,
|
|
2137
|
+
name: options.name,
|
|
2138
|
+
contentType,
|
|
2139
|
+
upload,
|
|
2140
|
+
signal: ctx.signal,
|
|
2141
|
+
onProgress
|
|
2142
|
+
});
|
|
2143
|
+
const sealIdHex = encodeHex(sealId);
|
|
2144
|
+
const human2 = [
|
|
2145
|
+
`Uploaded encrypted file ${result2.fileId}`,
|
|
2146
|
+
` blobId: ${result2.blobId}`,
|
|
2147
|
+
` sealId: ${sealIdHex}`,
|
|
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 });
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
const result = await uploadPublicFileFromBytes(ctx.adapter, ctx.config, {
|
|
2156
|
+
walrus: ctx.walrus,
|
|
2157
|
+
bytes,
|
|
2158
|
+
name: options.name,
|
|
2159
|
+
contentType,
|
|
2160
|
+
upload,
|
|
2161
|
+
signal: ctx.signal,
|
|
2162
|
+
onProgress
|
|
2163
|
+
});
|
|
2164
|
+
const aggregator = ctx.config.walrusEndpoints.aggregator;
|
|
2165
|
+
const viewUrl = aggregator.length > 0 ? `${aggregator}/v1/blobs/${result.blobId}` : undefined;
|
|
2166
|
+
const human = viewUrl === undefined ? `Uploaded public file ${result.fileId}. (tx: ${result.digest})` : `Uploaded public file ${result.fileId}. (tx: ${result.digest})
|
|
2167
|
+
view: ${viewUrl}`;
|
|
2168
|
+
ctx.output.result(human, { ...result, viewUrl: viewUrl ?? null });
|
|
2169
|
+
}
|
|
2170
|
+
async function decryptFile(ctx, file, ciphertext, sealIdHex) {
|
|
2171
|
+
if (sealIdHex === undefined) {
|
|
2172
|
+
throw new UsageError("This file is encrypted; pass --seal-id <hex> (printed when the file was uploaded).");
|
|
2173
|
+
}
|
|
2174
|
+
if (file.allowlistId === null) {
|
|
2175
|
+
throw new UsageError(`File ${file.id} is marked encrypted but carries no allowlist; it cannot be decrypted.`);
|
|
2176
|
+
}
|
|
2177
|
+
const sealId = decodeHex(sealIdHex);
|
|
2178
|
+
const { keypair, address } = await ctx.unlockSigner();
|
|
2179
|
+
ctx.output.info("Signing a SessionKey with the active account...");
|
|
2180
|
+
const sessionKey = await SessionKey2.create({
|
|
2181
|
+
address,
|
|
2182
|
+
packageId: ctx.config.originalPackageId ?? ctx.config.packageId,
|
|
2183
|
+
ttlMin: SESSION_KEY_TTL_MIN2,
|
|
2184
|
+
signer: keypair,
|
|
2185
|
+
suiClient: ctx.client
|
|
2186
|
+
});
|
|
2187
|
+
return ctx.seal.decryptUnderAllowlist(ciphertext, {
|
|
2188
|
+
sealId,
|
|
2189
|
+
allowlistId: file.allowlistId,
|
|
2190
|
+
sessionKey
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
async function runFileDownload(ctx, target, options) {
|
|
2194
|
+
if (ctx.output.isJson && options.out === undefined) {
|
|
2195
|
+
throw new UsageError("Downloading content to stdout is not supported in --json mode; pass --out <path>.");
|
|
2196
|
+
}
|
|
2197
|
+
const file = await ctx.filesReader.getEncryptedFile(toEncryptedFileId(target), ctx.signal);
|
|
2198
|
+
ctx.output.info("Fetching content from Walrus...");
|
|
2199
|
+
const bytes = await ctx.walrusRead.readBlob(file.blobId, {
|
|
2200
|
+
signal: ctx.signal
|
|
2201
|
+
});
|
|
2202
|
+
let content = bytes;
|
|
2203
|
+
if (file.encrypted) {
|
|
2204
|
+
content = await decryptFile(ctx, file, bytes, options.sealId);
|
|
2205
|
+
}
|
|
2206
|
+
if (options.out !== undefined) {
|
|
2207
|
+
await writeFileContents(options.out, content);
|
|
2208
|
+
ctx.output.result(`Wrote ${content.length} bytes to ${options.out}.`, {
|
|
2209
|
+
fileId: file.id,
|
|
2210
|
+
name: file.name,
|
|
2211
|
+
contentType: file.contentType,
|
|
2212
|
+
bytes: content.length,
|
|
2213
|
+
out: options.out
|
|
2214
|
+
});
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
process.stdout.write(content);
|
|
2218
|
+
}
|
|
2219
|
+
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
|
+
allowlistOption(upload).action(async (path, options, command) => {
|
|
2223
|
+
await runFileUpload(await buildEncryptContext(command), path, options);
|
|
2224
|
+
});
|
|
2225
|
+
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("--seal-id <hex>", "Seal id from upload (required to decrypt an encrypted file)");
|
|
2226
|
+
viaAggregatorOption(download).action(async (target, options, command) => {
|
|
2227
|
+
await runFileDownload(await buildFileDownloadContext(command, {
|
|
2228
|
+
viaAggregator: options.viaAggregator
|
|
2229
|
+
}), target, options);
|
|
2230
|
+
});
|
|
2231
|
+
file.command("get <file>").description("Fetch a file's on-chain metadata").action(async (target, _options, command) => {
|
|
2232
|
+
await runFileGet(await buildFilesReadContext(command), target);
|
|
2233
|
+
});
|
|
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
|
+
allowlistOption(register).action(async (options, command) => {
|
|
2236
|
+
await runFileRegister(await buildWriteContext(command), options);
|
|
2237
|
+
});
|
|
2238
|
+
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
|
+
await runFileUpdate(await buildWriteContext(command), target, options);
|
|
2240
|
+
});
|
|
2241
|
+
file.command("transfer-ownership <file> <newOwner>").description("Transfer a file's metadata-mutation right (not decrypt access)").action(async (target, newOwner, _options, command) => {
|
|
2242
|
+
await runFileTransferOwnership(await buildWriteContext(command), target, newOwner, globalOptions(command));
|
|
2243
|
+
});
|
|
2244
|
+
file.command("delete <file>").description("Delete a file's metadata record (does not delete the blob)").action(async (target, _options, command) => {
|
|
2245
|
+
await runFileDelete(await buildWriteContext(command), target, globalOptions(command));
|
|
1672
2246
|
});
|
|
1673
|
-
registerEncryptedEntryCommands(entry);
|
|
1674
2247
|
}
|
|
1675
2248
|
|
|
1676
2249
|
// src/commands/publication.ts
|
|
1677
2250
|
import {
|
|
1678
2251
|
createPublication,
|
|
1679
2252
|
deletePublication,
|
|
1680
|
-
toSuiAddress as
|
|
2253
|
+
toSuiAddress as toSuiAddress6,
|
|
1681
2254
|
transferOwnership
|
|
1682
2255
|
} from "@arcadiasystems/morse-sdk";
|
|
2256
|
+
async function runPublicationGet(ctx, target) {
|
|
2257
|
+
const id = await resolvePublication(ctx, target);
|
|
2258
|
+
const result = await ctx.reader.getPublication(id, ctx.signal);
|
|
2259
|
+
ctx.output.result(renderPublication(result), result);
|
|
2260
|
+
}
|
|
2261
|
+
async function runPublicationList(ctx, address, options) {
|
|
2262
|
+
const owner = address === undefined ? ctx.ownerAddress : toSuiAddress6(address);
|
|
2263
|
+
if (owner === undefined) {
|
|
2264
|
+
throw new UsageError("No address given and no active account. Pass an address or import an account.");
|
|
2265
|
+
}
|
|
2266
|
+
const page = await ctx.reader.listPublicationsOwnedBy(owner, {
|
|
2267
|
+
signal: ctx.signal,
|
|
2268
|
+
...options.limit === undefined ? {} : { limit: parseLimit(options.limit) },
|
|
2269
|
+
...options.cursor === undefined ? {} : { cursor: options.cursor }
|
|
2270
|
+
});
|
|
2271
|
+
if (page.nextCursor !== null) {
|
|
2272
|
+
ctx.output.info(`More results: pass --cursor "${page.nextCursor}"`);
|
|
2273
|
+
}
|
|
2274
|
+
if (options.idsOnly) {
|
|
2275
|
+
ctx.output.result(renderPublicationList(page.results), {
|
|
2276
|
+
results: page.results,
|
|
2277
|
+
nextCursor: page.nextCursor
|
|
2278
|
+
});
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
const enriched = [];
|
|
2282
|
+
for (const owned of page.results) {
|
|
2283
|
+
const pub = await ctx.reader.getPublication(owned.publicationId, ctx.signal);
|
|
2284
|
+
enriched.push({
|
|
2285
|
+
slug: pub.slug,
|
|
2286
|
+
name: pub.name,
|
|
2287
|
+
publicationId: owned.publicationId,
|
|
2288
|
+
ownerCapId: owned.ownerCapId
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
ctx.output.result(renderEnrichedPublicationList(enriched), {
|
|
2292
|
+
results: enriched,
|
|
2293
|
+
nextCursor: page.nextCursor
|
|
2294
|
+
});
|
|
2295
|
+
}
|
|
2296
|
+
async function runPublicationCreate(ctx, options, gopts) {
|
|
2297
|
+
ctx.output.info(`Creating "${options.name}"...`);
|
|
2298
|
+
const result = await createPublication(ctx.adapter, ctx.config, {
|
|
2299
|
+
name: options.name,
|
|
2300
|
+
slug: options.slug,
|
|
2301
|
+
signal: ctx.signal
|
|
2302
|
+
});
|
|
2303
|
+
await updateActiveProfile(gopts, {
|
|
2304
|
+
publication: result.publicationId,
|
|
2305
|
+
collection: undefined
|
|
2306
|
+
});
|
|
2307
|
+
const human = [
|
|
2308
|
+
`Created "${options.name}" (${result.publicationId})`,
|
|
2309
|
+
` ownerCap: ${result.ownerCapId}`,
|
|
2310
|
+
` publisherCap: ${result.publisherCapId}`,
|
|
2311
|
+
` tx: ${result.digest}`,
|
|
2312
|
+
"Selected as the active publication."
|
|
2313
|
+
].join(`
|
|
2314
|
+
`);
|
|
2315
|
+
ctx.output.result(human, result);
|
|
2316
|
+
}
|
|
2317
|
+
async function runPublicationDelete(ctx, target, options, gopts) {
|
|
2318
|
+
const id = await resolvePublication(ctx, target);
|
|
2319
|
+
const proceed = await confirm(`Delete publication ${shortId(id)}? This cannot be undone.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
2320
|
+
if (!proceed) {
|
|
2321
|
+
cancelled();
|
|
2322
|
+
}
|
|
2323
|
+
ctx.output.info("Resolving OwnerCap...");
|
|
2324
|
+
const ownerCapId = await resolveOwnerCap(ctx.reader, ctx.address, id, options.ownerCap, ctx.signal);
|
|
2325
|
+
ctx.output.info("Deleting...");
|
|
2326
|
+
const result = await deletePublication(ctx.reader, ctx.adapter, ctx.config, {
|
|
2327
|
+
publicationId: id,
|
|
2328
|
+
ownerCapId,
|
|
2329
|
+
signal: ctx.signal
|
|
2330
|
+
});
|
|
2331
|
+
if (ctx.settings.publication === id) {
|
|
2332
|
+
await updateActiveProfile(gopts, {
|
|
2333
|
+
publication: undefined,
|
|
2334
|
+
collection: undefined
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
ctx.output.result(`Deleted ${id}. (tx: ${result.digest})`, result);
|
|
2338
|
+
}
|
|
2339
|
+
async function runPublicationTransferOwnership(ctx, recipient, options, gopts) {
|
|
2340
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
2341
|
+
const to = toSuiAddress6(recipient);
|
|
2342
|
+
const proceed = await confirm(`Transfer ownership of ${shortId(id)} to ${to}? You will lose owner control.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
2343
|
+
if (!proceed) {
|
|
2344
|
+
cancelled();
|
|
2345
|
+
}
|
|
2346
|
+
ctx.output.info("Resolving OwnerCap...");
|
|
2347
|
+
const ownerCapId = await resolveOwnerCap(ctx.reader, ctx.address, id, options.ownerCap, ctx.signal);
|
|
2348
|
+
const result = await transferOwnership(ctx.adapter, ctx.config, {
|
|
2349
|
+
ownerCapId,
|
|
2350
|
+
recipient: to,
|
|
2351
|
+
signal: ctx.signal
|
|
2352
|
+
});
|
|
2353
|
+
ctx.output.result(`Transferred ownership of ${id} to ${to}. (tx: ${result.digest})`, result);
|
|
2354
|
+
}
|
|
1683
2355
|
function registerPublicationCommands(program) {
|
|
1684
2356
|
const publication = program.command("publication").alias("pub").description("Work with publications");
|
|
1685
2357
|
publication.command("get [publication]").description("Fetch a publication (slug or id; default: the active publication)").action(async (target, _options, command) => {
|
|
1686
|
-
|
|
1687
|
-
const id = await resolvePublication(ctx, target);
|
|
1688
|
-
const result = await ctx.reader.getPublication(id, ctx.signal);
|
|
1689
|
-
ctx.output.result(renderPublication(result), result);
|
|
2358
|
+
await runPublicationGet(await buildReadContext(command), target);
|
|
1690
2359
|
});
|
|
1691
2360
|
publication.command("list [address]").description("List publications owned by an address (default: the active account)").option("--limit <n>", "Maximum results per page").option("--cursor <cursor>", "Continue from a previous page cursor").option("--ids-only", "Skip slug/name resolution (one RPC, no per-publication reads)").action(async (address, options, command) => {
|
|
1692
|
-
|
|
1693
|
-
const owner = address === undefined ? ctx.ownerAddress : toSuiAddress4(address);
|
|
1694
|
-
if (owner === undefined) {
|
|
1695
|
-
throw new UsageError("No address given and no active account. Pass an address or import an account.");
|
|
1696
|
-
}
|
|
1697
|
-
const page = await ctx.reader.listPublicationsOwnedBy(owner, {
|
|
1698
|
-
signal: ctx.signal,
|
|
1699
|
-
...options.limit === undefined ? {} : { limit: parseLimit(options.limit) },
|
|
1700
|
-
...options.cursor === undefined ? {} : { cursor: options.cursor }
|
|
1701
|
-
});
|
|
1702
|
-
if (page.nextCursor !== null) {
|
|
1703
|
-
ctx.output.info(`More results: pass --cursor "${page.nextCursor}"`);
|
|
1704
|
-
}
|
|
1705
|
-
if (options.idsOnly) {
|
|
1706
|
-
ctx.output.result(renderPublicationList(page.results), {
|
|
1707
|
-
results: page.results,
|
|
1708
|
-
nextCursor: page.nextCursor
|
|
1709
|
-
});
|
|
1710
|
-
return;
|
|
1711
|
-
}
|
|
1712
|
-
const enriched = [];
|
|
1713
|
-
for (const owned of page.results) {
|
|
1714
|
-
const pub = await ctx.reader.getPublication(owned.publicationId, ctx.signal);
|
|
1715
|
-
enriched.push({
|
|
1716
|
-
slug: pub.slug,
|
|
1717
|
-
name: pub.name,
|
|
1718
|
-
publicationId: owned.publicationId,
|
|
1719
|
-
ownerCapId: owned.ownerCapId
|
|
1720
|
-
});
|
|
1721
|
-
}
|
|
1722
|
-
ctx.output.result(renderEnrichedPublicationList(enriched), {
|
|
1723
|
-
results: enriched,
|
|
1724
|
-
nextCursor: page.nextCursor
|
|
1725
|
-
});
|
|
2361
|
+
await runPublicationList(await buildReadContext(command), address, options);
|
|
1726
2362
|
});
|
|
1727
2363
|
publication.command("create").description("Create a publication and select it as the active publication").requiredOption("-n, --name <name>", "Publication name").requiredOption("-s, --slug <slug>", "URL slug: lowercase alphanumeric and hyphens, 1-64 chars").action(async (options, command) => {
|
|
1728
|
-
|
|
1729
|
-
ctx.output.info(`Creating "${options.name}"...`);
|
|
1730
|
-
const result = await createPublication(ctx.adapter, ctx.config, {
|
|
1731
|
-
name: options.name,
|
|
1732
|
-
slug: options.slug,
|
|
1733
|
-
signal: ctx.signal
|
|
1734
|
-
});
|
|
1735
|
-
await updateActiveProfile(globalOptions(command), {
|
|
1736
|
-
publication: result.publicationId,
|
|
1737
|
-
collection: undefined
|
|
1738
|
-
});
|
|
1739
|
-
const human = [
|
|
1740
|
-
`Created "${options.name}" (${result.publicationId})`,
|
|
1741
|
-
` ownerCap: ${result.ownerCapId}`,
|
|
1742
|
-
` publisherCap: ${result.publisherCapId}`,
|
|
1743
|
-
` tx: ${result.digest}`,
|
|
1744
|
-
"Selected as the active publication."
|
|
1745
|
-
].join(`
|
|
1746
|
-
`);
|
|
1747
|
-
ctx.output.result(human, result);
|
|
2364
|
+
await runPublicationCreate(await buildWriteContext(command), options, globalOptions(command));
|
|
1748
2365
|
});
|
|
1749
2366
|
publication.command("delete [publication]").description("Delete an empty publication (default: the active publication)").option("--owner-cap <id>", "OwnerCap ID (auto-resolved if omitted)").action(async (target, options, command) => {
|
|
1750
|
-
|
|
1751
|
-
const id = await resolvePublication(ctx, target);
|
|
1752
|
-
const proceed = await confirm(`Delete publication ${shortId(id)}? This cannot be undone.`, {
|
|
1753
|
-
assumeYes: Boolean(globalOptions(command).yes),
|
|
1754
|
-
signal: ctx.signal
|
|
1755
|
-
});
|
|
1756
|
-
if (!proceed) {
|
|
1757
|
-
cancelled();
|
|
1758
|
-
}
|
|
1759
|
-
ctx.output.info("Resolving OwnerCap...");
|
|
1760
|
-
const ownerCapId = await resolveOwnerCap(ctx.reader, ctx.address, id, options.ownerCap, ctx.signal);
|
|
1761
|
-
ctx.output.info("Deleting...");
|
|
1762
|
-
const result = await deletePublication(ctx.reader, ctx.adapter, ctx.config, {
|
|
1763
|
-
publicationId: id,
|
|
1764
|
-
ownerCapId,
|
|
1765
|
-
signal: ctx.signal
|
|
1766
|
-
});
|
|
1767
|
-
if (ctx.settings.publication === id) {
|
|
1768
|
-
await updateActiveProfile(globalOptions(command), {
|
|
1769
|
-
publication: undefined,
|
|
1770
|
-
collection: undefined
|
|
1771
|
-
});
|
|
1772
|
-
}
|
|
1773
|
-
ctx.output.result(`Deleted ${id}. (tx: ${result.digest})`, result);
|
|
2367
|
+
await runPublicationDelete(await buildWriteContext(command), target, options, globalOptions(command));
|
|
1774
2368
|
});
|
|
1775
2369
|
const transfer = publication.command("transfer-ownership <recipient>").description("Transfer a publication's OwnerCap to another address");
|
|
1776
2370
|
publicationOption(ownerCapOption(transfer)).action(async (recipient, options, command) => {
|
|
1777
|
-
|
|
1778
|
-
const id = await resolvePublication(ctx, options.publication);
|
|
1779
|
-
const to = toSuiAddress4(recipient);
|
|
1780
|
-
const proceed = await confirm(`Transfer ownership of ${shortId(id)} to ${to}? You will lose owner control.`, { assumeYes: Boolean(globalOptions(command).yes), signal: ctx.signal });
|
|
1781
|
-
if (!proceed) {
|
|
1782
|
-
cancelled();
|
|
1783
|
-
}
|
|
1784
|
-
ctx.output.info("Resolving OwnerCap...");
|
|
1785
|
-
const ownerCapId = await resolveOwnerCap(ctx.reader, ctx.address, id, options.ownerCap, ctx.signal);
|
|
1786
|
-
const result = await transferOwnership(ctx.adapter, ctx.config, {
|
|
1787
|
-
ownerCapId,
|
|
1788
|
-
recipient: to,
|
|
1789
|
-
signal: ctx.signal
|
|
1790
|
-
});
|
|
1791
|
-
ctx.output.result(`Transferred ownership of ${id} to ${to}. (tx: ${result.digest})`, result);
|
|
2371
|
+
await runPublicationTransferOwnership(await buildWriteContext(command), recipient, options, globalOptions(command));
|
|
1792
2372
|
});
|
|
1793
2373
|
}
|
|
1794
2374
|
|
|
@@ -1801,14 +2381,12 @@ import {
|
|
|
1801
2381
|
function revisionOptions(command) {
|
|
1802
2382
|
return collectionOption(publicationOption(publisherCapOption(contentOptions(command))));
|
|
1803
2383
|
}
|
|
1804
|
-
async function resolveTarget(
|
|
1805
|
-
const ctx = await buildContentContext(command);
|
|
2384
|
+
async function resolveTarget(ctx, options) {
|
|
1806
2385
|
const publicationId = await resolvePublication(ctx, options.publication);
|
|
1807
2386
|
const collection = resolveCollection(ctx, options.collection);
|
|
1808
|
-
return {
|
|
2387
|
+
return { publicationId, collection };
|
|
1809
2388
|
}
|
|
1810
|
-
async function prepareContent(
|
|
1811
|
-
const { ctx, publicationId } = target;
|
|
2389
|
+
async function prepareContent(ctx, publicationId, options) {
|
|
1812
2390
|
const epochs = parsePositiveInt(options.epochs, "--epochs");
|
|
1813
2391
|
const bytes = await readContentBytes(options);
|
|
1814
2392
|
const contentType = resolveContentType(options.contentType, options.stdin ? undefined : options.file);
|
|
@@ -1821,113 +2399,124 @@ async function prepareContent(target, options) {
|
|
|
1821
2399
|
ctx.output.info(`Blob uploaded (${upload.blobObjectId}). Submitting transaction...`);
|
|
1822
2400
|
return { blobObjectId: upload.blobObjectId, contentType, publisherCapId };
|
|
1823
2401
|
}
|
|
2402
|
+
async function runRevisionPublishDirect(ctx, entryId, options) {
|
|
2403
|
+
const { publicationId, collection } = await resolveTarget(ctx, options);
|
|
2404
|
+
const numericEntryId = parseId(entryId, "entryId");
|
|
2405
|
+
const prepared = await prepareContent(ctx, publicationId, options);
|
|
2406
|
+
const result = await publishDirect(ctx.adapter, ctx.config, {
|
|
2407
|
+
publicationId,
|
|
2408
|
+
publisherCapId: prepared.publisherCapId,
|
|
2409
|
+
collectionName: collection,
|
|
2410
|
+
entryId: numericEntryId,
|
|
2411
|
+
blobObjectId: prepared.blobObjectId,
|
|
2412
|
+
contentType: prepared.contentType,
|
|
2413
|
+
signal: ctx.signal
|
|
2414
|
+
});
|
|
2415
|
+
ctx.output.result(`Published revision #${result.revisionId} on entry #${numericEntryId}. (tx: ${result.digest})`, result);
|
|
2416
|
+
}
|
|
2417
|
+
async function runRevisionAppendDraft(ctx, entryId, options) {
|
|
2418
|
+
const { publicationId, collection } = await resolveTarget(ctx, options);
|
|
2419
|
+
const numericEntryId = parseId(entryId, "entryId");
|
|
2420
|
+
const prepared = await prepareContent(ctx, publicationId, options);
|
|
2421
|
+
const result = await appendDraftRevision(ctx.adapter, ctx.config, {
|
|
2422
|
+
publicationId,
|
|
2423
|
+
publisherCapId: prepared.publisherCapId,
|
|
2424
|
+
collectionName: collection,
|
|
2425
|
+
entryId: numericEntryId,
|
|
2426
|
+
blobObjectId: prepared.blobObjectId,
|
|
2427
|
+
contentType: prepared.contentType,
|
|
2428
|
+
signal: ctx.signal
|
|
2429
|
+
});
|
|
2430
|
+
ctx.output.result(`Appended draft revision #${result.revisionId} on entry #${numericEntryId}. (tx: ${result.digest})`, result);
|
|
2431
|
+
}
|
|
2432
|
+
async function runRevisionPublishFromDraft(ctx, entryId, draftRevisionId, options) {
|
|
2433
|
+
const { publicationId, collection } = await resolveTarget(ctx, options);
|
|
2434
|
+
const numericEntryId = parseId(entryId, "entryId");
|
|
2435
|
+
const draftId = parseId(draftRevisionId, "draftRevisionId");
|
|
2436
|
+
const prepared = await prepareContent(ctx, publicationId, options);
|
|
2437
|
+
const result = await publishFromDraft(ctx.adapter, ctx.config, {
|
|
2438
|
+
publicationId,
|
|
2439
|
+
publisherCapId: prepared.publisherCapId,
|
|
2440
|
+
collectionName: collection,
|
|
2441
|
+
entryId: numericEntryId,
|
|
2442
|
+
draftRevisionId: draftId,
|
|
2443
|
+
blobObjectId: prepared.blobObjectId,
|
|
2444
|
+
contentType: prepared.contentType,
|
|
2445
|
+
signal: ctx.signal
|
|
2446
|
+
});
|
|
2447
|
+
ctx.output.result(`Published revision #${result.revisionId} from draft #${draftId} on entry #${numericEntryId}. (tx: ${result.digest})`, result);
|
|
2448
|
+
}
|
|
1824
2449
|
function registerRevisionCommands(program) {
|
|
1825
2450
|
const revision = program.command("revision").description("Append or publish revisions on an entry");
|
|
1826
2451
|
revisionOptions(revision.command("publish-direct <entryId>").description("Upload content and append it as a public revision")).action(async (entryId, options, command) => {
|
|
1827
|
-
|
|
1828
|
-
const numericEntryId = parseId(entryId, "entryId");
|
|
1829
|
-
const prepared = await prepareContent(target, options);
|
|
1830
|
-
const result = await publishDirect(target.ctx.adapter, target.ctx.config, {
|
|
1831
|
-
publicationId: target.publicationId,
|
|
1832
|
-
publisherCapId: prepared.publisherCapId,
|
|
1833
|
-
collectionName: target.collection,
|
|
1834
|
-
entryId: numericEntryId,
|
|
1835
|
-
blobObjectId: prepared.blobObjectId,
|
|
1836
|
-
contentType: prepared.contentType,
|
|
1837
|
-
signal: target.ctx.signal
|
|
1838
|
-
});
|
|
1839
|
-
target.ctx.output.result(`Published revision #${result.revisionId} on entry #${numericEntryId}. (tx: ${result.digest})`, result);
|
|
2452
|
+
await runRevisionPublishDirect(await buildContentContext(command), entryId, options);
|
|
1840
2453
|
});
|
|
1841
2454
|
revisionOptions(revision.command("append-draft <entryId>").description("Upload content and append it as a draft revision")).action(async (entryId, options, command) => {
|
|
1842
|
-
|
|
1843
|
-
const numericEntryId = parseId(entryId, "entryId");
|
|
1844
|
-
const prepared = await prepareContent(target, options);
|
|
1845
|
-
const result = await appendDraftRevision(target.ctx.adapter, target.ctx.config, {
|
|
1846
|
-
publicationId: target.publicationId,
|
|
1847
|
-
publisherCapId: prepared.publisherCapId,
|
|
1848
|
-
collectionName: target.collection,
|
|
1849
|
-
entryId: numericEntryId,
|
|
1850
|
-
blobObjectId: prepared.blobObjectId,
|
|
1851
|
-
contentType: prepared.contentType,
|
|
1852
|
-
signal: target.ctx.signal
|
|
1853
|
-
});
|
|
1854
|
-
target.ctx.output.result(`Appended draft revision #${result.revisionId} on entry #${numericEntryId}. (tx: ${result.digest})`, result);
|
|
2455
|
+
await runRevisionAppendDraft(await buildContentContext(command), entryId, options);
|
|
1855
2456
|
});
|
|
1856
2457
|
revisionOptions(revision.command("publish-from-draft <entryId> <draftRevisionId>").description("Upload content and publish it as a new revision, referencing a draft")).action(async (entryId, draftRevisionId, options, command) => {
|
|
1857
|
-
|
|
1858
|
-
const numericEntryId = parseId(entryId, "entryId");
|
|
1859
|
-
const draftId = parseId(draftRevisionId, "draftRevisionId");
|
|
1860
|
-
const prepared = await prepareContent(target, options);
|
|
1861
|
-
const result = await publishFromDraft(target.ctx.adapter, target.ctx.config, {
|
|
1862
|
-
publicationId: target.publicationId,
|
|
1863
|
-
publisherCapId: prepared.publisherCapId,
|
|
1864
|
-
collectionName: target.collection,
|
|
1865
|
-
entryId: numericEntryId,
|
|
1866
|
-
draftRevisionId: draftId,
|
|
1867
|
-
blobObjectId: prepared.blobObjectId,
|
|
1868
|
-
contentType: prepared.contentType,
|
|
1869
|
-
signal: target.ctx.signal
|
|
1870
|
-
});
|
|
1871
|
-
target.ctx.output.result(`Published revision #${result.revisionId} from draft #${draftId} on entry #${numericEntryId}. (tx: ${result.digest})`, result);
|
|
2458
|
+
await runRevisionPublishFromDraft(await buildContentContext(command), entryId, draftRevisionId, options);
|
|
1872
2459
|
});
|
|
1873
2460
|
}
|
|
1874
2461
|
|
|
1875
2462
|
// src/commands/use.ts
|
|
2463
|
+
async function runUse(output, gopts, publication, collection, options, makeContext) {
|
|
2464
|
+
if (options.clear) {
|
|
2465
|
+
const profile2 = await updateActiveProfile(gopts, {
|
|
2466
|
+
publication: undefined,
|
|
2467
|
+
collection: undefined
|
|
2468
|
+
});
|
|
2469
|
+
output.result(`Cleared the active publication and collection (profile "${profile2}").`, { profile: profile2, publication: null, collection: null });
|
|
2470
|
+
return;
|
|
2471
|
+
}
|
|
2472
|
+
if (publication === undefined) {
|
|
2473
|
+
throw new UsageError("Provide a publication (slug or id), or pass --clear.");
|
|
2474
|
+
}
|
|
2475
|
+
const ctx = await makeContext();
|
|
2476
|
+
const publicationId = await resolvePublication(ctx, publication);
|
|
2477
|
+
let collectionName;
|
|
2478
|
+
if (collection !== undefined) {
|
|
2479
|
+
const pub = await ctx.reader.getPublication(publicationId, ctx.signal);
|
|
2480
|
+
if (!pub.collections.some((c) => c.name === collection)) {
|
|
2481
|
+
throw new UsageError(`Publication ${publicationId} has no collection "${collection}".`);
|
|
2482
|
+
}
|
|
2483
|
+
collectionName = collection;
|
|
2484
|
+
}
|
|
2485
|
+
const profile = await updateActiveProfile(gopts, {
|
|
2486
|
+
publication: publicationId,
|
|
2487
|
+
collection: collectionName
|
|
2488
|
+
});
|
|
2489
|
+
const human = collectionName === undefined ? `Active publication set to ${publicationId} (profile "${profile}").` : `Active publication set to ${publicationId}, collection "${collectionName}" (profile "${profile}").`;
|
|
2490
|
+
output.result(human, {
|
|
2491
|
+
profile,
|
|
2492
|
+
publication: publicationId,
|
|
2493
|
+
collection: collectionName ?? null
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
function runStatus(output, settings) {
|
|
2497
|
+
const account = accountAddress(settings.account);
|
|
2498
|
+
const human = [
|
|
2499
|
+
`profile: ${settings.profileName}`,
|
|
2500
|
+
`network: ${settings.network}`,
|
|
2501
|
+
`account: ${account ?? "(none)"}`,
|
|
2502
|
+
`publication: ${settings.publication ?? "(none)"}`,
|
|
2503
|
+
`collection: ${settings.collection ?? "(none)"}`
|
|
2504
|
+
].join(`
|
|
2505
|
+
`);
|
|
2506
|
+
output.result(human, {
|
|
2507
|
+
profile: settings.profileName,
|
|
2508
|
+
network: settings.network,
|
|
2509
|
+
account: account ?? null,
|
|
2510
|
+
publication: settings.publication ?? null,
|
|
2511
|
+
collection: settings.collection ?? null
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
1876
2514
|
function registerContextCommands(program) {
|
|
1877
2515
|
program.command("use [publication] [collection]").description("Set the active publication (slug or id) and optional collection. Omitting the collection clears any active collection.").option("--clear", "Clear the active publication and collection").action(async (publication, collection, options, command) => {
|
|
1878
|
-
|
|
1879
|
-
const output = outputFor(command);
|
|
1880
|
-
if (options.clear) {
|
|
1881
|
-
const profile2 = await updateActiveProfile(opts, {
|
|
1882
|
-
publication: undefined,
|
|
1883
|
-
collection: undefined
|
|
1884
|
-
});
|
|
1885
|
-
output.result(`Cleared the active publication and collection (profile "${profile2}").`, { profile: profile2, publication: null, collection: null });
|
|
1886
|
-
return;
|
|
1887
|
-
}
|
|
1888
|
-
if (publication === undefined) {
|
|
1889
|
-
throw new UsageError("Provide a publication (slug or id), or pass --clear.");
|
|
1890
|
-
}
|
|
1891
|
-
const ctx = await buildReadContext(command);
|
|
1892
|
-
const publicationId = await resolvePublication(ctx, publication);
|
|
1893
|
-
let collectionName;
|
|
1894
|
-
if (collection !== undefined) {
|
|
1895
|
-
const pub = await ctx.reader.getPublication(publicationId, ctx.signal);
|
|
1896
|
-
if (!pub.collections.some((c) => c.name === collection)) {
|
|
1897
|
-
throw new UsageError(`Publication ${publicationId} has no collection "${collection}".`);
|
|
1898
|
-
}
|
|
1899
|
-
collectionName = collection;
|
|
1900
|
-
}
|
|
1901
|
-
const profile = await updateActiveProfile(opts, {
|
|
1902
|
-
publication: publicationId,
|
|
1903
|
-
collection: collectionName
|
|
1904
|
-
});
|
|
1905
|
-
const human = collectionName === undefined ? `Active publication set to ${publicationId} (profile "${profile}").` : `Active publication set to ${publicationId}, collection "${collectionName}" (profile "${profile}").`;
|
|
1906
|
-
output.result(human, {
|
|
1907
|
-
profile,
|
|
1908
|
-
publication: publicationId,
|
|
1909
|
-
collection: collectionName ?? null
|
|
1910
|
-
});
|
|
2516
|
+
await runUse(outputFor(command), globalOptions(command), publication, collection, options, () => buildReadContext(command));
|
|
1911
2517
|
});
|
|
1912
2518
|
program.command("status").description("Show the active profile, network, account, publication, and collection").action(async (_options, command) => {
|
|
1913
|
-
|
|
1914
|
-
const settings = resolveSettings(globalOptions(command), await loadConfig());
|
|
1915
|
-
const account = accountAddress(settings.account);
|
|
1916
|
-
const human = [
|
|
1917
|
-
`profile: ${settings.profileName}`,
|
|
1918
|
-
`network: ${settings.network}`,
|
|
1919
|
-
`account: ${account ?? "(none)"}`,
|
|
1920
|
-
`publication: ${settings.publication ?? "(none)"}`,
|
|
1921
|
-
`collection: ${settings.collection ?? "(none)"}`
|
|
1922
|
-
].join(`
|
|
1923
|
-
`);
|
|
1924
|
-
output.result(human, {
|
|
1925
|
-
profile: settings.profileName,
|
|
1926
|
-
network: settings.network,
|
|
1927
|
-
account: account ?? null,
|
|
1928
|
-
publication: settings.publication ?? null,
|
|
1929
|
-
collection: settings.collection ?? null
|
|
1930
|
-
});
|
|
2519
|
+
runStatus(outputFor(command), resolveSettings(globalOptions(command), await loadConfig()));
|
|
1931
2520
|
});
|
|
1932
2521
|
}
|
|
1933
2522
|
|
|
@@ -1941,6 +2530,8 @@ function registerCommands(program) {
|
|
|
1941
2530
|
registerEntryCommands(program);
|
|
1942
2531
|
registerRevisionCommands(program);
|
|
1943
2532
|
registerCapCommands(program);
|
|
2533
|
+
registerAllowlistCommands(program);
|
|
2534
|
+
registerFileCommands(program);
|
|
1944
2535
|
}
|
|
1945
2536
|
|
|
1946
2537
|
// src/index.ts
|