@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.
package/dist/index.cjs CHANGED
@@ -5363,6 +5363,23 @@ var unifiedEncryptionNetworkSchema = external_exports.string().refine((value) =>
5363
5363
  const network = separator < 0 ? "" : rest.slice(separator + 1);
5364
5364
  return owner.startsWith("did:") && owner.length > 4 && network.length > 0 && !/[:/%?#\s]/.test(network);
5365
5365
  }, { message: "expected canonical TinyCloud encryption network" });
5366
+ var unifiedEncryptionResourceSchema = external_exports.string().refine((value) => {
5367
+ const prefix = "urn:tinycloud:encryption:";
5368
+ if (!value.startsWith(prefix)) return false;
5369
+ const match = /^(did:.+):([^/:?#\s]+)(?:\/(.+))?$/.exec(value.slice(prefix.length));
5370
+ if (match === null) return false;
5371
+ const path = match[3];
5372
+ return path === void 0 || path.split("/").every(
5373
+ (segment) => segment.length > 0 && segment !== "." && segment !== ".." && !/[\\:%?#\s]/.test(segment)
5374
+ );
5375
+ }, { message: "expected a canonical TinyCloud encryption resource" });
5376
+ var unifiedEncryptionKeyPathSchema = external_exports.string().regex(/^share\/[^/]+$/).refine(
5377
+ (value) => {
5378
+ const segment = value.slice("share/".length);
5379
+ return segment !== "." && segment !== ".." && !/[\\:%?#\s]/.test(segment);
5380
+ },
5381
+ { message: "expected an exact share key path" }
5382
+ );
5366
5383
  var unifiedKvCapabilitySchema = external_exports.object({
5367
5384
  kind: external_exports.literal("kv"),
5368
5385
  resource: unifiedResourceSchema,
@@ -5376,7 +5393,7 @@ var unifiedKvCapabilitySchema = external_exports.object({
5376
5393
  }).strict();
5377
5394
  var unifiedEncryptionCapabilitySchema = external_exports.object({
5378
5395
  kind: external_exports.literal("encryption"),
5379
- resource: unifiedEncryptionNetworkSchema,
5396
+ resource: unifiedEncryptionResourceSchema,
5380
5397
  action: external_exports.literal("tinycloud.encryption/decrypt")
5381
5398
  }).strict();
5382
5399
  var unifiedCapabilitySchema = external_exports.union([unifiedKvCapabilitySchema, unifiedEncryptionCapabilitySchema]);
@@ -5385,6 +5402,7 @@ var unifiedContentSourceSchema = external_exports.object({
5385
5402
  kvResource: unifiedResourceSchema,
5386
5403
  selector: external_exports.union([external_exports.literal("exact"), external_exports.literal("prefix")]),
5387
5404
  encryptionNetwork: unifiedEncryptionNetworkSchema,
5405
+ encryptionKeyPath: unifiedEncryptionKeyPathSchema.optional(),
5388
5406
  encryptedSymmetricKeyDigestHex: external_exports.string().regex(/^[0-9a-f]{64}$/),
5389
5407
  keyVersion: external_exports.number().int().positive(),
5390
5408
  mode: external_exports.union([external_exports.literal("mutable"), external_exports.literal("immutable")]),
@@ -5502,16 +5520,21 @@ function validateV3Invariants(value, ctx) {
5502
5520
  if (value.attestedEnforcerBinding.nodeAudience !== value.target.nodeAudience || value.attestedEnforcerBinding.signature.signerDid !== value.attestedEnforcerBinding.nodeAudience || Date.parse(value.attestedEnforcerBinding.expiresAt) < Date.parse(value.expiry)) {
5503
5521
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["attestedEnforcerBinding"], message: "enforcer binding does not cover the target and share lifetime" });
5504
5522
  }
5505
- 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) {
5523
+ const encryptionKeyPath = value.contentSource.encryptionKeyPath;
5524
+ if (encryptionKeyPath !== void 0 && encryptionKeyPath !== `share/${value.contentSource.shareId}`) {
5525
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["contentSource", "encryptionKeyPath"], message: "share key path must name this share" });
5526
+ }
5527
+ 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) {
5506
5528
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["contentSource"], message: "content source is not bound to policy" });
5507
5529
  }
5508
5530
  if (value.policy.signature.suite !== "Ed25519" || value.policy.signature.signerDid !== value.policy.ownerDid) {
5509
5531
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["policy", "signature"], message: "policy must be signed by its owner" });
5510
5532
  }
5511
5533
  const kvCapabilities = value.policy.capabilityCeiling.filter((capability) => capability.kind === "kv");
5512
- const decryptCapabilities = value.policy.capabilityCeiling.filter((capability) => capability.kind === "encryption" && capability.resource === value.encryptionNetwork && capability.action === "tinycloud.encryption/decrypt");
5534
+ const expectedEncryptionResource = encryptionKeyPath === void 0 ? value.encryptionNetwork : `${value.encryptionNetwork}/${encryptionKeyPath}`;
5535
+ const decryptCapabilities = value.policy.capabilityCeiling.filter((capability) => capability.kind === "encryption" && capability.resource === expectedEncryptionResource && capability.action === "tinycloud.encryption/decrypt");
5513
5536
  if (value.policy.capabilityCeiling.length !== 2 || kvCapabilities.length !== 1 || decryptCapabilities.length !== 1 || kvCapabilities[0]?.resource !== value.contentSource.kvResource || kvCapabilities[0]?.selector !== value.contentSource.selector) {
5514
- ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["policy", "capabilityCeiling"], message: "policy ceiling must contain exact decrypt network" });
5537
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["policy", "capabilityCeiling"], message: "policy ceiling must contain the exact decrypt resource" });
5515
5538
  }
5516
5539
  }
5517
5540
  var unsignedShareEnvelopeV3Schema = unsignedShareEnvelopeV3BaseSchema.superRefine(validateV3Invariants);
@@ -5699,6 +5722,7 @@ var SEALED_BLOB_VERSION = 1;
5699
5722
  var AAD = utf8Bytes(ENVELOPE_AAD_LABEL);
5700
5723
  var KEY_LENGTH = 32;
5701
5724
  var NONCE_LENGTH = 12;
5725
+ var TAG_LENGTH = 16;
5702
5726
  var HEADER_LENGTH = 1;
5703
5727
  function assertKey(key32) {
5704
5728
  if (key32.length !== KEY_LENGTH) {
@@ -5726,6 +5750,17 @@ async function seal(plaintextBytes, key32) {
5726
5750
  blob.set(ciphertext, HEADER_LENGTH + nonce.length);
5727
5751
  return { blob, cid: await computeCid(blob) };
5728
5752
  }
5753
+ async function open(blob, key32) {
5754
+ if (blob.length < HEADER_LENGTH + NONCE_LENGTH + TAG_LENGTH) {
5755
+ throw new TypeError(`sealed blob too short: ${blob.length} bytes`);
5756
+ }
5757
+ if (blob[0] !== SEALED_BLOB_VERSION) {
5758
+ throw new TypeError(`unknown sealed blob version: ${blob[0]}`);
5759
+ }
5760
+ const nonce = blob.subarray(HEADER_LENGTH, HEADER_LENGTH + NONCE_LENGTH);
5761
+ const ciphertext = blob.subarray(HEADER_LENGTH + NONCE_LENGTH);
5762
+ return decryptEnvelope(nonce, ciphertext, key32);
5763
+ }
5729
5764
  async function encryptEnvelope(plaintextBytes, key32) {
5730
5765
  const key = await importAesKey(key32, "encrypt");
5731
5766
  const nonce = globalThis.crypto.getRandomValues(new Uint8Array(NONCE_LENGTH));
@@ -5738,6 +5773,19 @@ async function encryptEnvelope(plaintextBytes, key32) {
5738
5773
  );
5739
5774
  return { nonce, ciphertext };
5740
5775
  }
5776
+ async function decryptEnvelope(nonce, ciphertext, key32) {
5777
+ if (nonce.length !== NONCE_LENGTH) {
5778
+ throw new TypeError(`nonce must be ${NONCE_LENGTH} bytes, got ${nonce.length}`);
5779
+ }
5780
+ const key = await importAesKey(key32, "decrypt");
5781
+ return new Uint8Array(
5782
+ await globalThis.crypto.subtle.decrypt(
5783
+ { name: "AES-GCM", iv: nonce, additionalData: AAD },
5784
+ key,
5785
+ ciphertext
5786
+ )
5787
+ );
5788
+ }
5741
5789
  function isPlainRecord(value) {
5742
5790
  const proto = Object.getPrototypeOf(value);
5743
5791
  return proto === Object.prototype || proto === null;
@@ -5883,6 +5931,7 @@ function equal(left, right) {
5883
5931
  return left.length === right.length && left.every((byte, index) => byte === right[index]);
5884
5932
  }
5885
5933
  var ED25519_VERIFY_OPTS2 = { zip215: false };
5934
+ var ENVELOPE_V2_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v2\0";
5886
5935
  var ENVELOPE_V3_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v3\0";
5887
5936
  var POLICY_V1_SIGNATURE_DOMAIN = "xyz.tinycloud.policy/policy/v1\0";
5888
5937
  var POLICY_V2_SIGNATURE_DOMAIN = "xyz.tinycloud.policy/policy/v2\0";
@@ -5890,6 +5939,14 @@ var CONTENT_SOURCE_V1_DOMAIN = "xyz.tinycloud.policy/ContentSource/v1\0";
5890
5939
  var POLICY_CAPABILITY_V1_DOMAIN = "xyz.tinycloud.policy/PolicyCapability/v1\0";
5891
5940
  var NATIVE_PROJECTION_V1_DOMAIN = "xyz.tinycloud.policy/NativeProjection/v1\0";
5892
5941
  var ATTESTED_ENFORCER_V2_DOMAIN = "xyz.tinycloud.policy/AttestedEnforcerBinding/v2\0";
5942
+ function signingBytesV2(unsigned) {
5943
+ const domain = utf8Bytes(ENVELOPE_V2_SIGNATURE_DOMAIN);
5944
+ const body = utf8Bytes(canonicalize(unsigned));
5945
+ const bytes3 = new Uint8Array(domain.length + body.length);
5946
+ bytes3.set(domain);
5947
+ bytes3.set(body, domain.length);
5948
+ return bytes3;
5949
+ }
5893
5950
  function signingBytesV3(unsigned) {
5894
5951
  const domain = utf8Bytes(ENVELOPE_V3_SIGNATURE_DOMAIN);
5895
5952
  const body = utf8Bytes(canonicalize(unsigned));
@@ -5898,6 +5955,17 @@ function signingBytesV3(unsigned) {
5898
5955
  bytes3.set(body, domain.length);
5899
5956
  return bytes3;
5900
5957
  }
5958
+ function verifyEnvelopeV2SignatureOnly(envelope) {
5959
+ const parsed = shareEnvelopeV2Schema.parse(envelope);
5960
+ const { signature, ...unsigned } = parsed;
5961
+ return import_ed255193.ed25519.verify(fromBase64Url(signature.value), signingBytesV2(unsigned), ed25519PublicKeyFromDidKey(signature.signerDid), ED25519_VERIFY_OPTS2);
5962
+ }
5963
+ async function verifyEnvelopeV2(envelope, options) {
5964
+ const parsed = shareEnvelopeV2Schema.parse(envelope);
5965
+ if (parsed.signature.signerDid !== options.expectedSignerDid || !verifyEnvelopeV2SignatureOnly(parsed)) return false;
5966
+ if (parsed.authorizationTarget.kind === "policy") return await computeCid(fromBase64Url(parsed.authorizationTarget.policyBytes)) === parsed.authorizationTarget.policyCid;
5967
+ return true;
5968
+ }
5901
5969
  function verifyEnvelopeV3SignatureOnly(envelope) {
5902
5970
  const parsed = shareEnvelopeV3Schema.parse(envelope);
5903
5971
  const { signature, ...unsigned } = parsed;
@@ -5984,6 +6052,52 @@ function base32Lower(bytes3) {
5984
6052
  if (bits > 0) output += alphabet[buffer << 5 - bits & 31];
5985
6053
  return output;
5986
6054
  }
6055
+ var KEY_LENGTH2 = 32;
6056
+ function assertCanonicalCid(cidString) {
6057
+ const cid2 = CID.parse(cidString);
6058
+ if (cid2.version !== 1 || cid2.code !== code || cid2.multihash.code !== 18 || // sha2-256 (0x12) only, at the link layer too
6059
+ cid2.toString() !== cidString) {
6060
+ throw new TypeError(`not a canonical CIDv1 raw sha2-256 base32 CID: ${cidString}`);
6061
+ }
6062
+ }
6063
+ function parseShareUrl(url, options = {}) {
6064
+ const parsed = new URL(url);
6065
+ if (parsed.protocol !== "https:") {
6066
+ throw new TypeError(`share URL must be https, got ${parsed.protocol}`);
6067
+ }
6068
+ if (parsed.username !== "" || parsed.password !== "") {
6069
+ throw new TypeError("share URL must not carry userinfo");
6070
+ }
6071
+ if (parsed.search !== "") {
6072
+ throw new TypeError("share URL must not have a query string");
6073
+ }
6074
+ if (options.expectedOrigin !== void 0) {
6075
+ if (!isCanonicalHttpsOrigin(options.expectedOrigin)) {
6076
+ throw new TypeError(
6077
+ `expectedOrigin must be a canonical https origin, got ${options.expectedOrigin}`
6078
+ );
6079
+ }
6080
+ if (parsed.origin !== options.expectedOrigin) {
6081
+ throw new TypeError(
6082
+ `share URL origin ${parsed.origin} does not match expected ${options.expectedOrigin}`
6083
+ );
6084
+ }
6085
+ }
6086
+ const match = /^\/s\/([a-z2-7]+)$/.exec(parsed.pathname);
6087
+ if (!match || match[1] === void 0) {
6088
+ throw new TypeError(`not a share URL path: ${parsed.pathname}`);
6089
+ }
6090
+ const ciphertextCid = match[1];
6091
+ assertCanonicalCid(ciphertextCid);
6092
+ if (!parsed.hash.startsWith("#k=")) {
6093
+ throw new TypeError("share URL is missing the #k= key fragment");
6094
+ }
6095
+ const key32 = fromBase64Url(parsed.hash.slice("#k=".length));
6096
+ if (key32.length !== KEY_LENGTH2) {
6097
+ throw new TypeError(`fragment key must be ${KEY_LENGTH2} bytes, got ${key32.length}`);
6098
+ }
6099
+ return { ciphertextCid, key32 };
6100
+ }
5987
6101
 
5988
6102
  // src/artifact-core.ts
5989
6103
  var ARTIFACT_ROOT = "artifacts";
@@ -6085,7 +6199,7 @@ function toShareErrorInfo(error) {
6085
6199
  return { protocol: "tinycloud-share", version: SHARE_RESULT_VERSION, error: { code: "fetch-failed" } };
6086
6200
  }
6087
6201
  function metadataFor(envelope, origin) {
6088
- const kind = envelope.recipientMatcher.kind === "recipientDid" ? "recipientDid" : envelope.recipientMatcher.kind === "exactEmail" ? "email" : "emailDomain";
6202
+ const kind = envelope.recipientMatcher.kind === "recipientDid" ? "recipientDid" : envelope.recipientMatcher.kind === "exactEmail" ? "email" : envelope.recipientMatcher.kind === "emailDomain" ? "emailDomain" : "bearer";
6089
6203
  return {
6090
6204
  protocol: "tinycloud-share",
6091
6205
  version: 1,
@@ -6100,7 +6214,7 @@ function metadataFor(envelope, origin) {
6100
6214
  ...envelope.display.filename === void 0 ? {} : { filename: envelope.display.filename },
6101
6215
  ...envelope.display.mode === void 0 ? {} : { mode: envelope.display.mode }
6102
6216
  },
6103
- ...envelope.artifactPin === void 0 ? {} : { artifact: { id: envelope.artifactPin.artifactId, version: envelope.artifactPin.version } }
6217
+ ...envelope.version === 3 && envelope.artifactPin !== void 0 ? { artifact: { id: envelope.artifactPin.artifactId, version: envelope.artifactPin.version } } : {}
6104
6218
  };
6105
6219
  }
6106
6220
  function receiveErrorFor(error) {
@@ -6198,14 +6312,165 @@ async function resolvePolicyShare(link, options) {
6198
6312
  options.onResolvedAddressedEnvelope?.(envelope, cid2);
6199
6313
  return { envelope, origin: url.origin, cid: cid2 };
6200
6314
  }
6315
+ async function resolveLegacyV2(link, options) {
6316
+ let parsed;
6317
+ try {
6318
+ parsed = parseShareUrl(link, { ...options.expectedOrigin === void 0 ? {} : { expectedOrigin: options.expectedOrigin } });
6319
+ } catch {
6320
+ throw new ShareReceiveError("invalid-link", "legacy share link format is invalid");
6321
+ }
6322
+ try {
6323
+ options.signal?.throwIfAborted();
6324
+ const origin = new URL(link).origin;
6325
+ const limit = options.maxSealedBlobBytes ?? DEFAULT_MAX_SEALED_BLOB_BYTES;
6326
+ if (!Number.isSafeInteger(limit) || limit <= 0) throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope limit is invalid");
6327
+ let sealed;
6328
+ if (options.fetchBlob !== void 0) {
6329
+ try {
6330
+ sealed = await options.fetchBlob({ origin, cid: parsed.ciphertextCid, ...options.signal === void 0 ? {} : { signal: options.signal } });
6331
+ } catch {
6332
+ throw new ShareReceiveError("fetch-failed", "legacy registry unavailable");
6333
+ }
6334
+ } else {
6335
+ if (options.registryBaseUrl === void 0) throw new ShareReceiveError("fetch-failed", "legacy registry is required");
6336
+ const response = await (options.fetch ?? globalThis.fetch)(`${options.registryBaseUrl.replace(/\/+$/, "")}/ipfs/${parsed.ciphertextCid}?format=raw`, {
6337
+ headers: { accept: "application/vnd.ipld.raw" },
6338
+ redirect: "error",
6339
+ ...options.signal === void 0 ? {} : { signal: options.signal }
6340
+ });
6341
+ if (!response.ok) throw new ShareReceiveError("fetch-failed", "legacy registry unavailable");
6342
+ if (Number(response.headers.get("content-length")) > limit) throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope exceeds the size limit");
6343
+ const reader = response.body?.getReader();
6344
+ if (reader === void 0) throw new ShareReceiveError("fetch-failed", "legacy registry returned no body");
6345
+ const chunks = [];
6346
+ let length3 = 0;
6347
+ for (; ; ) {
6348
+ const part = await reader.read();
6349
+ if (part.done) break;
6350
+ length3 += part.value.length;
6351
+ if (length3 > limit) {
6352
+ await reader.cancel();
6353
+ throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope exceeds the size limit");
6354
+ }
6355
+ chunks.push(part.value);
6356
+ }
6357
+ sealed = new Uint8Array(length3);
6358
+ let offset = 0;
6359
+ for (const chunk of chunks) {
6360
+ sealed.set(chunk, offset);
6361
+ offset += chunk.length;
6362
+ }
6363
+ }
6364
+ options.signal?.throwIfAborted();
6365
+ if (!(sealed instanceof Uint8Array) || sealed.length > limit) throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope exceeds the size limit");
6366
+ if (await computeCid(sealed) !== parsed.ciphertextCid) throw new ShareReceiveError("cid-mismatch", "legacy envelope CID does not match");
6367
+ let plaintext;
6368
+ try {
6369
+ plaintext = await open(sealed, parsed.key32);
6370
+ } catch {
6371
+ throw new ShareReceiveError("decrypt-failed", "legacy envelope could not be opened");
6372
+ }
6373
+ let envelope;
6374
+ try {
6375
+ const encoded = new TextDecoder("utf-8", { fatal: true }).decode(plaintext);
6376
+ const value = JSON.parse(encoded);
6377
+ if (canonicalize(value) !== encoded) throw new Error("non-canonical envelope");
6378
+ envelope = shareEnvelopeV2Schema.parse(value);
6379
+ } catch {
6380
+ throw new ShareReceiveError("envelope-invalid", "legacy envelope is invalid");
6381
+ }
6382
+ return { envelope, origin, cid: parsed.ciphertextCid };
6383
+ } finally {
6384
+ parsed.key32.fill(0);
6385
+ }
6386
+ }
6387
+ function evidenceMatchesV2(envelope, evidence) {
6388
+ const actions = new Set(evidence.actions.map((action) => action === "read" ? "tinycloud.kv/get" : action === "list" ? "tinycloud.kv/list" : action === "edit" ? "tinycloud.kv/put" : action));
6389
+ 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}/`);
6390
+ 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);
6391
+ }
6392
+ async function verifyLegacyV2(envelope, options) {
6393
+ try {
6394
+ if (!await verifyEnvelopeV2(envelope, { expectedSignerDid: envelope.signature.signerDid })) throw new Error("signature");
6395
+ } catch {
6396
+ throw new ShareReceiveError("signature-invalid", "legacy envelope signature is invalid");
6397
+ }
6398
+ const expiry = Date.parse(envelope.expiry);
6399
+ if (!Number.isFinite(expiry)) throw new ShareReceiveError("envelope-invalid", "legacy share expiry is invalid");
6400
+ if (expiry <= (options.now?.() ?? Date.now())) throw new ShareReceiveError("expired", "legacy share has expired", { expiresAt: envelope.expiry });
6401
+ if (envelope.authorizationTarget.kind === "policy") {
6402
+ if (options.trustedPolicyAuthority === void 0) throw new ShareReceiveError("envelope-invalid", "a legacy policy authority is required");
6403
+ let evidence;
6404
+ try {
6405
+ evidence = await options.trustedPolicyAuthority.resolve({ policyCid: envelope.authorizationTarget.policyCid, envelope });
6406
+ } catch {
6407
+ }
6408
+ if (evidence === void 0 || !evidenceMatchesV2(envelope, evidence)) throw new ShareReceiveError("capability-invalid", "legacy policy evidence does not match");
6409
+ if (options.trustedSignerDid !== void 0 && options.trustedSignerDid !== evidence.signerDid) throw new ShareReceiveError("signature-invalid", "legacy signer is not trusted");
6410
+ } 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) {
6411
+ throw new ShareReceiveError("capability-invalid", "legacy recipient authority does not match");
6412
+ }
6413
+ }
6201
6414
  async function inspectShare(link, options = {}) {
6415
+ if (isLegacyV2Link(link)) {
6416
+ const legacy = await resolveLegacyV2(link, options);
6417
+ await verifyLegacyV2(legacy.envelope, options);
6418
+ options.onResolvedAddressedEnvelope?.(legacy.envelope, legacy.cid);
6419
+ return { metadata: metadataFor(legacy.envelope, legacy.origin), link: { origin: legacy.origin, cid: legacy.cid, kind: "compact" } };
6420
+ }
6202
6421
  const resolved = await resolvePolicyShare(link, options);
6203
6422
  return { metadata: metadataFor(resolved.envelope, resolved.origin), link: { origin: resolved.origin, cid: resolved.cid, kind: "policy" } };
6204
6423
  }
6205
6424
  async function receiveShare(link, options = {}) {
6425
+ if (isLegacyV2Link(link)) return receiveLegacyV2(link, options);
6206
6426
  const resolved = await resolvePolicyShare(link, options);
6207
6427
  return { state: "authorization-required", method: resolved.envelope.recipientMatcher.kind === "recipientDid" ? "openkey-device" : "email-claim" };
6208
6428
  }
6429
+ function isLegacyV2Link(link) {
6430
+ try {
6431
+ const url = new URL(link);
6432
+ return /^\/s\/bafkrei[a-z2-7]{52}$/.test(url.pathname) && url.hash.startsWith("#k=");
6433
+ } catch {
6434
+ return false;
6435
+ }
6436
+ }
6437
+ async function receiveLegacyV2(link, options) {
6438
+ const resolved = await resolveLegacyV2(link, options);
6439
+ const { envelope } = resolved;
6440
+ await verifyLegacyV2(envelope, options);
6441
+ options.onResolvedAddressedEnvelope?.(envelope, resolved.cid);
6442
+ const method = envelope.recipientMatcher.kind === "recipientDid" ? "openkey-device" : "email-claim";
6443
+ if (options.authorization === void 0) return { state: "authorization-required", method };
6444
+ const result = options.authorizationResumeToken === void 0 ? await options.authorization.begin({ envelope, method }) : await options.authorization.resume({
6445
+ envelope,
6446
+ method,
6447
+ resumeToken: options.authorizationResumeToken,
6448
+ ...options.authorizationProof === void 0 ? {} : { proof: options.authorizationProof }
6449
+ });
6450
+ if (result.state === "authorization-required") return result;
6451
+ if (result.state === "denied") throw new ShareReceiveError("authorization-denied", "legacy share authorization was denied");
6452
+ if (result.value === null || typeof result.value !== "object" || !(result.value.bytes instanceof Uint8Array) || result.value.proof === void 0 || options.authorization.verifyResult === void 0) {
6453
+ throw new ShareReceiveError("content-integrity-failed", "legacy content lacks a verifiable proof");
6454
+ }
6455
+ const authorized = result.value;
6456
+ try {
6457
+ if (!await options.authorization.verifyResult({ envelope, value: authorized, proof: authorized.proof })) throw new Error("proof");
6458
+ } catch {
6459
+ throw new ShareReceiveError("content-integrity-failed", "legacy content proof is invalid");
6460
+ }
6461
+ const bytes3 = authorized.bytes.slice();
6462
+ if (bytes3.length > (options.maxContentBlobBytes ?? DEFAULT_MAX_CONTENT_BLOB_BYTES)) throw new ShareReceiveError("max-bytes-exceeded", "legacy content exceeds the size limit");
6463
+ const bodyDigest = toBase64Url(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes3)));
6464
+ if (authorized.bodyDigest !== bodyDigest || authorized.contentSourceDigest !== envelope.contentSourceDigest || authorized.binding.shareId !== envelope.shareId || authorized.binding.delegationCid !== envelope.delegationCid || authorized.binding.authorityMaterialHandle !== envelope.authorityMaterialHandle || authorized.binding.authorityMaterialDigest !== envelope.authorityMaterialDigest || authorized.binding.resource.kind !== envelope.resource.kind || authorized.binding.resource.path !== envelope.resource.path || authorized.binding.action !== void 0 && !envelope.actions.some((action) => action === authorized.binding.action || `tinycloud.kv/${action}` === authorized.binding.action) || envelope.metadata.byteLength !== void 0 && envelope.metadata.byteLength !== bytes3.length) {
6465
+ throw new ShareReceiveError("content-integrity-failed", "legacy content binding does not match");
6466
+ }
6467
+ let text;
6468
+ try {
6469
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes3);
6470
+ } catch {
6471
+ }
6472
+ return { metadata: metadataFor(envelope, resolved.origin), link: { origin: resolved.origin, cid: resolved.cid, kind: "compact" }, bytes: bytes3, ...text === void 0 ? {} : { text } };
6473
+ }
6209
6474
 
6210
6475
  // src/publish.ts
6211
6476
  var SHARE_CONTENT_LIMIT = 100 * 1024 * 1024;
@@ -6740,7 +7005,10 @@ async function publishTargetShare(input) {
6740
7005
  expiresAt,
6741
7006
  expiryWasExplicit: input.expiryWasExplicit ?? input.expiresAt !== void 0,
6742
7007
  origin: input.origin,
6743
- ...input.notify === void 0 ? {} : { notify: input.notify }
7008
+ ...input.notify === void 0 ? {} : { notify: input.notify },
7009
+ ...input.credentialDescriptor === void 0 ? {} : { credentialDescriptor: input.credentialDescriptor },
7010
+ ...input.issuerDid === void 0 ? {} : { issuerDid: input.issuerDid },
7011
+ ...input.issuerKid === void 0 ? {} : { issuerKid: input.issuerKid }
6744
7012
  });
6745
7013
  }
6746
7014
 
@@ -6828,7 +7096,7 @@ async function createPolicy(options, capabilities) {
6828
7096
  function addressedPlan(options) {
6829
7097
  const capabilities = sortCanonical([
6830
7098
  { kind: "kv", resource: options.contentSource.kvResource, selector: options.resource.kind, actions: [...options.policyActions] },
6831
- { kind: "encryption", resource: options.contentSource.encryptionNetwork, action: "tinycloud.encryption/decrypt" }
7099
+ { kind: "encryption", resource: options.contentSource.encryptionKeyPath === void 0 ? options.contentSource.encryptionNetwork : `${options.contentSource.encryptionNetwork}/${options.contentSource.encryptionKeyPath}`, action: "tinycloud.encryption/decrypt" }
6832
7100
  ]);
6833
7101
  return {
6834
7102
  expiry: rfc3339Seconds(options.expiresAt),
@@ -7158,6 +7426,7 @@ async function addressedShareBodyUpperBound(input) {
7158
7426
  kvResource: `${input.spaceId}/kv/${path}`,
7159
7427
  selector: input.resource.kind,
7160
7428
  encryptionNetwork: input.encryptionNetwork,
7429
+ ...input.encryptionKeyPath === void 0 ? {} : { encryptionKeyPath: input.encryptionKeyPath },
7161
7430
  encryptedSymmetricKeyDigestHex: "f".repeat(64),
7162
7431
  keyVersion: Number.MAX_SAFE_INTEGER,
7163
7432
  mode: "immutable",
@@ -8543,41 +8812,18 @@ function bytes2(value, label) {
8543
8812
  if (decoded.length !== 64 || toBase64Url(decoded) !== value) throw new Error(`${label} is invalid`);
8544
8813
  return decoded;
8545
8814
  }
8546
- var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
8547
8815
  function toBase64(bytes3) {
8548
- let out = "";
8549
- for (let i = 0; i < bytes3.length; i += 3) {
8550
- const b0 = bytes3[i];
8551
- const b1 = i + 1 < bytes3.length ? bytes3[i + 1] : 0;
8552
- const b2 = i + 2 < bytes3.length ? bytes3[i + 2] : 0;
8553
- out += BASE64_ALPHABET[b0 >> 2 & 63];
8554
- out += BASE64_ALPHABET[(b0 << 4 | b1 >> 4) & 63];
8555
- out += i + 1 < bytes3.length ? BASE64_ALPHABET[(b1 << 2 | b2 >> 6) & 63] : "=";
8556
- out += i + 2 < bytes3.length ? BASE64_ALPHABET[b2 & 63] : "=";
8557
- }
8558
- return out;
8816
+ return toBase64Url(bytes3).replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(bytes3.length / 3) * 4, "=");
8559
8817
  }
8560
8818
  function fromBase64(value, label) {
8561
8819
  if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
8562
8820
  throw new Error(`${label} is invalid`);
8563
8821
  }
8564
- const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
8565
- const out = new Uint8Array(value.length / 4 * 3 - padding);
8566
- let outIdx = 0;
8567
- for (let i = 0; i < value.length; i += 4) {
8568
- const v0 = BASE64_ALPHABET.indexOf(value[i]);
8569
- const v1 = BASE64_ALPHABET.indexOf(value[i + 1]);
8570
- const v2 = value[i + 2] === "=" ? 0 : BASE64_ALPHABET.indexOf(value[i + 2]);
8571
- const v3 = value[i + 3] === "=" ? 0 : BASE64_ALPHABET.indexOf(value[i + 3]);
8572
- const b0 = v0 << 2 | v1 >> 4;
8573
- const b1 = (v1 & 15) << 4 | v2 >> 2;
8574
- const b2 = (v2 & 3) << 6 | v3;
8575
- if (outIdx < out.length) out[outIdx++] = b0;
8576
- if (outIdx < out.length) out[outIdx++] = b1;
8577
- if (outIdx < out.length) out[outIdx++] = b2;
8578
- }
8579
- if (toBase64(out) !== value) throw new Error(`${label} is invalid`);
8580
- return out;
8822
+ try {
8823
+ return fromBase64Url(value.replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_"));
8824
+ } catch {
8825
+ throw new Error(`${label} is invalid`);
8826
+ }
8581
8827
  }
8582
8828
  async function digestBytes2(value) {
8583
8829
  return toBase64Url(new Uint8Array(await crypto.subtle.digest("SHA-256", value)));
@@ -8694,16 +8940,16 @@ function matchesInitialCiphertextDigest(bytes3, expected) {
8694
8940
  function canonicalHashHex(value) {
8695
8941
  return hex3((0, import_sha2565.sha256)(new TextEncoder().encode(canonicalize(value))));
8696
8942
  }
8697
- async function aesGcmDecrypt(key, blob) {
8943
+ async function aesGcmDecrypt(key, blob, aad) {
8698
8944
  if (key.length !== 32 || blob.length < 28) throw new Error("encrypted content is malformed");
8699
8945
  const cryptoKey = await crypto.subtle.importKey("raw", key, "AES-GCM", false, ["decrypt"]);
8700
- return new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv: blob.slice(0, 12) }, cryptoKey, blob.slice(12)));
8946
+ return new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv: blob.slice(0, 12), ...aad && { additionalData: aad } }, cryptoKey, blob.slice(12)));
8701
8947
  }
8702
- async function aesGcmEncrypt(key, plaintext) {
8948
+ async function aesGcmEncrypt(key, plaintext, aad) {
8703
8949
  if (key.length !== 32) throw new Error("encrypted content key is malformed");
8704
8950
  const nonce = crypto.getRandomValues(new Uint8Array(12));
8705
8951
  const cryptoKey = await crypto.subtle.importKey("raw", key, "AES-GCM", false, ["encrypt"]);
8706
- const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: nonce }, cryptoKey, plaintext));
8952
+ const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: nonce, ...aad && { additionalData: aad } }, cryptoKey, plaintext));
8707
8953
  const output = new Uint8Array(nonce.length + ciphertext.length);
8708
8954
  output.set(nonce);
8709
8955
  output.set(ciphertext, nonce.length);
@@ -8718,8 +8964,9 @@ function parseV3InlineEncryptedEnvelope(bytes3, expected) {
8718
8964
  }
8719
8965
  if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("encrypted content envelope is malformed");
8720
8966
  const record = value;
8721
- const allowed = ["v", "networkId", "alg", "keyVersion", "encryptedSymmetricKey", "encryptedSymmetricKeyHash", "ciphertext", "metadata"];
8722
- 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"))) {
8967
+ const allowed = ["v", "networkId", "keyPath", "alg", "keyVersion", "encryptedSymmetricKey", "encryptedSymmetricKeyHash", "ciphertext", "aad", "metadata"];
8968
+ const source = expected.contentSource;
8969
+ 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" || hex3((0, import_sha2565.sha256)(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"))) {
8723
8970
  throw new Error("encrypted content envelope binding is invalid");
8724
8971
  }
8725
8972
  return record;
@@ -8840,7 +9087,7 @@ var ShareRecipientClient = class {
8840
9087
  this.session = { sessionId: delegation.sessionCid, expiresAt: new Date(compact.payload.exp * 1e3).toISOString(), actions: envelope.actions, resource: envelope.resource };
8841
9088
  return this.session;
8842
9089
  }
8843
- /** Decrypt a v3 KV value through a fresh recipient-signed decrypt invocation. */
9090
+ /** Decrypt unscoped v1 or path-bound v2 content, independently checking canonical and registered raw-key digests. */
8844
9091
  async decryptV3Content(bytes3) {
8845
9092
  const envelope = this.options.envelope;
8846
9093
  const signer = this.nativeSigner ?? this.signer;
@@ -8848,18 +9095,22 @@ var ShareRecipientClient = class {
8848
9095
  assertArtifactBinding(artifactBindingView(envelope));
8849
9096
  if (!matchesInitialCiphertextDigest(bytes3, envelope.contentSource.initialCiphertextDigestHex)) throw new Error("v3 owner KV ciphertext digest mismatch");
8850
9097
  const encrypted = parseV3InlineEncryptedEnvelope(bytes3, envelope);
9098
+ const ciphertext = fromBase64(encrypted.ciphertext, "v3 encrypted content");
9099
+ if (ciphertext.length < 28) throw new Error("encrypted content is malformed");
9100
+ const aad = encrypted.aad === void 0 ? void 0 : fromBase64(encrypted.aad, "v3 encrypted content AAD");
8851
9101
  const receiverPrivateKey = crypto.getRandomValues(new Uint8Array(32));
8852
9102
  const receiverPublicKey = toBase64(import_ed255195.x25519.getPublicKey(receiverPrivateKey));
8853
9103
  const receiverPublicKeyHash = canonicalHashHex(receiverPublicKey);
8854
- 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 };
9104
+ 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 };
8855
9105
  const bodyHash = hex3((0, import_sha2565.sha256)(new TextEncoder().encode(canonicalize(body))));
8856
9106
  const session = verifyCompactUcanAuthorization(this.v3Authorization, this.session.sessionId);
8857
9107
  const now = Math.floor(Date.now() / 1e3);
9108
+ const decryptResource = encrypted.keyPath === void 0 ? encrypted.networkId : `${encrypted.networkId}/${encrypted.keyPath}`;
8858
9109
  const invocation = await signCompactUcanAuthorization({
8859
9110
  issuerDid: this.options.holderDid,
8860
9111
  audienceDid: this.v3NodeAudience,
8861
- attenuation: { [encrypted.networkId]: { "tinycloud.encryption/decrypt": [{}] } },
8862
- facts: [{ type: body.type, targetNode: body.targetNode, networkId: body.networkId, bodyHash, encryptedSymmetricKeyHash: body.encryptedSymmetricKeyHash, receiverPublicKeyHash, alg: body.alg, keyVersion: body.keyVersion }],
9112
+ attenuation: { [decryptResource]: { "tinycloud.encryption/decrypt": [{}] } },
9113
+ 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 }],
8863
9114
  proofs: [session.cid],
8864
9115
  notBefore: now,
8865
9116
  expiresAt: Math.min(now + 60, session.payload.exp),
@@ -8883,7 +9134,7 @@ var ShareRecipientClient = class {
8883
9134
  const symmetricKey = await aesGcmDecrypt(shared, wrapped.slice(33));
8884
9135
  shared.fill(0);
8885
9136
  if (symmetricKey.length !== 32) throw new Error("v3 content key is malformed");
8886
- const plaintext = await aesGcmDecrypt(symmetricKey, fromBase64(encrypted.ciphertext, "v3 encrypted content"));
9137
+ const plaintext = await aesGcmDecrypt(symmetricKey, ciphertext, aad);
8887
9138
  this.v3ContentKey?.fill(0);
8888
9139
  this.v3ContentKey = symmetricKey;
8889
9140
  this.v3ContentEnvelope = encrypted;
@@ -8895,7 +9146,9 @@ var ShareRecipientClient = class {
8895
9146
  /** Re-encrypt edited v3 content with the admitted content key. */
8896
9147
  async encryptV3Content(bytes3, mediaType) {
8897
9148
  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");
8898
- return new TextEncoder().encode(canonicalize({ ...this.v3ContentEnvelope, ciphertext: toBase64Url(await aesGcmEncrypt(this.v3ContentKey, bytes3)), metadata: { ...this.v3ContentEnvelope.metadata ?? {}, contentType: mediaType } }));
9149
+ const envelope = this.v3ContentEnvelope;
9150
+ const aad = envelope.aad === void 0 ? void 0 : fromBase64(envelope.aad, "v3 encrypted content AAD");
9151
+ return new TextEncoder().encode(canonicalize({ ...envelope, ciphertext: toBase64(await aesGcmEncrypt(this.v3ContentKey, bytes3, aad)), metadata: { ...envelope.metadata ?? {}, contentType: mediaType } }));
8899
9152
  }
8900
9153
  async resumeWithProof(envelope, resumeToken, proof) {
8901
9154
  void envelope;