@tinycloud/share-sdk 1.0.0-beta.5 → 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");
@@ -6099,6 +6116,7 @@ function redactPublishedShare(result) {
6099
6116
  target: { ...result.metadata.target },
6100
6117
  resource: { ...result.metadata.resource },
6101
6118
  actions: [...result.metadata.actions],
6119
+ ...result.metadata.expiryClamped === void 0 ? {} : { expiryClamped: result.metadata.expiryClamped },
6102
6120
  expiresAt: result.metadata.expiresAt,
6103
6121
  display: { ...result.metadata.display }
6104
6122
  }
@@ -6508,12 +6526,6 @@ var base58flickr2 = baseX2({
6508
6526
  });
6509
6527
 
6510
6528
  // src/targets.ts
6511
- function validEmail(value) {
6512
- 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);
6513
- }
6514
- function validDomain(value) {
6515
- 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);
6516
- }
6517
6529
  function validRecipientDid(value) {
6518
6530
  if (value.length === 0 || value.length > 2048 || /[\u0000-\u0020\u007f]/.test(value)) return false;
6519
6531
  const parts = value.split(":");
@@ -6543,12 +6555,13 @@ function normalizeShareTarget(target) {
6543
6555
  return { kind: target.kind, did: target.did };
6544
6556
  }
6545
6557
  if (target.kind === "email") {
6546
- if (!validEmail(target.address)) throw new TypeError("recipient email is invalid");
6547
- const at = target.address.lastIndexOf("@");
6548
- 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 };
6549
6561
  }
6550
- if (!validDomain(target.domain)) throw new TypeError("recipient email domain is invalid");
6551
- 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 };
6552
6565
  }
6553
6566
  function targetAuthorizationMethod(target) {
6554
6567
  return authorizationMethodForTarget(target);
@@ -6611,6 +6624,7 @@ async function publishTargetShare(input) {
6611
6624
  ...input.actions === void 0 ? {} : { actions: input.actions },
6612
6625
  target,
6613
6626
  expiresAt,
6627
+ expiryWasExplicit: input.expiryWasExplicit ?? input.expiresAt !== void 0,
6614
6628
  origin: input.origin,
6615
6629
  ...input.notify === void 0 ? {} : { notify: input.notify }
6616
6630
  });
@@ -6634,9 +6648,7 @@ function targetKind(target) {
6634
6648
  }
6635
6649
  function assertSafeInput(input) {
6636
6650
  if (!/^[A-Za-z0-9_-]{16,128}$/.test(input.shareId)) throw new TypeError("addressed share id is invalid");
6637
- if (input.filename.length === 0 || input.filename === "." || input.filename === ".." || /[/\\\u0000-\u001f\u007f]/.test(input.filename)) throw new TypeError("addressed filename is invalid");
6638
6651
  if (!Number.isSafeInteger(input.byteLength) || input.byteLength < 0 || input.byteLength > SHARE_CONTENT_LIMIT) throw new TypeError("addressed content length is invalid");
6639
- if (input.actions.length === 0 || input.policyActions.length === 0) throw new TypeError("addressed share actions are empty");
6640
6652
  if (!Number.isFinite(input.expiresAt.getTime()) || input.expiresAt.getTime() <= Date.now()) throw new TypeError("addressed share expiry must be in the future");
6641
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");
6642
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");
@@ -6729,23 +6741,55 @@ function publicationResult(input) {
6729
6741
  Object.defineProperty(result, "deliveryMaterial", { enumerable: false, value: input.deliveryMaterial });
6730
6742
  return result;
6731
6743
  }
6744
+ var EMAIL_PROFILE = { id: "tinycloud.email-proof/v1", version: 1 };
6732
6745
  var EMAIL_DOMAIN_PROFILE = { id: "tinycloud.email-domain-proof/v1", version: 1 };
6733
6746
  var EMAIL_CREDENTIAL_TYPE = { id: "opencredentials.email/v1", version: 1 };
6747
+ var EMAIL_DESCRIPTOR_DIGEST = "1tg-qphmKBVtNwzVg9xyz-xxqt_xtMXAsQyXw46m8S0";
6734
6748
  var EMAIL_DOMAIN_DESCRIPTOR_DIGEST = "33X5mAkZZgApdD3xh_T-3KS5moop0J2Nloi2nqWsdWY";
6735
- function assertEmailDomainPolicy(options, domain) {
6736
- 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");
6737
- if (options.deliveryEmail !== void 0) throw new TypeError("email-domain shares are not emailed");
6738
- const commitment = options.credentialRequirement;
6739
- if (commitment === void 0) throw new TypeError("email-domain shares require a credential requirement");
6740
- 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");
6741
- const expected = { type: "TinyCloudCredentialRequirement", version: 1, profile: EMAIL_DOMAIN_PROFILE, credentialType: EMAIL_CREDENTIAL_TYPE, claims: { emailDomain: domain }, maxAgeSeconds: 300 };
6742
- 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"}`);
6743
6788
  }
6744
6789
  async function publishAddressedShare(options) {
6745
6790
  assertSafeInput(options);
6746
- const target = normalizeShareTarget(options.target);
6747
- if (target.kind === "bearer") throw new TypeError("addressed target is required");
6748
- if (target.kind === "emailDomain") assertEmailDomainPolicy(options, target.domain);
6791
+ const { target } = prepareAddressedShare(options);
6792
+ if (target.kind !== "recipientDid") assertMailboxCommitment(options.credentialRequirement, target);
6749
6793
  const expiry = rfc3339Seconds(options.expiresAt);
6750
6794
  const matcher = targetMatcher(target);
6751
6795
  const capabilities = sortCanonical([
@@ -7856,17 +7900,20 @@ var ShareNotifyError = class extends Error {
7856
7900
  };
7857
7901
  function recipientMatchesShareRecord(record, recipient) {
7858
7902
  const matcher = record.recipientMatcher;
7859
- if (matcher.kind === "bearer" || matcher.kind === "recipientDid") return false;
7860
- if (matcher.kind === "exactEmail") return matcher.value === recipient;
7861
- const at = recipient.lastIndexOf("@");
7862
- 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;
7863
7908
  }
7864
7909
  async function notifyShare(input) {
7865
- if (!input.shareId || !input.recipient || !input.recipient.includes("@")) throw new ShareNotifyError("recipient is invalid");
7866
- 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)) {
7867
7914
  throw new ShareNotifyError("recipient does not match the stored share target");
7868
7915
  }
7869
- const idempotencyKey = input.idempotencyKey ?? await defaultIdempotencyKey(input.shareId, input.recipient);
7916
+ const idempotencyKey = input.idempotencyKey ?? await defaultIdempotencyKey(input.shareId, recipient);
7870
7917
  const attemptsLimit = input.maxAttempts ?? 3;
7871
7918
  if (!Number.isSafeInteger(attemptsLimit) || attemptsLimit < 1 || attemptsLimit > 8) throw new ShareNotifyError("maxAttempts is invalid");
7872
7919
  let attempts = 0;
@@ -7875,7 +7922,7 @@ async function notifyShare(input) {
7875
7922
  if (input.signal?.aborted) throw new ShareNotifyError("share delivery was cancelled");
7876
7923
  attempts += 1;
7877
7924
  try {
7878
- 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 } });
7879
7926
  return { protocol: "tinycloud-share", version: 1, shareId: input.shareId, state, idempotencyKey, attempts };
7880
7927
  } catch (error) {
7881
7928
  lastError = error;
@@ -8558,7 +8605,11 @@ function canonicalViewerUrl(viewerOrigin) {
8558
8605
  async function createNativeShare(sharing, input) {
8559
8606
  if (!input.path || input.path.startsWith("/") || input.path.endsWith("/") || input.path.includes("..") || input.path.includes("//")) throw new TypeError("native share path must be one canonical TinyCloud KV key");
8560
8607
  const generated = await sharing.generate({ path: input.path, actions: ["tinycloud.kv/get"], expiry: input.expiresAt });
8561
- if (generated.ok !== true) throw new Error(typeof generated.error?.message === "string" ? generated.error.message : "TinyCloud sharing service rejected delegation generation");
8608
+ if (generated.ok !== true) {
8609
+ const error = new Error(typeof generated.error?.message === "string" ? generated.error.message : "TinyCloud sharing service rejected delegation generation");
8610
+ if (typeof generated.error?.code === "string") Object.assign(error, { code: generated.error.code });
8611
+ throw error;
8612
+ }
8562
8613
  if (typeof generated.data?.token !== "string" || typeof generated.data.delegation?.cid !== "string" || !(generated.data.expiresAt instanceof Date)) throw new Error("TinyCloud sharing service returned incomplete delegation metadata");
8563
8614
  const delegation = sharing.decodeLink(generated.data.token);
8564
8615
  if (typeof delegation.spaceId !== "string" || delegation.spaceId.length === 0 || typeof delegation.path !== "string" || delegation.path.length === 0) throw new Error("TinyCloud sharing service returned a non-canonical delegation authority");
@@ -8600,6 +8651,7 @@ async function openNativeShare(sharing, link) {
8600
8651
  SharePublishError,
8601
8652
  ShareReceiveError,
8602
8653
  ShareRecipientClient,
8654
+ addressedCredentialRequirement,
8603
8655
  authorizationMethodForTarget,
8604
8656
  authorizeShare,
8605
8657
  canonicalOwnerSharePolicy,
@@ -8622,6 +8674,7 @@ async function openNativeShare(sharing, link) {
8622
8674
  openNativeShare,
8623
8675
  parseAddressedEnvelope,
8624
8676
  parseNativeShareUrl,
8677
+ prepareAddressedShare,
8625
8678
  publishAddressedShare,
8626
8679
  publishPolicyShare,
8627
8680
  publishTargetShare,