@arcadiasystems/morse-cli 0.1.0 → 0.3.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 +63 -1
- package/README.md +69 -3
- package/dist/index.js +1398 -699
- 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.3.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.3.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,33 +882,38 @@ 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
|
|
878
904
|
import {
|
|
905
|
+
buildFilesEventTypes,
|
|
879
906
|
DefaultSealAdapter,
|
|
880
907
|
DefaultWalrusReadAdapter,
|
|
881
908
|
DefaultWalrusWriteAdapter,
|
|
909
|
+
HttpAggregatorReadAdapter,
|
|
882
910
|
KeypairAdapter,
|
|
883
911
|
morseConfig,
|
|
912
|
+
RpcFilesReader,
|
|
884
913
|
RpcPublicationReader
|
|
885
914
|
} from "@arcadiasystems/morse-sdk";
|
|
886
915
|
import { SuiGrpcClient } from "@mysten/sui/grpc";
|
|
916
|
+
import { SuiJsonRpcClient } from "@mysten/sui/jsonRpc";
|
|
887
917
|
async function buildReadContext(command) {
|
|
888
918
|
const opts = globalOptions(command);
|
|
889
919
|
const output = outputFor(command);
|
|
@@ -920,6 +950,20 @@ async function buildWriteContext(command) {
|
|
|
920
950
|
address
|
|
921
951
|
};
|
|
922
952
|
}
|
|
953
|
+
async function buildFilesReadContext(command) {
|
|
954
|
+
const base = await buildReadContext(command);
|
|
955
|
+
return {
|
|
956
|
+
...base,
|
|
957
|
+
filesReader: RpcFilesReader.fromMorseConfig(base.config, base.client)
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
async function buildAllowlistWriteContext(command) {
|
|
961
|
+
const base = await buildWriteContext(command);
|
|
962
|
+
return {
|
|
963
|
+
...base,
|
|
964
|
+
filesReader: RpcFilesReader.fromMorseConfig(base.config, base.client)
|
|
965
|
+
};
|
|
966
|
+
}
|
|
923
967
|
async function buildContentContext(command) {
|
|
924
968
|
const { base, keypair, address } = await buildSignedBase(command);
|
|
925
969
|
const network = base.settings.network;
|
|
@@ -935,74 +979,71 @@ async function buildEncryptContext(command) {
|
|
|
935
979
|
const seal = DefaultSealAdapter.fromMorseConfig(ctx.config, {}, ctx.client);
|
|
936
980
|
return { ...ctx, seal };
|
|
937
981
|
}
|
|
938
|
-
|
|
982
|
+
function walrusReadAdapter(base, network, viaAggregator) {
|
|
983
|
+
if (viaAggregator) {
|
|
984
|
+
return HttpAggregatorReadAdapter.fromMorseConfig(base.config, base.client);
|
|
985
|
+
}
|
|
986
|
+
return DefaultWalrusReadAdapter.fromConfig({
|
|
987
|
+
network,
|
|
988
|
+
suiClient: base.client
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
async function buildReadContentContext(command, opts = {}) {
|
|
939
992
|
const base = await buildReadContext(command);
|
|
940
993
|
const network = base.settings.network;
|
|
941
994
|
if (network === "localnet") {
|
|
942
995
|
throw new CliError("Walrus reads are not available on localnet. Use testnet or mainnet.", ExitCode.Usage);
|
|
943
996
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
}
|
|
948
|
-
return { ...base, walrusRead };
|
|
997
|
+
return {
|
|
998
|
+
...base,
|
|
999
|
+
walrusRead: walrusReadAdapter(base, network, Boolean(opts.viaAggregator))
|
|
1000
|
+
};
|
|
949
1001
|
}
|
|
950
|
-
async function buildDecryptContext(command) {
|
|
1002
|
+
async function buildDecryptContext(command, opts = {}) {
|
|
951
1003
|
const { base, keypair, address } = await buildSignedBase(command);
|
|
952
1004
|
const network = base.settings.network;
|
|
953
1005
|
if (network === "localnet") {
|
|
954
1006
|
throw new CliError("Seal decryption is not available on localnet. Use testnet or mainnet.", ExitCode.Usage);
|
|
955
1007
|
}
|
|
956
1008
|
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);
|
|
1009
|
+
return {
|
|
1010
|
+
...base,
|
|
1011
|
+
keypair,
|
|
1012
|
+
address,
|
|
1013
|
+
seal,
|
|
1014
|
+
walrusRead: walrusReadAdapter(base, network, Boolean(opts.viaAggregator))
|
|
1015
|
+
};
|
|
977
1016
|
}
|
|
978
|
-
function
|
|
979
|
-
const
|
|
980
|
-
|
|
981
|
-
|
|
1017
|
+
async function buildFileDownloadContext(command, opts = {}) {
|
|
1018
|
+
const base = await buildReadContext(command);
|
|
1019
|
+
const network = base.settings.network;
|
|
1020
|
+
if (network === "localnet") {
|
|
1021
|
+
throw new CliError("Walrus reads are not available on localnet. Use testnet or mainnet.", ExitCode.Usage);
|
|
982
1022
|
}
|
|
983
|
-
return
|
|
1023
|
+
return {
|
|
1024
|
+
...base,
|
|
1025
|
+
filesReader: RpcFilesReader.fromMorseConfig(base.config, base.client),
|
|
1026
|
+
walrusRead: walrusReadAdapter(base, network, Boolean(opts.viaAggregator)),
|
|
1027
|
+
seal: DefaultSealAdapter.fromMorseConfig(base.config, {}, base.client),
|
|
1028
|
+
unlockSigner: () => resolveSigner(base.settings.account, process.env, base.signal)
|
|
1029
|
+
};
|
|
984
1030
|
}
|
|
985
|
-
async function
|
|
986
|
-
|
|
987
|
-
|
|
1031
|
+
async function buildFileListContext(command, opts = {}) {
|
|
1032
|
+
const base = await buildReadContext(command);
|
|
1033
|
+
const originPackageId = base.config.filesEventOriginPackageId;
|
|
1034
|
+
if (originPackageId === undefined) {
|
|
1035
|
+
throw new CliError("File listing is unavailable on this network: no filesEventOriginPackageId in the config. Use testnet, or supply a config that sets it.", ExitCode.Usage);
|
|
988
1036
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
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.`);
|
|
1037
|
+
const events = new SuiJsonRpcClient({
|
|
1038
|
+
network: base.settings.network,
|
|
1039
|
+
url: opts.indexerUrl ?? base.config.rpcUrl
|
|
1040
|
+
});
|
|
1041
|
+
return {
|
|
1042
|
+
...base,
|
|
1043
|
+
filesReader: RpcFilesReader.fromMorseConfig(base.config, base.client),
|
|
1044
|
+
events,
|
|
1045
|
+
eventTypes: buildFilesEventTypes(originPackageId)
|
|
1046
|
+
};
|
|
1006
1047
|
}
|
|
1007
1048
|
|
|
1008
1049
|
// src/format/ids.ts
|
|
@@ -1072,6 +1113,52 @@ function renderEntryList(entries) {
|
|
|
1072
1113
|
return entries.map((entry) => `#${entry.id} ${entry.name} (${entry.revisions.length} revisions)`).join(`
|
|
1073
1114
|
`);
|
|
1074
1115
|
}
|
|
1116
|
+
function renderAllowlist(allowlist) {
|
|
1117
|
+
const members = allowlist.members.length === 0 ? "(none)" : allowlist.members.map((m) => ` ${m}`).join(`
|
|
1118
|
+
`);
|
|
1119
|
+
return [
|
|
1120
|
+
`${allowlist.name} (${shortId(allowlist.id)})`,
|
|
1121
|
+
`members (${allowlist.members.length}):`,
|
|
1122
|
+
members
|
|
1123
|
+
].join(`
|
|
1124
|
+
`);
|
|
1125
|
+
}
|
|
1126
|
+
function renderAllowlistCapList(caps) {
|
|
1127
|
+
if (caps.length === 0) {
|
|
1128
|
+
return "No allowlist caps held by this address.";
|
|
1129
|
+
}
|
|
1130
|
+
return caps.map((cap) => `${cap.id} allowlist ${cap.allowlistId}`).join(`
|
|
1131
|
+
`);
|
|
1132
|
+
}
|
|
1133
|
+
function renderFileList(items) {
|
|
1134
|
+
if (items.length === 0) {
|
|
1135
|
+
return "No files.";
|
|
1136
|
+
}
|
|
1137
|
+
const header = "id name size encrypted allowlist created";
|
|
1138
|
+
const rows = items.map((file) => {
|
|
1139
|
+
const allowlist = file.allowlistId === null ? "public" : shortId(file.allowlistId);
|
|
1140
|
+
const created = new Date(file.createdAtMs).toISOString().slice(0, 10);
|
|
1141
|
+
const encrypted = file.encrypted ? "yes" : "no";
|
|
1142
|
+
return `${shortId(file.id)} ${file.name} ${file.size} ${encrypted} ${allowlist} ${created}`;
|
|
1143
|
+
});
|
|
1144
|
+
return [header, ...rows].join(`
|
|
1145
|
+
`);
|
|
1146
|
+
}
|
|
1147
|
+
function renderEncryptedFile(file) {
|
|
1148
|
+
const lines = [
|
|
1149
|
+
`${file.name} (${shortId(file.id)})`,
|
|
1150
|
+
`contentType: ${file.contentType}`,
|
|
1151
|
+
`size: ${file.size}`,
|
|
1152
|
+
`encrypted: ${file.encrypted}`,
|
|
1153
|
+
`blobId: ${file.blobId}`,
|
|
1154
|
+
`owner: ${file.owner}`
|
|
1155
|
+
];
|
|
1156
|
+
if (file.allowlistId !== null) {
|
|
1157
|
+
lines.push(`allowlist: ${file.allowlistId}`);
|
|
1158
|
+
}
|
|
1159
|
+
return lines.join(`
|
|
1160
|
+
`);
|
|
1161
|
+
}
|
|
1075
1162
|
function headLabel(value) {
|
|
1076
1163
|
return value === null ? "none" : String(value);
|
|
1077
1164
|
}
|
|
@@ -1094,12 +1181,22 @@ function publisherCapOption(command) {
|
|
|
1094
1181
|
function ownerCapOption(command) {
|
|
1095
1182
|
return command.option("--owner-cap <id>", "OwnerCap ID (auto-resolved if omitted)");
|
|
1096
1183
|
}
|
|
1184
|
+
function allowlistOption(command) {
|
|
1185
|
+
return command.option("-a, --allowlist <id>", "Allowlist object id");
|
|
1186
|
+
}
|
|
1187
|
+
function allowlistCapOption(command) {
|
|
1188
|
+
return command.option("--cap <id>", "Allowlist admin Cap id (auto-resolved from owned caps if omitted)");
|
|
1189
|
+
}
|
|
1190
|
+
function viaAggregatorOption(command) {
|
|
1191
|
+
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.");
|
|
1192
|
+
}
|
|
1097
1193
|
function contentOptions(command) {
|
|
1098
1194
|
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
1195
|
}
|
|
1100
1196
|
|
|
1101
1197
|
// src/commands/resolve.ts
|
|
1102
1198
|
import {
|
|
1199
|
+
toAllowlistCapId,
|
|
1103
1200
|
toOwnerCapId,
|
|
1104
1201
|
toPublisherCapId
|
|
1105
1202
|
} from "@arcadiasystems/morse-sdk";
|
|
@@ -1142,6 +1239,25 @@ async function resolvePublisherCap(reader, address, publicationId, override, sig
|
|
|
1142
1239
|
} while (cursor !== undefined);
|
|
1143
1240
|
throw new CliError(`No PublisherCap for ${publicationId} held by ${address}. Pass --publisher-cap, or check that the active account holds one.`, ExitCode.NotFound);
|
|
1144
1241
|
}
|
|
1242
|
+
async function resolveAllowlistCap(filesReader, address, allowlistId, override, signal) {
|
|
1243
|
+
if (override !== undefined) {
|
|
1244
|
+
return toAllowlistCapId(override);
|
|
1245
|
+
}
|
|
1246
|
+
let cursor;
|
|
1247
|
+
do {
|
|
1248
|
+
const page = await filesReader.listAllowlistCapsOwnedBy(address, {
|
|
1249
|
+
limit: PAGE_LIMIT,
|
|
1250
|
+
signal,
|
|
1251
|
+
...cursor === undefined ? {} : { cursor }
|
|
1252
|
+
});
|
|
1253
|
+
const match = page.results.find((c) => c.allowlistId === allowlistId);
|
|
1254
|
+
if (match !== undefined) {
|
|
1255
|
+
return match.id;
|
|
1256
|
+
}
|
|
1257
|
+
cursor = page.nextCursor ?? undefined;
|
|
1258
|
+
} while (cursor !== undefined);
|
|
1259
|
+
throw new CliError(`No allowlist Cap for ${allowlistId} held by ${address}. Pass --cap, or check that the active account administers it.`, ExitCode.NotFound);
|
|
1260
|
+
}
|
|
1145
1261
|
|
|
1146
1262
|
// src/commands/shared.ts
|
|
1147
1263
|
import { StorageMode } from "@arcadiasystems/morse-sdk";
|
|
@@ -1162,6 +1278,12 @@ function parsePositiveInt(value, name) {
|
|
|
1162
1278
|
function parseLimit(value) {
|
|
1163
1279
|
return parsePositiveInt(value, "--limit");
|
|
1164
1280
|
}
|
|
1281
|
+
function parseByteSize(value, name) {
|
|
1282
|
+
if (!/^\d+$/.test(value)) {
|
|
1283
|
+
throw new UsageError(`${name} must be a non-negative integer, got "${value}".`);
|
|
1284
|
+
}
|
|
1285
|
+
return BigInt(value);
|
|
1286
|
+
}
|
|
1165
1287
|
function parseId(value, name) {
|
|
1166
1288
|
const id = Number(value);
|
|
1167
1289
|
if (!Number.isInteger(id) || id < 0) {
|
|
@@ -1170,219 +1292,428 @@ function parseId(value, name) {
|
|
|
1170
1292
|
return id;
|
|
1171
1293
|
}
|
|
1172
1294
|
|
|
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
|
-
});
|
|
1295
|
+
// src/commands/allowlist.ts
|
|
1296
|
+
function requireAllowlistId(value) {
|
|
1297
|
+
if (value === undefined) {
|
|
1298
|
+
throw new UsageError("Pass --allowlist <id>.");
|
|
1299
|
+
}
|
|
1300
|
+
return toAllowlistId(value);
|
|
1301
|
+
}
|
|
1302
|
+
async function runAllowlistCreate(ctx, options) {
|
|
1303
|
+
ctx.output.info(`Creating allowlist "${options.name}"...`);
|
|
1304
|
+
const result = await createAllowlist(ctx.adapter, ctx.config, {
|
|
1305
|
+
name: options.name,
|
|
1306
|
+
signal: ctx.signal
|
|
1194
1307
|
});
|
|
1195
|
-
const
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1308
|
+
const human = [
|
|
1309
|
+
`Created allowlist "${options.name}" (${result.allowlistId})`,
|
|
1310
|
+
` cap: ${result.capId}`,
|
|
1311
|
+
` tx: ${result.digest}`,
|
|
1312
|
+
"Save the cap id: it is the admin token for managing members."
|
|
1313
|
+
].join(`
|
|
1314
|
+
`);
|
|
1315
|
+
ctx.output.result(human, result);
|
|
1316
|
+
}
|
|
1317
|
+
async function runAllowlistAddMember(ctx, member, options) {
|
|
1318
|
+
const allowlistId = requireAllowlistId(options.allowlist);
|
|
1319
|
+
const memberAddress = toSuiAddress3(member);
|
|
1320
|
+
const capId = await resolveAllowlistCap(ctx.filesReader, ctx.address, allowlistId, options.cap, ctx.signal);
|
|
1321
|
+
const result = await addMember(ctx.adapter, ctx.config, {
|
|
1322
|
+
allowlistId,
|
|
1323
|
+
capId,
|
|
1324
|
+
member: memberAddress,
|
|
1325
|
+
signal: ctx.signal
|
|
1209
1326
|
});
|
|
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);
|
|
1327
|
+
ctx.output.result(`Added ${memberAddress} to allowlist ${shortId(allowlistId)}. (tx: ${result.digest})`, result);
|
|
1328
|
+
}
|
|
1329
|
+
async function runAllowlistRemoveMember(ctx, member, options) {
|
|
1330
|
+
const allowlistId = requireAllowlistId(options.allowlist);
|
|
1331
|
+
const memberAddress = toSuiAddress3(member);
|
|
1332
|
+
const capId = await resolveAllowlistCap(ctx.filesReader, ctx.address, allowlistId, options.cap, ctx.signal);
|
|
1333
|
+
const result = await removeMember(ctx.adapter, ctx.config, {
|
|
1334
|
+
allowlistId,
|
|
1335
|
+
capId,
|
|
1336
|
+
member: memberAddress,
|
|
1337
|
+
signal: ctx.signal
|
|
1228
1338
|
});
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1339
|
+
ctx.output.result(`Removed ${memberAddress} from allowlist ${shortId(allowlistId)}. (tx: ${result.digest})`, result);
|
|
1340
|
+
}
|
|
1341
|
+
async function runAllowlistTransferCap(ctx, recipient, options, gopts) {
|
|
1342
|
+
const allowlistId = requireAllowlistId(options.allowlist);
|
|
1343
|
+
const to = toSuiAddress3(recipient);
|
|
1344
|
+
const proceed = await confirm(`Transfer admin of allowlist ${shortId(allowlistId)} to ${to}? You will lose member-management rights.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
1345
|
+
if (!proceed) {
|
|
1346
|
+
cancelled();
|
|
1347
|
+
}
|
|
1348
|
+
ctx.output.info("Resolving allowlist Cap...");
|
|
1349
|
+
const capId = await resolveAllowlistCap(ctx.filesReader, ctx.address, allowlistId, options.cap, ctx.signal);
|
|
1350
|
+
const result = await transferAllowlistCap(ctx.adapter, ctx.config, {
|
|
1351
|
+
capId,
|
|
1352
|
+
recipient: to,
|
|
1353
|
+
signal: ctx.signal
|
|
1244
1354
|
});
|
|
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);
|
|
1355
|
+
ctx.output.result(`Transferred allowlist admin to ${to}. (tx: ${result.digest})`, result);
|
|
1356
|
+
}
|
|
1357
|
+
async function runAllowlistDelete(ctx, options, gopts) {
|
|
1358
|
+
const allowlistId = requireAllowlistId(options.allowlist);
|
|
1359
|
+
const proceed = await confirm(`Delete allowlist ${shortId(allowlistId)}? Files gated by it become permanently undecryptable.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
1360
|
+
if (!proceed) {
|
|
1361
|
+
cancelled();
|
|
1362
|
+
}
|
|
1363
|
+
ctx.output.info("Resolving allowlist Cap...");
|
|
1364
|
+
const capId = await resolveAllowlistCap(ctx.filesReader, ctx.address, allowlistId, options.cap, ctx.signal);
|
|
1365
|
+
const result = await deleteAllowlist(ctx.adapter, ctx.config, {
|
|
1366
|
+
allowlistId,
|
|
1367
|
+
capId,
|
|
1368
|
+
signal: ctx.signal
|
|
1262
1369
|
});
|
|
1370
|
+
ctx.output.result(`Deleted allowlist ${allowlistId}. (tx: ${result.digest})`, result);
|
|
1263
1371
|
}
|
|
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);
|
|
1372
|
+
async function runAllowlistGet(ctx, target) {
|
|
1373
|
+
const result = await ctx.filesReader.getAllowlist(toAllowlistId(target), ctx.signal);
|
|
1374
|
+
ctx.output.result(renderAllowlist(result), result);
|
|
1375
|
+
}
|
|
1376
|
+
async function runAllowlistListCaps(ctx, address, options) {
|
|
1377
|
+
const holder = address === undefined ? ctx.ownerAddress : toSuiAddress3(address);
|
|
1378
|
+
if (holder === undefined) {
|
|
1379
|
+
throw new UsageError("No address given and no active account. Pass an address or import an account.");
|
|
1380
|
+
}
|
|
1381
|
+
const page = await ctx.filesReader.listAllowlistCapsOwnedBy(holder, {
|
|
1382
|
+
signal: ctx.signal,
|
|
1383
|
+
...options.limit === undefined ? {} : { limit: parseLimit(options.limit) },
|
|
1384
|
+
...options.cursor === undefined ? {} : { cursor: options.cursor }
|
|
1295
1385
|
});
|
|
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);
|
|
1386
|
+
if (page.nextCursor !== null) {
|
|
1387
|
+
ctx.output.info(`More results: pass --cursor "${page.nextCursor}"`);
|
|
1388
|
+
}
|
|
1389
|
+
ctx.output.result(renderAllowlistCapList(page.results), {
|
|
1390
|
+
results: page.results,
|
|
1391
|
+
nextCursor: page.nextCursor
|
|
1317
1392
|
});
|
|
1318
1393
|
}
|
|
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 });
|
|
1394
|
+
function registerAllowlistCommands(program) {
|
|
1395
|
+
const allowlist = program.command("allowlist").description("Manage allowlists that gate decryption of encrypted files");
|
|
1396
|
+
allowlist.command("create").description("Create an allowlist and transfer its admin cap to you").requiredOption("-n, --name <name>", "Allowlist name").action(async (options, command) => {
|
|
1397
|
+
await runAllowlistCreate(await buildWriteContext(command), options);
|
|
1326
1398
|
});
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
defaultProfile: cfg.defaultProfile,
|
|
1331
|
-
profiles: cfg.profiles
|
|
1332
|
-
});
|
|
1399
|
+
const addMemberCmd = allowlist.command("add-member <member>").description("Add a wallet address to an allowlist");
|
|
1400
|
+
allowlistCapOption(allowlistOption(addMemberCmd)).action(async (member, options, command) => {
|
|
1401
|
+
await runAllowlistAddMember(await buildAllowlistWriteContext(command), member, options);
|
|
1333
1402
|
});
|
|
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
|
-
});
|
|
1403
|
+
const removeMemberCmd = allowlist.command("remove-member <member>").description("Remove a wallet address from an allowlist");
|
|
1404
|
+
allowlistCapOption(allowlistOption(removeMemberCmd)).action(async (member, options, command) => {
|
|
1405
|
+
await runAllowlistRemoveMember(await buildAllowlistWriteContext(command), member, options);
|
|
1352
1406
|
});
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
await saveConfig({ ...cfg, defaultProfile: name });
|
|
1357
|
-
outputFor(command).result(`Default profile set to "${name}".`, {
|
|
1358
|
-
defaultProfile: name
|
|
1359
|
-
});
|
|
1407
|
+
const transferCmd = allowlist.command("transfer-cap <recipient>").description("Transfer allowlist admin rights to another address");
|
|
1408
|
+
allowlistCapOption(allowlistOption(transferCmd)).action(async (recipient, options, command) => {
|
|
1409
|
+
await runAllowlistTransferCap(await buildAllowlistWriteContext(command), recipient, options, globalOptions(command));
|
|
1360
1410
|
});
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
await
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
profiles: Object.keys(rest)
|
|
1371
|
-
});
|
|
1411
|
+
const deleteCmd = allowlist.command("delete").description("Delete an allowlist (dependent files become undecryptable)");
|
|
1412
|
+
allowlistCapOption(allowlistOption(deleteCmd)).action(async (options, command) => {
|
|
1413
|
+
await runAllowlistDelete(await buildAllowlistWriteContext(command), options, globalOptions(command));
|
|
1414
|
+
});
|
|
1415
|
+
allowlist.command("get <allowlist>").description("Fetch an allowlist's name and members").action(async (target, _options, command) => {
|
|
1416
|
+
await runAllowlistGet(await buildFilesReadContext(command), target);
|
|
1417
|
+
});
|
|
1418
|
+
allowlist.command("list-caps [address]").description("List allowlist admin caps held by an address (default: the active account)").option("--limit <n>", "Maximum results per page").option("--cursor <cursor>", "Continue from a previous page cursor").action(async (address, options, command) => {
|
|
1419
|
+
await runAllowlistListCaps(await buildFilesReadContext(command), address, options);
|
|
1372
1420
|
});
|
|
1373
1421
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1422
|
+
|
|
1423
|
+
// src/commands/cap.ts
|
|
1424
|
+
import {
|
|
1425
|
+
destroyPublisherCap,
|
|
1426
|
+
issuePublisherCap,
|
|
1427
|
+
revokePublisherCap,
|
|
1428
|
+
toPublisherCapId as toPublisherCapId2,
|
|
1429
|
+
toSuiAddress as toSuiAddress4,
|
|
1430
|
+
transferPublisherCap
|
|
1431
|
+
} from "@arcadiasystems/morse-sdk";
|
|
1432
|
+
|
|
1433
|
+
// src/cli/target.ts
|
|
1434
|
+
import { toPublicationId } from "@arcadiasystems/morse-sdk";
|
|
1435
|
+
var OBJECT_ID = /^0x[0-9a-f]{1,64}$/i;
|
|
1436
|
+
var SLUG_PAGE_LIMIT = 50;
|
|
1437
|
+
async function resolvePublication(ctx, override) {
|
|
1438
|
+
const value = override ?? ctx.settings.publication;
|
|
1439
|
+
if (value === undefined) {
|
|
1440
|
+
throw new UsageError("No publication selected. Pass --publication <slug|id> or run `morse use <slug|id>`.");
|
|
1441
|
+
}
|
|
1442
|
+
if (OBJECT_ID.test(value)) {
|
|
1443
|
+
return toPublicationId(value.toLowerCase());
|
|
1444
|
+
}
|
|
1445
|
+
return resolveSlug(ctx, value);
|
|
1446
|
+
}
|
|
1447
|
+
function resolveCollection(ctx, override) {
|
|
1448
|
+
const value = override ?? ctx.settings.collection;
|
|
1449
|
+
if (value === undefined) {
|
|
1450
|
+
throw new UsageError("No collection selected. Pass --collection <name> or run `morse use <slug|id> <collection>`.");
|
|
1451
|
+
}
|
|
1452
|
+
return value;
|
|
1453
|
+
}
|
|
1454
|
+
async function resolveSlug(ctx, slug) {
|
|
1455
|
+
if (ctx.ownerAddress === undefined) {
|
|
1456
|
+
throw new UsageError(`Cannot resolve the slug "${slug}" without an active account. Pass the publication id, or select an account.`);
|
|
1457
|
+
}
|
|
1458
|
+
ctx.output.info(`Resolving slug "${slug}" among owned publications...`);
|
|
1459
|
+
let cursor;
|
|
1460
|
+
do {
|
|
1461
|
+
const page = await ctx.reader.listPublicationsOwnedBy(ctx.ownerAddress, {
|
|
1462
|
+
limit: SLUG_PAGE_LIMIT,
|
|
1463
|
+
signal: ctx.signal,
|
|
1464
|
+
...cursor === undefined ? {} : { cursor }
|
|
1465
|
+
});
|
|
1466
|
+
for (const owned of page.results) {
|
|
1467
|
+
const publication = await ctx.reader.getPublication(owned.publicationId, ctx.signal);
|
|
1468
|
+
if (publication.slug === slug) {
|
|
1469
|
+
return publication.id;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
cursor = page.nextCursor ?? undefined;
|
|
1473
|
+
} while (cursor !== undefined);
|
|
1474
|
+
throw new UsageError(`No publication with slug "${slug}" owned by the active account. Pass the publication id instead.`);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// src/commands/cap.ts
|
|
1478
|
+
async function runCapList(ctx, address, options) {
|
|
1479
|
+
const holder = address === undefined ? ctx.ownerAddress : toSuiAddress4(address);
|
|
1480
|
+
if (holder === undefined) {
|
|
1481
|
+
throw new UsageError("No address given and no active account. Pass an address or import an account.");
|
|
1482
|
+
}
|
|
1483
|
+
const page = await ctx.reader.listPublisherCapsOwnedBy(holder, {
|
|
1484
|
+
signal: ctx.signal,
|
|
1485
|
+
...options.limit === undefined ? {} : { limit: parseLimit(options.limit) },
|
|
1486
|
+
...options.cursor === undefined ? {} : { cursor: options.cursor }
|
|
1487
|
+
});
|
|
1488
|
+
if (page.nextCursor !== null) {
|
|
1489
|
+
ctx.output.info(`More results: pass --cursor "${page.nextCursor}"`);
|
|
1490
|
+
}
|
|
1491
|
+
ctx.output.result(renderPublisherCapList(page.results), {
|
|
1492
|
+
results: page.results,
|
|
1493
|
+
nextCursor: page.nextCursor
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
async function runCapIssue(ctx, holder, options) {
|
|
1497
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1498
|
+
const holderAddress = toSuiAddress4(holder);
|
|
1499
|
+
ctx.output.info("Resolving OwnerCap...");
|
|
1500
|
+
const ownerCapId = await resolveOwnerCap(ctx.reader, ctx.address, id, options.ownerCap, ctx.signal);
|
|
1501
|
+
const result = await issuePublisherCap(ctx.adapter, ctx.config, {
|
|
1502
|
+
publicationId: id,
|
|
1503
|
+
ownerCapId,
|
|
1504
|
+
holder: holderAddress,
|
|
1505
|
+
signal: ctx.signal
|
|
1506
|
+
});
|
|
1507
|
+
ctx.output.result(`Issued PublisherCap ${result.publisherCapId} to ${holderAddress}. (tx: ${result.digest})`, result);
|
|
1508
|
+
}
|
|
1509
|
+
async function runCapRevoke(ctx, publisherCapId, options, gopts) {
|
|
1510
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1511
|
+
const capId = toPublisherCapId2(publisherCapId);
|
|
1512
|
+
const proceed = await confirm(`Revoke PublisherCap ${capId}? It can no longer be used to write.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
1513
|
+
if (!proceed) {
|
|
1514
|
+
cancelled();
|
|
1515
|
+
}
|
|
1516
|
+
ctx.output.info("Resolving OwnerCap...");
|
|
1517
|
+
const ownerCapId = await resolveOwnerCap(ctx.reader, ctx.address, id, options.ownerCap, ctx.signal);
|
|
1518
|
+
const result = await revokePublisherCap(ctx.adapter, ctx.config, {
|
|
1519
|
+
publicationId: id,
|
|
1520
|
+
ownerCapId,
|
|
1521
|
+
publisherCapId: capId,
|
|
1522
|
+
signal: ctx.signal
|
|
1523
|
+
});
|
|
1524
|
+
ctx.output.result(`Revoked PublisherCap ${capId}. (tx: ${result.digest})`, result);
|
|
1525
|
+
}
|
|
1526
|
+
async function runCapDestroy(ctx, publisherCapId, options, gopts) {
|
|
1527
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1528
|
+
const capId = toPublisherCapId2(publisherCapId);
|
|
1529
|
+
const proceed = await confirm(`Destroy PublisherCap ${capId}? This is permanent.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
1530
|
+
if (!proceed) {
|
|
1531
|
+
cancelled();
|
|
1532
|
+
}
|
|
1533
|
+
const result = await destroyPublisherCap(ctx.adapter, ctx.config, {
|
|
1534
|
+
publicationId: id,
|
|
1535
|
+
publisherCapId: capId,
|
|
1536
|
+
signal: ctx.signal
|
|
1537
|
+
});
|
|
1538
|
+
ctx.output.result(`Destroyed PublisherCap ${capId}. (tx: ${result.digest})`, result);
|
|
1539
|
+
}
|
|
1540
|
+
async function runCapTransfer(ctx, publisherCapId, recipient, gopts) {
|
|
1541
|
+
const capId = toPublisherCapId2(publisherCapId);
|
|
1542
|
+
const to = toSuiAddress4(recipient);
|
|
1543
|
+
const proceed = await confirm(`Transfer PublisherCap ${capId} to ${to}?`, {
|
|
1544
|
+
assumeYes: Boolean(gopts.yes),
|
|
1545
|
+
signal: ctx.signal
|
|
1546
|
+
});
|
|
1547
|
+
if (!proceed) {
|
|
1548
|
+
cancelled();
|
|
1549
|
+
}
|
|
1550
|
+
const result = await transferPublisherCap(ctx.adapter, ctx.config, {
|
|
1551
|
+
publisherCapId: capId,
|
|
1552
|
+
recipient: to,
|
|
1553
|
+
signal: ctx.signal
|
|
1554
|
+
});
|
|
1555
|
+
ctx.output.result(`Transferred PublisherCap ${capId} to ${to}. (tx: ${result.digest})`, result);
|
|
1556
|
+
}
|
|
1557
|
+
function registerCapCommands(program) {
|
|
1558
|
+
const cap = program.command("cap").description("Manage PublisherCaps (write-access capabilities)");
|
|
1559
|
+
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) => {
|
|
1560
|
+
await runCapList(await buildReadContext(command), address, options);
|
|
1561
|
+
});
|
|
1562
|
+
const issue = cap.command("issue <holder>").description("Issue a PublisherCap bound to an address");
|
|
1563
|
+
publicationOption(ownerCapOption(issue)).action(async (holder, options, command) => {
|
|
1564
|
+
await runCapIssue(await buildWriteContext(command), holder, options);
|
|
1565
|
+
});
|
|
1566
|
+
const revoke = cap.command("revoke <publisherCapId>").description("Revoke a PublisherCap so it can no longer write");
|
|
1567
|
+
publicationOption(ownerCapOption(revoke)).action(async (publisherCapId, options, command) => {
|
|
1568
|
+
await runCapRevoke(await buildWriteContext(command), publisherCapId, options, globalOptions(command));
|
|
1569
|
+
});
|
|
1570
|
+
const destroy = cap.command("destroy <publisherCapId>").description("Destroy a PublisherCap held by the active account");
|
|
1571
|
+
publicationOption(destroy).action(async (publisherCapId, options, command) => {
|
|
1572
|
+
await runCapDestroy(await buildWriteContext(command), publisherCapId, options, globalOptions(command));
|
|
1573
|
+
});
|
|
1574
|
+
cap.command("transfer <publisherCapId> <recipient>").description("Transfer a PublisherCap object to another address").action(async (publisherCapId, recipient, _options, command) => {
|
|
1575
|
+
await runCapTransfer(await buildWriteContext(command), publisherCapId, recipient, globalOptions(command));
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// src/commands/collection.ts
|
|
1580
|
+
import { createCollection, deleteCollection } from "@arcadiasystems/morse-sdk";
|
|
1581
|
+
async function runCollectionList(ctx, options) {
|
|
1582
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1583
|
+
const publication = await ctx.reader.getPublication(id, ctx.signal);
|
|
1584
|
+
ctx.output.result(renderCollectionList(publication.collections), {
|
|
1585
|
+
publication: id,
|
|
1586
|
+
collections: publication.collections
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
async function runCollectionCreate(ctx, name, options, gopts) {
|
|
1590
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1591
|
+
const storageMode = coerceStorageMode(options.mode);
|
|
1592
|
+
const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
|
|
1593
|
+
ctx.output.info(`Creating collection "${name}" (${storageMode})...`);
|
|
1594
|
+
const result = await createCollection(ctx.adapter, ctx.config, {
|
|
1595
|
+
publicationId: id,
|
|
1596
|
+
publisherCapId,
|
|
1597
|
+
name,
|
|
1598
|
+
storageMode,
|
|
1599
|
+
signal: ctx.signal
|
|
1600
|
+
});
|
|
1601
|
+
await updateActiveProfile(gopts, { collection: name });
|
|
1602
|
+
ctx.output.result(`Created collection "${name}". Selected as the active collection. (tx: ${result.digest})`, result);
|
|
1603
|
+
}
|
|
1604
|
+
async function runCollectionDelete(ctx, name, options, gopts) {
|
|
1605
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1606
|
+
const proceed = await confirm(`Delete collection "${name}" from ${shortId(id)}? It must be empty.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
1607
|
+
if (!proceed) {
|
|
1608
|
+
cancelled();
|
|
1609
|
+
}
|
|
1610
|
+
const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
|
|
1611
|
+
const result = await deleteCollection(ctx.adapter, ctx.config, {
|
|
1612
|
+
publicationId: id,
|
|
1613
|
+
publisherCapId,
|
|
1614
|
+
name,
|
|
1615
|
+
signal: ctx.signal
|
|
1616
|
+
});
|
|
1617
|
+
if (ctx.settings.collection === name) {
|
|
1618
|
+
await updateActiveProfile(gopts, { collection: undefined });
|
|
1619
|
+
}
|
|
1620
|
+
ctx.output.result(`Deleted collection "${name}". (tx: ${result.digest})`, result);
|
|
1621
|
+
}
|
|
1622
|
+
function registerCollectionCommands(program) {
|
|
1623
|
+
const collection = program.command("collection").description("Manage collections within a publication");
|
|
1624
|
+
const list = collection.command("list").description("List the collections in a publication");
|
|
1625
|
+
publicationOption(list).action(async (options, command) => {
|
|
1626
|
+
await runCollectionList(await buildReadContext(command), options);
|
|
1627
|
+
});
|
|
1628
|
+
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");
|
|
1629
|
+
publicationOption(publisherCapOption(create)).action(async (name, options, command) => {
|
|
1630
|
+
await runCollectionCreate(await buildWriteContext(command), name, options, globalOptions(command));
|
|
1631
|
+
});
|
|
1632
|
+
const remove = collection.command("delete <name>").description("Delete an empty collection");
|
|
1633
|
+
publicationOption(publisherCapOption(remove)).action(async (name, options, command) => {
|
|
1634
|
+
await runCollectionDelete(await buildWriteContext(command), name, options, globalOptions(command));
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// src/commands/config.ts
|
|
1639
|
+
function runConfigPath(output) {
|
|
1640
|
+
const path = configFilePath();
|
|
1641
|
+
output.result(path, { path });
|
|
1642
|
+
}
|
|
1643
|
+
async function runConfigList(output) {
|
|
1644
|
+
const cfg = await loadConfig();
|
|
1645
|
+
output.result(renderProfiles(cfg), {
|
|
1646
|
+
defaultProfile: cfg.defaultProfile,
|
|
1647
|
+
profiles: cfg.profiles
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
async function runConfigAdd(output, name, options) {
|
|
1651
|
+
const network = coerceNetwork(options.network);
|
|
1652
|
+
const cfg = await loadConfig();
|
|
1653
|
+
const profiles = {
|
|
1654
|
+
...cfg.profiles,
|
|
1655
|
+
[name]: {
|
|
1656
|
+
network,
|
|
1657
|
+
...options.rpc === undefined ? {} : { rpc: options.rpc }
|
|
1658
|
+
}
|
|
1659
|
+
};
|
|
1660
|
+
const defaultProfile = Object.keys(cfg.profiles).length === 0 ? name : cfg.defaultProfile;
|
|
1661
|
+
await saveConfig({ ...cfg, profiles, defaultProfile });
|
|
1662
|
+
output.result(`Saved profile "${name}" (${network}).`, {
|
|
1663
|
+
profile: name,
|
|
1664
|
+
network,
|
|
1665
|
+
rpc: options.rpc,
|
|
1666
|
+
default: defaultProfile === name
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
async function runConfigUse(output, name) {
|
|
1670
|
+
const cfg = await loadConfig();
|
|
1671
|
+
requireProfile(cfg, name);
|
|
1672
|
+
await saveConfig({ ...cfg, defaultProfile: name });
|
|
1673
|
+
output.result(`Default profile set to "${name}".`, { defaultProfile: name });
|
|
1674
|
+
}
|
|
1675
|
+
async function runConfigRemove(output, name) {
|
|
1676
|
+
const cfg = await loadConfig();
|
|
1677
|
+
requireProfile(cfg, name);
|
|
1678
|
+
const { [name]: _removed, ...rest } = cfg.profiles;
|
|
1679
|
+
const defaultProfile = cfg.defaultProfile === name ? Object.keys(rest)[0] ?? "default" : cfg.defaultProfile;
|
|
1680
|
+
await saveConfig({ ...cfg, profiles: rest, defaultProfile });
|
|
1681
|
+
output.result(`Removed profile "${name}".`, {
|
|
1682
|
+
removed: name,
|
|
1683
|
+
defaultProfile,
|
|
1684
|
+
profiles: Object.keys(rest)
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
function registerConfigCommands(program) {
|
|
1688
|
+
const config = program.command("config").description("Manage profiles and CLI configuration");
|
|
1689
|
+
config.command("path").description("Print the config file path").action((_options, command) => {
|
|
1690
|
+
runConfigPath(outputFor(command));
|
|
1691
|
+
});
|
|
1692
|
+
config.command("list").description("List profiles and show the default").action(async (_options, command) => {
|
|
1693
|
+
await runConfigList(outputFor(command));
|
|
1694
|
+
});
|
|
1695
|
+
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) => {
|
|
1696
|
+
await runConfigAdd(outputFor(command), name, options);
|
|
1697
|
+
});
|
|
1698
|
+
config.command("use <name>").description("Set the default profile").action(async (name, _options, command) => {
|
|
1699
|
+
await runConfigUse(outputFor(command), name);
|
|
1700
|
+
});
|
|
1701
|
+
config.command("remove <name>").description("Delete a profile").action(async (name, _options, command) => {
|
|
1702
|
+
await runConfigRemove(outputFor(command), name);
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
function requireProfile(config, name) {
|
|
1706
|
+
if (!(name in config.profiles)) {
|
|
1707
|
+
throw new UsageError(`No profile named "${name}". Create it with: morse config add ${name} --network testnet`);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
function renderProfiles(config) {
|
|
1711
|
+
const names = Object.keys(config.profiles);
|
|
1712
|
+
if (names.length === 0) {
|
|
1713
|
+
return "No profiles configured. Add one with: morse config add <name> --network testnet";
|
|
1714
|
+
}
|
|
1715
|
+
return names.map((name) => {
|
|
1716
|
+
const profile = config.profiles[name];
|
|
1386
1717
|
if (profile === undefined) {
|
|
1387
1718
|
return name;
|
|
1388
1719
|
}
|
|
@@ -1462,333 +1793,690 @@ import {
|
|
|
1462
1793
|
import { SessionKey } from "@mysten/seal";
|
|
1463
1794
|
var SESSION_KEY_TTL_MIN = 10;
|
|
1464
1795
|
var SEAL_NONCE_BYTES = 16;
|
|
1796
|
+
async function runEntryAddEncrypted(ctx, name, options) {
|
|
1797
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1798
|
+
const collection = resolveCollection(ctx, options.collection);
|
|
1799
|
+
const epochs = parsePositiveInt(options.epochs, "--epochs");
|
|
1800
|
+
const plaintext = await readContentBytes(options);
|
|
1801
|
+
const contentType = resolveContentType(options.contentType, options.stdin ? undefined : options.file);
|
|
1802
|
+
const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
|
|
1803
|
+
const sealId = buildPublisherSealId(id, crypto.getRandomValues(new Uint8Array(SEAL_NONCE_BYTES)));
|
|
1804
|
+
ctx.output.info(`Encrypting and uploading ${plaintext.length} bytes...`);
|
|
1805
|
+
const result = await addEncryptedEntryFromBytes(ctx.adapter, ctx.config, {
|
|
1806
|
+
walrus: ctx.walrus,
|
|
1807
|
+
seal: ctx.seal,
|
|
1808
|
+
publicationId: id,
|
|
1809
|
+
publisherCapId,
|
|
1810
|
+
collectionName: collection,
|
|
1811
|
+
name,
|
|
1812
|
+
plaintext,
|
|
1813
|
+
contentType,
|
|
1814
|
+
sealId,
|
|
1815
|
+
upload: { epochs, deletable: true },
|
|
1816
|
+
signal: ctx.signal
|
|
1817
|
+
});
|
|
1818
|
+
ctx.output.result(`Added encrypted entry #${result.entryId} "${name}". (tx: ${result.digest})`, { ...result, sealId });
|
|
1819
|
+
}
|
|
1820
|
+
async function runEntryDecrypt(ctx, entryId, revisionId, options) {
|
|
1821
|
+
if (ctx.output.isJson && options.out === undefined) {
|
|
1822
|
+
throw new UsageError("Decrypting to stdout is not supported in --json mode; pass --out <path>.");
|
|
1823
|
+
}
|
|
1824
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1825
|
+
const collection = resolveCollection(ctx, options.collection);
|
|
1826
|
+
const numericEntryId = parseId(entryId, "entryId");
|
|
1827
|
+
const entryData = await ctx.reader.getEntry(id, collection, numericEntryId, ctx.signal);
|
|
1828
|
+
if (revisionId === undefined && entryData.revisions.length === 0) {
|
|
1829
|
+
throw new UsageError(`Entry #${numericEntryId} has no revisions.`);
|
|
1830
|
+
}
|
|
1831
|
+
const revisionIndex = revisionId === undefined ? entryData.revisions.length - 1 : parseId(revisionId, "revision");
|
|
1832
|
+
const revision = entryData.revisions[revisionIndex];
|
|
1833
|
+
if (revision === undefined) {
|
|
1834
|
+
throw new UsageError(`Entry #${numericEntryId} has no revision at index ${revisionIndex}.`);
|
|
1835
|
+
}
|
|
1836
|
+
if (!revision.encrypted || revision.sealId === null) {
|
|
1837
|
+
throw new UsageError(`Revision #${revisionIndex} of entry #${numericEntryId} is not encrypted.`);
|
|
1838
|
+
}
|
|
1839
|
+
const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
|
|
1840
|
+
ctx.output.info("Fetching ciphertext from Walrus...");
|
|
1841
|
+
const ciphertext = await ctx.walrusRead.readBlobRef(revision.blobRef, {
|
|
1842
|
+
signal: ctx.signal
|
|
1843
|
+
});
|
|
1844
|
+
ctx.output.info("Signing a SessionKey with the active account...");
|
|
1845
|
+
const sessionKey = await SessionKey.create({
|
|
1846
|
+
address: ctx.address,
|
|
1847
|
+
packageId: ctx.config.originalPackageId ?? ctx.config.packageId,
|
|
1848
|
+
ttlMin: SESSION_KEY_TTL_MIN,
|
|
1849
|
+
signer: ctx.keypair,
|
|
1850
|
+
suiClient: ctx.client
|
|
1851
|
+
});
|
|
1852
|
+
const plaintext = await ctx.seal.decrypt(ciphertext, {
|
|
1853
|
+
sessionKey,
|
|
1854
|
+
sealId: revision.sealId,
|
|
1855
|
+
publisherCapId
|
|
1856
|
+
});
|
|
1857
|
+
if (options.out !== undefined) {
|
|
1858
|
+
await writeFileContents(options.out, plaintext);
|
|
1859
|
+
ctx.output.result(`Wrote ${plaintext.length} bytes to ${options.out}.`, {
|
|
1860
|
+
entryId: numericEntryId,
|
|
1861
|
+
revisionId: revisionIndex,
|
|
1862
|
+
bytes: plaintext.length,
|
|
1863
|
+
contentType: revision.contentType,
|
|
1864
|
+
out: options.out
|
|
1865
|
+
});
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
process.stdout.write(plaintext);
|
|
1869
|
+
}
|
|
1465
1870
|
function registerEncryptedEntryCommands(entry) {
|
|
1466
1871
|
const add = entry.command("add-encrypted <name>").description("Encrypt a file or stdin with Seal and add it as a new entry");
|
|
1467
1872
|
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 });
|
|
1873
|
+
await runEntryAddEncrypted(await buildEncryptContext(command), name, options);
|
|
1491
1874
|
});
|
|
1492
1875
|
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);
|
|
1876
|
+
collectionOption(publicationOption(publisherCapOption(viaAggregatorOption(decrypt)))).action(async (entryId, revisionId, options, command) => {
|
|
1877
|
+
await runEntryDecrypt(await buildDecryptContext(command, {
|
|
1878
|
+
viaAggregator: options.viaAggregator
|
|
1879
|
+
}), entryId, revisionId, options);
|
|
1543
1880
|
});
|
|
1544
1881
|
}
|
|
1545
1882
|
|
|
1546
1883
|
// src/commands/entry.ts
|
|
1884
|
+
async function runEntryGet(ctx, entryId, options) {
|
|
1885
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1886
|
+
const collection = resolveCollection(ctx, options.collection);
|
|
1887
|
+
const result = await ctx.reader.getEntry(id, collection, parseId(entryId, "entryId"), ctx.signal);
|
|
1888
|
+
ctx.output.result(renderEntry(result), result);
|
|
1889
|
+
}
|
|
1890
|
+
async function runEntryList(ctx, options) {
|
|
1891
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1892
|
+
const collection = resolveCollection(ctx, options.collection);
|
|
1893
|
+
const page = await ctx.reader.listEntries(id, collection, {
|
|
1894
|
+
signal: ctx.signal,
|
|
1895
|
+
...options.limit === undefined ? {} : { limit: parseLimit(options.limit) },
|
|
1896
|
+
...options.cursor === undefined ? {} : { cursor: options.cursor }
|
|
1897
|
+
});
|
|
1898
|
+
if (page.nextCursor !== null) {
|
|
1899
|
+
ctx.output.info(`More results: pass --cursor "${page.nextCursor}"`);
|
|
1900
|
+
}
|
|
1901
|
+
ctx.output.result(renderEntryList(page.results), {
|
|
1902
|
+
results: page.results,
|
|
1903
|
+
nextCursor: page.nextCursor
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
async function runEntryScan(ctx, options) {
|
|
1907
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1908
|
+
const collection = resolveCollection(ctx, options.collection);
|
|
1909
|
+
const entries = [];
|
|
1910
|
+
for await (const item of ctx.reader.scanEntries(id, collection, {
|
|
1911
|
+
signal: ctx.signal
|
|
1912
|
+
})) {
|
|
1913
|
+
entries.push(item);
|
|
1914
|
+
}
|
|
1915
|
+
ctx.output.result(renderEntryList(entries), { results: entries });
|
|
1916
|
+
}
|
|
1917
|
+
async function runEntryAdd(ctx, name, options) {
|
|
1918
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1919
|
+
const collection = resolveCollection(ctx, options.collection);
|
|
1920
|
+
const epochs = parsePositiveInt(options.epochs, "--epochs");
|
|
1921
|
+
const bytes = await readContentBytes(options);
|
|
1922
|
+
const contentType = resolveContentType(options.contentType, options.stdin ? undefined : options.file);
|
|
1923
|
+
const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
|
|
1924
|
+
ctx.output.info(`Uploading ${bytes.length} bytes to Walrus...`);
|
|
1925
|
+
const result = await addEntryFromBytes(ctx.adapter, ctx.config, {
|
|
1926
|
+
walrus: ctx.walrus,
|
|
1927
|
+
publicationId: id,
|
|
1928
|
+
publisherCapId,
|
|
1929
|
+
collectionName: collection,
|
|
1930
|
+
name,
|
|
1931
|
+
bytes,
|
|
1932
|
+
contentType,
|
|
1933
|
+
upload: { epochs, deletable: true },
|
|
1934
|
+
signal: ctx.signal
|
|
1935
|
+
});
|
|
1936
|
+
const aggregator = ctx.config.walrusEndpoints.aggregator;
|
|
1937
|
+
const viewUrl = aggregator.length > 0 ? `${aggregator}/v1/blobs/${result.blobId}` : undefined;
|
|
1938
|
+
const human = viewUrl === undefined ? `Added entry #${result.entryId} "${name}". (tx: ${result.digest})` : `Added entry #${result.entryId} "${name}". (tx: ${result.digest})
|
|
1939
|
+
view: ${viewUrl}`;
|
|
1940
|
+
ctx.output.result(human, { ...result, viewUrl: viewUrl ?? null });
|
|
1941
|
+
}
|
|
1942
|
+
async function runEntryDelete(ctx, entryId, options, gopts) {
|
|
1943
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1944
|
+
const collection = resolveCollection(ctx, options.collection);
|
|
1945
|
+
const numericEntryId = parseId(entryId, "entryId");
|
|
1946
|
+
const proceed = await confirm(`Delete entry #${numericEntryId} from ${shortId(id)}/${collection}? This cannot be undone.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
1947
|
+
if (!proceed) {
|
|
1948
|
+
cancelled();
|
|
1949
|
+
}
|
|
1950
|
+
const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
|
|
1951
|
+
const result = await deleteEntry(ctx.adapter, ctx.config, {
|
|
1952
|
+
publicationId: id,
|
|
1953
|
+
publisherCapId,
|
|
1954
|
+
collectionName: collection,
|
|
1955
|
+
entryId: numericEntryId,
|
|
1956
|
+
signal: ctx.signal
|
|
1957
|
+
});
|
|
1958
|
+
ctx.output.result(`Deleted entry #${numericEntryId}. (tx: ${result.digest})`, result);
|
|
1959
|
+
}
|
|
1960
|
+
async function runEntryRead(ctx, entryId, revisionId, options) {
|
|
1961
|
+
if (ctx.output.isJson && options.out === undefined) {
|
|
1962
|
+
throw new UsageError("Reading content to stdout is not supported in --json mode; pass --out <path>.");
|
|
1963
|
+
}
|
|
1964
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
1965
|
+
const collection = resolveCollection(ctx, options.collection);
|
|
1966
|
+
const numericEntryId = parseId(entryId, "entryId");
|
|
1967
|
+
const entryData = await ctx.reader.getEntry(id, collection, numericEntryId, ctx.signal);
|
|
1968
|
+
if (revisionId === undefined && entryData.revisions.length === 0) {
|
|
1969
|
+
throw new UsageError(`Entry #${numericEntryId} has no revisions.`);
|
|
1970
|
+
}
|
|
1971
|
+
const revisionIndex = revisionId === undefined ? entryData.revisions.length - 1 : parseId(revisionId, "revision");
|
|
1972
|
+
const revision = entryData.revisions[revisionIndex];
|
|
1973
|
+
if (revision === undefined) {
|
|
1974
|
+
throw new UsageError(`Entry #${numericEntryId} has no revision at index ${revisionIndex}.`);
|
|
1975
|
+
}
|
|
1976
|
+
if (revision.encrypted) {
|
|
1977
|
+
throw new UsageError(`Revision #${revisionIndex} of entry #${numericEntryId} is encrypted; use \`morse entry decrypt\`.`);
|
|
1978
|
+
}
|
|
1979
|
+
ctx.output.info("Fetching content from Walrus...");
|
|
1980
|
+
const bytes = await ctx.walrusRead.readBlobRef(revision.blobRef, {
|
|
1981
|
+
signal: ctx.signal
|
|
1982
|
+
});
|
|
1983
|
+
if (options.out !== undefined) {
|
|
1984
|
+
await writeFileContents(options.out, bytes);
|
|
1985
|
+
ctx.output.result(`Wrote ${bytes.length} bytes to ${options.out}.`, {
|
|
1986
|
+
entryId: numericEntryId,
|
|
1987
|
+
revisionId: revisionIndex,
|
|
1988
|
+
bytes: bytes.length,
|
|
1989
|
+
contentType: revision.contentType,
|
|
1990
|
+
out: options.out
|
|
1991
|
+
});
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
process.stdout.write(bytes);
|
|
1995
|
+
}
|
|
1547
1996
|
function registerEntryCommands(program) {
|
|
1548
1997
|
const entry = program.command("entry").description("Read, add, and delete entries in a collection");
|
|
1549
1998
|
const get = entry.command("get <entryId>").description("Fetch a single entry");
|
|
1550
1999
|
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);
|
|
2000
|
+
await runEntryGet(await buildReadContext(command), entryId, options);
|
|
1556
2001
|
});
|
|
1557
2002
|
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
2003
|
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
|
-
});
|
|
2004
|
+
await runEntryList(await buildReadContext(command), options);
|
|
1574
2005
|
});
|
|
1575
2006
|
const scan = entry.command("scan").description("List every entry in a collection (auto-paginated)");
|
|
1576
2007
|
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 });
|
|
2008
|
+
await runEntryScan(await buildReadContext(command), options);
|
|
1587
2009
|
});
|
|
1588
2010
|
const add = entry.command("add <name>").description("Upload content from a file or stdin and add it as a new entry");
|
|
1589
2011
|
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 });
|
|
2012
|
+
await runEntryAdd(await buildContentContext(command), name, options);
|
|
1614
2013
|
});
|
|
1615
2014
|
const remove = entry.command("delete <entryId>").description("Delete an entry and its revisions");
|
|
1616
2015
|
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);
|
|
2016
|
+
await runEntryDelete(await buildWriteContext(command), entryId, options, globalOptions(command));
|
|
1634
2017
|
});
|
|
1635
2018
|
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
|
-
|
|
2019
|
+
collectionOption(publicationOption(viaAggregatorOption(read))).action(async (entryId, revisionId, options, command) => {
|
|
2020
|
+
await runEntryRead(await buildReadContentContext(command, {
|
|
2021
|
+
viaAggregator: options.viaAggregator
|
|
2022
|
+
}), entryId, revisionId, options);
|
|
2023
|
+
});
|
|
2024
|
+
registerEncryptedEntryCommands(entry);
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// src/commands/file.ts
|
|
2028
|
+
import {
|
|
2029
|
+
buildAllowlistSealId,
|
|
2030
|
+
createEncryptedFile,
|
|
2031
|
+
createPublicFile,
|
|
2032
|
+
deleteFile,
|
|
2033
|
+
reconcileFilesAccessibleBy,
|
|
2034
|
+
reconcileFilesOwnedBy,
|
|
2035
|
+
toAllowlistId as toAllowlistId2,
|
|
2036
|
+
toBlobObjectId,
|
|
2037
|
+
toEncryptedFileId,
|
|
2038
|
+
toSuiAddress as toSuiAddress5,
|
|
2039
|
+
toWalrusBlobId,
|
|
2040
|
+
transferFileOwnership,
|
|
2041
|
+
updateFileMetadata,
|
|
2042
|
+
uploadEncryptedFileFromBytes,
|
|
2043
|
+
uploadPublicFileFromBytes
|
|
2044
|
+
} from "@arcadiasystems/morse-sdk";
|
|
2045
|
+
import { SessionKey as SessionKey2 } from "@mysten/seal";
|
|
2046
|
+
|
|
2047
|
+
// src/cli/events.ts
|
|
2048
|
+
var PAGE_LIMIT2 = 50;
|
|
2049
|
+
async function fetchEventStream(client, eventType, signal) {
|
|
2050
|
+
const out = [];
|
|
2051
|
+
let cursor;
|
|
2052
|
+
for (;; ) {
|
|
2053
|
+
const page = await client.queryEvents({
|
|
2054
|
+
query: { MoveEventType: eventType },
|
|
2055
|
+
limit: PAGE_LIMIT2,
|
|
2056
|
+
order: "descending",
|
|
2057
|
+
...cursor === undefined ? {} : { cursor },
|
|
2058
|
+
...signal === undefined ? {} : { signal }
|
|
2059
|
+
});
|
|
2060
|
+
for (const event of page.data) {
|
|
2061
|
+
out.push({
|
|
2062
|
+
type: event.type,
|
|
2063
|
+
parsedJson: event.parsedJson,
|
|
2064
|
+
timestampMs: event.timestampMs
|
|
2065
|
+
});
|
|
1647
2066
|
}
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
if (revision === undefined) {
|
|
1651
|
-
throw new UsageError(`Entry #${numericEntryId} has no revision at index ${revisionIndex}.`);
|
|
2067
|
+
if (!page.hasNextPage || page.nextCursor === null || page.nextCursor === undefined) {
|
|
2068
|
+
break;
|
|
1652
2069
|
}
|
|
1653
|
-
|
|
1654
|
-
|
|
2070
|
+
cursor = page.nextCursor;
|
|
2071
|
+
}
|
|
2072
|
+
return out;
|
|
2073
|
+
}
|
|
2074
|
+
async function fetchEventStreams(client, eventTypes, signal) {
|
|
2075
|
+
const streams = await Promise.all(eventTypes.map((type) => fetchEventStream(client, type, signal)));
|
|
2076
|
+
return streams.flat();
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
// src/format/hex.ts
|
|
2080
|
+
var HEX = /^[0-9a-fA-F]+$/;
|
|
2081
|
+
function encodeHex(bytes) {
|
|
2082
|
+
let out = "0x";
|
|
2083
|
+
for (const byte of bytes) {
|
|
2084
|
+
out += byte.toString(16).padStart(2, "0");
|
|
2085
|
+
}
|
|
2086
|
+
return out;
|
|
2087
|
+
}
|
|
2088
|
+
function decodeHex(value) {
|
|
2089
|
+
const body = value.startsWith("0x") ? value.slice(2) : value;
|
|
2090
|
+
if (body.length === 0 || body.length % 2 !== 0 || !HEX.test(body)) {
|
|
2091
|
+
throw new UsageError(`Invalid hex value: expected an even number of hex digits, got ${JSON.stringify(value)}.`);
|
|
2092
|
+
}
|
|
2093
|
+
const bytes = new Uint8Array(body.length / 2);
|
|
2094
|
+
for (let i = 0;i < bytes.length; i += 1) {
|
|
2095
|
+
bytes[i] = Number.parseInt(body.slice(i * 2, i * 2 + 2), 16);
|
|
2096
|
+
}
|
|
2097
|
+
return bytes;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
// src/commands/file.ts
|
|
2101
|
+
var SESSION_KEY_TTL_MIN2 = 10;
|
|
2102
|
+
var SEAL_NONCE_BYTES2 = 16;
|
|
2103
|
+
function uploadProgress(output) {
|
|
2104
|
+
const labels = {
|
|
2105
|
+
encrypting: "Encrypting...",
|
|
2106
|
+
uploading: "Uploading to Walrus...",
|
|
2107
|
+
submitting: "Submitting transaction...",
|
|
2108
|
+
complete: ""
|
|
2109
|
+
};
|
|
2110
|
+
return (event) => {
|
|
2111
|
+
const label = labels[event.phase];
|
|
2112
|
+
if (label.length > 0) {
|
|
2113
|
+
output.info(label);
|
|
1655
2114
|
}
|
|
1656
|
-
|
|
1657
|
-
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
async function runFileGet(ctx, target) {
|
|
2118
|
+
const result = await ctx.filesReader.getEncryptedFile(toEncryptedFileId(target), ctx.signal);
|
|
2119
|
+
ctx.output.result(renderEncryptedFile(result), result);
|
|
2120
|
+
}
|
|
2121
|
+
async function runFileRegister(ctx, options) {
|
|
2122
|
+
if (options.allowlist === undefined && options.public !== true) {
|
|
2123
|
+
throw new UsageError("Pass --allowlist <id> to register an encrypted file, or --public for a world-readable one.");
|
|
2124
|
+
}
|
|
2125
|
+
const blobId = toWalrusBlobId(options.blobId);
|
|
2126
|
+
const size = parseByteSize(options.size, "--size");
|
|
2127
|
+
const blobObjectId = options.blobObjectId === undefined ? undefined : toBlobObjectId(options.blobObjectId);
|
|
2128
|
+
if (options.allowlist !== undefined) {
|
|
2129
|
+
const result2 = await createEncryptedFile(ctx.adapter, ctx.config, {
|
|
2130
|
+
allowlistId: toAllowlistId2(options.allowlist),
|
|
2131
|
+
blobId,
|
|
2132
|
+
...blobObjectId === undefined ? {} : { blobObjectId },
|
|
2133
|
+
name: options.name,
|
|
2134
|
+
contentType: options.contentType,
|
|
2135
|
+
size,
|
|
1658
2136
|
signal: ctx.signal
|
|
1659
2137
|
});
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
2138
|
+
ctx.output.result(`Registered encrypted file ${result2.fileId}. (tx: ${result2.digest})`, result2);
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
const result = await createPublicFile(ctx.adapter, ctx.config, {
|
|
2142
|
+
blobId,
|
|
2143
|
+
...blobObjectId === undefined ? {} : { blobObjectId },
|
|
2144
|
+
name: options.name,
|
|
2145
|
+
contentType: options.contentType,
|
|
2146
|
+
size,
|
|
2147
|
+
signal: ctx.signal
|
|
2148
|
+
});
|
|
2149
|
+
ctx.output.result(`Registered public file ${result.fileId}. (tx: ${result.digest})`, result);
|
|
2150
|
+
}
|
|
2151
|
+
async function runFileUpdate(ctx, target, options) {
|
|
2152
|
+
const result = await updateFileMetadata(ctx.adapter, ctx.config, {
|
|
2153
|
+
fileId: toEncryptedFileId(target),
|
|
2154
|
+
name: options.name,
|
|
2155
|
+
contentType: options.contentType,
|
|
2156
|
+
signal: ctx.signal
|
|
2157
|
+
});
|
|
2158
|
+
ctx.output.result(`Updated file ${shortId(target)} metadata. (tx: ${result.digest})`, result);
|
|
2159
|
+
}
|
|
2160
|
+
async function runFileTransferOwnership(ctx, target, newOwner, gopts) {
|
|
2161
|
+
const fileId = toEncryptedFileId(target);
|
|
2162
|
+
const to = toSuiAddress5(newOwner);
|
|
2163
|
+
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 });
|
|
2164
|
+
if (!proceed) {
|
|
2165
|
+
cancelled();
|
|
2166
|
+
}
|
|
2167
|
+
const result = await transferFileOwnership(ctx.adapter, ctx.config, {
|
|
2168
|
+
fileId,
|
|
2169
|
+
newOwner: to,
|
|
2170
|
+
signal: ctx.signal
|
|
2171
|
+
});
|
|
2172
|
+
ctx.output.result(`Transferred file ownership to ${to}. (tx: ${result.digest})`, result);
|
|
2173
|
+
}
|
|
2174
|
+
async function runFileDelete(ctx, target, gopts) {
|
|
2175
|
+
const fileId = toEncryptedFileId(target);
|
|
2176
|
+
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 });
|
|
2177
|
+
if (!proceed) {
|
|
2178
|
+
cancelled();
|
|
2179
|
+
}
|
|
2180
|
+
const result = await deleteFile(ctx.adapter, ctx.config, {
|
|
2181
|
+
fileId,
|
|
2182
|
+
signal: ctx.signal
|
|
2183
|
+
});
|
|
2184
|
+
ctx.output.result(`Deleted file ${fileId}. (tx: ${result.digest})`, result);
|
|
2185
|
+
}
|
|
2186
|
+
async function runFileUpload(ctx, path, options) {
|
|
2187
|
+
if (options.allowlist === undefined && options.public !== true) {
|
|
2188
|
+
throw new UsageError("Pass --allowlist <id> to upload an encrypted file, or --public for a world-readable one.");
|
|
2189
|
+
}
|
|
2190
|
+
const epochs = parsePositiveInt(options.epochs ?? "3", "--epochs");
|
|
2191
|
+
const bytes = await readContentBytes({ file: path });
|
|
2192
|
+
const contentType = resolveContentType(options.contentType, path);
|
|
2193
|
+
const upload = { epochs, deletable: true };
|
|
2194
|
+
const onProgress = uploadProgress(ctx.output);
|
|
2195
|
+
if (options.allowlist !== undefined) {
|
|
2196
|
+
const allowlistId = toAllowlistId2(options.allowlist);
|
|
2197
|
+
const sealId = buildAllowlistSealId(allowlistId, crypto.getRandomValues(new Uint8Array(SEAL_NONCE_BYTES2)));
|
|
2198
|
+
const result2 = await uploadEncryptedFileFromBytes(ctx.adapter, ctx.config, {
|
|
2199
|
+
walrus: ctx.walrus,
|
|
2200
|
+
seal: ctx.seal,
|
|
2201
|
+
allowlistId,
|
|
2202
|
+
sealId,
|
|
2203
|
+
plaintext: bytes,
|
|
2204
|
+
name: options.name,
|
|
2205
|
+
contentType,
|
|
2206
|
+
upload,
|
|
2207
|
+
signal: ctx.signal,
|
|
2208
|
+
onProgress
|
|
2209
|
+
});
|
|
2210
|
+
const sealIdHex = encodeHex(sealId);
|
|
2211
|
+
const human2 = [
|
|
2212
|
+
`Uploaded encrypted file ${result2.fileId}`,
|
|
2213
|
+
` blobId: ${result2.blobId}`,
|
|
2214
|
+
` sealId: ${sealIdHex}`,
|
|
2215
|
+
` tx: ${result2.digest}`,
|
|
2216
|
+
"Save the seal id: decrypting later needs it plus allowlist membership."
|
|
2217
|
+
].join(`
|
|
2218
|
+
`);
|
|
2219
|
+
ctx.output.result(human2, { ...result2, sealId: sealIdHex });
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
const result = await uploadPublicFileFromBytes(ctx.adapter, ctx.config, {
|
|
2223
|
+
walrus: ctx.walrus,
|
|
2224
|
+
bytes,
|
|
2225
|
+
name: options.name,
|
|
2226
|
+
contentType,
|
|
2227
|
+
upload,
|
|
2228
|
+
signal: ctx.signal,
|
|
2229
|
+
onProgress
|
|
2230
|
+
});
|
|
2231
|
+
const aggregator = ctx.config.walrusEndpoints.aggregator;
|
|
2232
|
+
const viewUrl = aggregator.length > 0 ? `${aggregator}/v1/blobs/${result.blobId}` : undefined;
|
|
2233
|
+
const human = viewUrl === undefined ? `Uploaded public file ${result.fileId}. (tx: ${result.digest})` : `Uploaded public file ${result.fileId}. (tx: ${result.digest})
|
|
2234
|
+
view: ${viewUrl}`;
|
|
2235
|
+
ctx.output.result(human, { ...result, viewUrl: viewUrl ?? null });
|
|
2236
|
+
}
|
|
2237
|
+
async function decryptFile(ctx, file, ciphertext, sealIdHex) {
|
|
2238
|
+
if (sealIdHex === undefined) {
|
|
2239
|
+
throw new UsageError("This file is encrypted; pass --seal-id <hex> (printed when the file was uploaded).");
|
|
2240
|
+
}
|
|
2241
|
+
if (file.allowlistId === null) {
|
|
2242
|
+
throw new UsageError(`File ${file.id} is marked encrypted but carries no allowlist; it cannot be decrypted.`);
|
|
2243
|
+
}
|
|
2244
|
+
const sealId = decodeHex(sealIdHex);
|
|
2245
|
+
const { keypair, address } = await ctx.unlockSigner();
|
|
2246
|
+
ctx.output.info("Signing a SessionKey with the active account...");
|
|
2247
|
+
const sessionKey = await SessionKey2.create({
|
|
2248
|
+
address,
|
|
2249
|
+
packageId: ctx.config.originalPackageId ?? ctx.config.packageId,
|
|
2250
|
+
ttlMin: SESSION_KEY_TTL_MIN2,
|
|
2251
|
+
signer: keypair,
|
|
2252
|
+
suiClient: ctx.client
|
|
2253
|
+
});
|
|
2254
|
+
return ctx.seal.decryptUnderAllowlist(ciphertext, {
|
|
2255
|
+
sealId,
|
|
2256
|
+
allowlistId: file.allowlistId,
|
|
2257
|
+
sessionKey
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
async function runFileDownload(ctx, target, options) {
|
|
2261
|
+
if (ctx.output.isJson && options.out === undefined) {
|
|
2262
|
+
throw new UsageError("Downloading content to stdout is not supported in --json mode; pass --out <path>.");
|
|
2263
|
+
}
|
|
2264
|
+
const file = await ctx.filesReader.getEncryptedFile(toEncryptedFileId(target), ctx.signal);
|
|
2265
|
+
ctx.output.info("Fetching content from Walrus...");
|
|
2266
|
+
const bytes = await ctx.walrusRead.readBlob(file.blobId, {
|
|
2267
|
+
signal: ctx.signal
|
|
2268
|
+
});
|
|
2269
|
+
let content = bytes;
|
|
2270
|
+
if (file.encrypted) {
|
|
2271
|
+
content = await decryptFile(ctx, file, bytes, options.sealId);
|
|
2272
|
+
}
|
|
2273
|
+
if (options.out !== undefined) {
|
|
2274
|
+
await writeFileContents(options.out, content);
|
|
2275
|
+
ctx.output.result(`Wrote ${content.length} bytes to ${options.out}.`, {
|
|
2276
|
+
fileId: file.id,
|
|
2277
|
+
name: file.name,
|
|
2278
|
+
contentType: file.contentType,
|
|
2279
|
+
bytes: content.length,
|
|
2280
|
+
out: options.out
|
|
2281
|
+
});
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
process.stdout.write(content);
|
|
2285
|
+
}
|
|
2286
|
+
async function runFileList(ctx, options) {
|
|
2287
|
+
const limit = options.limit === undefined ? undefined : parseLimit(options.limit);
|
|
2288
|
+
const address = options.address === undefined ? ctx.ownerAddress : toSuiAddress5(options.address);
|
|
2289
|
+
if (address === undefined) {
|
|
2290
|
+
throw new UsageError("No address given and no active account. Pass --address or import an account.");
|
|
2291
|
+
}
|
|
2292
|
+
const types = ctx.eventTypes;
|
|
2293
|
+
let summaries;
|
|
2294
|
+
if (options.accessible) {
|
|
2295
|
+
ctx.output.info("Fetching membership and file events...");
|
|
2296
|
+
const events = await fetchEventStreams(ctx.events, [
|
|
2297
|
+
types.MemberAdded,
|
|
2298
|
+
types.MemberRemoved,
|
|
2299
|
+
types.AllowlistDeleted,
|
|
2300
|
+
types.FileCreated,
|
|
2301
|
+
types.FileDeleted
|
|
2302
|
+
], ctx.signal);
|
|
2303
|
+
summaries = reconcileFilesAccessibleBy(events, address, types);
|
|
2304
|
+
} else {
|
|
2305
|
+
ctx.output.info("Fetching file events...");
|
|
2306
|
+
const events = await fetchEventStreams(ctx.events, [types.FileCreated, types.FileOwnershipTransferred, types.FileDeleted], ctx.signal);
|
|
2307
|
+
summaries = reconcileFilesOwnedBy(events, address, types);
|
|
2308
|
+
}
|
|
2309
|
+
const limited = limit === undefined ? summaries : summaries.slice(0, limit);
|
|
2310
|
+
let items = limited;
|
|
2311
|
+
if (options.hydrate) {
|
|
2312
|
+
ctx.output.info(`Hydrating ${limited.length} file(s)...`);
|
|
2313
|
+
const full = [];
|
|
2314
|
+
for (const summary of limited) {
|
|
2315
|
+
const record = await ctx.filesReader.getEncryptedFile(summary.id, ctx.signal);
|
|
2316
|
+
full.push({ ...record, kind: "full" });
|
|
1670
2317
|
}
|
|
1671
|
-
|
|
2318
|
+
items = full;
|
|
2319
|
+
}
|
|
2320
|
+
ctx.output.result(renderFileList(items), items);
|
|
2321
|
+
}
|
|
2322
|
+
function registerFileCommands(program) {
|
|
2323
|
+
const file = program.command("file").description("Upload, register, read, and manage files on Walrus");
|
|
2324
|
+
const upload = file.command("upload <path>").description("Encrypt (or not), upload to Walrus, and register a file").requiredOption("-n, --name <name>", "File name").option("--public", "Upload a world-readable (unencrypted) file").option("--content-type <mime>", "MIME content type (inferred from the path if omitted)").option("--epochs <n>", "Walrus storage epochs", "3");
|
|
2325
|
+
allowlistOption(upload).action(async (path, options, command) => {
|
|
2326
|
+
await runFileUpload(await buildEncryptContext(command), path, options);
|
|
2327
|
+
});
|
|
2328
|
+
file.command("list").description("List files owned by (or accessible to) an address, via event indexing").option("--address <addr>", "Address to query (default: the active account)").option("--accessible", "List files you can decrypt via allowlist membership, not just owned").option("--hydrate", "Fetch full records (adds blobId; one read per file)").option("--limit <n>", "Cap the number of results").option("--indexer-url <url>", "Event source URL (default: the RPC; uses suix_queryEvents, a deprecated Sui endpoint)").action(async (options, command) => {
|
|
2329
|
+
await runFileList(await buildFileListContext(command, {
|
|
2330
|
+
indexerUrl: options.indexerUrl
|
|
2331
|
+
}), options);
|
|
2332
|
+
});
|
|
2333
|
+
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)");
|
|
2334
|
+
viaAggregatorOption(download).action(async (target, options, command) => {
|
|
2335
|
+
await runFileDownload(await buildFileDownloadContext(command, {
|
|
2336
|
+
viaAggregator: options.viaAggregator
|
|
2337
|
+
}), target, options);
|
|
2338
|
+
});
|
|
2339
|
+
file.command("get <file>").description("Fetch a file's on-chain metadata").action(async (target, _options, command) => {
|
|
2340
|
+
await runFileGet(await buildFilesReadContext(command), target);
|
|
2341
|
+
});
|
|
2342
|
+
const register = file.command("register").description("Register on-chain metadata for a blob already on Walrus").requiredOption("--blob-id <id>", "Walrus content id").requiredOption("-n, --name <name>", "File name").requiredOption("--content-type <mime>", "MIME content type").requiredOption("--size <bytes>", "Plaintext byte length").option("--public", "Register a world-readable (unencrypted) file").option("--blob-object-id <id>", "On-chain Walrus Blob object id, if known");
|
|
2343
|
+
allowlistOption(register).action(async (options, command) => {
|
|
2344
|
+
await runFileRegister(await buildWriteContext(command), options);
|
|
2345
|
+
});
|
|
2346
|
+
file.command("update <file>").description("Update a file's name and content type (owner only)").requiredOption("-n, --name <name>", "New file name").requiredOption("--content-type <mime>", "New MIME content type").action(async (target, options, command) => {
|
|
2347
|
+
await runFileUpdate(await buildWriteContext(command), target, options);
|
|
2348
|
+
});
|
|
2349
|
+
file.command("transfer-ownership <file> <newOwner>").description("Transfer a file's metadata-mutation right (not decrypt access)").action(async (target, newOwner, _options, command) => {
|
|
2350
|
+
await runFileTransferOwnership(await buildWriteContext(command), target, newOwner, globalOptions(command));
|
|
2351
|
+
});
|
|
2352
|
+
file.command("delete <file>").description("Delete a file's metadata record (does not delete the blob)").action(async (target, _options, command) => {
|
|
2353
|
+
await runFileDelete(await buildWriteContext(command), target, globalOptions(command));
|
|
1672
2354
|
});
|
|
1673
|
-
registerEncryptedEntryCommands(entry);
|
|
1674
2355
|
}
|
|
1675
2356
|
|
|
1676
2357
|
// src/commands/publication.ts
|
|
1677
2358
|
import {
|
|
1678
2359
|
createPublication,
|
|
1679
2360
|
deletePublication,
|
|
1680
|
-
toSuiAddress as
|
|
2361
|
+
toSuiAddress as toSuiAddress6,
|
|
1681
2362
|
transferOwnership
|
|
1682
2363
|
} from "@arcadiasystems/morse-sdk";
|
|
2364
|
+
async function runPublicationGet(ctx, target) {
|
|
2365
|
+
const id = await resolvePublication(ctx, target);
|
|
2366
|
+
const result = await ctx.reader.getPublication(id, ctx.signal);
|
|
2367
|
+
ctx.output.result(renderPublication(result), result);
|
|
2368
|
+
}
|
|
2369
|
+
async function runPublicationList(ctx, address, options) {
|
|
2370
|
+
const owner = address === undefined ? ctx.ownerAddress : toSuiAddress6(address);
|
|
2371
|
+
if (owner === undefined) {
|
|
2372
|
+
throw new UsageError("No address given and no active account. Pass an address or import an account.");
|
|
2373
|
+
}
|
|
2374
|
+
const page = await ctx.reader.listPublicationsOwnedBy(owner, {
|
|
2375
|
+
signal: ctx.signal,
|
|
2376
|
+
...options.limit === undefined ? {} : { limit: parseLimit(options.limit) },
|
|
2377
|
+
...options.cursor === undefined ? {} : { cursor: options.cursor }
|
|
2378
|
+
});
|
|
2379
|
+
if (page.nextCursor !== null) {
|
|
2380
|
+
ctx.output.info(`More results: pass --cursor "${page.nextCursor}"`);
|
|
2381
|
+
}
|
|
2382
|
+
if (options.idsOnly) {
|
|
2383
|
+
ctx.output.result(renderPublicationList(page.results), {
|
|
2384
|
+
results: page.results,
|
|
2385
|
+
nextCursor: page.nextCursor
|
|
2386
|
+
});
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
2389
|
+
const enriched = [];
|
|
2390
|
+
for (const owned of page.results) {
|
|
2391
|
+
const pub = await ctx.reader.getPublication(owned.publicationId, ctx.signal);
|
|
2392
|
+
enriched.push({
|
|
2393
|
+
slug: pub.slug,
|
|
2394
|
+
name: pub.name,
|
|
2395
|
+
publicationId: owned.publicationId,
|
|
2396
|
+
ownerCapId: owned.ownerCapId
|
|
2397
|
+
});
|
|
2398
|
+
}
|
|
2399
|
+
ctx.output.result(renderEnrichedPublicationList(enriched), {
|
|
2400
|
+
results: enriched,
|
|
2401
|
+
nextCursor: page.nextCursor
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2404
|
+
async function runPublicationCreate(ctx, options, gopts) {
|
|
2405
|
+
ctx.output.info(`Creating "${options.name}"...`);
|
|
2406
|
+
const result = await createPublication(ctx.adapter, ctx.config, {
|
|
2407
|
+
name: options.name,
|
|
2408
|
+
slug: options.slug,
|
|
2409
|
+
signal: ctx.signal
|
|
2410
|
+
});
|
|
2411
|
+
await updateActiveProfile(gopts, {
|
|
2412
|
+
publication: result.publicationId,
|
|
2413
|
+
collection: undefined
|
|
2414
|
+
});
|
|
2415
|
+
const human = [
|
|
2416
|
+
`Created "${options.name}" (${result.publicationId})`,
|
|
2417
|
+
` ownerCap: ${result.ownerCapId}`,
|
|
2418
|
+
` publisherCap: ${result.publisherCapId}`,
|
|
2419
|
+
` tx: ${result.digest}`,
|
|
2420
|
+
"Selected as the active publication."
|
|
2421
|
+
].join(`
|
|
2422
|
+
`);
|
|
2423
|
+
ctx.output.result(human, result);
|
|
2424
|
+
}
|
|
2425
|
+
async function runPublicationDelete(ctx, target, options, gopts) {
|
|
2426
|
+
const id = await resolvePublication(ctx, target);
|
|
2427
|
+
const proceed = await confirm(`Delete publication ${shortId(id)}? This cannot be undone.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
2428
|
+
if (!proceed) {
|
|
2429
|
+
cancelled();
|
|
2430
|
+
}
|
|
2431
|
+
ctx.output.info("Resolving OwnerCap...");
|
|
2432
|
+
const ownerCapId = await resolveOwnerCap(ctx.reader, ctx.address, id, options.ownerCap, ctx.signal);
|
|
2433
|
+
ctx.output.info("Deleting...");
|
|
2434
|
+
const result = await deletePublication(ctx.reader, ctx.adapter, ctx.config, {
|
|
2435
|
+
publicationId: id,
|
|
2436
|
+
ownerCapId,
|
|
2437
|
+
signal: ctx.signal
|
|
2438
|
+
});
|
|
2439
|
+
if (ctx.settings.publication === id) {
|
|
2440
|
+
await updateActiveProfile(gopts, {
|
|
2441
|
+
publication: undefined,
|
|
2442
|
+
collection: undefined
|
|
2443
|
+
});
|
|
2444
|
+
}
|
|
2445
|
+
ctx.output.result(`Deleted ${id}. (tx: ${result.digest})`, result);
|
|
2446
|
+
}
|
|
2447
|
+
async function runPublicationTransferOwnership(ctx, recipient, options, gopts) {
|
|
2448
|
+
const id = await resolvePublication(ctx, options.publication);
|
|
2449
|
+
const to = toSuiAddress6(recipient);
|
|
2450
|
+
const proceed = await confirm(`Transfer ownership of ${shortId(id)} to ${to}? You will lose owner control.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
2451
|
+
if (!proceed) {
|
|
2452
|
+
cancelled();
|
|
2453
|
+
}
|
|
2454
|
+
ctx.output.info("Resolving OwnerCap...");
|
|
2455
|
+
const ownerCapId = await resolveOwnerCap(ctx.reader, ctx.address, id, options.ownerCap, ctx.signal);
|
|
2456
|
+
const result = await transferOwnership(ctx.adapter, ctx.config, {
|
|
2457
|
+
ownerCapId,
|
|
2458
|
+
recipient: to,
|
|
2459
|
+
signal: ctx.signal
|
|
2460
|
+
});
|
|
2461
|
+
ctx.output.result(`Transferred ownership of ${id} to ${to}. (tx: ${result.digest})`, result);
|
|
2462
|
+
}
|
|
1683
2463
|
function registerPublicationCommands(program) {
|
|
1684
2464
|
const publication = program.command("publication").alias("pub").description("Work with publications");
|
|
1685
2465
|
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);
|
|
2466
|
+
await runPublicationGet(await buildReadContext(command), target);
|
|
1690
2467
|
});
|
|
1691
2468
|
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
|
-
});
|
|
2469
|
+
await runPublicationList(await buildReadContext(command), address, options);
|
|
1726
2470
|
});
|
|
1727
2471
|
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);
|
|
2472
|
+
await runPublicationCreate(await buildWriteContext(command), options, globalOptions(command));
|
|
1748
2473
|
});
|
|
1749
2474
|
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);
|
|
2475
|
+
await runPublicationDelete(await buildWriteContext(command), target, options, globalOptions(command));
|
|
1774
2476
|
});
|
|
1775
2477
|
const transfer = publication.command("transfer-ownership <recipient>").description("Transfer a publication's OwnerCap to another address");
|
|
1776
2478
|
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);
|
|
2479
|
+
await runPublicationTransferOwnership(await buildWriteContext(command), recipient, options, globalOptions(command));
|
|
1792
2480
|
});
|
|
1793
2481
|
}
|
|
1794
2482
|
|
|
@@ -1801,14 +2489,12 @@ import {
|
|
|
1801
2489
|
function revisionOptions(command) {
|
|
1802
2490
|
return collectionOption(publicationOption(publisherCapOption(contentOptions(command))));
|
|
1803
2491
|
}
|
|
1804
|
-
async function resolveTarget(
|
|
1805
|
-
const ctx = await buildContentContext(command);
|
|
2492
|
+
async function resolveTarget(ctx, options) {
|
|
1806
2493
|
const publicationId = await resolvePublication(ctx, options.publication);
|
|
1807
2494
|
const collection = resolveCollection(ctx, options.collection);
|
|
1808
|
-
return {
|
|
2495
|
+
return { publicationId, collection };
|
|
1809
2496
|
}
|
|
1810
|
-
async function prepareContent(
|
|
1811
|
-
const { ctx, publicationId } = target;
|
|
2497
|
+
async function prepareContent(ctx, publicationId, options) {
|
|
1812
2498
|
const epochs = parsePositiveInt(options.epochs, "--epochs");
|
|
1813
2499
|
const bytes = await readContentBytes(options);
|
|
1814
2500
|
const contentType = resolveContentType(options.contentType, options.stdin ? undefined : options.file);
|
|
@@ -1821,113 +2507,124 @@ async function prepareContent(target, options) {
|
|
|
1821
2507
|
ctx.output.info(`Blob uploaded (${upload.blobObjectId}). Submitting transaction...`);
|
|
1822
2508
|
return { blobObjectId: upload.blobObjectId, contentType, publisherCapId };
|
|
1823
2509
|
}
|
|
2510
|
+
async function runRevisionPublishDirect(ctx, entryId, options) {
|
|
2511
|
+
const { publicationId, collection } = await resolveTarget(ctx, options);
|
|
2512
|
+
const numericEntryId = parseId(entryId, "entryId");
|
|
2513
|
+
const prepared = await prepareContent(ctx, publicationId, options);
|
|
2514
|
+
const result = await publishDirect(ctx.adapter, ctx.config, {
|
|
2515
|
+
publicationId,
|
|
2516
|
+
publisherCapId: prepared.publisherCapId,
|
|
2517
|
+
collectionName: collection,
|
|
2518
|
+
entryId: numericEntryId,
|
|
2519
|
+
blobObjectId: prepared.blobObjectId,
|
|
2520
|
+
contentType: prepared.contentType,
|
|
2521
|
+
signal: ctx.signal
|
|
2522
|
+
});
|
|
2523
|
+
ctx.output.result(`Published revision #${result.revisionId} on entry #${numericEntryId}. (tx: ${result.digest})`, result);
|
|
2524
|
+
}
|
|
2525
|
+
async function runRevisionAppendDraft(ctx, entryId, options) {
|
|
2526
|
+
const { publicationId, collection } = await resolveTarget(ctx, options);
|
|
2527
|
+
const numericEntryId = parseId(entryId, "entryId");
|
|
2528
|
+
const prepared = await prepareContent(ctx, publicationId, options);
|
|
2529
|
+
const result = await appendDraftRevision(ctx.adapter, ctx.config, {
|
|
2530
|
+
publicationId,
|
|
2531
|
+
publisherCapId: prepared.publisherCapId,
|
|
2532
|
+
collectionName: collection,
|
|
2533
|
+
entryId: numericEntryId,
|
|
2534
|
+
blobObjectId: prepared.blobObjectId,
|
|
2535
|
+
contentType: prepared.contentType,
|
|
2536
|
+
signal: ctx.signal
|
|
2537
|
+
});
|
|
2538
|
+
ctx.output.result(`Appended draft revision #${result.revisionId} on entry #${numericEntryId}. (tx: ${result.digest})`, result);
|
|
2539
|
+
}
|
|
2540
|
+
async function runRevisionPublishFromDraft(ctx, entryId, draftRevisionId, options) {
|
|
2541
|
+
const { publicationId, collection } = await resolveTarget(ctx, options);
|
|
2542
|
+
const numericEntryId = parseId(entryId, "entryId");
|
|
2543
|
+
const draftId = parseId(draftRevisionId, "draftRevisionId");
|
|
2544
|
+
const prepared = await prepareContent(ctx, publicationId, options);
|
|
2545
|
+
const result = await publishFromDraft(ctx.adapter, ctx.config, {
|
|
2546
|
+
publicationId,
|
|
2547
|
+
publisherCapId: prepared.publisherCapId,
|
|
2548
|
+
collectionName: collection,
|
|
2549
|
+
entryId: numericEntryId,
|
|
2550
|
+
draftRevisionId: draftId,
|
|
2551
|
+
blobObjectId: prepared.blobObjectId,
|
|
2552
|
+
contentType: prepared.contentType,
|
|
2553
|
+
signal: ctx.signal
|
|
2554
|
+
});
|
|
2555
|
+
ctx.output.result(`Published revision #${result.revisionId} from draft #${draftId} on entry #${numericEntryId}. (tx: ${result.digest})`, result);
|
|
2556
|
+
}
|
|
1824
2557
|
function registerRevisionCommands(program) {
|
|
1825
2558
|
const revision = program.command("revision").description("Append or publish revisions on an entry");
|
|
1826
2559
|
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);
|
|
2560
|
+
await runRevisionPublishDirect(await buildContentContext(command), entryId, options);
|
|
1840
2561
|
});
|
|
1841
2562
|
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);
|
|
2563
|
+
await runRevisionAppendDraft(await buildContentContext(command), entryId, options);
|
|
1855
2564
|
});
|
|
1856
2565
|
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);
|
|
2566
|
+
await runRevisionPublishFromDraft(await buildContentContext(command), entryId, draftRevisionId, options);
|
|
1872
2567
|
});
|
|
1873
2568
|
}
|
|
1874
2569
|
|
|
1875
2570
|
// src/commands/use.ts
|
|
2571
|
+
async function runUse(output, gopts, publication, collection, options, makeContext) {
|
|
2572
|
+
if (options.clear) {
|
|
2573
|
+
const profile2 = await updateActiveProfile(gopts, {
|
|
2574
|
+
publication: undefined,
|
|
2575
|
+
collection: undefined
|
|
2576
|
+
});
|
|
2577
|
+
output.result(`Cleared the active publication and collection (profile "${profile2}").`, { profile: profile2, publication: null, collection: null });
|
|
2578
|
+
return;
|
|
2579
|
+
}
|
|
2580
|
+
if (publication === undefined) {
|
|
2581
|
+
throw new UsageError("Provide a publication (slug or id), or pass --clear.");
|
|
2582
|
+
}
|
|
2583
|
+
const ctx = await makeContext();
|
|
2584
|
+
const publicationId = await resolvePublication(ctx, publication);
|
|
2585
|
+
let collectionName;
|
|
2586
|
+
if (collection !== undefined) {
|
|
2587
|
+
const pub = await ctx.reader.getPublication(publicationId, ctx.signal);
|
|
2588
|
+
if (!pub.collections.some((c) => c.name === collection)) {
|
|
2589
|
+
throw new UsageError(`Publication ${publicationId} has no collection "${collection}".`);
|
|
2590
|
+
}
|
|
2591
|
+
collectionName = collection;
|
|
2592
|
+
}
|
|
2593
|
+
const profile = await updateActiveProfile(gopts, {
|
|
2594
|
+
publication: publicationId,
|
|
2595
|
+
collection: collectionName
|
|
2596
|
+
});
|
|
2597
|
+
const human = collectionName === undefined ? `Active publication set to ${publicationId} (profile "${profile}").` : `Active publication set to ${publicationId}, collection "${collectionName}" (profile "${profile}").`;
|
|
2598
|
+
output.result(human, {
|
|
2599
|
+
profile,
|
|
2600
|
+
publication: publicationId,
|
|
2601
|
+
collection: collectionName ?? null
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
function runStatus(output, settings) {
|
|
2605
|
+
const account = accountAddress(settings.account);
|
|
2606
|
+
const human = [
|
|
2607
|
+
`profile: ${settings.profileName}`,
|
|
2608
|
+
`network: ${settings.network}`,
|
|
2609
|
+
`account: ${account ?? "(none)"}`,
|
|
2610
|
+
`publication: ${settings.publication ?? "(none)"}`,
|
|
2611
|
+
`collection: ${settings.collection ?? "(none)"}`
|
|
2612
|
+
].join(`
|
|
2613
|
+
`);
|
|
2614
|
+
output.result(human, {
|
|
2615
|
+
profile: settings.profileName,
|
|
2616
|
+
network: settings.network,
|
|
2617
|
+
account: account ?? null,
|
|
2618
|
+
publication: settings.publication ?? null,
|
|
2619
|
+
collection: settings.collection ?? null
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
1876
2622
|
function registerContextCommands(program) {
|
|
1877
2623
|
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
|
-
});
|
|
2624
|
+
await runUse(outputFor(command), globalOptions(command), publication, collection, options, () => buildReadContext(command));
|
|
1911
2625
|
});
|
|
1912
2626
|
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
|
-
});
|
|
2627
|
+
runStatus(outputFor(command), resolveSettings(globalOptions(command), await loadConfig()));
|
|
1931
2628
|
});
|
|
1932
2629
|
}
|
|
1933
2630
|
|
|
@@ -1941,6 +2638,8 @@ function registerCommands(program) {
|
|
|
1941
2638
|
registerEntryCommands(program);
|
|
1942
2639
|
registerRevisionCommands(program);
|
|
1943
2640
|
registerCapCommands(program);
|
|
2641
|
+
registerAllowlistCommands(program);
|
|
2642
|
+
registerFileCommands(program);
|
|
1944
2643
|
}
|
|
1945
2644
|
|
|
1946
2645
|
// src/index.ts
|