@tinycloud/share-sdk 1.0.1-beta.0 → 1.1.0-beta.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.
package/dist/index.cjs CHANGED
@@ -78,6 +78,7 @@ __export(index_exports, {
78
78
  restoreDelegatedShareKey: () => restoreDelegatedShareKey,
79
79
  resumeShareAuthorization: () => resumeShareAuthorization,
80
80
  revokeShare: () => revokeShare,
81
+ shareDeliveryWindowExpiresAt: () => shareDeliveryWindowExpiresAt,
81
82
  showShare: () => showShare,
82
83
  targetAuthorizationMethod: () => targetAuthorizationMethod,
83
84
  toBase64Url: () => toBase64Url,
@@ -7783,8 +7784,25 @@ function historyRecordForPublishedShare(result, now = /* @__PURE__ */ new Date()
7783
7784
  }
7784
7785
  var MemorySenderShareRecordStorage = class {
7785
7786
  records = /* @__PURE__ */ new Map();
7787
+ operation = Promise.resolve();
7788
+ serialize(action) {
7789
+ const next = this.operation.then(action, action);
7790
+ this.operation = next.then(() => void 0, () => void 0);
7791
+ return next;
7792
+ }
7786
7793
  async put(record) {
7787
- this.records.set(record.shareId, record);
7794
+ return this.serialize(async () => {
7795
+ this.records.set(record.shareId, record);
7796
+ });
7797
+ }
7798
+ async update(shareId, change) {
7799
+ return this.serialize(async () => {
7800
+ const current = this.records.get(shareId);
7801
+ if (current === void 0) return void 0;
7802
+ const updated = await change(current);
7803
+ this.records.set(shareId, updated);
7804
+ return updated;
7805
+ });
7788
7806
  }
7789
7807
  async list() {
7790
7808
  return [...this.records.values()];
@@ -7793,7 +7811,9 @@ var MemorySenderShareRecordStorage = class {
7793
7811
  return this.records.get(shareId);
7794
7812
  }
7795
7813
  async delete(shareId) {
7796
- this.records.delete(shareId);
7814
+ return this.serialize(async () => {
7815
+ this.records.delete(shareId);
7816
+ });
7797
7817
  }
7798
7818
  };
7799
7819
  var MemorySenderShareKeyStorage = class {
@@ -7877,6 +7897,11 @@ var SenderShareStore = class {
7877
7897
  }
7878
7898
  /** Mark a share revoked locally after the caller's node-side revoke call succeeds. */
7879
7899
  async markRevoked(shareId, revokedAt = this.now().toISOString()) {
7900
+ if (this.records.update !== void 0) {
7901
+ const updated = await this.records.update(shareId, (record2) => ({ ...record2, revokedAt }));
7902
+ if (updated === void 0) throw new Error(`Unknown share: ${shareId}`);
7903
+ return updated;
7904
+ }
7880
7905
  const record = await this.records.get(shareId);
7881
7906
  if (record === void 0) throw new Error(`Unknown share: ${shareId}`);
7882
7907
  const revoked = { ...record, revokedAt };
@@ -7924,11 +7949,12 @@ var EncryptedSenderShareHistory = class {
7924
7949
 
7925
7950
  // src/notify.ts
7926
7951
  var ShareNotifyError = class extends Error {
7927
- code = "delivery-failed";
7928
- constructor(message = "share delivery did not complete") {
7952
+ constructor(message = "share delivery did not complete", reason) {
7929
7953
  super(message);
7954
+ this.reason = reason;
7930
7955
  this.name = "ShareNotifyError";
7931
7956
  }
7957
+ code = "delivery-failed";
7932
7958
  };
7933
7959
  function recipientMatchesShareRecord(record, recipient) {
7934
7960
  const matcher = record.recipientMatcher;
@@ -7938,6 +7964,11 @@ function recipientMatchesShareRecord(record, recipient) {
7938
7964
  if (matcher.kind === "emailDomain") return mailbox.domain === matcher.value;
7939
7965
  return false;
7940
7966
  }
7967
+ function shareDeliveryWindowExpiresAt(record) {
7968
+ const expiresAt = Math.min(Date.parse(record.expiresAt), Date.parse(record.registeredAt) + 5 * 60 * 1e3);
7969
+ if (!Number.isFinite(expiresAt)) throw new ShareNotifyError("share delivery history has invalid timestamps");
7970
+ return expiresAt;
7971
+ }
7941
7972
  async function notifyShare(input) {
7942
7973
  const mailbox = canonicalMailbox(input.recipient);
7943
7974
  if (!input.shareId || mailbox === void 0) throw new ShareNotifyError("recipient is invalid");
@@ -7948,19 +7979,59 @@ async function notifyShare(input) {
7948
7979
  const idempotencyKey = input.idempotencyKey ?? await defaultIdempotencyKey(input.shareId, recipient);
7949
7980
  const attemptsLimit = input.maxAttempts ?? 3;
7950
7981
  if (!Number.isSafeInteger(attemptsLimit) || attemptsLimit < 1 || attemptsLimit > 8) throw new ShareNotifyError("maxAttempts is invalid");
7982
+ if (input.signal?.aborted) throw new ShareNotifyError("share delivery was cancelled");
7983
+ if (input.checkDeliveryWindow && input.record !== void 0 && shareDeliveryWindowExpiresAt(input.record) <= Date.now()) {
7984
+ return {
7985
+ protocol: "tinycloud-share",
7986
+ version: 1,
7987
+ shareId: input.shareId,
7988
+ state: "partial-failure",
7989
+ idempotencyKey,
7990
+ attempts: 1,
7991
+ retryable: false,
7992
+ reason: "delivery-window-expired"
7993
+ };
7994
+ }
7951
7995
  let attempts = 0;
7952
- let lastError;
7953
7996
  while (attempts < attemptsLimit) {
7954
7997
  if (input.signal?.aborted) throw new ShareNotifyError("share delivery was cancelled");
7955
7998
  attempts += 1;
7956
7999
  try {
7957
8000
  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 } });
7958
- return { protocol: "tinycloud-share", version: 1, shareId: input.shareId, state, idempotencyKey, attempts };
8001
+ return {
8002
+ protocol: "tinycloud-share",
8003
+ version: 1,
8004
+ shareId: input.shareId,
8005
+ state: state === "delivered" && input.record?.deliveredRecipients?.includes(recipient) ? "already-delivered" : state,
8006
+ idempotencyKey,
8007
+ attempts
8008
+ };
7959
8009
  } catch (error) {
7960
- lastError = error;
8010
+ if (error instanceof ShareNotifyError && error.reason === "delivery-window-expired") {
8011
+ return {
8012
+ protocol: "tinycloud-share",
8013
+ version: 1,
8014
+ shareId: input.shareId,
8015
+ state: "partial-failure",
8016
+ idempotencyKey,
8017
+ attempts,
8018
+ retryable: false,
8019
+ reason: "delivery-window-expired"
8020
+ };
8021
+ }
8022
+ if (error !== null && typeof error === "object" && "status" in error && (error.status === 401 || error.status === 403)) {
8023
+ return {
8024
+ protocol: "tinycloud-share",
8025
+ version: 1,
8026
+ shareId: input.shareId,
8027
+ state: "partial-failure",
8028
+ idempotencyKey,
8029
+ attempts,
8030
+ retryable: false
8031
+ };
8032
+ }
7961
8033
  }
7962
8034
  }
7963
- void lastError;
7964
8035
  return { protocol: "tinycloud-share", version: 1, shareId: input.shareId, state: "partial-failure", idempotencyKey, attempts, retryable: true };
7965
8036
  }
7966
8037
  async function defaultIdempotencyKey(shareId, recipient) {
@@ -8119,7 +8190,11 @@ async function revokeShare(input) {
8119
8190
  });
8120
8191
  }
8121
8192
  const revokedAt = (input.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
8122
- if (input.records !== void 0) await input.records.put({ ...input.record, revokedAt });
8193
+ if (input.records?.update !== void 0) {
8194
+ await input.records.update(input.record.shareId, (record) => ({ ...record, revokedAt }));
8195
+ } else if (input.records !== void 0) {
8196
+ await input.records.put({ ...input.record, revokedAt });
8197
+ }
8123
8198
  return { state: "revoked", target, delegationCid, revokedAt };
8124
8199
  }
8125
8200
  function redactRecord(record, revealLink, link) {
@@ -8382,6 +8457,10 @@ function parseV3InlineEncryptedEnvelope(bytes3, expected) {
8382
8457
  }
8383
8458
  return record;
8384
8459
  }
8460
+ async function recipientHttpError(response, context) {
8461
+ const body = (await response.text().catch(() => "")).trim().slice(0, 512);
8462
+ return Object.assign(new Error(`${context} (${response.status})${body ? `: ${body}` : ""}`), { status: response.status });
8463
+ }
8385
8464
  var ShareRecipientClient = class {
8386
8465
  constructor(options) {
8387
8466
  this.options = options;
@@ -8455,7 +8534,7 @@ var ShareRecipientClient = class {
8455
8534
  body: JSON.stringify({ policyCid: envelope.policyCid, recipientDid: this.options.holderDid, requestedCapabilities: envelope.policy.capabilityCeiling }),
8456
8535
  ...this.options.signal === void 0 ? {} : { signal: this.options.signal }
8457
8536
  });
8458
- if (!challengeResponse.ok) throw new Error(`v3 policy challenge rejected (${challengeResponse.status})`);
8537
+ if (!challengeResponse.ok) throw await recipientHttpError(challengeResponse, "v3 policy challenge rejected");
8459
8538
  const challenge = object3(await challengeResponse.json(), "v3 policy challenge");
8460
8539
  if (typeof challenge.challengeId !== "string" || typeof challenge.nonce !== "string" || challenge.policyCid !== envelope.policyCid || challenge.recipientDid !== this.options.holderDid || challenge.nodeAudience !== attestedEnforcer.nodeAudience || challenge.enforcerDid !== void 0 && challenge.enforcerDid !== attestedEnforcer.enforcerDid || typeof challenge.expiresAt !== "string" || !Number.isFinite(Date.parse(challenge.expiresAt)) || Date.parse(challenge.expiresAt) <= Date.now()) throw new Error("v3 policy challenge binding mismatch");
8461
8540
  const material = await this.options.buildPresentation({
@@ -8480,14 +8559,14 @@ var ShareRecipientClient = class {
8480
8559
  const requestedExpiresAt = requestedSessionExpiry(envelope.expiry);
8481
8560
  let delegationResponse = await mint(requestedExpiresAt === void 0 ? mintBody : { ...mintBody, requestedExpiresAt });
8482
8561
  if (requestedExpiresAt !== void 0 && delegationResponse.status === 422) delegationResponse = await mint(mintBody);
8483
- if (!delegationResponse.ok) throw new Error(`v3 policy delegation rejected (${delegationResponse.status})`);
8562
+ if (!delegationResponse.ok) throw await recipientHttpError(delegationResponse, "v3 policy delegation rejected");
8484
8563
  const delegation = object3(await delegationResponse.json(), "v3 policy delegation");
8485
8564
  if (delegation.admitted !== true || typeof delegation.sessionCid !== "string" || typeof delegation.authorization !== "string") throw new Error("v3 policy delegation response is not admitted");
8486
8565
  const compact = verifyV3PolicyAuthorization({ authorization: delegation.authorization, cid: delegation.sessionCid, envelope, holderDid: this.options.holderDid });
8487
8566
  const fact = compact.payload.fct[0];
8488
8567
  this.options.onStage?.("delegation-import");
8489
8568
  const imported = await this.fetchFn(new URL("/delegate", this.options.nodeOrigin), { method: "POST", redirect: "error", headers: { Authorization: delegation.authorization }, ...this.options.signal === void 0 ? {} : { signal: this.options.signal } });
8490
- if (!imported.ok) throw new Error(`ordinary delegation import rejected (${imported.status})`);
8569
+ if (!imported.ok) throw await recipientHttpError(imported, "ordinary delegation import rejected");
8491
8570
  this.signer = material.sign;
8492
8571
  this.v3Authorization = delegation.authorization;
8493
8572
  this.v3NodeAudience = fact.nodeAudience;
@@ -8522,7 +8601,7 @@ var ShareRecipientClient = class {
8522
8601
  try {
8523
8602
  this.options.onStage?.("decryption");
8524
8603
  const response = await this.fetchFn(new URL("/invoke", this.options.nodeOrigin), { method: "POST", redirect: "error", headers: { accept: "application/json", "content-type": "application/json", Authorization: invocation.authorization }, body: JSON.stringify(body), ...this.options.signal === void 0 ? {} : { signal: this.options.signal } });
8525
- if (!response.ok) throw new Error(`v3 decrypt invocation rejected (${response.status})`);
8604
+ if (!response.ok) throw await recipientHttpError(response, "v3 decrypt invocation rejected");
8526
8605
  const value = object3(await response.json(), "v3 decrypt response");
8527
8606
  const allowed = ["type", "targetNode", "networkId", "invocationCid", "encryptedSymmetricKeyHash", "receiverPublicKeyHash", "wrappedKey", "alg", "keyVersion", "requestHash", "nodeId", "nodeSignature"];
8528
8607
  if (Object.keys(value).length !== allowed.length || Object.keys(value).some((key) => !allowed.includes(key)) || value.type !== "tinycloud.encryption.decrypt-result/v1" || value.targetNode !== body.targetNode || value.nodeId !== body.targetNode || value.networkId !== body.networkId || value.invocationCid !== invocation.cid || value.encryptedSymmetricKeyHash !== body.encryptedSymmetricKeyHash || value.receiverPublicKeyHash !== receiverPublicKeyHash || value.alg !== body.alg || value.keyVersion !== body.keyVersion || value.requestHash !== hex3((0, import_sha2565.sha256)(new TextEncoder().encode(`${invocation.cid}${bodyHash}`))) || typeof value.wrappedKey !== "string" || typeof value.nodeSignature !== "string") throw new Error("v3 decrypt response binding is invalid");
@@ -8746,6 +8825,7 @@ async function openNativeShare(sharing, link) {
8746
8825
  restoreDelegatedShareKey,
8747
8826
  resumeShareAuthorization,
8748
8827
  revokeShare,
8828
+ shareDeliveryWindowExpiresAt,
8749
8829
  showShare,
8750
8830
  targetAuthorizationMethod,
8751
8831
  toBase64Url,