@unicitylabs/sphere-sdk 0.17.2-dev.2 → 0.17.2

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.
Files changed (39) hide show
  1. package/dist/connect/index.cjs +7 -4
  2. package/dist/connect/index.cjs.map +1 -1
  3. package/dist/connect/index.d.cts +4 -0
  4. package/dist/connect/index.d.ts +4 -0
  5. package/dist/connect/index.js +7 -4
  6. package/dist/connect/index.js.map +1 -1
  7. package/dist/core/index.cjs +69 -22
  8. package/dist/core/index.cjs.map +1 -1
  9. package/dist/core/index.d.cts +2 -0
  10. package/dist/core/index.d.ts +2 -0
  11. package/dist/core/index.js +69 -22
  12. package/dist/core/index.js.map +1 -1
  13. package/dist/impl/browser/connect/index.cjs +1 -1
  14. package/dist/impl/browser/connect/index.cjs.map +1 -1
  15. package/dist/impl/browser/connect/index.js +1 -1
  16. package/dist/impl/browser/connect/index.js.map +1 -1
  17. package/dist/impl/wallet-api-v2/index.cjs +3 -0
  18. package/dist/impl/wallet-api-v2/index.cjs.map +1 -1
  19. package/dist/impl/wallet-api-v2/index.js +3 -0
  20. package/dist/impl/wallet-api-v2/index.js.map +1 -1
  21. package/dist/index.cjs +88 -22
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +22 -1
  24. package/dist/index.d.ts +22 -1
  25. package/dist/index.js +85 -22
  26. package/dist/index.js.map +1 -1
  27. package/dist/modules/payments-v2/index.cjs +40 -6
  28. package/dist/modules/payments-v2/index.cjs.map +1 -1
  29. package/dist/modules/payments-v2/index.d.cts +22 -1
  30. package/dist/modules/payments-v2/index.d.ts +22 -1
  31. package/dist/modules/payments-v2/index.js +37 -6
  32. package/dist/modules/payments-v2/index.js.map +1 -1
  33. package/dist/token-engine/index.cjs +67 -21
  34. package/dist/token-engine/index.cjs.map +1 -1
  35. package/dist/token-engine/index.d.cts +28 -4
  36. package/dist/token-engine/index.d.ts +28 -4
  37. package/dist/token-engine/index.js +64 -21
  38. package/dist/token-engine/index.js.map +1 -1
  39. package/package.json +1 -1
@@ -112,6 +112,8 @@ interface NftMetadata {
112
112
  readonly external_url: string | null;
113
113
  readonly attributes: readonly NftAttribute[];
114
114
  readonly collection: string | null;
115
+ /** The collection the item claims: 1–64 bytes as even-length hex (decoded lowercase). Never verified here. */
116
+ readonly collection_id: string | null;
115
117
  }
116
118
  type NftContent = NftMetadata | NftMedia | NftLink;
117
119
  type NftSignatureStatus = 'unsigned' | 'valid' | 'invalid';
@@ -112,6 +112,8 @@ interface NftMetadata {
112
112
  readonly external_url: string | null;
113
113
  readonly attributes: readonly NftAttribute[];
114
114
  readonly collection: string | null;
115
+ /** The collection the item claims: 1–64 bytes as even-length hex (decoded lowercase). Never verified here. */
116
+ readonly collection_id: string | null;
115
117
  }
116
118
  type NftContent = NftMetadata | NftMedia | NftLink;
117
119
  type NftSignatureStatus = 'unsigned' | 'valid' | 'invalid';
@@ -4539,6 +4539,26 @@ var GroupVisibility = {
4539
4539
  PRIVATE: "PRIVATE"
4540
4540
  };
4541
4541
 
4542
+ // modules/groupchat/group-id.ts
4543
+ var MAX_SLUG_LENGTH = 24;
4544
+ var RANDOM_BYTES = 8;
4545
+ function groupIdSlug(name) {
4546
+ const slug = name.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase().replace(/['\u2019]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
4547
+ if (slug.length <= MAX_SLUG_LENGTH) return slug;
4548
+ const head = slug.slice(0, MAX_SLUG_LENGTH + 1);
4549
+ const hyphen = head.lastIndexOf("-");
4550
+ return hyphen > 0 ? head.slice(0, hyphen) : slug.slice(0, MAX_SLUG_LENGTH);
4551
+ }
4552
+ function newGroupId(name) {
4553
+ const crypto2 = globalThis.crypto;
4554
+ if (typeof crypto2?.getRandomValues !== "function") {
4555
+ throw new SphereError("Creating a group requires crypto.getRandomValues", "INVALID_CONFIG");
4556
+ }
4557
+ const bytes = crypto2.getRandomValues(new Uint8Array(RANDOM_BYTES));
4558
+ const random = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
4559
+ return `${groupIdSlug(name) || "group"}-${random}`;
4560
+ }
4561
+
4542
4562
  // modules/groupchat/GroupChatModule.ts
4543
4563
  function createNip29Filter(data) {
4544
4564
  return new Filter3(data);
@@ -5150,9 +5170,9 @@ var GroupChatModule = class {
5150
5170
  if (!this.client) return null;
5151
5171
  const creatorPubkey = this.getMyPublicKey();
5152
5172
  if (!creatorPubkey) return null;
5153
- const proposedGroupId = options.name.toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 20) || this.randomId();
5154
5173
  try {
5155
5174
  const isPrivate = options.visibility === GroupVisibility.PRIVATE;
5175
+ const proposedGroupId = newGroupId(options.name);
5156
5176
  const eventId = await this.client.createAndPublishEvent({
5157
5177
  kind: NIP29_KINDS.CREATE_GROUP,
5158
5178
  tags: [["h", proposedGroupId]],
@@ -9784,7 +9804,8 @@ var NFT_MEDIA_TAG = 39053n;
9784
9804
  var NFT_LINK_TAG = 39054n;
9785
9805
  var NFT_SIGNED_TAG = 39055n;
9786
9806
  var NFT_FORMAT_VERSION = 1n;
9787
- var METADATA_ARITY = 8;
9807
+ var NFT_DOCUMENT_MEDIA_TYPE = "application/vnd.unicity.nft+cbor";
9808
+ var METADATA_ARITY = 9;
9788
9809
  var MEDIA_ARITY = 3;
9789
9810
  var LINK_ARITY = 4;
9790
9811
  var SIGNED_ARITY = 4;
@@ -9792,12 +9813,15 @@ var CREATOR_BYTES = 33;
9792
9813
  var SIGNATURE_BYTES = 65;
9793
9814
  var MAX_URI_LENGTH = 2048;
9794
9815
  var LINK_SCHEMES = ["https://", "ipfs://", "ar://"];
9795
- var SIGNED_DOMAIN = "NftSigned";
9816
+ var SIGNED_DOMAIN = new TextEncoder().encode("UNICITY_NFT_SIGNED");
9796
9817
  var MEDIA_TYPE_PATTERN = /^[a-z0-9][a-z0-9!#$&^_.+-]{0,126}\/[a-z0-9][a-z0-9!#$&^_.+-]{0,126}$/;
9797
9818
  var SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/i;
9819
+ var COLLECTION_ID_PATTERN = /^(?:[0-9a-f]{2}){1,64}$/i;
9798
9820
  var LONE_SURROGATE = /\p{Cs}/u;
9799
9821
  var CBOR_MAJOR_TYPE_MASK = 224;
9800
9822
  var ATTRIBUTE_VALUE_RULE = "must be non-empty text or an integer within \xB1(2^53 \u2212 1) \u2014 write decimals and larger numbers as text";
9823
+ var COLLECTION_ID_RULE = "must be null or 1\u201364 bytes as even-length hex (2\u2013128 characters)";
9824
+ var DOCUMENT_AS_MEDIA_RULE = `must not be ${NFT_DOCUMENT_MEDIA_TYPE}: a document link is a token's content, never media`;
9801
9825
  var UTF8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
9802
9826
  function refuse(field, rule, cause) {
9803
9827
  return new SphereError(`Invalid NFT ${field}: ${rule}`, "VALIDATION_ERROR", cause);
@@ -9809,6 +9833,7 @@ var fieldName = (prefix, name) => prefix === "" ? name : `${prefix}.${name}`;
9809
9833
  var isText = (value) => typeof value === "string" && value.length > 0 && !LONE_SURROGATE.test(value);
9810
9834
  var isBytes3 = (value, length) => value instanceof Uint8Array && (length === void 0 ? value.length > 0 : value.length === length);
9811
9835
  var isMediaType = (value) => typeof value === "string" && MEDIA_TYPE_PATTERN.test(value);
9836
+ var isCollectionId = (value) => typeof value === "string" && COLLECTION_ID_PATTERN.test(value);
9812
9837
  var isAttributeValue = (value) => typeof value === "number" ? Number.isSafeInteger(value) : isText(value);
9813
9838
  function hasSpaceOrControl(value) {
9814
9839
  for (let i = 0; i < value.length; i++) {
@@ -9851,8 +9876,9 @@ function checkLink(prefix, link) {
9851
9876
  function checkMediaRef(field, ref) {
9852
9877
  if (ref === null) return;
9853
9878
  if (ref?.kind === "media") return checkMedia(field, ref);
9854
- if (ref?.kind === "link") return checkLink(field, ref);
9855
- throw refuse(field, "must be an NftMedia, an NftLink or null");
9879
+ if (ref?.kind !== "link") throw refuse(field, "must be an NftMedia, an NftLink or null");
9880
+ checkLink(field, ref);
9881
+ ensure(!isNftDocumentLink(ref), fieldName(field, "media_type"), DOCUMENT_AS_MEDIA_RULE);
9856
9882
  }
9857
9883
  function checkAttributes(attributes) {
9858
9884
  ensure(Array.isArray(attributes), "attributes", "must be an array \u2014 [] when there are none");
@@ -9869,6 +9895,7 @@ function checkMetadata(metadata) {
9869
9895
  checkOptionalText("external_url", metadata.external_url);
9870
9896
  checkAttributes(metadata.attributes);
9871
9897
  checkOptionalText("collection", metadata.collection);
9898
+ ensure(metadata.collection_id === null || isCollectionId(metadata.collection_id), "collection_id", COLLECTION_ID_RULE);
9872
9899
  }
9873
9900
  function checkContent(content) {
9874
9901
  switch (content?.kind) {
@@ -9926,6 +9953,7 @@ function arrayCbor(items) {
9926
9953
  }
9927
9954
  return out;
9928
9955
  }
9956
+ var optionalHexBytes = (value) => CborSerializer.encodeNullable(value, (hex) => CborSerializer.encodeByteString(HexConverter.decode(hex.toLowerCase())));
9929
9957
  function metadataCbor(metadata) {
9930
9958
  return versioned(
9931
9959
  NFT_METADATA_TAG,
@@ -9935,7 +9963,8 @@ function metadataCbor(metadata) {
9935
9963
  mediaRefCbor(metadata.animation_url),
9936
9964
  optionalText(metadata.external_url),
9937
9965
  arrayCbor(metadata.attributes.map(attributeCbor)),
9938
- optionalText(metadata.collection)
9966
+ optionalText(metadata.collection),
9967
+ optionalHexBytes(metadata.collection_id)
9939
9968
  );
9940
9969
  }
9941
9970
  function encodeNftContent(content) {
@@ -9993,13 +10022,14 @@ function decodeMedia(body) {
9993
10022
  const [, mediaType, bytes] = versionedFields(body, MEDIA_ARITY);
9994
10023
  return { kind: "media", media_type: decodeText(mediaType), bytes: CborDeserializer.decodeByteString(bytes) };
9995
10024
  }
10025
+ var decodeHex = (item) => HexConverter.encode(CborDeserializer.decodeByteString(item));
9996
10026
  function decodeLink(body) {
9997
10027
  const [, mediaType, uri, digest] = versionedFields(body, LINK_ARITY);
9998
10028
  return {
9999
10029
  kind: "link",
10000
10030
  media_type: decodeText(mediaType),
10001
10031
  uri: decodeText(uri),
10002
- sha256: HexConverter.encode(CborDeserializer.decodeByteString(digest))
10032
+ sha256: decodeHex(digest)
10003
10033
  };
10004
10034
  }
10005
10035
  function decodeMetadata(body) {
@@ -10012,7 +10042,8 @@ function decodeMetadata(body) {
10012
10042
  animation_url: decodeMediaRef("animation_url", fields[4]),
10013
10043
  external_url: decodeOptionalText(fields[5]),
10014
10044
  attributes: decodeAttributes(fields[6]),
10015
- collection: decodeOptionalText(fields[7])
10045
+ collection: decodeOptionalText(fields[7]),
10046
+ collection_id: CborDeserializer.decodeNullable(fields[8], decodeHex)
10016
10047
  };
10017
10048
  }
10018
10049
  var MEDIA_REF_DECODERS = /* @__PURE__ */ new Map([
@@ -10058,20 +10089,26 @@ function parseNftPayload(data) {
10058
10089
  return null;
10059
10090
  }
10060
10091
  }
10061
- function nftSignedDigest(tokenIdCbor, recipientPredicateCbor, payload) {
10092
+ function isNftDocumentLink(content) {
10093
+ return content?.kind === "link" && content.media_type === NFT_DOCUMENT_MEDIA_TYPE;
10094
+ }
10095
+ function nftSignedDigest(context, payload) {
10062
10096
  const preimage = CborSerializer.encodeArray(
10063
- CborSerializer.encodeTextString(SIGNED_DOMAIN),
10064
- tokenIdCbor,
10065
- recipientPredicateCbor,
10097
+ CborSerializer.encodeByteString(SIGNED_DOMAIN),
10098
+ CborSerializer.encodeUnsignedInteger(NFT_FORMAT_VERSION),
10099
+ CborSerializer.encodeUnsignedInteger(NetworkId.fromId(context.networkId).id),
10100
+ CborSerializer.encodeByteString(context.recipientPredicate),
10101
+ CborSerializer.encodeByteString(context.tokenId),
10102
+ CborSerializer.encodeByteString(context.tokenType),
10066
10103
  CborSerializer.encodeByteString(payload)
10067
10104
  );
10068
10105
  return new DataHasher(HashAlgorithm.SHA256).update(preimage).digest();
10069
10106
  }
10070
- async function verifyNftSignature(signed, tokenIdCbor, recipientPredicateCbor) {
10107
+ async function verifyNftSignature(signed, context) {
10071
10108
  try {
10072
10109
  if (!SigningService.isPublicKeyValid(signed.creator)) return "invalid";
10073
10110
  const signature = Signature.decode(signed.signature);
10074
- const digest = await nftSignedDigest(tokenIdCbor, recipientPredicateCbor, signed.payload);
10111
+ const digest = await nftSignedDigest(context, signed.payload);
10075
10112
  return await SigningService.verifyWithPublicKey(digest, signature, signed.creator) ? "valid" : "invalid";
10076
10113
  } catch {
10077
10114
  return "invalid";
@@ -10202,8 +10239,8 @@ function assertMintableData(data) {
10202
10239
  function nftRecipientCbor(pubkey) {
10203
10240
  return EncodedPredicate.fromPredicate(SignaturePredicate.create(pubkey)).toCBOR();
10204
10241
  }
10205
- async function signPayload(deps, payload, tokenId, recipientPubkey) {
10206
- const digest = await nftSignedDigest(tokenId.toCBOR(), nftRecipientCbor(recipientPubkey), payload);
10242
+ async function signPayload(deps, payload, context) {
10243
+ const digest = await nftSignedDigest(context, payload);
10207
10244
  const signature = await deps.signingService.sign(digest);
10208
10245
  return encodeNftSigned(deps.signingService.publicKey, payload, signature.encode());
10209
10246
  }
@@ -10212,31 +10249,41 @@ async function planNftMint(deps, params) {
10212
10249
  const tokenType = new TokenType(params.tokenType).bytes;
10213
10250
  const salt = TokenSalt.generate();
10214
10251
  const tokenId = await TokenId.fromSalt(deps.networkId, salt);
10215
- const data = params.sign ? await signPayload(deps, payload, tokenId, params.recipientPubkey) : payload;
10252
+ const data = params.sign ? await signPayload(deps, payload, {
10253
+ networkId: deps.networkId.id,
10254
+ recipientPredicate: nftRecipientCbor(params.recipientPubkey),
10255
+ tokenId: tokenId.bytes,
10256
+ tokenType
10257
+ }) : payload;
10216
10258
  assertMintableData(data);
10217
10259
  return { data, salt: salt.toBytes(), tokenType, tokenId: HexConverter.encode(tokenId.bytes) };
10218
10260
  }
10219
- async function verifyAgainst(signed, tokenIdCbor, genesisRecipientCbor) {
10261
+ async function verifyAgainst(signed, genesisContext) {
10220
10262
  try {
10221
- return await verifyNftSignature(signed, tokenIdCbor, genesisRecipientCbor());
10263
+ return await verifyNftSignature(signed, genesisContext());
10222
10264
  } catch {
10223
10265
  return "invalid";
10224
10266
  }
10225
10267
  }
10226
- async function readNftData(data, tokenIdCbor, genesisRecipientCbor) {
10268
+ async function readNftData(data, genesisContext) {
10227
10269
  const parsed = parseNftPayload(data);
10228
10270
  if (parsed === null) return null;
10229
10271
  if (parsed.signed === null) return { content: parsed.content, creator: null, signature: "unsigned" };
10230
10272
  return {
10231
10273
  content: parsed.content,
10232
10274
  creator: HexConverter.encode(parsed.signed.creator),
10233
- signature: await verifyAgainst(parsed.signed, tokenIdCbor, genesisRecipientCbor)
10275
+ signature: await verifyAgainst(parsed.signed, genesisContext)
10234
10276
  };
10235
10277
  }
10236
10278
  async function readTokenNft(token) {
10237
10279
  try {
10238
10280
  const { genesis } = token.sdkToken;
10239
- return await readNftData(genesis.data, genesis.tokenId.toCBOR(), () => genesis.recipient.toCBOR());
10281
+ return await readNftData(genesis.data, () => ({
10282
+ networkId: genesis.networkId.id,
10283
+ recipientPredicate: genesis.recipient.toCBOR(),
10284
+ tokenId: genesis.tokenId.bytes,
10285
+ tokenType: genesis.tokenType.bytes
10286
+ }));
10240
10287
  } catch {
10241
10288
  return null;
10242
10289
  }