@tinycloud/share-sdk 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +6 -0
  2. package/dist/{addressed-publish-Db-6rqkA.d.cts → addressed-publish-BP6nsgRq.d.cts} +58 -2
  3. package/dist/{addressed-publish-CAdGCqiX.d.ts → addressed-publish-CpkwV-Au.d.ts} +58 -2
  4. package/dist/artifact.cjs +2 -2
  5. package/dist/artifact.cjs.map +1 -1
  6. package/dist/artifact.js +2 -2
  7. package/dist/artifact.js.map +1 -1
  8. package/dist/{history-ZJFHV-IU.d.cts → history-Bg25JIsi.d.cts} +1 -1
  9. package/dist/{history-q0ALXTv7.d.ts → history-kwJxrZgq.d.ts} +1 -1
  10. package/dist/index.cjs +304 -154
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +6 -6
  13. package/dist/index.d.ts +6 -6
  14. package/dist/index.js +290 -154
  15. package/dist/index.js.map +1 -1
  16. package/dist/{owner-policy-CRFeEC98.d.cts → owner-policy-aPmXIR3e.d.cts} +1 -1
  17. package/dist/{owner-policy-CRFeEC98.d.ts → owner-policy-aPmXIR3e.d.ts} +1 -1
  18. package/dist/share-index.cjs +47 -87
  19. package/dist/share-index.cjs.map +1 -1
  20. package/dist/share-index.d.cts +28 -30
  21. package/dist/share-index.d.ts +28 -30
  22. package/dist/share-index.js +46 -86
  23. package/dist/share-index.js.map +1 -1
  24. package/dist/share-root.cjs +2 -2
  25. package/dist/share-root.cjs.map +1 -1
  26. package/dist/share-root.d.cts +2 -2
  27. package/dist/share-root.d.ts +2 -2
  28. package/dist/share-root.js +2 -2
  29. package/dist/share-root.js.map +1 -1
  30. package/dist/{web-recipient-entry-D9IJEyRA.d.cts → web-recipient-entry-CiUzdSQs.d.cts} +9 -1
  31. package/dist/{web-recipient-entry-CKA_vCi5.d.ts → web-recipient-entry-DVyTQBl0.d.ts} +9 -1
  32. package/dist/web-recipient.cjs +76 -105
  33. package/dist/web-recipient.cjs.map +1 -1
  34. package/dist/web-recipient.d.cts +1 -1
  35. package/dist/web-recipient.d.ts +1 -1
  36. package/dist/web-recipient.js +66 -105
  37. package/dist/web-recipient.js.map +1 -1
  38. package/package.json +2 -2
@@ -27,9 +27,17 @@ interface ShareErrorInfo {
27
27
  };
28
28
  }
29
29
  declare function toShareErrorInfo(error: unknown): ShareErrorInfo;
30
- /** Policy/v3 links seal their authorization metadata in a fragment; no blob fetch adapter exists. */
30
+ /**
31
+ * A Policy/v3 link is a `v=4` link: a short link whose sealed envelope the links
32
+ * API serves, or a full link that carries it. The key and body CID stay in the
33
+ * fragment and are never sent.
34
+ */
31
35
  interface ShareFetchOptions {
36
+ /** The trusted viewer origin. Defaults to the link's own origin. */
32
37
  readonly expectedOrigin?: string;
38
+ /** The links API origin a short link is read from. A link never names it. Defaults to `https://api.tinycloud.xyz`. */
39
+ readonly apiOrigin?: string;
40
+ readonly fetch?: typeof globalThis.fetch;
33
41
  readonly signal?: AbortSignal;
34
42
  readonly now?: () => number;
35
43
  readonly maxSealedBlobBytes?: number;
@@ -27,9 +27,17 @@ interface ShareErrorInfo {
27
27
  };
28
28
  }
29
29
  declare function toShareErrorInfo(error: unknown): ShareErrorInfo;
30
- /** Policy/v3 links seal their authorization metadata in a fragment; no blob fetch adapter exists. */
30
+ /**
31
+ * A Policy/v3 link is a `v=4` link: a short link whose sealed envelope the links
32
+ * API serves, or a full link that carries it. The key and body CID stay in the
33
+ * fragment and are never sent.
34
+ */
31
35
  interface ShareFetchOptions {
36
+ /** The trusted viewer origin. Defaults to the link's own origin. */
32
37
  readonly expectedOrigin?: string;
38
+ /** The links API origin a short link is read from. A link never names it. Defaults to `https://api.tinycloud.xyz`. */
39
+ readonly apiOrigin?: string;
40
+ readonly fetch?: typeof globalThis.fetch;
33
41
  readonly signal?: AbortSignal;
34
42
  readonly now?: () => number;
35
43
  readonly maxSealedBlobBytes?: number;
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from2, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/web-recipient-entry.ts
@@ -4942,7 +4952,8 @@ function isCanonicalRawCid(cidString) {
4942
4952
  } catch {
4943
4953
  return false;
4944
4954
  }
4945
- return cid.version === 1 && cid.code === code && cid.multihash.code === SHA256_CODE && cid.toString() === cidString;
4955
+ return cid.version === 1 && cid.code === code && cid.multihash.code === SHA256_CODE && // `toString` of a parsed CID gives back the string it was parsed from; a fresh CID is encoded again.
4956
+ CID.createV1(cid.code, cid.multihash).toString() === cidString;
4946
4957
  }
4947
4958
  var base64 = rfc4648({
4948
4959
  prefix: "m",
@@ -5399,51 +5410,7 @@ function validateV3Invariants(value, ctx) {
5399
5410
  var unsignedShareEnvelopeV3Schema = unsignedShareEnvelopeV3BaseSchema.superRefine(validateV3Invariants);
5400
5411
  var shareEnvelopeV3Schema = unsignedShareEnvelopeV3BaseSchema.extend({ signature: signatureSchema }).strict().superRefine(validateV3Invariants);
5401
5412
  var ENVELOPE_AAD_LABEL = "tinycloud-share-envelope-v1";
5402
- var SEALED_BLOB_VERSION = 1;
5403
5413
  var AAD = utf8Bytes(ENVELOPE_AAD_LABEL);
5404
- var KEY_LENGTH = 32;
5405
- var NONCE_LENGTH = 12;
5406
- var TAG_LENGTH = 16;
5407
- var HEADER_LENGTH = 1;
5408
- function assertKey(key32) {
5409
- if (key32.length !== KEY_LENGTH) {
5410
- throw new TypeError(`key must be ${KEY_LENGTH} bytes, got ${key32.length}`);
5411
- }
5412
- }
5413
- async function importAesKey(key32, usage) {
5414
- assertKey(key32);
5415
- return globalThis.crypto.subtle.importKey(
5416
- "raw",
5417
- key32,
5418
- "AES-GCM",
5419
- false,
5420
- [usage]
5421
- );
5422
- }
5423
- async function open(blob, key32) {
5424
- if (blob.length < HEADER_LENGTH + NONCE_LENGTH + TAG_LENGTH) {
5425
- throw new TypeError(`sealed blob too short: ${blob.length} bytes`);
5426
- }
5427
- if (blob[0] !== SEALED_BLOB_VERSION) {
5428
- throw new TypeError(`unknown sealed blob version: ${blob[0]}`);
5429
- }
5430
- const nonce = blob.subarray(HEADER_LENGTH, HEADER_LENGTH + NONCE_LENGTH);
5431
- const ciphertext = blob.subarray(HEADER_LENGTH + NONCE_LENGTH);
5432
- return decryptEnvelope(nonce, ciphertext, key32);
5433
- }
5434
- async function decryptEnvelope(nonce, ciphertext, key32) {
5435
- if (nonce.length !== NONCE_LENGTH) {
5436
- throw new TypeError(`nonce must be ${NONCE_LENGTH} bytes, got ${nonce.length}`);
5437
- }
5438
- const key = await importAesKey(key32, "decrypt");
5439
- return new Uint8Array(
5440
- await globalThis.crypto.subtle.decrypt(
5441
- { name: "AES-GCM", iv: nonce, additionalData: AAD },
5442
- key,
5443
- ciphertext
5444
- )
5445
- );
5446
- }
5447
5414
  function isPlainRecord(value) {
5448
5415
  const proto = Object.getPrototypeOf(value);
5449
5416
  return proto === Object.prototype || proto === null;
@@ -5675,56 +5642,6 @@ function base32Lower(bytes) {
5675
5642
  if (bits > 0) output += alphabet[buffer << 5 - bits & 31];
5676
5643
  return output;
5677
5644
  }
5678
- var KEY_LENGTH2 = 32;
5679
- var MAX_INLINE_BYTES = 256 * 1024;
5680
- function assertCanonicalCid(cidString) {
5681
- const cid = CID.parse(cidString);
5682
- if (cid.version !== 1 || cid.code !== code || cid.multihash.code !== 18 || // sha2-256 (0x12) only, at the link layer too
5683
- cid.toString() !== cidString) {
5684
- throw new TypeError(`not a canonical CIDv1 raw sha2-256 base32 CID: ${cidString}`);
5685
- }
5686
- }
5687
- async function parseSealedInlineShareUrl(url, options = {}) {
5688
- const parsed = new URL(url);
5689
- if (parsed.protocol !== "https:" || parsed.username !== "" || parsed.password !== "") throw new TypeError("sealed inline share URL must be canonical HTTPS without userinfo");
5690
- if (!isCanonicalHttpsOrigin(parsed.origin) || options.expectedOrigin !== void 0 && parsed.origin !== options.expectedOrigin) throw new TypeError("sealed inline share URL origin is not trusted");
5691
- if (parsed.pathname !== "/s/inline" || parsed.search !== "") throw new TypeError("not a Node sealed-inline share URL");
5692
- if (url !== `${parsed.origin}/s/inline${parsed.hash}`) throw new TypeError("sealed inline share URL is not lexically canonical");
5693
- const prefix = "#v=2&p=";
5694
- if (!parsed.hash.startsWith(prefix)) throw new TypeError("sealed inline URL is missing its canonical fragment");
5695
- const encoded = parsed.hash.slice(prefix.length);
5696
- if (encoded.length === 0 || parsed.hash !== `${prefix}${encoded}`) throw new TypeError("sealed inline URL fragment is not canonical");
5697
- let payloadBytes;
5698
- try {
5699
- payloadBytes = fromBase64Url(encoded);
5700
- } catch {
5701
- throw new TypeError("sealed inline payload is not canonical base64url");
5702
- }
5703
- if (payloadBytes.byteLength === 0 || payloadBytes.byteLength > MAX_INLINE_BYTES * 2) throw new TypeError("sealed inline payload is too large");
5704
- const payloadText = new TextDecoder("utf-8", { fatal: true }).decode(payloadBytes);
5705
- let value;
5706
- try {
5707
- value = JSON.parse(payloadText);
5708
- } catch {
5709
- throw new TypeError("sealed inline payload is not valid JSON");
5710
- }
5711
- if (typeof value !== "object" || value === null || Array.isArray(value) || canonicalize(value) !== payloadText) throw new TypeError("sealed inline payload is not canonical JSON");
5712
- const record = value;
5713
- if (Object.keys(record).length !== 4 || record.v !== 2 || typeof record.c !== "string" || typeof record.cid !== "string" || typeof record.k !== "string") throw new TypeError("sealed inline payload has invalid fields");
5714
- let ciphertext;
5715
- let key32;
5716
- try {
5717
- ciphertext = fromBase64Url(record.c);
5718
- key32 = fromBase64Url(record.k);
5719
- } catch {
5720
- throw new TypeError("sealed inline payload has invalid base64url material");
5721
- }
5722
- if (ciphertext.byteLength === 0 || ciphertext.byteLength > MAX_INLINE_BYTES) throw new TypeError("sealed inline ciphertext is outside the allowed size");
5723
- if (key32.byteLength !== KEY_LENGTH2) throw new TypeError("sealed inline key must be 32 bytes");
5724
- assertCanonicalCid(record.cid);
5725
- if (await computeCid(ciphertext) !== record.cid) throw new TypeError("sealed inline ciphertext does not match its CID");
5726
- return { kind: "inline", ciphertextCid: record.cid, ciphertext, key32 };
5727
- }
5728
5645
 
5729
5646
  // src/artifact-core.ts
5730
5647
  var ARTIFACT_ROOT = "artifacts";
@@ -5839,26 +5756,80 @@ function metadataFor(envelope, origin) {
5839
5756
  ...envelope.artifactPin === void 0 ? {} : { artifact: { id: envelope.artifactPin.artifactId, version: envelope.artifactPin.version } }
5840
5757
  };
5841
5758
  }
5759
+ function receiveErrorFor(error) {
5760
+ switch (error.code) {
5761
+ case "origin_not_trusted":
5762
+ return new ShareReceiveError("origin-mismatch", "share link origin is not trusted");
5763
+ case "body_over_cap":
5764
+ case "response_too_large":
5765
+ return new ShareReceiveError("max-bytes-exceeded", "sealed policy envelope exceeds the size limit");
5766
+ case "body_hash_mismatch":
5767
+ case "link_cid_mismatch":
5768
+ return new ShareReceiveError("cid-mismatch", "sealed envelope bytes do not match the link CID");
5769
+ case "body_open_failed":
5770
+ return new ShareReceiveError("decrypt-failed", "share envelope could not be opened");
5771
+ case "gone":
5772
+ case "expired":
5773
+ return new ShareReceiveError("expired", "share link has expired, was deleted, or never existed");
5774
+ case "bad_signature":
5775
+ case "issuer_binding_mismatch":
5776
+ return new ShareReceiveError("signature-invalid", "share link signature is invalid");
5777
+ case "service_refused":
5778
+ case "service_unavailable":
5779
+ case "invalid_response":
5780
+ return new ShareReceiveError("fetch-failed", "share link could not be fetched");
5781
+ case "not_a_url":
5782
+ case "not_https":
5783
+ case "userinfo":
5784
+ case "not_canonical":
5785
+ case "query_present":
5786
+ case "removed_version":
5787
+ case "path_grammar":
5788
+ case "fragment_grammar":
5789
+ case "key_noncanonical":
5790
+ case "cid_grammar":
5791
+ case "record_encoding":
5792
+ case "record_json":
5793
+ case "record_not_canonical":
5794
+ case "type_path_mismatch":
5795
+ case "full_non_share":
5796
+ return new ShareReceiveError("invalid-link", "share link format is invalid");
5797
+ default:
5798
+ return new ShareReceiveError("envelope-invalid", "share envelope is invalid");
5799
+ }
5800
+ }
5842
5801
  async function resolvePolicyShare(link, options) {
5843
5802
  options.signal?.throwIfAborted();
5844
5803
  let url;
5845
- let parsed;
5846
5804
  try {
5847
5805
  url = new URL(link);
5848
- parsed = await parseSealedInlineShareUrl(link, { ...options.expectedOrigin === void 0 ? {} : { expectedOrigin: options.expectedOrigin } });
5849
5806
  } catch {
5850
5807
  throw new ShareReceiveError("invalid-link", "share link format is invalid");
5851
5808
  }
5809
+ const links = await import("@tinycloud/share-envelope/links");
5810
+ let resolved;
5811
+ try {
5812
+ resolved = await links.resolveLink(link, {
5813
+ trustedOrigins: [options.expectedOrigin ?? url.origin],
5814
+ apiOrigin: options.apiOrigin ?? links.DEFAULT_LINKS_API_ORIGIN,
5815
+ ...options.fetch === void 0 ? {} : { fetch: options.fetch },
5816
+ ...options.signal === void 0 ? {} : { signal: options.signal },
5817
+ now: options.now?.() ?? Date.now()
5818
+ });
5819
+ } catch (error) {
5820
+ options.signal?.throwIfAborted();
5821
+ throw error instanceof links.LinkError ? receiveErrorFor(error) : error;
5822
+ }
5823
+ if (resolved.type !== "share") throw new ShareReceiveError("invalid-link", "share link is not a share");
5852
5824
  const limit = options.maxSealedBlobBytes ?? DEFAULT_MAX_SEALED_BLOB_BYTES;
5853
- if (!Number.isSafeInteger(limit) || limit <= 0 || parsed.ciphertext.byteLength > limit) throw new ShareReceiveError("max-bytes-exceeded", "sealed policy envelope exceeds the configured byte limit");
5854
- if (await computeCid(parsed.ciphertext) !== parsed.ciphertextCid) throw new ShareReceiveError("cid-mismatch", "sealed envelope bytes do not match the link CID");
5825
+ if (!Number.isSafeInteger(limit) || limit <= 0 || resolved.body.byteLength > limit) throw new ShareReceiveError("max-bytes-exceeded", "sealed policy envelope exceeds the configured byte limit");
5826
+ const cid = resolved.header.bodyCid;
5855
5827
  options.signal?.throwIfAborted();
5856
5828
  let envelope;
5857
5829
  try {
5858
- const encoded = new TextDecoder("utf-8", { fatal: true }).decode(await open(parsed.ciphertext, parsed.key32));
5859
- const value = JSON.parse(encoded);
5860
- if (canonicalize(value) !== encoded) throw new Error("non-canonical envelope");
5861
- envelope = shareEnvelopeV3Schema.parse(value);
5830
+ const encoded = new TextDecoder("utf-8", { fatal: true }).decode(resolved.plaintext);
5831
+ if (canonicalize(resolved.value) !== encoded) throw new Error("non-canonical envelope");
5832
+ envelope = shareEnvelopeV3Schema.parse(resolved.value);
5862
5833
  } catch {
5863
5834
  throw new ShareReceiveError("envelope-invalid", "share envelope is invalid");
5864
5835
  }
@@ -5877,8 +5848,8 @@ async function resolvePolicyShare(link, options) {
5877
5848
  const expiry = Date.parse(envelope.expiry);
5878
5849
  if (!Number.isFinite(expiry)) throw new ShareReceiveError("envelope-invalid", "share expiry is invalid");
5879
5850
  if (expiry <= (options.now?.() ?? Date.now())) throw new ShareReceiveError("expired", "share has expired", { expiresAt: envelope.expiry });
5880
- options.onResolvedAddressedEnvelope?.(envelope, parsed.ciphertextCid);
5881
- return { envelope, origin: url.origin, cid: parsed.ciphertextCid };
5851
+ options.onResolvedAddressedEnvelope?.(envelope, cid);
5852
+ return { envelope, origin: url.origin, cid };
5882
5853
  }
5883
5854
  async function inspectShare(link, options = {}) {
5884
5855
  const resolved = await resolvePolicyShare(link, options);