@tinycloud/share-sdk 1.4.1-beta.0 → 1.4.1-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.
@@ -1,3 +1,3 @@
1
- export { f as ShareMetadata, p as ShareRecipientClient, s as inspectShare } from './web-recipient-entry-CiUzdSQs.cjs';
1
+ export { f as ShareMetadata, r as ShareRecipientClient, u as inspectShare } from './web-recipient-entry-Bn3yRB7g.cjs';
2
2
  import '@tinycloud/share-envelope';
3
3
  import './authorization-CmP1skO8.cjs';
@@ -1,3 +1,3 @@
1
- export { f as ShareMetadata, p as ShareRecipientClient, s as inspectShare } from './web-recipient-entry-DVyTQBl0.js';
1
+ export { f as ShareMetadata, r as ShareRecipientClient, u as inspectShare } from './web-recipient-entry-CTFoIH8l.js';
2
2
  import '@tinycloud/share-envelope';
3
3
  import './authorization-CmP1skO8.js';
@@ -5225,6 +5225,23 @@ var unifiedEncryptionNetworkSchema = external_exports.string().refine((value) =>
5225
5225
  const network = separator < 0 ? "" : rest.slice(separator + 1);
5226
5226
  return owner.startsWith("did:") && owner.length > 4 && network.length > 0 && !/[:/%?#\s]/.test(network);
5227
5227
  }, { message: "expected canonical TinyCloud encryption network" });
5228
+ var unifiedEncryptionResourceSchema = external_exports.string().refine((value) => {
5229
+ const prefix = "urn:tinycloud:encryption:";
5230
+ if (!value.startsWith(prefix)) return false;
5231
+ const match = /^(did:.+):([^/:?#\s]+)(?:\/(.+))?$/.exec(value.slice(prefix.length));
5232
+ if (match === null) return false;
5233
+ const path = match[3];
5234
+ return path === void 0 || path.split("/").every(
5235
+ (segment) => segment.length > 0 && segment !== "." && segment !== ".." && !/[\\:%?#\s]/.test(segment)
5236
+ );
5237
+ }, { message: "expected a canonical TinyCloud encryption resource" });
5238
+ var unifiedEncryptionKeyPathSchema = external_exports.string().regex(/^share\/[^/]+$/).refine(
5239
+ (value) => {
5240
+ const segment = value.slice("share/".length);
5241
+ return segment !== "." && segment !== ".." && !/[\\:%?#\s]/.test(segment);
5242
+ },
5243
+ { message: "expected an exact share key path" }
5244
+ );
5228
5245
  var unifiedKvCapabilitySchema = external_exports.object({
5229
5246
  kind: external_exports.literal("kv"),
5230
5247
  resource: unifiedResourceSchema,
@@ -5238,7 +5255,7 @@ var unifiedKvCapabilitySchema = external_exports.object({
5238
5255
  }).strict();
5239
5256
  var unifiedEncryptionCapabilitySchema = external_exports.object({
5240
5257
  kind: external_exports.literal("encryption"),
5241
- resource: unifiedEncryptionNetworkSchema,
5258
+ resource: unifiedEncryptionResourceSchema,
5242
5259
  action: external_exports.literal("tinycloud.encryption/decrypt")
5243
5260
  }).strict();
5244
5261
  var unifiedCapabilitySchema = external_exports.union([unifiedKvCapabilitySchema, unifiedEncryptionCapabilitySchema]);
@@ -5247,6 +5264,7 @@ var unifiedContentSourceSchema = external_exports.object({
5247
5264
  kvResource: unifiedResourceSchema,
5248
5265
  selector: external_exports.union([external_exports.literal("exact"), external_exports.literal("prefix")]),
5249
5266
  encryptionNetwork: unifiedEncryptionNetworkSchema,
5267
+ encryptionKeyPath: unifiedEncryptionKeyPathSchema.optional(),
5250
5268
  encryptedSymmetricKeyDigestHex: external_exports.string().regex(/^[0-9a-f]{64}$/),
5251
5269
  keyVersion: external_exports.number().int().positive(),
5252
5270
  mode: external_exports.union([external_exports.literal("mutable"), external_exports.literal("immutable")]),
@@ -5364,22 +5382,71 @@ function validateV3Invariants(value, ctx) {
5364
5382
  if (value.attestedEnforcerBinding.nodeAudience !== value.target.nodeAudience || value.attestedEnforcerBinding.signature.signerDid !== value.attestedEnforcerBinding.nodeAudience || Date.parse(value.attestedEnforcerBinding.expiresAt) < Date.parse(value.expiry)) {
5365
5383
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["attestedEnforcerBinding"], message: "enforcer binding does not cover the target and share lifetime" });
5366
5384
  }
5367
- if (value.policy.contentSource.shareId !== value.contentSource.shareId || value.policy.contentSource.kvResource !== value.contentSource.kvResource || value.policy.contentSource.selector !== value.contentSource.selector || value.policy.contentSource.encryptionNetwork !== value.encryptionNetwork || value.policy.contentSource.encryptedSymmetricKeyDigestHex !== value.contentSource.encryptedSymmetricKeyDigestHex || value.policy.contentSource.keyVersion !== value.contentSource.keyVersion || value.policy.contentSource.mode !== value.contentSource.mode || value.policy.contentSource.initialCiphertextDigestHex !== value.contentSource.initialCiphertextDigestHex) {
5385
+ const encryptionKeyPath = value.contentSource.encryptionKeyPath;
5386
+ if (encryptionKeyPath !== void 0 && encryptionKeyPath !== `share/${value.contentSource.shareId}`) {
5387
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["contentSource", "encryptionKeyPath"], message: "share key path must name this share" });
5388
+ }
5389
+ if (value.policy.contentSource.shareId !== value.contentSource.shareId || value.policy.contentSource.kvResource !== value.contentSource.kvResource || value.policy.contentSource.selector !== value.contentSource.selector || value.policy.contentSource.encryptionNetwork !== value.encryptionNetwork || value.policy.contentSource.encryptionKeyPath !== encryptionKeyPath || value.policy.contentSource.encryptedSymmetricKeyDigestHex !== value.contentSource.encryptedSymmetricKeyDigestHex || value.policy.contentSource.keyVersion !== value.contentSource.keyVersion || value.policy.contentSource.mode !== value.contentSource.mode || value.policy.contentSource.initialCiphertextDigestHex !== value.contentSource.initialCiphertextDigestHex) {
5368
5390
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["contentSource"], message: "content source is not bound to policy" });
5369
5391
  }
5370
5392
  if (value.policy.signature.suite !== "Ed25519" || value.policy.signature.signerDid !== value.policy.ownerDid) {
5371
5393
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["policy", "signature"], message: "policy must be signed by its owner" });
5372
5394
  }
5373
5395
  const kvCapabilities = value.policy.capabilityCeiling.filter((capability) => capability.kind === "kv");
5374
- const decryptCapabilities = value.policy.capabilityCeiling.filter((capability) => capability.kind === "encryption" && capability.resource === value.encryptionNetwork && capability.action === "tinycloud.encryption/decrypt");
5396
+ const expectedEncryptionResource = encryptionKeyPath === void 0 ? value.encryptionNetwork : `${value.encryptionNetwork}/${encryptionKeyPath}`;
5397
+ const decryptCapabilities = value.policy.capabilityCeiling.filter((capability) => capability.kind === "encryption" && capability.resource === expectedEncryptionResource && capability.action === "tinycloud.encryption/decrypt");
5375
5398
  if (value.policy.capabilityCeiling.length !== 2 || kvCapabilities.length !== 1 || decryptCapabilities.length !== 1 || kvCapabilities[0]?.resource !== value.contentSource.kvResource || kvCapabilities[0]?.selector !== value.contentSource.selector) {
5376
- ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["policy", "capabilityCeiling"], message: "policy ceiling must contain exact decrypt network" });
5399
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["policy", "capabilityCeiling"], message: "policy ceiling must contain the exact decrypt resource" });
5377
5400
  }
5378
5401
  }
5379
5402
  var unsignedShareEnvelopeV3Schema = unsignedShareEnvelopeV3BaseSchema.superRefine(validateV3Invariants);
5380
5403
  var shareEnvelopeV3Schema = unsignedShareEnvelopeV3BaseSchema.extend({ signature: signatureSchema }).strict().superRefine(validateV3Invariants);
5381
5404
  var ENVELOPE_AAD_LABEL = "tinycloud-share-envelope-v1";
5405
+ var SEALED_BLOB_VERSION = 1;
5382
5406
  var AAD = utf8Bytes(ENVELOPE_AAD_LABEL);
5407
+ var KEY_LENGTH = 32;
5408
+ var NONCE_LENGTH = 12;
5409
+ var TAG_LENGTH = 16;
5410
+ var HEADER_LENGTH = 1;
5411
+ function assertKey(key32) {
5412
+ if (key32.length !== KEY_LENGTH) {
5413
+ throw new TypeError(`key must be ${KEY_LENGTH} bytes, got ${key32.length}`);
5414
+ }
5415
+ }
5416
+ async function importAesKey(key32, usage) {
5417
+ assertKey(key32);
5418
+ return globalThis.crypto.subtle.importKey(
5419
+ "raw",
5420
+ key32,
5421
+ "AES-GCM",
5422
+ false,
5423
+ [usage]
5424
+ );
5425
+ }
5426
+ async function open(blob, key32) {
5427
+ if (blob.length < HEADER_LENGTH + NONCE_LENGTH + TAG_LENGTH) {
5428
+ throw new TypeError(`sealed blob too short: ${blob.length} bytes`);
5429
+ }
5430
+ if (blob[0] !== SEALED_BLOB_VERSION) {
5431
+ throw new TypeError(`unknown sealed blob version: ${blob[0]}`);
5432
+ }
5433
+ const nonce = blob.subarray(HEADER_LENGTH, HEADER_LENGTH + NONCE_LENGTH);
5434
+ const ciphertext = blob.subarray(HEADER_LENGTH + NONCE_LENGTH);
5435
+ return decryptEnvelope(nonce, ciphertext, key32);
5436
+ }
5437
+ async function decryptEnvelope(nonce, ciphertext, key32) {
5438
+ if (nonce.length !== NONCE_LENGTH) {
5439
+ throw new TypeError(`nonce must be ${NONCE_LENGTH} bytes, got ${nonce.length}`);
5440
+ }
5441
+ const key = await importAesKey(key32, "decrypt");
5442
+ return new Uint8Array(
5443
+ await globalThis.crypto.subtle.decrypt(
5444
+ { name: "AES-GCM", iv: nonce, additionalData: AAD },
5445
+ key,
5446
+ ciphertext
5447
+ )
5448
+ );
5449
+ }
5383
5450
  function isPlainRecord(value) {
5384
5451
  const proto = Object.getPrototypeOf(value);
5385
5452
  return proto === Object.prototype || proto === null;
@@ -5510,6 +5577,7 @@ function equal(left, right) {
5510
5577
  return left.length === right.length && left.every((byte, index) => byte === right[index]);
5511
5578
  }
5512
5579
  var ED25519_VERIFY_OPTS2 = { zip215: false };
5580
+ var ENVELOPE_V2_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v2\0";
5513
5581
  var ENVELOPE_V3_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v3\0";
5514
5582
  var POLICY_V1_SIGNATURE_DOMAIN = "xyz.tinycloud.policy/policy/v1\0";
5515
5583
  var POLICY_V2_SIGNATURE_DOMAIN = "xyz.tinycloud.policy/policy/v2\0";
@@ -5517,6 +5585,14 @@ var CONTENT_SOURCE_V1_DOMAIN = "xyz.tinycloud.policy/ContentSource/v1\0";
5517
5585
  var POLICY_CAPABILITY_V1_DOMAIN = "xyz.tinycloud.policy/PolicyCapability/v1\0";
5518
5586
  var NATIVE_PROJECTION_V1_DOMAIN = "xyz.tinycloud.policy/NativeProjection/v1\0";
5519
5587
  var ATTESTED_ENFORCER_V2_DOMAIN = "xyz.tinycloud.policy/AttestedEnforcerBinding/v2\0";
5588
+ function signingBytesV2(unsigned) {
5589
+ const domain = utf8Bytes(ENVELOPE_V2_SIGNATURE_DOMAIN);
5590
+ const body = utf8Bytes(canonicalize(unsigned));
5591
+ const bytes = new Uint8Array(domain.length + body.length);
5592
+ bytes.set(domain);
5593
+ bytes.set(body, domain.length);
5594
+ return bytes;
5595
+ }
5520
5596
  function signingBytesV3(unsigned) {
5521
5597
  const domain = utf8Bytes(ENVELOPE_V3_SIGNATURE_DOMAIN);
5522
5598
  const body = utf8Bytes(canonicalize(unsigned));
@@ -5525,6 +5601,17 @@ function signingBytesV3(unsigned) {
5525
5601
  bytes.set(body, domain.length);
5526
5602
  return bytes;
5527
5603
  }
5604
+ function verifyEnvelopeV2SignatureOnly(envelope) {
5605
+ const parsed = shareEnvelopeV2Schema.parse(envelope);
5606
+ const { signature, ...unsigned } = parsed;
5607
+ return ed255193.verify(fromBase64Url(signature.value), signingBytesV2(unsigned), ed25519PublicKeyFromDidKey(signature.signerDid), ED25519_VERIFY_OPTS2);
5608
+ }
5609
+ async function verifyEnvelopeV2(envelope, options) {
5610
+ const parsed = shareEnvelopeV2Schema.parse(envelope);
5611
+ if (parsed.signature.signerDid !== options.expectedSignerDid || !verifyEnvelopeV2SignatureOnly(parsed)) return false;
5612
+ if (parsed.authorizationTarget.kind === "policy") return await computeCid(fromBase64Url(parsed.authorizationTarget.policyBytes)) === parsed.authorizationTarget.policyCid;
5613
+ return true;
5614
+ }
5528
5615
  function verifyEnvelopeV3SignatureOnly(envelope) {
5529
5616
  const parsed = shareEnvelopeV3Schema.parse(envelope);
5530
5617
  const { signature, ...unsigned } = parsed;
@@ -5611,6 +5698,52 @@ function base32Lower(bytes) {
5611
5698
  if (bits > 0) output += alphabet[buffer << 5 - bits & 31];
5612
5699
  return output;
5613
5700
  }
5701
+ var KEY_LENGTH2 = 32;
5702
+ function assertCanonicalCid(cidString) {
5703
+ const cid = CID.parse(cidString);
5704
+ if (cid.version !== 1 || cid.code !== code || cid.multihash.code !== 18 || // sha2-256 (0x12) only, at the link layer too
5705
+ cid.toString() !== cidString) {
5706
+ throw new TypeError(`not a canonical CIDv1 raw sha2-256 base32 CID: ${cidString}`);
5707
+ }
5708
+ }
5709
+ function parseShareUrl(url, options = {}) {
5710
+ const parsed = new URL(url);
5711
+ if (parsed.protocol !== "https:") {
5712
+ throw new TypeError(`share URL must be https, got ${parsed.protocol}`);
5713
+ }
5714
+ if (parsed.username !== "" || parsed.password !== "") {
5715
+ throw new TypeError("share URL must not carry userinfo");
5716
+ }
5717
+ if (parsed.search !== "") {
5718
+ throw new TypeError("share URL must not have a query string");
5719
+ }
5720
+ if (options.expectedOrigin !== void 0) {
5721
+ if (!isCanonicalHttpsOrigin(options.expectedOrigin)) {
5722
+ throw new TypeError(
5723
+ `expectedOrigin must be a canonical https origin, got ${options.expectedOrigin}`
5724
+ );
5725
+ }
5726
+ if (parsed.origin !== options.expectedOrigin) {
5727
+ throw new TypeError(
5728
+ `share URL origin ${parsed.origin} does not match expected ${options.expectedOrigin}`
5729
+ );
5730
+ }
5731
+ }
5732
+ const match = /^\/s\/([a-z2-7]+)$/.exec(parsed.pathname);
5733
+ if (!match || match[1] === void 0) {
5734
+ throw new TypeError(`not a share URL path: ${parsed.pathname}`);
5735
+ }
5736
+ const ciphertextCid = match[1];
5737
+ assertCanonicalCid(ciphertextCid);
5738
+ if (!parsed.hash.startsWith("#k=")) {
5739
+ throw new TypeError("share URL is missing the #k= key fragment");
5740
+ }
5741
+ const key32 = fromBase64Url(parsed.hash.slice("#k=".length));
5742
+ if (key32.length !== KEY_LENGTH2) {
5743
+ throw new TypeError(`fragment key must be ${KEY_LENGTH2} bytes, got ${key32.length}`);
5744
+ }
5745
+ return { ciphertextCid, key32 };
5746
+ }
5614
5747
 
5615
5748
  // src/artifact-core.ts
5616
5749
  var ARTIFACT_ROOT = "artifacts";
@@ -5707,7 +5840,7 @@ var ShareReceiveError = class extends Error {
5707
5840
  }
5708
5841
  };
5709
5842
  function metadataFor(envelope, origin) {
5710
- const kind = envelope.recipientMatcher.kind === "recipientDid" ? "recipientDid" : envelope.recipientMatcher.kind === "exactEmail" ? "email" : "emailDomain";
5843
+ const kind = envelope.recipientMatcher.kind === "recipientDid" ? "recipientDid" : envelope.recipientMatcher.kind === "exactEmail" ? "email" : envelope.recipientMatcher.kind === "emailDomain" ? "emailDomain" : "bearer";
5711
5844
  return {
5712
5845
  protocol: "tinycloud-share",
5713
5846
  version: 1,
@@ -5722,7 +5855,7 @@ function metadataFor(envelope, origin) {
5722
5855
  ...envelope.display.filename === void 0 ? {} : { filename: envelope.display.filename },
5723
5856
  ...envelope.display.mode === void 0 ? {} : { mode: envelope.display.mode }
5724
5857
  },
5725
- ...envelope.artifactPin === void 0 ? {} : { artifact: { id: envelope.artifactPin.artifactId, version: envelope.artifactPin.version } }
5858
+ ...envelope.version === 3 && envelope.artifactPin !== void 0 ? { artifact: { id: envelope.artifactPin.artifactId, version: envelope.artifactPin.version } } : {}
5726
5859
  };
5727
5860
  }
5728
5861
  function receiveErrorFor(error) {
@@ -5820,10 +5953,123 @@ async function resolvePolicyShare(link, options) {
5820
5953
  options.onResolvedAddressedEnvelope?.(envelope, cid);
5821
5954
  return { envelope, origin: url.origin, cid };
5822
5955
  }
5956
+ async function resolveLegacyV2(link, options) {
5957
+ let parsed;
5958
+ try {
5959
+ parsed = parseShareUrl(link, { ...options.expectedOrigin === void 0 ? {} : { expectedOrigin: options.expectedOrigin } });
5960
+ } catch {
5961
+ throw new ShareReceiveError("invalid-link", "legacy share link format is invalid");
5962
+ }
5963
+ try {
5964
+ options.signal?.throwIfAborted();
5965
+ const origin = new URL(link).origin;
5966
+ const limit = options.maxSealedBlobBytes ?? DEFAULT_MAX_SEALED_BLOB_BYTES;
5967
+ if (!Number.isSafeInteger(limit) || limit <= 0) throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope limit is invalid");
5968
+ let sealed;
5969
+ if (options.fetchBlob !== void 0) {
5970
+ try {
5971
+ sealed = await options.fetchBlob({ origin, cid: parsed.ciphertextCid, ...options.signal === void 0 ? {} : { signal: options.signal } });
5972
+ } catch {
5973
+ throw new ShareReceiveError("fetch-failed", "legacy registry unavailable");
5974
+ }
5975
+ } else {
5976
+ if (options.registryBaseUrl === void 0) throw new ShareReceiveError("fetch-failed", "legacy registry is required");
5977
+ const response = await (options.fetch ?? globalThis.fetch)(`${options.registryBaseUrl.replace(/\/+$/, "")}/ipfs/${parsed.ciphertextCid}?format=raw`, {
5978
+ headers: { accept: "application/vnd.ipld.raw" },
5979
+ redirect: "error",
5980
+ ...options.signal === void 0 ? {} : { signal: options.signal }
5981
+ });
5982
+ if (!response.ok) throw new ShareReceiveError("fetch-failed", "legacy registry unavailable");
5983
+ if (Number(response.headers.get("content-length")) > limit) throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope exceeds the size limit");
5984
+ const reader = response.body?.getReader();
5985
+ if (reader === void 0) throw new ShareReceiveError("fetch-failed", "legacy registry returned no body");
5986
+ const chunks = [];
5987
+ let length2 = 0;
5988
+ for (; ; ) {
5989
+ const part = await reader.read();
5990
+ if (part.done) break;
5991
+ length2 += part.value.length;
5992
+ if (length2 > limit) {
5993
+ await reader.cancel();
5994
+ throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope exceeds the size limit");
5995
+ }
5996
+ chunks.push(part.value);
5997
+ }
5998
+ sealed = new Uint8Array(length2);
5999
+ let offset = 0;
6000
+ for (const chunk of chunks) {
6001
+ sealed.set(chunk, offset);
6002
+ offset += chunk.length;
6003
+ }
6004
+ }
6005
+ options.signal?.throwIfAborted();
6006
+ if (!(sealed instanceof Uint8Array) || sealed.length > limit) throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope exceeds the size limit");
6007
+ if (await computeCid(sealed) !== parsed.ciphertextCid) throw new ShareReceiveError("cid-mismatch", "legacy envelope CID does not match");
6008
+ let plaintext;
6009
+ try {
6010
+ plaintext = await open(sealed, parsed.key32);
6011
+ } catch {
6012
+ throw new ShareReceiveError("decrypt-failed", "legacy envelope could not be opened");
6013
+ }
6014
+ let envelope;
6015
+ try {
6016
+ const encoded = new TextDecoder("utf-8", { fatal: true }).decode(plaintext);
6017
+ const value = JSON.parse(encoded);
6018
+ if (canonicalize(value) !== encoded) throw new Error("non-canonical envelope");
6019
+ envelope = shareEnvelopeV2Schema.parse(value);
6020
+ } catch {
6021
+ throw new ShareReceiveError("envelope-invalid", "legacy envelope is invalid");
6022
+ }
6023
+ return { envelope, origin, cid: parsed.ciphertextCid };
6024
+ } finally {
6025
+ parsed.key32.fill(0);
6026
+ }
6027
+ }
6028
+ function evidenceMatchesV2(envelope, evidence) {
6029
+ const actions = new Set(evidence.actions.map((action) => action === "read" ? "tinycloud.kv/get" : action === "list" ? "tinycloud.kv/list" : action === "edit" ? "tinycloud.kv/put" : action));
6030
+ const resourceCovers = evidence.resource.kind === "exact" ? envelope.resource.kind === "exact" && evidence.resource.path === envelope.resource.path : envelope.resource.path === evidence.resource.path || envelope.resource.path.startsWith(evidence.resource.path.endsWith("/") ? evidence.resource.path : `${evidence.resource.path}/`);
6031
+ return envelope.authorizationTarget.kind === "policy" && evidence.policyCid === envelope.authorizationTarget.policyCid && evidence.signerDid === envelope.signature.signerDid && evidence.registrationCid.length > 0 && evidence.shareId === envelope.shareId && canonicalize(evidence.recipientMatcher) === canonicalize(envelope.recipientMatcher) && canonicalize(evidence.target) === canonicalize(envelope.target) && resourceCovers && envelope.actions.every((action) => actions.has(action === "read" ? "tinycloud.kv/get" : action === "list" ? "tinycloud.kv/list" : "tinycloud.kv/put")) && canonicalize(evidence.contentSource) === canonicalize(envelope.contentSource) && evidence.contentSourceDigest === envelope.contentSourceDigest && evidence.delegationCid === envelope.delegationCid && evidence.authorityMaterialHandle === envelope.authorityMaterialHandle && evidence.authorityMaterialDigest === envelope.authorityMaterialDigest && evidence.expiresAt === envelope.expiry && Number.isFinite(Date.parse(evidence.expiresAt)) && Date.parse(evidence.expiresAt) === Date.parse(envelope.expiry);
6032
+ }
6033
+ async function verifyLegacyV2(envelope, options) {
6034
+ try {
6035
+ if (!await verifyEnvelopeV2(envelope, { expectedSignerDid: envelope.signature.signerDid })) throw new Error("signature");
6036
+ } catch {
6037
+ throw new ShareReceiveError("signature-invalid", "legacy envelope signature is invalid");
6038
+ }
6039
+ const expiry = Date.parse(envelope.expiry);
6040
+ if (!Number.isFinite(expiry)) throw new ShareReceiveError("envelope-invalid", "legacy share expiry is invalid");
6041
+ if (expiry <= (options.now?.() ?? Date.now())) throw new ShareReceiveError("expired", "legacy share has expired", { expiresAt: envelope.expiry });
6042
+ if (envelope.authorizationTarget.kind === "policy") {
6043
+ if (options.trustedPolicyAuthority === void 0) throw new ShareReceiveError("envelope-invalid", "a legacy policy authority is required");
6044
+ let evidence;
6045
+ try {
6046
+ evidence = await options.trustedPolicyAuthority.resolve({ policyCid: envelope.authorizationTarget.policyCid, envelope });
6047
+ } catch {
6048
+ }
6049
+ if (evidence === void 0 || !evidenceMatchesV2(envelope, evidence)) throw new ShareReceiveError("capability-invalid", "legacy policy evidence does not match");
6050
+ if (options.trustedSignerDid !== void 0 && options.trustedSignerDid !== evidence.signerDid) throw new ShareReceiveError("signature-invalid", "legacy signer is not trusted");
6051
+ } else if (options.trustedSignerDid === void 0 || options.trustedSignerDid !== envelope.signature.signerDid || envelope.authorizationTarget.kind === "recipientDid" && envelope.recipientMatcher.kind === "recipientDid" && envelope.authorizationTarget.did !== envelope.recipientMatcher.value) {
6052
+ throw new ShareReceiveError("capability-invalid", "legacy recipient authority does not match");
6053
+ }
6054
+ }
5823
6055
  async function inspectShare(link, options = {}) {
6056
+ if (isLegacyV2Link(link)) {
6057
+ const legacy = await resolveLegacyV2(link, options);
6058
+ await verifyLegacyV2(legacy.envelope, options);
6059
+ options.onResolvedAddressedEnvelope?.(legacy.envelope, legacy.cid);
6060
+ return { metadata: metadataFor(legacy.envelope, legacy.origin), link: { origin: legacy.origin, cid: legacy.cid, kind: "compact" } };
6061
+ }
5824
6062
  const resolved = await resolvePolicyShare(link, options);
5825
6063
  return { metadata: metadataFor(resolved.envelope, resolved.origin), link: { origin: resolved.origin, cid: resolved.cid, kind: "policy" } };
5826
6064
  }
6065
+ function isLegacyV2Link(link) {
6066
+ try {
6067
+ const url = new URL(link);
6068
+ return /^\/s\/bafkrei[a-z2-7]{52}$/.test(url.pathname) && url.hash.startsWith("#k=");
6069
+ } catch {
6070
+ return false;
6071
+ }
6072
+ }
5827
6073
 
5828
6074
  // src/recipient.ts
5829
6075
  import { ed25519 as ed255194, x25519 } from "@noble/curves/ed25519";
@@ -5850,41 +6096,18 @@ function object2(value, label) {
5850
6096
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${label} is invalid`);
5851
6097
  return value;
5852
6098
  }
5853
- var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
5854
6099
  function toBase64(bytes) {
5855
- let out = "";
5856
- for (let i = 0; i < bytes.length; i += 3) {
5857
- const b0 = bytes[i];
5858
- const b1 = i + 1 < bytes.length ? bytes[i + 1] : 0;
5859
- const b2 = i + 2 < bytes.length ? bytes[i + 2] : 0;
5860
- out += BASE64_ALPHABET[b0 >> 2 & 63];
5861
- out += BASE64_ALPHABET[(b0 << 4 | b1 >> 4) & 63];
5862
- out += i + 1 < bytes.length ? BASE64_ALPHABET[(b1 << 2 | b2 >> 6) & 63] : "=";
5863
- out += i + 2 < bytes.length ? BASE64_ALPHABET[b2 & 63] : "=";
5864
- }
5865
- return out;
6100
+ return toBase64Url(bytes).replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(bytes.length / 3) * 4, "=");
5866
6101
  }
5867
6102
  function fromBase64(value, label) {
5868
6103
  if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
5869
6104
  throw new Error(`${label} is invalid`);
5870
6105
  }
5871
- const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
5872
- const out = new Uint8Array(value.length / 4 * 3 - padding);
5873
- let outIdx = 0;
5874
- for (let i = 0; i < value.length; i += 4) {
5875
- const v0 = BASE64_ALPHABET.indexOf(value[i]);
5876
- const v1 = BASE64_ALPHABET.indexOf(value[i + 1]);
5877
- const v2 = value[i + 2] === "=" ? 0 : BASE64_ALPHABET.indexOf(value[i + 2]);
5878
- const v3 = value[i + 3] === "=" ? 0 : BASE64_ALPHABET.indexOf(value[i + 3]);
5879
- const b0 = v0 << 2 | v1 >> 4;
5880
- const b1 = (v1 & 15) << 4 | v2 >> 2;
5881
- const b2 = (v2 & 3) << 6 | v3;
5882
- if (outIdx < out.length) out[outIdx++] = b0;
5883
- if (outIdx < out.length) out[outIdx++] = b1;
5884
- if (outIdx < out.length) out[outIdx++] = b2;
5885
- }
5886
- if (toBase64(out) !== value) throw new Error(`${label} is invalid`);
5887
- return out;
6106
+ try {
6107
+ return fromBase64Url(value.replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_"));
6108
+ } catch {
6109
+ throw new Error(`${label} is invalid`);
6110
+ }
5888
6111
  }
5889
6112
  async function digestBytes(value) {
5890
6113
  return toBase64Url(new Uint8Array(await crypto.subtle.digest("SHA-256", value)));
@@ -5995,16 +6218,16 @@ function matchesInitialCiphertextDigest(bytes, expected) {
5995
6218
  function canonicalHashHex(value) {
5996
6219
  return hex2(sha2563(new TextEncoder().encode(canonicalize(value))));
5997
6220
  }
5998
- async function aesGcmDecrypt(key, blob) {
6221
+ async function aesGcmDecrypt(key, blob, aad) {
5999
6222
  if (key.length !== 32 || blob.length < 28) throw new Error("encrypted content is malformed");
6000
6223
  const cryptoKey = await crypto.subtle.importKey("raw", key, "AES-GCM", false, ["decrypt"]);
6001
- return new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv: blob.slice(0, 12) }, cryptoKey, blob.slice(12)));
6224
+ return new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv: blob.slice(0, 12), ...aad && { additionalData: aad } }, cryptoKey, blob.slice(12)));
6002
6225
  }
6003
- async function aesGcmEncrypt(key, plaintext) {
6226
+ async function aesGcmEncrypt(key, plaintext, aad) {
6004
6227
  if (key.length !== 32) throw new Error("encrypted content key is malformed");
6005
6228
  const nonce = crypto.getRandomValues(new Uint8Array(12));
6006
6229
  const cryptoKey = await crypto.subtle.importKey("raw", key, "AES-GCM", false, ["encrypt"]);
6007
- const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: nonce }, cryptoKey, plaintext));
6230
+ const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: nonce, ...aad && { additionalData: aad } }, cryptoKey, plaintext));
6008
6231
  const output = new Uint8Array(nonce.length + ciphertext.length);
6009
6232
  output.set(nonce);
6010
6233
  output.set(ciphertext, nonce.length);
@@ -6019,8 +6242,9 @@ function parseV3InlineEncryptedEnvelope(bytes, expected) {
6019
6242
  }
6020
6243
  if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("encrypted content envelope is malformed");
6021
6244
  const record = value;
6022
- const allowed = ["v", "networkId", "alg", "keyVersion", "encryptedSymmetricKey", "encryptedSymmetricKeyHash", "ciphertext", "metadata"];
6023
- if (Object.keys(record).some((key) => !allowed.includes(key)) || record.v !== 1 || record.networkId !== expected.encryptionNetwork || record.alg !== "x25519-aes256gcm/v1" || record.keyVersion !== expected.contentSource.keyVersion || typeof record.encryptedSymmetricKey !== "string" || typeof record.encryptedSymmetricKeyHash !== "string" || record.encryptedSymmetricKeyHash !== expected.contentSource.encryptedSymmetricKeyDigestHex || record.encryptedSymmetricKeyHash !== canonicalHashHex(record.encryptedSymmetricKey) || typeof record.ciphertext !== "string" || record.metadata !== void 0 && (record.metadata === null || typeof record.metadata !== "object" || Array.isArray(record.metadata) || Object.values(record.metadata).some((entry) => typeof entry !== "string"))) {
6245
+ const allowed = ["v", "networkId", "keyPath", "alg", "keyVersion", "encryptedSymmetricKey", "encryptedSymmetricKeyHash", "ciphertext", "aad", "metadata"];
6246
+ const source = expected.contentSource;
6247
+ if (Object.keys(record).some((key) => !allowed.includes(key)) || record.v !== (source.encryptionKeyPath === void 0 ? 1 : 2) || record.networkId !== source.encryptionNetwork || record.networkId !== expected.encryptionNetwork || record.keyPath !== source.encryptionKeyPath || record.keyPath !== void 0 && record.keyPath !== `share/${expected.shareId}` || record.alg !== "x25519-aes256gcm/v1" || record.keyVersion !== source.keyVersion || typeof record.encryptedSymmetricKey !== "string" || typeof record.encryptedSymmetricKeyHash !== "string" || hex2(sha2563(fromBase64(record.encryptedSymmetricKey, "v3 encrypted symmetric key"))) !== source.encryptedSymmetricKeyDigestHex || record.encryptedSymmetricKeyHash !== canonicalHashHex(record.encryptedSymmetricKey) || typeof record.ciphertext !== "string" || record.aad !== void 0 && typeof record.aad !== "string" || record.metadata !== void 0 && (record.metadata === null || typeof record.metadata !== "object" || Array.isArray(record.metadata) || Object.values(record.metadata).some((entry) => typeof entry !== "string"))) {
6024
6248
  throw new Error("encrypted content envelope binding is invalid");
6025
6249
  }
6026
6250
  return record;
@@ -6141,7 +6365,7 @@ var ShareRecipientClient = class {
6141
6365
  this.session = { sessionId: delegation.sessionCid, expiresAt: new Date(compact.payload.exp * 1e3).toISOString(), actions: envelope.actions, resource: envelope.resource };
6142
6366
  return this.session;
6143
6367
  }
6144
- /** Decrypt a v3 KV value through a fresh recipient-signed decrypt invocation. */
6368
+ /** Decrypt unscoped v1 or path-bound v2 content, independently checking canonical and registered raw-key digests. */
6145
6369
  async decryptV3Content(bytes) {
6146
6370
  const envelope = this.options.envelope;
6147
6371
  const signer = this.nativeSigner ?? this.signer;
@@ -6149,18 +6373,22 @@ var ShareRecipientClient = class {
6149
6373
  assertArtifactBinding(artifactBindingView(envelope));
6150
6374
  if (!matchesInitialCiphertextDigest(bytes, envelope.contentSource.initialCiphertextDigestHex)) throw new Error("v3 owner KV ciphertext digest mismatch");
6151
6375
  const encrypted = parseV3InlineEncryptedEnvelope(bytes, envelope);
6376
+ const ciphertext = fromBase64(encrypted.ciphertext, "v3 encrypted content");
6377
+ if (ciphertext.length < 28) throw new Error("encrypted content is malformed");
6378
+ const aad = encrypted.aad === void 0 ? void 0 : fromBase64(encrypted.aad, "v3 encrypted content AAD");
6152
6379
  const receiverPrivateKey = crypto.getRandomValues(new Uint8Array(32));
6153
6380
  const receiverPublicKey = toBase64(x25519.getPublicKey(receiverPrivateKey));
6154
6381
  const receiverPublicKeyHash = canonicalHashHex(receiverPublicKey);
6155
- const body = { type: "tinycloud.encryption.decrypt/v1", targetNode: this.v3NodeAudience, networkId: encrypted.networkId, alg: encrypted.alg, keyVersion: encrypted.keyVersion, encryptedSymmetricKey: encrypted.encryptedSymmetricKey, encryptedSymmetricKeyHash: encrypted.encryptedSymmetricKeyHash, receiverPublicKey, receiverPublicKeyHash };
6382
+ const body = { type: "tinycloud.encryption.decrypt/v1", targetNode: this.v3NodeAudience, networkId: encrypted.networkId, ...encrypted.keyPath === void 0 ? {} : { keyPath: encrypted.keyPath }, alg: encrypted.alg, keyVersion: encrypted.keyVersion, encryptedSymmetricKey: encrypted.encryptedSymmetricKey, encryptedSymmetricKeyHash: encrypted.encryptedSymmetricKeyHash, receiverPublicKey, receiverPublicKeyHash };
6156
6383
  const bodyHash = hex2(sha2563(new TextEncoder().encode(canonicalize(body))));
6157
6384
  const session = verifyCompactUcanAuthorization(this.v3Authorization, this.session.sessionId);
6158
6385
  const now = Math.floor(Date.now() / 1e3);
6386
+ const decryptResource = encrypted.keyPath === void 0 ? encrypted.networkId : `${encrypted.networkId}/${encrypted.keyPath}`;
6159
6387
  const invocation = await signCompactUcanAuthorization({
6160
6388
  issuerDid: this.options.holderDid,
6161
6389
  audienceDid: this.v3NodeAudience,
6162
- attenuation: { [encrypted.networkId]: { "tinycloud.encryption/decrypt": [{}] } },
6163
- facts: [{ type: body.type, targetNode: body.targetNode, networkId: body.networkId, bodyHash, encryptedSymmetricKeyHash: body.encryptedSymmetricKeyHash, receiverPublicKeyHash, alg: body.alg, keyVersion: body.keyVersion }],
6390
+ attenuation: { [decryptResource]: { "tinycloud.encryption/decrypt": [{}] } },
6391
+ facts: [{ type: body.type, targetNode: body.targetNode, networkId: body.networkId, ...encrypted.keyPath === void 0 ? {} : { keyPath: encrypted.keyPath }, bodyHash, encryptedSymmetricKeyHash: body.encryptedSymmetricKeyHash, receiverPublicKeyHash, alg: body.alg, keyVersion: body.keyVersion }],
6164
6392
  proofs: [session.cid],
6165
6393
  notBefore: now,
6166
6394
  expiresAt: Math.min(now + 60, session.payload.exp),
@@ -6184,7 +6412,7 @@ var ShareRecipientClient = class {
6184
6412
  const symmetricKey = await aesGcmDecrypt(shared, wrapped.slice(33));
6185
6413
  shared.fill(0);
6186
6414
  if (symmetricKey.length !== 32) throw new Error("v3 content key is malformed");
6187
- const plaintext = await aesGcmDecrypt(symmetricKey, fromBase64(encrypted.ciphertext, "v3 encrypted content"));
6415
+ const plaintext = await aesGcmDecrypt(symmetricKey, ciphertext, aad);
6188
6416
  this.v3ContentKey?.fill(0);
6189
6417
  this.v3ContentKey = symmetricKey;
6190
6418
  this.v3ContentEnvelope = encrypted;
@@ -6196,7 +6424,9 @@ var ShareRecipientClient = class {
6196
6424
  /** Re-encrypt edited v3 content with the admitted content key. */
6197
6425
  async encryptV3Content(bytes, mediaType) {
6198
6426
  if (this.options.envelope.version !== 3 || this.v3ContentKey === void 0 || this.v3ContentEnvelope === void 0) throw new Error("v3 content must be decrypted before it can be saved");
6199
- return new TextEncoder().encode(canonicalize({ ...this.v3ContentEnvelope, ciphertext: toBase64Url(await aesGcmEncrypt(this.v3ContentKey, bytes)), metadata: { ...this.v3ContentEnvelope.metadata ?? {}, contentType: mediaType } }));
6427
+ const envelope = this.v3ContentEnvelope;
6428
+ const aad = envelope.aad === void 0 ? void 0 : fromBase64(envelope.aad, "v3 encrypted content AAD");
6429
+ return new TextEncoder().encode(canonicalize({ ...envelope, ciphertext: toBase64(await aesGcmEncrypt(this.v3ContentKey, bytes, aad)), metadata: { ...envelope.metadata ?? {}, contentType: mediaType } }));
6200
6430
  }
6201
6431
  async resumeWithProof(envelope, resumeToken, proof) {
6202
6432
  void envelope;