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

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
@@ -45,6 +45,7 @@ __export(index_exports, {
45
45
  authorizationMethodForTarget: () => authorizationMethodForTarget,
46
46
  authorizeShare: () => authorizeShare,
47
47
  canonicalOwnerSharePolicy: () => canonicalOwnerSharePolicy,
48
+ canonicalShareFilename: () => canonicalShareFilename,
48
49
  canonicalize: () => canonicalize,
49
50
  checkBearerDelegation: () => checkBearerDelegation,
50
51
  claimShare: () => claimShare,
@@ -56,6 +57,7 @@ __export(index_exports, {
56
57
  createShareV2HolderBindingArtifact: () => createShareV2HolderBindingArtifact,
57
58
  deliverCredentialInvitation: () => deliverCredentialInvitation,
58
59
  fromBase64Url: () => fromBase64Url,
60
+ hasUnsafeFilenameCodePoint: () => hasUnsafeFilenameCodePoint,
59
61
  historyRecordForPublishedShare: () => historyRecordForPublishedShare,
60
62
  inspectShare: () => inspectShare,
61
63
  listShares: () => listShares,
@@ -85,6 +87,19 @@ __export(index_exports, {
85
87
  });
86
88
  module.exports = __toCommonJS(index_exports);
87
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
+
88
103
  // ../share-envelope/dist/index.js
89
104
  var import_sha2 = require("@noble/hashes/sha2");
90
105
  var import_ed25519 = require("@noble/curves/ed25519");
@@ -6566,8 +6581,17 @@ function normalizeShareTarget(target) {
6566
6581
  function targetAuthorizationMethod(target) {
6567
6582
  return authorizationMethodForTarget(target);
6568
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
+ }
6569
6591
  async function publishTargetShare(input) {
6570
6592
  const target = normalizeShareTarget(input.target);
6593
+ const filename = publishFilename(input.filename);
6594
+ const files = input.files?.map((file) => ({ ...file, filename: publishFilename(file.filename) }));
6571
6595
  if (input.targetAdapter === void 0) {
6572
6596
  if (target.kind === "bearer") throw new SharePublishError("authority-required", "native bearer publication requires an authenticated TinyCloud node");
6573
6597
  return {
@@ -6593,9 +6617,6 @@ async function publishTargetShare(input) {
6593
6617
  return bytes3;
6594
6618
  })();
6595
6619
  if (source.byteLength === 0) throw new SharePublishError("invalid-argument", "share content is empty");
6596
- if (!input.filename || input.filename === "." || input.filename === ".." || /[/\\\u0000-\u001f\u007f]/.test(input.filename)) {
6597
- throw new SharePublishError("invalid-argument", "filename must be one safe path segment");
6598
- }
6599
6620
  if (target.kind === "bearer" && input.allowBinary !== true) {
6600
6621
  try {
6601
6622
  new TextDecoder("utf-8", { fatal: true }).decode(source);
@@ -6603,10 +6624,10 @@ async function publishTargetShare(input) {
6603
6624
  throw new SharePublishError("invalid-argument", "Markdown input must be valid UTF-8");
6604
6625
  }
6605
6626
  }
6606
- 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;
6607
6628
  const limit = input.maxBytes ?? SHARE_CONTENT_LIMIT;
6608
6629
  let totalBytes = 0;
6609
- for (const file of files) {
6630
+ for (const file of publishFiles) {
6610
6631
  totalBytes += file.bytes.byteLength;
6611
6632
  if (!Number.isSafeInteger(totalBytes) || totalBytes > limit) {
6612
6633
  throw new SharePublishError("max-bytes-exceeded", "share publication exceeds the combined byte limit");
@@ -6617,8 +6638,8 @@ async function publishTargetShare(input) {
6617
6638
  if (!Number.isFinite(expiresAt.getTime()) || expiresAt.getTime() <= nowMs) throw new SharePublishError("invalid-argument", "expiresAt must be a valid future time");
6618
6639
  return input.targetAdapter.publish({
6619
6640
  source,
6620
- filename: input.filename,
6621
- ...input.files === void 0 ? {} : { files: input.files },
6641
+ filename,
6642
+ ...files === void 0 ? {} : { files },
6622
6643
  ...input.mediaType === void 0 ? {} : { mediaType: input.mediaType },
6623
6644
  ...input.resourceKind === void 0 ? {} : { resourceKind: input.resourceKind },
6624
6645
  ...input.actions === void 0 ? {} : { actions: input.actions },
@@ -6646,6 +6667,7 @@ function targetMatcher(target) {
6646
6667
  function targetKind(target) {
6647
6668
  return target.kind;
6648
6669
  }
6670
+ var OWNER_SHARE_ACTIONS = /* @__PURE__ */ new Set(["tinycloud.kv/get", "tinycloud.kv/list", "tinycloud.kv/metadata", "tinycloud.kv/put"]);
6649
6671
  function assertSafeInput(input) {
6650
6672
  if (!/^[A-Za-z0-9_-]{16,128}$/.test(input.shareId)) throw new TypeError("addressed share id is invalid");
6651
6673
  if (!Number.isSafeInteger(input.byteLength) || input.byteLength < 0 || input.byteLength > SHARE_CONTENT_LIMIT) throw new TypeError("addressed content length is invalid");
@@ -6768,14 +6790,18 @@ function addressedCredentialRequirement(target) {
6768
6790
  return mailboxCredentialCommitment(normalized);
6769
6791
  }
6770
6792
  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");
6793
+ try {
6794
+ canonicalShareFilename(request.filename);
6795
+ } catch {
6796
+ throw new TypeError("addressed filename is invalid");
6797
+ }
6772
6798
  if (request.actions.length === 0 || request.policyActions.length === 0) throw new TypeError("addressed share actions are empty");
6799
+ if (request.policyActions.some((action) => !OWNER_SHARE_ACTIONS.has(action))) throw new TypeError("addressed share action is not supported");
6773
6800
  const target = normalizeShareTarget(request.target);
6774
6801
  if (target.kind === "bearer") throw new TypeError("addressed target is required");
6775
6802
  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");
6803
+ if (target.kind === "emailDomain" && request.deliveryEmail !== void 0 && (request.deliveryEmail !== request.deliveryEmail.toLowerCase() || !request.deliveryEmail.endsWith(`@${target.domain}`))) {
6804
+ throw new TypeError("email-domain delivery address must be a lowercase mailbox at the domain");
6779
6805
  }
6780
6806
  return { target, credentialRequirement: mailboxCredentialCommitment(target) };
6781
6807
  }
@@ -6786,9 +6812,10 @@ function assertMailboxCommitment(commitment, target) {
6786
6812
  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
6813
  if (commitment.requirementDigest !== expected.requirementDigest) throw new TypeError(`credential requirement is not bound to the email ${target.kind === "email" ? "address" : "domain"}`);
6788
6814
  }
6789
- async function publishAddressedShare(options) {
6790
- assertSafeInput(options);
6791
- const { target } = prepareAddressedShare(options);
6815
+ async function publishAddressedShare(input) {
6816
+ assertSafeInput(input);
6817
+ const { target } = prepareAddressedShare(input);
6818
+ const options = { ...input, filename: canonicalShareFilename(input.filename) };
6792
6819
  if (target.kind !== "recipientDid") assertMailboxCommitment(options.credentialRequirement, target);
6793
6820
  const expiry = rfc3339Seconds(options.expiresAt);
6794
6821
  const matcher = targetMatcher(target);
@@ -8283,9 +8310,28 @@ function verifyV3PolicyAuthorization(input) {
8283
8310
  const binding = object3(input.envelope.attestedEnforcerBinding, "v3 attested enforcer binding");
8284
8311
  const policy = object3(input.envelope.policy, "v3 policy");
8285
8312
  const now = Math.floor(Date.now() / 1e3);
8286
- if (!legacyShape && !v4Shape || POLICY_SESSION_DIGEST_FACT_KEYS.some((key) => !LOWER_SHA256_HEX.test(fact[key])) || v4Shape && (!LOWER_SHA256_HEX.test(fact.credentialIdAuditDigestHex) || !LOWER_SHA256_HEX.test(fact.presentationJtiAuditDigestHex)) || compact.payload.aud !== input.holderDid || compact.payload.iss.split("#", 1)[0] !== fact.nodeAudience || fact.profile !== "policy-session-ucan/v1" || fact.ownerDid !== policy.ownerDid || fact.policyId !== policy.policyId || fact.policyCid !== input.envelope.policyCid || fact.contentSourceDigestHex !== input.envelope.contentSourceDigestHex || fact.enforcerDid !== binding.enforcerDid || fact.nodeAudience !== binding.nodeAudience || fact.recipientDid !== input.holderDid || fact.policyDelegationCid !== input.envelope.policyRoot.cid || fact.enforcementDelegationCid !== input.envelope.enforcementRoot.cid || typeof fact.remainingRedelegationDepth !== "number" || !Number.isInteger(fact.remainingRedelegationDepth) || fact.remainingRedelegationDepth < 0 || fact.remainingRedelegationDepth > 8 || compact.payload.prf.length !== 2 || compact.payload.prf[0] !== input.envelope.policyRoot.cid || compact.payload.prf[1] !== input.envelope.enforcementRoot.cid || compact.payload.nbf > now || compact.payload.exp <= now || compact.payload.exp - compact.payload.nbf > 60 || canonicalize(compact.payload.att) !== canonicalize(policyAttenuationForV3(input.envelope))) throw new Error("v3 policy delegation signed binding mismatch");
8313
+ if (!legacyShape && !v4Shape || POLICY_SESSION_DIGEST_FACT_KEYS.some((key) => !LOWER_SHA256_HEX.test(fact[key])) || v4Shape && (!LOWER_SHA256_HEX.test(fact.credentialIdAuditDigestHex) || !LOWER_SHA256_HEX.test(fact.presentationJtiAuditDigestHex)) || compact.payload.aud !== input.holderDid || compact.payload.iss.split("#", 1)[0] !== fact.nodeAudience || fact.profile !== "policy-session-ucan/v1" || fact.ownerDid !== policy.ownerDid || fact.policyId !== policy.policyId || fact.policyCid !== input.envelope.policyCid || fact.contentSourceDigestHex !== input.envelope.contentSourceDigestHex || fact.enforcerDid !== binding.enforcerDid || fact.nodeAudience !== binding.nodeAudience || fact.recipientDid !== input.holderDid || fact.policyDelegationCid !== input.envelope.policyRoot.cid || fact.enforcementDelegationCid !== input.envelope.enforcementRoot.cid || typeof fact.remainingRedelegationDepth !== "number" || !Number.isInteger(fact.remainingRedelegationDepth) || fact.remainingRedelegationDepth < 0 || fact.remainingRedelegationDepth > 8 || compact.payload.prf.length !== 2 || compact.payload.prf[0] !== input.envelope.policyRoot.cid || compact.payload.prf[1] !== input.envelope.enforcementRoot.cid || compact.payload.nbf > now || compact.payload.exp <= now || !(compact.payload.exp * 1e3 <= Date.parse(input.envelope.expiry)) || canonicalize(compact.payload.att) !== canonicalize(policyAttenuationForV3(input.envelope))) throw new Error("v3 policy delegation signed binding mismatch");
8287
8314
  return compact;
8288
8315
  }
8316
+ function verifyV3PolicyDescendants(session, descendants, holderDid) {
8317
+ let parent = session;
8318
+ for (const link of descendants) {
8319
+ const child = verifyCompactUcanAuthorization(link.authorization, link.cid);
8320
+ const { remainingRedelegationDepth: parentDepth, ...parentFacts } = parent.payload.fct[0];
8321
+ const { remainingRedelegationDepth: depth, ...facts } = child.payload.fct[0];
8322
+ if (child.payload.iss.split("#", 1)[0] !== parent.payload.aud || child.payload.prf.length !== 1 || child.payload.prf[0] !== parent.cid || child.payload.nbf <= parent.payload.nbf || child.payload.exp >= parent.payload.exp || typeof parentDepth !== "number" || parentDepth <= 0 || depth !== parentDepth - 1 || canonicalize(facts) !== canonicalize(parentFacts) || canonicalize(child.payload.att) !== canonicalize(parent.payload.att)) throw new Error("v3 policy descendant does not extend its parent");
8323
+ parent = child;
8324
+ }
8325
+ const now = Math.floor(Date.now() / 1e3);
8326
+ if (parent.payload.aud !== holderDid) throw new Error("v3 policy delegation chain does not end at this holder");
8327
+ if (parent.payload.nbf > now || parent.payload.exp <= now) throw new Error("v3 policy delegation chain is not currently valid");
8328
+ return parent;
8329
+ }
8330
+ function requestedSessionExpiry(expiry) {
8331
+ const seconds = Math.floor(Date.parse(expiry) / 1e3);
8332
+ if (!Number.isFinite(seconds) || seconds <= Math.floor(Date.now() / 1e3) + 60) return void 0;
8333
+ return new Date(seconds * 1e3).toISOString().replace(/\.\d{3}Z$/, "Z");
8334
+ }
8289
8335
  function hex3(value) {
8290
8336
  return [...value].map((byte) => byte.toString(16).padStart(2, "0")).join("");
8291
8337
  }
@@ -8338,14 +8384,17 @@ var ShareRecipientClient = class {
8338
8384
  this.signer = options.sign;
8339
8385
  if (options.policyAuthorization !== void 0) {
8340
8386
  if (options.envelope.version !== 3) throw new Error("policy delegation fast path requires a v3 envelope");
8341
- const compact = verifyV3PolicyAuthorization({ ...options.policyAuthorization, envelope: options.envelope, holderDid: options.holderDid });
8387
+ const { authorization, cid: cid2, descendants = [] } = options.policyAuthorization;
8388
+ const sessionHolder = descendants.length === 0 ? options.holderDid : verifyCompactUcanAuthorization(authorization, cid2).payload.aud;
8389
+ const compact = verifyV3PolicyAuthorization({ authorization, cid: cid2, envelope: options.envelope, holderDid: sessionHolder });
8390
+ const leaf = verifyV3PolicyDescendants(compact, descendants, options.holderDid);
8342
8391
  const nodeAudience = compact.payload.fct[0].nodeAudience;
8343
- this.v3Authorization = options.policyAuthorization.authorization;
8392
+ this.v3Authorization = leaf.authorization;
8344
8393
  this.v3NodeAudience = nodeAudience;
8345
8394
  this.nativeSigner = options.sign;
8346
8395
  this.session = {
8347
- sessionId: options.policyAuthorization.cid,
8348
- expiresAt: new Date(compact.payload.exp * 1e3).toISOString(),
8396
+ sessionId: leaf.cid,
8397
+ expiresAt: new Date(leaf.payload.exp * 1e3).toISOString(),
8349
8398
  actions: options.envelope.actions,
8350
8399
  resource: options.envelope.resource
8351
8400
  };
@@ -8415,13 +8464,17 @@ var ShareRecipientClient = class {
8415
8464
  throw new Error(accountless ? "v4 ceremony requires a verified credential and requirement" : "v3 ceremony requires a claim");
8416
8465
  }
8417
8466
  this.options.onStage?.("policy-admission");
8418
- const delegationResponse = await this.fetchFn(new URL("/policy/v3/delegations", this.options.nodeOrigin), {
8467
+ const mintBody = accountless ? { policyCid: envelope.policyCid, challengeId: challenge.challengeId, nonce: challenge.nonce, requirement: material.requirement, credential: material.credentialEnvelope, presentation: material.presentation } : { policyCid: envelope.policyCid, challengeId: challenge.challengeId, nonce: challenge.nonce, claim: material.claim, presentation: material.presentation };
8468
+ const mint = (body) => this.fetchFn(new URL("/policy/v3/delegations", this.options.nodeOrigin), {
8419
8469
  method: "POST",
8420
8470
  redirect: "error",
8421
8471
  headers: { accept: "application/json", "content-type": "application/json" },
8422
- body: JSON.stringify(accountless ? { policyCid: envelope.policyCid, challengeId: challenge.challengeId, nonce: challenge.nonce, requirement: material.requirement, credential: material.credentialEnvelope, presentation: material.presentation } : { policyCid: envelope.policyCid, challengeId: challenge.challengeId, nonce: challenge.nonce, claim: material.claim, presentation: material.presentation }),
8472
+ body: JSON.stringify(body),
8423
8473
  ...this.options.signal === void 0 ? {} : { signal: this.options.signal }
8424
8474
  });
8475
+ const requestedExpiresAt = requestedSessionExpiry(envelope.expiry);
8476
+ let delegationResponse = await mint(requestedExpiresAt === void 0 ? mintBody : { ...mintBody, requestedExpiresAt });
8477
+ if (requestedExpiresAt !== void 0 && delegationResponse.status === 422) delegationResponse = await mint(mintBody);
8425
8478
  if (!delegationResponse.ok) throw new Error(`v3 policy delegation rejected (${delegationResponse.status})`);
8426
8479
  const delegation = object3(await delegationResponse.json(), "v3 policy delegation");
8427
8480
  if (delegation.admitted !== true || typeof delegation.sessionCid !== "string" || typeof delegation.authorization !== "string") throw new Error("v3 policy delegation response is not admitted");
@@ -8655,6 +8708,7 @@ async function openNativeShare(sharing, link) {
8655
8708
  authorizationMethodForTarget,
8656
8709
  authorizeShare,
8657
8710
  canonicalOwnerSharePolicy,
8711
+ canonicalShareFilename,
8658
8712
  canonicalize,
8659
8713
  checkBearerDelegation,
8660
8714
  claimShare,
@@ -8666,6 +8720,7 @@ async function openNativeShare(sharing, link) {
8666
8720
  createShareV2HolderBindingArtifact,
8667
8721
  deliverCredentialInvitation,
8668
8722
  fromBase64Url,
8723
+ hasUnsafeFilenameCodePoint,
8669
8724
  historyRecordForPublishedShare,
8670
8725
  inspectShare,
8671
8726
  listShares,