@arcadiasystems/morse-cli 0.1.0 → 0.2.0

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