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

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,6 +41,7 @@ __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,
@@ -63,6 +64,7 @@ __export(index_exports, {
63
64
  openNativeShare: () => openNativeShare,
64
65
  parseAddressedEnvelope: () => parseAddressedEnvelope,
65
66
  parseNativeShareUrl: () => parseNativeShareUrl,
67
+ prepareAddressedShare: () => prepareAddressedShare,
66
68
  publishAddressedShare: () => publishAddressedShare,
67
69
  publishPolicyShare: () => publishPolicyShare,
68
70
  publishTargetShare: () => publishTargetShare,
@@ -5764,6 +5766,21 @@ function serialize(value) {
5764
5766
  function canonicalize(value) {
5765
5767
  return serialize(value);
5766
5768
  }
5769
+ var LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
5770
+ function isCanonicalEmailDomain(value) {
5771
+ if (value.length === 0 || value.length > 253) return false;
5772
+ const labels = value.split(".");
5773
+ return labels.length >= 2 && labels.every((label) => LABEL.test(label)) && !/^[0-9]+$/.test(labels.at(-1));
5774
+ }
5775
+ function canonicalMailbox(value) {
5776
+ const email = value.trim().toLowerCase();
5777
+ const at = email.indexOf("@");
5778
+ if (at <= 0 || at !== email.lastIndexOf("@")) return void 0;
5779
+ const local = email.slice(0, at);
5780
+ const domain = email.slice(at + 1);
5781
+ if (local.length > 64 || !/^[a-z0-9#$&'*+/=?^_`{|}~-]+(?:\.[a-z0-9#$&'*+/=?^_`{|}~-]+)*$/.test(local) || !isCanonicalEmailDomain(domain)) return void 0;
5782
+ return Object.freeze({ email, domain });
5783
+ }
5767
5784
  async function signCompactUcanAuthorization(input) {
5768
5785
  if (input.notBefore >= input.expiresAt || input.expiresAt - input.notBefore > 60) {
5769
5786
  throw new TypeError("compact invocation lifetime must be between one and 60 seconds");
@@ -6509,12 +6526,6 @@ var base58flickr2 = baseX2({
6509
6526
  });
6510
6527
 
6511
6528
  // 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
6529
  function validRecipientDid(value) {
6519
6530
  if (value.length === 0 || value.length > 2048 || /[\u0000-\u0020\u007f]/.test(value)) return false;
6520
6531
  const parts = value.split(":");
@@ -6544,12 +6555,13 @@ function normalizeShareTarget(target) {
6544
6555
  return { kind: target.kind, did: target.did };
6545
6556
  }
6546
6557
  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()}` };
6558
+ const mailbox = canonicalMailbox(target.address);
6559
+ if (mailbox === void 0) throw new TypeError("recipient email is invalid");
6560
+ return { kind: target.kind, address: mailbox.email };
6550
6561
  }
6551
- if (!validDomain(target.domain)) throw new TypeError("recipient email domain is invalid");
6552
- return { kind: target.kind, domain: target.domain.toLowerCase() };
6562
+ const domain = target.domain.toLowerCase();
6563
+ if (!isCanonicalEmailDomain(domain)) throw new TypeError("recipient email domain is invalid");
6564
+ return { kind: target.kind, domain };
6553
6565
  }
6554
6566
  function targetAuthorizationMethod(target) {
6555
6567
  return authorizationMethodForTarget(target);
@@ -6636,9 +6648,7 @@ function targetKind(target) {
6636
6648
  }
6637
6649
  function assertSafeInput(input) {
6638
6650
  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
6651
  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
6652
  if (!Number.isFinite(input.expiresAt.getTime()) || input.expiresAt.getTime() <= Date.now()) throw new TypeError("addressed share expiry must be in the future");
6643
6653
  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
6654
  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 +6741,55 @@ function publicationResult(input) {
6731
6741
  Object.defineProperty(result, "deliveryMaterial", { enumerable: false, value: input.deliveryMaterial });
6732
6742
  return result;
6733
6743
  }
6744
+ var EMAIL_PROFILE = { id: "tinycloud.email-proof/v1", version: 1 };
6734
6745
  var EMAIL_DOMAIN_PROFILE = { id: "tinycloud.email-domain-proof/v1", version: 1 };
6735
6746
  var EMAIL_CREDENTIAL_TYPE = { id: "opencredentials.email/v1", version: 1 };
6747
+ var EMAIL_DESCRIPTOR_DIGEST = "1tg-qphmKBVtNwzVg9xyz-xxqt_xtMXAsQyXw46m8S0";
6736
6748
  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");
6749
+ var EMAIL_CREDENTIAL_ISSUER_DID = "did:web:issuer.credentials.org";
6750
+ var EMAIL_CREDENTIAL_ISSUER_KID = `${EMAIL_CREDENTIAL_ISSUER_DID}#controller`;
6751
+ function mailboxCredentialCommitment(target) {
6752
+ const profile = target.kind === "email" ? EMAIL_PROFILE : EMAIL_DOMAIN_PROFILE;
6753
+ 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 };
6754
+ return {
6755
+ type: "TinyCloudPolicyCredentialRequirement",
6756
+ version: 1,
6757
+ requirementDigest: toBase64Url((0, import_sha2562.sha256)(textEncoder.encode(canonicalize(requirement)))),
6758
+ descriptorDigest: target.kind === "email" ? EMAIL_DESCRIPTOR_DIGEST : EMAIL_DOMAIN_DESCRIPTOR_DIGEST,
6759
+ issuerDid: EMAIL_CREDENTIAL_ISSUER_DID,
6760
+ issuerKid: EMAIL_CREDENTIAL_ISSUER_KID,
6761
+ profile,
6762
+ credentialType: EMAIL_CREDENTIAL_TYPE
6763
+ };
6764
+ }
6765
+ function addressedCredentialRequirement(target) {
6766
+ const normalized = normalizeShareTarget(target);
6767
+ if (normalized.kind !== "email" && normalized.kind !== "emailDomain") throw new TypeError("a mailbox target is required");
6768
+ return mailboxCredentialCommitment(normalized);
6769
+ }
6770
+ function prepareAddressedShare(request) {
6771
+ if (request.filename.length === 0 || request.filename === "." || request.filename === ".." || /[/\\\u0000-\u001f\u007f]/.test(request.filename)) throw new TypeError("addressed filename is invalid");
6772
+ if (request.actions.length === 0 || request.policyActions.length === 0) throw new TypeError("addressed share actions are empty");
6773
+ const target = normalizeShareTarget(request.target);
6774
+ if (target.kind === "bearer") throw new TypeError("addressed target is required");
6775
+ if (target.kind === "recipientDid") return { target };
6776
+ if (target.kind === "emailDomain") {
6777
+ 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");
6778
+ if (request.deliveryEmail !== void 0) throw new TypeError("email-domain shares are not emailed");
6779
+ }
6780
+ return { target, credentialRequirement: mailboxCredentialCommitment(target) };
6781
+ }
6782
+ function assertMailboxCommitment(commitment, target) {
6783
+ const label = target.kind === "email" ? "email" : "email-domain";
6784
+ if (commitment === void 0) throw new TypeError(`${label} shares require a credential requirement`);
6785
+ const expected = mailboxCredentialCommitment(target);
6786
+ 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`);
6787
+ if (commitment.requirementDigest !== expected.requirementDigest) throw new TypeError(`credential requirement is not bound to the email ${target.kind === "email" ? "address" : "domain"}`);
6745
6788
  }
6746
6789
  async function publishAddressedShare(options) {
6747
6790
  assertSafeInput(options);
6748
- const target = normalizeShareTarget(options.target);
6749
- if (target.kind === "bearer") throw new TypeError("addressed target is required");
6750
- if (target.kind === "emailDomain") assertEmailDomainPolicy(options, target.domain);
6791
+ const { target } = prepareAddressedShare(options);
6792
+ if (target.kind !== "recipientDid") assertMailboxCommitment(options.credentialRequirement, target);
6751
6793
  const expiry = rfc3339Seconds(options.expiresAt);
6752
6794
  const matcher = targetMatcher(target);
6753
6795
  const capabilities = sortCanonical([
@@ -7858,17 +7900,20 @@ var ShareNotifyError = class extends Error {
7858
7900
  };
7859
7901
  function recipientMatchesShareRecord(record, recipient) {
7860
7902
  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;
7903
+ const mailbox = canonicalMailbox(recipient);
7904
+ if (mailbox === void 0) return false;
7905
+ if (matcher.kind === "exactEmail") return canonicalMailbox(matcher.value)?.email === mailbox.email;
7906
+ if (matcher.kind === "emailDomain") return mailbox.domain === matcher.value;
7907
+ return false;
7865
7908
  }
7866
7909
  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)) {
7910
+ const mailbox = canonicalMailbox(input.recipient);
7911
+ if (!input.shareId || mailbox === void 0) throw new ShareNotifyError("recipient is invalid");
7912
+ const recipient = mailbox.email;
7913
+ if (input.record !== void 0 && !recipientMatchesShareRecord(input.record, recipient)) {
7869
7914
  throw new ShareNotifyError("recipient does not match the stored share target");
7870
7915
  }
7871
- const idempotencyKey = input.idempotencyKey ?? await defaultIdempotencyKey(input.shareId, input.recipient);
7916
+ const idempotencyKey = input.idempotencyKey ?? await defaultIdempotencyKey(input.shareId, recipient);
7872
7917
  const attemptsLimit = input.maxAttempts ?? 3;
7873
7918
  if (!Number.isSafeInteger(attemptsLimit) || attemptsLimit < 1 || attemptsLimit > 8) throw new ShareNotifyError("maxAttempts is invalid");
7874
7919
  let attempts = 0;
@@ -7877,7 +7922,7 @@ async function notifyShare(input) {
7877
7922
  if (input.signal?.aborted) throw new ShareNotifyError("share delivery was cancelled");
7878
7923
  attempts += 1;
7879
7924
  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 } });
7925
+ 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
7926
  return { protocol: "tinycloud-share", version: 1, shareId: input.shareId, state, idempotencyKey, attempts };
7882
7927
  } catch (error) {
7883
7928
  lastError = error;
@@ -8606,6 +8651,7 @@ async function openNativeShare(sharing, link) {
8606
8651
  SharePublishError,
8607
8652
  ShareReceiveError,
8608
8653
  ShareRecipientClient,
8654
+ addressedCredentialRequirement,
8609
8655
  authorizationMethodForTarget,
8610
8656
  authorizeShare,
8611
8657
  canonicalOwnerSharePolicy,
@@ -8628,6 +8674,7 @@ async function openNativeShare(sharing, link) {
8628
8674
  openNativeShare,
8629
8675
  parseAddressedEnvelope,
8630
8676
  parseNativeShareUrl,
8677
+ prepareAddressedShare,
8631
8678
  publishAddressedShare,
8632
8679
  publishPolicyShare,
8633
8680
  publishTargetShare,