@tinycloud/share-sdk 1.0.0-beta.6 → 1.0.0-beta.8

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.cjs CHANGED
@@ -41,9 +41,11 @@ __export(index_exports, {
41
41
  SharePublishError: () => SharePublishError,
42
42
  ShareReceiveError: () => ShareReceiveError,
43
43
  ShareRecipientClient: () => ShareRecipientClient,
44
+ addressedCredentialRequirement: () => addressedCredentialRequirement,
44
45
  authorizationMethodForTarget: () => authorizationMethodForTarget,
45
46
  authorizeShare: () => authorizeShare,
46
47
  canonicalOwnerSharePolicy: () => canonicalOwnerSharePolicy,
48
+ canonicalShareFilename: () => canonicalShareFilename,
47
49
  canonicalize: () => canonicalize,
48
50
  checkBearerDelegation: () => checkBearerDelegation,
49
51
  claimShare: () => claimShare,
@@ -55,6 +57,7 @@ __export(index_exports, {
55
57
  createShareV2HolderBindingArtifact: () => createShareV2HolderBindingArtifact,
56
58
  deliverCredentialInvitation: () => deliverCredentialInvitation,
57
59
  fromBase64Url: () => fromBase64Url,
60
+ hasUnsafeFilenameCodePoint: () => hasUnsafeFilenameCodePoint,
58
61
  historyRecordForPublishedShare: () => historyRecordForPublishedShare,
59
62
  inspectShare: () => inspectShare,
60
63
  listShares: () => listShares,
@@ -63,6 +66,7 @@ __export(index_exports, {
63
66
  openNativeShare: () => openNativeShare,
64
67
  parseAddressedEnvelope: () => parseAddressedEnvelope,
65
68
  parseNativeShareUrl: () => parseNativeShareUrl,
69
+ prepareAddressedShare: () => prepareAddressedShare,
66
70
  publishAddressedShare: () => publishAddressedShare,
67
71
  publishPolicyShare: () => publishPolicyShare,
68
72
  publishTargetShare: () => publishTargetShare,
@@ -83,6 +87,19 @@ __export(index_exports, {
83
87
  });
84
88
  module.exports = __toCommonJS(index_exports);
85
89
 
90
+ // src/filename-policy.ts
91
+ var UNSAFE_FILENAME_CODE_POINT = /[\p{Cc}\p{Cf}\p{Cs}\u2028\u2029]/u;
92
+ function hasUnsafeFilenameCodePoint(value) {
93
+ return UNSAFE_FILENAME_CODE_POINT.test(value);
94
+ }
95
+ function canonicalShareFilename(value) {
96
+ const canonical = value.normalize("NFC");
97
+ if (canonical.length === 0 || canonical === "." || canonical === ".." || canonical.includes("/") || canonical.includes("\\") || hasUnsafeFilenameCodePoint(canonical)) {
98
+ throw new TypeError("share filename is unsafe");
99
+ }
100
+ return canonical;
101
+ }
102
+
86
103
  // ../share-envelope/dist/index.js
87
104
  var import_sha2 = require("@noble/hashes/sha2");
88
105
  var import_ed25519 = require("@noble/curves/ed25519");
@@ -5764,6 +5781,21 @@ function serialize(value) {
5764
5781
  function canonicalize(value) {
5765
5782
  return serialize(value);
5766
5783
  }
5784
+ var LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
5785
+ function isCanonicalEmailDomain(value) {
5786
+ if (value.length === 0 || value.length > 253) return false;
5787
+ const labels = value.split(".");
5788
+ return labels.length >= 2 && labels.every((label) => LABEL.test(label)) && !/^[0-9]+$/.test(labels.at(-1));
5789
+ }
5790
+ function canonicalMailbox(value) {
5791
+ const email = value.trim().toLowerCase();
5792
+ const at = email.indexOf("@");
5793
+ if (at <= 0 || at !== email.lastIndexOf("@")) return void 0;
5794
+ const local = email.slice(0, at);
5795
+ const domain = email.slice(at + 1);
5796
+ if (local.length > 64 || !/^[a-z0-9#$&'*+/=?^_`{|}~-]+(?:\.[a-z0-9#$&'*+/=?^_`{|}~-]+)*$/.test(local) || !isCanonicalEmailDomain(domain)) return void 0;
5797
+ return Object.freeze({ email, domain });
5798
+ }
5767
5799
  async function signCompactUcanAuthorization(input) {
5768
5800
  if (input.notBefore >= input.expiresAt || input.expiresAt - input.notBefore > 60) {
5769
5801
  throw new TypeError("compact invocation lifetime must be between one and 60 seconds");
@@ -6509,12 +6541,6 @@ var base58flickr2 = baseX2({
6509
6541
  });
6510
6542
 
6511
6543
  // src/targets.ts
6512
- function validEmail(value) {
6513
- return /^[^@\s]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i.test(value);
6514
- }
6515
- function validDomain(value) {
6516
- return /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?))+$/i.test(value);
6517
- }
6518
6544
  function validRecipientDid(value) {
6519
6545
  if (value.length === 0 || value.length > 2048 || /[\u0000-\u0020\u007f]/.test(value)) return false;
6520
6546
  const parts = value.split(":");
@@ -6544,18 +6570,28 @@ function normalizeShareTarget(target) {
6544
6570
  return { kind: target.kind, did: target.did };
6545
6571
  }
6546
6572
  if (target.kind === "email") {
6547
- if (!validEmail(target.address)) throw new TypeError("recipient email is invalid");
6548
- const at = target.address.lastIndexOf("@");
6549
- return { kind: target.kind, address: `${target.address.slice(0, at)}@${target.address.slice(at + 1).toLowerCase()}` };
6573
+ const mailbox = canonicalMailbox(target.address);
6574
+ if (mailbox === void 0) throw new TypeError("recipient email is invalid");
6575
+ return { kind: target.kind, address: mailbox.email };
6550
6576
  }
6551
- if (!validDomain(target.domain)) throw new TypeError("recipient email domain is invalid");
6552
- return { kind: target.kind, domain: target.domain.toLowerCase() };
6577
+ const domain = target.domain.toLowerCase();
6578
+ if (!isCanonicalEmailDomain(domain)) throw new TypeError("recipient email domain is invalid");
6579
+ return { kind: target.kind, domain };
6553
6580
  }
6554
6581
  function targetAuthorizationMethod(target) {
6555
6582
  return authorizationMethodForTarget(target);
6556
6583
  }
6584
+ function publishFilename(value) {
6585
+ try {
6586
+ return canonicalShareFilename(value);
6587
+ } catch {
6588
+ throw new SharePublishError("invalid-argument", hasUnsafeFilenameCodePoint(value) ? "filename contains control or invisible characters" : "filename must be one safe path segment");
6589
+ }
6590
+ }
6557
6591
  async function publishTargetShare(input) {
6558
6592
  const target = normalizeShareTarget(input.target);
6593
+ const filename = publishFilename(input.filename);
6594
+ const files = input.files?.map((file) => ({ ...file, filename: publishFilename(file.filename) }));
6559
6595
  if (input.targetAdapter === void 0) {
6560
6596
  if (target.kind === "bearer") throw new SharePublishError("authority-required", "native bearer publication requires an authenticated TinyCloud node");
6561
6597
  return {
@@ -6581,9 +6617,6 @@ async function publishTargetShare(input) {
6581
6617
  return bytes3;
6582
6618
  })();
6583
6619
  if (source.byteLength === 0) throw new SharePublishError("invalid-argument", "share content is empty");
6584
- if (!input.filename || input.filename === "." || input.filename === ".." || /[/\\\u0000-\u001f\u007f]/.test(input.filename)) {
6585
- throw new SharePublishError("invalid-argument", "filename must be one safe path segment");
6586
- }
6587
6620
  if (target.kind === "bearer" && input.allowBinary !== true) {
6588
6621
  try {
6589
6622
  new TextDecoder("utf-8", { fatal: true }).decode(source);
@@ -6591,10 +6624,10 @@ async function publishTargetShare(input) {
6591
6624
  throw new SharePublishError("invalid-argument", "Markdown input must be valid UTF-8");
6592
6625
  }
6593
6626
  }
6594
- const files = input.files === void 0 || input.files.length === 0 ? [{ bytes: source }] : input.files;
6627
+ const publishFiles = files === void 0 || files.length === 0 ? [{ bytes: source }] : files;
6595
6628
  const limit = input.maxBytes ?? SHARE_CONTENT_LIMIT;
6596
6629
  let totalBytes = 0;
6597
- for (const file of files) {
6630
+ for (const file of publishFiles) {
6598
6631
  totalBytes += file.bytes.byteLength;
6599
6632
  if (!Number.isSafeInteger(totalBytes) || totalBytes > limit) {
6600
6633
  throw new SharePublishError("max-bytes-exceeded", "share publication exceeds the combined byte limit");
@@ -6605,8 +6638,8 @@ async function publishTargetShare(input) {
6605
6638
  if (!Number.isFinite(expiresAt.getTime()) || expiresAt.getTime() <= nowMs) throw new SharePublishError("invalid-argument", "expiresAt must be a valid future time");
6606
6639
  return input.targetAdapter.publish({
6607
6640
  source,
6608
- filename: input.filename,
6609
- ...input.files === void 0 ? {} : { files: input.files },
6641
+ filename,
6642
+ ...files === void 0 ? {} : { files },
6610
6643
  ...input.mediaType === void 0 ? {} : { mediaType: input.mediaType },
6611
6644
  ...input.resourceKind === void 0 ? {} : { resourceKind: input.resourceKind },
6612
6645
  ...input.actions === void 0 ? {} : { actions: input.actions },
@@ -6636,9 +6669,7 @@ function targetKind(target) {
6636
6669
  }
6637
6670
  function assertSafeInput(input) {
6638
6671
  if (!/^[A-Za-z0-9_-]{16,128}$/.test(input.shareId)) throw new TypeError("addressed share id is invalid");
6639
- if (input.filename.length === 0 || input.filename === "." || input.filename === ".." || /[/\\\u0000-\u001f\u007f]/.test(input.filename)) throw new TypeError("addressed filename is invalid");
6640
6672
  if (!Number.isSafeInteger(input.byteLength) || input.byteLength < 0 || input.byteLength > SHARE_CONTENT_LIMIT) throw new TypeError("addressed content length is invalid");
6641
- if (input.actions.length === 0 || input.policyActions.length === 0) throw new TypeError("addressed share actions are empty");
6642
6673
  if (!Number.isFinite(input.expiresAt.getTime()) || input.expiresAt.getTime() <= Date.now()) throw new TypeError("addressed share expiry must be in the future");
6643
6674
  if (input.artifact === "html" && (input.resource.kind !== "prefix" || !input.actions.includes("read") || !input.actions.includes("list"))) throw new TypeError("html artifacts require a readable prefix");
6644
6675
  if (input.contentSource.shareId !== input.shareId || input.contentSource.selector !== input.resource.kind) throw new TypeError("addressed content source is not bound to the share");
@@ -6731,23 +6762,60 @@ function publicationResult(input) {
6731
6762
  Object.defineProperty(result, "deliveryMaterial", { enumerable: false, value: input.deliveryMaterial });
6732
6763
  return result;
6733
6764
  }
6765
+ var EMAIL_PROFILE = { id: "tinycloud.email-proof/v1", version: 1 };
6734
6766
  var EMAIL_DOMAIN_PROFILE = { id: "tinycloud.email-domain-proof/v1", version: 1 };
6735
6767
  var EMAIL_CREDENTIAL_TYPE = { id: "opencredentials.email/v1", version: 1 };
6768
+ var EMAIL_DESCRIPTOR_DIGEST = "1tg-qphmKBVtNwzVg9xyz-xxqt_xtMXAsQyXw46m8S0";
6736
6769
  var EMAIL_DOMAIN_DESCRIPTOR_DIGEST = "33X5mAkZZgApdD3xh_T-3KS5moop0J2Nloi2nqWsdWY";
6737
- function assertEmailDomainPolicy(options, domain) {
6738
- if (!options.actions.every((action) => action === "read" || action === "list") || !options.policyActions.every((action) => action === "tinycloud.kv/get" || action === "tinycloud.kv/list" || action === "tinycloud.kv/metadata")) throw new TypeError("email-domain shares are view-only");
6739
- if (options.deliveryEmail !== void 0) throw new TypeError("email-domain shares are not emailed");
6740
- const commitment = options.credentialRequirement;
6741
- if (commitment === void 0) throw new TypeError("email-domain shares require a credential requirement");
6742
- if (canonicalize(commitment.profile) !== canonicalize(EMAIL_DOMAIN_PROFILE) || canonicalize(commitment.credentialType) !== canonicalize(EMAIL_CREDENTIAL_TYPE) || commitment.descriptorDigest !== EMAIL_DOMAIN_DESCRIPTOR_DIGEST) throw new TypeError("email-domain shares require the email-domain credential profile");
6743
- const expected = { type: "TinyCloudCredentialRequirement", version: 1, profile: EMAIL_DOMAIN_PROFILE, credentialType: EMAIL_CREDENTIAL_TYPE, claims: { emailDomain: domain }, maxAgeSeconds: 300 };
6744
- if (toBase64Url((0, import_sha2562.sha256)(textEncoder.encode(canonicalize(expected)))) !== commitment.requirementDigest) throw new TypeError("credential requirement is not bound to the email domain");
6745
- }
6746
- async function publishAddressedShare(options) {
6747
- assertSafeInput(options);
6748
- const target = normalizeShareTarget(options.target);
6770
+ var EMAIL_CREDENTIAL_ISSUER_DID = "did:web:issuer.credentials.org";
6771
+ var EMAIL_CREDENTIAL_ISSUER_KID = `${EMAIL_CREDENTIAL_ISSUER_DID}#controller`;
6772
+ function mailboxCredentialCommitment(target) {
6773
+ const profile = target.kind === "email" ? EMAIL_PROFILE : EMAIL_DOMAIN_PROFILE;
6774
+ const requirement = target.kind === "email" ? { type: "TinyCloudCredentialRequirement", version: 1, profile, credentialType: EMAIL_CREDENTIAL_TYPE, claims: { email: target.address }, maxAgeSeconds: 3600 } : { type: "TinyCloudCredentialRequirement", version: 1, profile, credentialType: EMAIL_CREDENTIAL_TYPE, claims: { emailDomain: target.domain }, maxAgeSeconds: 300 };
6775
+ return {
6776
+ type: "TinyCloudPolicyCredentialRequirement",
6777
+ version: 1,
6778
+ requirementDigest: toBase64Url((0, import_sha2562.sha256)(textEncoder.encode(canonicalize(requirement)))),
6779
+ descriptorDigest: target.kind === "email" ? EMAIL_DESCRIPTOR_DIGEST : EMAIL_DOMAIN_DESCRIPTOR_DIGEST,
6780
+ issuerDid: EMAIL_CREDENTIAL_ISSUER_DID,
6781
+ issuerKid: EMAIL_CREDENTIAL_ISSUER_KID,
6782
+ profile,
6783
+ credentialType: EMAIL_CREDENTIAL_TYPE
6784
+ };
6785
+ }
6786
+ function addressedCredentialRequirement(target) {
6787
+ const normalized = normalizeShareTarget(target);
6788
+ if (normalized.kind !== "email" && normalized.kind !== "emailDomain") throw new TypeError("a mailbox target is required");
6789
+ return mailboxCredentialCommitment(normalized);
6790
+ }
6791
+ function prepareAddressedShare(request) {
6792
+ try {
6793
+ canonicalShareFilename(request.filename);
6794
+ } catch {
6795
+ throw new TypeError("addressed filename is invalid");
6796
+ }
6797
+ if (request.actions.length === 0 || request.policyActions.length === 0) throw new TypeError("addressed share actions are empty");
6798
+ const target = normalizeShareTarget(request.target);
6749
6799
  if (target.kind === "bearer") throw new TypeError("addressed target is required");
6750
- if (target.kind === "emailDomain") assertEmailDomainPolicy(options, target.domain);
6800
+ if (target.kind === "recipientDid") return { target };
6801
+ if (target.kind === "emailDomain") {
6802
+ if (!request.actions.every((action) => action === "read" || action === "list") || !request.policyActions.every((action) => action === "tinycloud.kv/get" || action === "tinycloud.kv/list" || action === "tinycloud.kv/metadata")) throw new TypeError("email-domain shares are view-only");
6803
+ if (request.deliveryEmail !== void 0) throw new TypeError("email-domain shares are not emailed");
6804
+ }
6805
+ return { target, credentialRequirement: mailboxCredentialCommitment(target) };
6806
+ }
6807
+ function assertMailboxCommitment(commitment, target) {
6808
+ const label = target.kind === "email" ? "email" : "email-domain";
6809
+ if (commitment === void 0) throw new TypeError(`${label} shares require a credential requirement`);
6810
+ const expected = mailboxCredentialCommitment(target);
6811
+ if (canonicalize(commitment.profile) !== canonicalize(expected.profile) || canonicalize(commitment.credentialType) !== canonicalize(expected.credentialType) || commitment.descriptorDigest !== expected.descriptorDigest || commitment.issuerDid !== expected.issuerDid || commitment.issuerKid !== expected.issuerKid) throw new TypeError(`${label} shares require the ${label} credential profile`);
6812
+ if (commitment.requirementDigest !== expected.requirementDigest) throw new TypeError(`credential requirement is not bound to the email ${target.kind === "email" ? "address" : "domain"}`);
6813
+ }
6814
+ async function publishAddressedShare(input) {
6815
+ assertSafeInput(input);
6816
+ const { target } = prepareAddressedShare(input);
6817
+ const options = { ...input, filename: canonicalShareFilename(input.filename) };
6818
+ if (target.kind !== "recipientDid") assertMailboxCommitment(options.credentialRequirement, target);
6751
6819
  const expiry = rfc3339Seconds(options.expiresAt);
6752
6820
  const matcher = targetMatcher(target);
6753
6821
  const capabilities = sortCanonical([
@@ -7858,17 +7926,20 @@ var ShareNotifyError = class extends Error {
7858
7926
  };
7859
7927
  function recipientMatchesShareRecord(record, recipient) {
7860
7928
  const matcher = record.recipientMatcher;
7861
- if (matcher.kind === "bearer" || matcher.kind === "recipientDid") return false;
7862
- if (matcher.kind === "exactEmail") return matcher.value === recipient;
7863
- const at = recipient.lastIndexOf("@");
7864
- return at > 0 && recipient.slice(at + 1).toLowerCase() === matcher.value;
7929
+ const mailbox = canonicalMailbox(recipient);
7930
+ if (mailbox === void 0) return false;
7931
+ if (matcher.kind === "exactEmail") return canonicalMailbox(matcher.value)?.email === mailbox.email;
7932
+ if (matcher.kind === "emailDomain") return mailbox.domain === matcher.value;
7933
+ return false;
7865
7934
  }
7866
7935
  async function notifyShare(input) {
7867
- if (!input.shareId || !input.recipient || !input.recipient.includes("@")) throw new ShareNotifyError("recipient is invalid");
7868
- if (input.record !== void 0 && !recipientMatchesShareRecord(input.record, input.recipient)) {
7936
+ const mailbox = canonicalMailbox(input.recipient);
7937
+ if (!input.shareId || mailbox === void 0) throw new ShareNotifyError("recipient is invalid");
7938
+ const recipient = mailbox.email;
7939
+ if (input.record !== void 0 && !recipientMatchesShareRecord(input.record, recipient)) {
7869
7940
  throw new ShareNotifyError("recipient does not match the stored share target");
7870
7941
  }
7871
- const idempotencyKey = input.idempotencyKey ?? await defaultIdempotencyKey(input.shareId, input.recipient);
7942
+ const idempotencyKey = input.idempotencyKey ?? await defaultIdempotencyKey(input.shareId, recipient);
7872
7943
  const attemptsLimit = input.maxAttempts ?? 3;
7873
7944
  if (!Number.isSafeInteger(attemptsLimit) || attemptsLimit < 1 || attemptsLimit > 8) throw new ShareNotifyError("maxAttempts is invalid");
7874
7945
  let attempts = 0;
@@ -7877,7 +7948,7 @@ async function notifyShare(input) {
7877
7948
  if (input.signal?.aborted) throw new ShareNotifyError("share delivery was cancelled");
7878
7949
  attempts += 1;
7879
7950
  try {
7880
- const state = await input.adapter.deliver({ shareId: input.shareId, recipient: input.recipient, idempotencyKey, ...input.record === void 0 ? {} : { record: input.record }, ...input.signal === void 0 ? {} : { signal: input.signal } });
7951
+ const state = await input.adapter.deliver({ shareId: input.shareId, recipient, idempotencyKey, ...input.record === void 0 ? {} : { record: input.record }, ...input.signal === void 0 ? {} : { signal: input.signal } });
7881
7952
  return { protocol: "tinycloud-share", version: 1, shareId: input.shareId, state, idempotencyKey, attempts };
7882
7953
  } catch (error) {
7883
7954
  lastError = error;
@@ -8606,9 +8677,11 @@ async function openNativeShare(sharing, link) {
8606
8677
  SharePublishError,
8607
8678
  ShareReceiveError,
8608
8679
  ShareRecipientClient,
8680
+ addressedCredentialRequirement,
8609
8681
  authorizationMethodForTarget,
8610
8682
  authorizeShare,
8611
8683
  canonicalOwnerSharePolicy,
8684
+ canonicalShareFilename,
8612
8685
  canonicalize,
8613
8686
  checkBearerDelegation,
8614
8687
  claimShare,
@@ -8620,6 +8693,7 @@ async function openNativeShare(sharing, link) {
8620
8693
  createShareV2HolderBindingArtifact,
8621
8694
  deliverCredentialInvitation,
8622
8695
  fromBase64Url,
8696
+ hasUnsafeFilenameCodePoint,
8623
8697
  historyRecordForPublishedShare,
8624
8698
  inspectShare,
8625
8699
  listShares,
@@ -8628,6 +8702,7 @@ async function openNativeShare(sharing, link) {
8628
8702
  openNativeShare,
8629
8703
  parseAddressedEnvelope,
8630
8704
  parseNativeShareUrl,
8705
+ prepareAddressedShare,
8631
8706
  publishAddressedShare,
8632
8707
  publishPolicyShare,
8633
8708
  publishTargetShare,