@tinycloud/share-sdk 0.2.0 → 0.3.0-beta.0

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
@@ -5310,9 +5310,12 @@ function validateV2Invariants(value, ctx) {
5310
5310
  var unsignedShareEnvelopeV2Schema = unsignedShareEnvelopeV2BaseSchema.superRefine(validateV2Invariants);
5311
5311
  var shareEnvelopeV2Schema = unsignedShareEnvelopeV2BaseSchema.extend({ signature: signatureSchema }).strict().superRefine(validateV2Invariants);
5312
5312
  var unifiedResourceSchema = external_exports.string().refine((value) => {
5313
- const match = /^tinycloud:\/\/([^/]+)\/kv\/(.+)$/.exec(value);
5314
- if (match === null || /[:?#%]/.test(match[1])) return false;
5315
- const path = match[2];
5313
+ const marker = value.indexOf("/kv/");
5314
+ if (marker < 1) return false;
5315
+ const space = value.slice(0, marker);
5316
+ const legacy = space.startsWith("tinycloud://");
5317
+ if (legacy ? /[:/?#%]/.test(space.slice("tinycloud://".length)) : !space.startsWith("tinycloud:") || /[/?#%]/.test(space)) return false;
5318
+ const path = value.slice(marker + 4);
5316
5319
  return !path.startsWith("/") && !path.endsWith("/") && !path.includes("//") && path.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
5317
5320
  }, { message: "expected canonical TinyCloud KV resource" });
5318
5321
  var unifiedEncryptionNetworkSchema = external_exports.string().refine((value) => {
@@ -5350,7 +5353,7 @@ var unifiedContentSourceSchema = external_exports.object({
5350
5353
  mode: external_exports.union([external_exports.literal("mutable"), external_exports.literal("immutable")]),
5351
5354
  initialCiphertextDigestHex: external_exports.string().regex(/^[0-9a-f]{64}$/).optional()
5352
5355
  }).strict();
5353
- var unifiedPolicySchema = external_exports.object({
5356
+ var unifiedPolicyV1Schema = external_exports.object({
5354
5357
  schema: external_exports.literal("xyz.tinycloud.policy/policy/v1"),
5355
5358
  policyId: external_exports.string().regex(/^pol_[a-z2-7]+$/),
5356
5359
  ownerDid: external_exports.string().min(1),
@@ -5360,6 +5363,31 @@ var unifiedPolicySchema = external_exports.object({
5360
5363
  capabilityCeiling: external_exports.array(unifiedCapabilitySchema).min(2),
5361
5364
  signature: external_exports.object({ suite: external_exports.string().min(1), signerDid: external_exports.string().min(1), value: external_exports.string().min(1) }).strict()
5362
5365
  }).strict();
5366
+ var policyCredentialRequirementV1Schema = external_exports.object({
5367
+ type: external_exports.literal("TinyCloudPolicyCredentialRequirement"),
5368
+ version: external_exports.literal(1),
5369
+ requirementDigest: external_exports.string().regex(/^[A-Za-z0-9_-]{43}$/),
5370
+ descriptorDigest: external_exports.string().regex(/^[A-Za-z0-9_-]{43}$/),
5371
+ issuerDid: external_exports.string().min(1),
5372
+ issuerKid: external_exports.string().min(1),
5373
+ profile: external_exports.object({ id: external_exports.string().min(1), version: external_exports.literal(1) }).strict(),
5374
+ credentialType: external_exports.object({ id: external_exports.string().min(1), version: external_exports.literal(1) }).strict()
5375
+ }).strict();
5376
+ var unifiedPolicyV2Schema = external_exports.object({
5377
+ schema: external_exports.literal("xyz.tinycloud.policy/policy/v2"),
5378
+ policyId: external_exports.string().regex(/^pol_[a-z2-7]+$/),
5379
+ ownerDid: external_exports.string().min(1),
5380
+ createdAt: external_exports.string().datetime({ offset: true }),
5381
+ expiresAt: external_exports.string().datetime({ offset: true }).optional(),
5382
+ contentSource: unifiedContentSourceSchema,
5383
+ capabilityCeiling: external_exports.array(unifiedCapabilitySchema).min(2),
5384
+ credentialRequirement: policyCredentialRequirementV1Schema,
5385
+ signature: external_exports.object({ suite: external_exports.literal("Ed25519"), signerDid: external_exports.string().min(1), value: external_exports.string().min(1) }).strict()
5386
+ }).strict();
5387
+ var unifiedPolicySchema = external_exports.discriminatedUnion("schema", [
5388
+ unifiedPolicyV1Schema,
5389
+ unifiedPolicyV2Schema
5390
+ ]);
5363
5391
  var unifiedRootSchema = external_exports.object({
5364
5392
  cid: external_exports.string().min(1),
5365
5393
  authorization: external_exports.string().regex(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/),
@@ -5414,7 +5442,7 @@ function validateV3Invariants(value, ctx) {
5414
5442
  if (value.contentSource.encryptionNetwork !== value.encryptionNetwork) {
5415
5443
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["encryptionNetwork"], message: "encryption network is not bound to the source" });
5416
5444
  }
5417
- if (value.attestedEnforcerBinding.enforcerDid !== value.target.nodeAudience || value.attestedEnforcerBinding.nodeAudience !== value.target.nodeAudience || value.attestedEnforcerBinding.signature.signerDid !== value.target.nodeAudience || Date.parse(value.attestedEnforcerBinding.expiresAt) < Date.parse(value.expiry)) {
5445
+ if (value.attestedEnforcerBinding.enforcerDid !== value.target.nodeAudience || value.attestedEnforcerBinding.signature.signerDid !== value.attestedEnforcerBinding.nodeAudience || Date.parse(value.attestedEnforcerBinding.expiresAt) < Date.parse(value.expiry)) {
5418
5446
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["attestedEnforcerBinding"], message: "enforcer binding does not cover the target and share lifetime" });
5419
5447
  }
5420
5448
  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) {
@@ -5850,6 +5878,13 @@ function equal(left, right) {
5850
5878
  var ED25519_VERIFY_OPTS2 = { zip215: false };
5851
5879
  var ENVELOPE_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v1\0";
5852
5880
  var ENVELOPE_V2_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v2\0";
5881
+ var ENVELOPE_V3_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v3\0";
5882
+ var POLICY_V1_SIGNATURE_DOMAIN = "xyz.tinycloud.policy/policy/v1\0";
5883
+ var POLICY_V2_SIGNATURE_DOMAIN = "xyz.tinycloud.policy/policy/v2\0";
5884
+ var CONTENT_SOURCE_V1_DOMAIN = "xyz.tinycloud.policy/ContentSource/v1\0";
5885
+ var POLICY_CAPABILITY_V1_DOMAIN = "xyz.tinycloud.policy/PolicyCapability/v1\0";
5886
+ var NATIVE_PROJECTION_V1_DOMAIN = "xyz.tinycloud.policy/NativeProjection/v1\0";
5887
+ var ATTESTED_ENFORCER_V2_DOMAIN = "xyz.tinycloud.policy/AttestedEnforcerBinding/v2\0";
5853
5888
  function signingBytes(unsigned) {
5854
5889
  const domain = utf8Bytes(ENVELOPE_SIGNATURE_DOMAIN);
5855
5890
  const body = utf8Bytes(canonicalize(unsigned));
@@ -5866,6 +5901,14 @@ function signingBytesV2(unsigned) {
5866
5901
  bytes3.set(body, domain.length);
5867
5902
  return bytes3;
5868
5903
  }
5904
+ function signingBytesV3(unsigned) {
5905
+ const domain = utf8Bytes(ENVELOPE_V3_SIGNATURE_DOMAIN);
5906
+ const body = utf8Bytes(canonicalize(unsigned));
5907
+ const bytes3 = new Uint8Array(domain.length + body.length);
5908
+ bytes3.set(domain);
5909
+ bytes3.set(body, domain.length);
5910
+ return bytes3;
5911
+ }
5869
5912
  function signEnvelope(envelopeWithoutSig, ed25519PrivKey) {
5870
5913
  const unsigned = unsignedShareEnvelopeSchema.parse(envelopeWithoutSig);
5871
5914
  const publicKey = import_ed255193.ed25519.getPublicKey(ed25519PrivKey);
@@ -5913,6 +5956,92 @@ async function verifyEnvelopeV2(envelope, options) {
5913
5956
  if (parsed.authorizationTarget.kind === "policy") return await computeCid(fromBase64Url(parsed.authorizationTarget.policyBytes)) === parsed.authorizationTarget.policyCid;
5914
5957
  return true;
5915
5958
  }
5959
+ function verifyEnvelopeV3SignatureOnly(envelope) {
5960
+ const parsed = shareEnvelopeV3Schema.parse(envelope);
5961
+ const { signature, ...unsigned } = parsed;
5962
+ return import_ed255193.ed25519.verify(fromBase64Url(signature.value), (0, import_sha256.sha256)(signingBytesV3(unsigned)), ed25519PublicKeyFromDidKey(signature.signerDid), ED25519_VERIFY_OPTS2);
5963
+ }
5964
+ async function verifyEnvelopeV3(envelope, options) {
5965
+ const parsed = shareEnvelopeV3Schema.parse(envelope);
5966
+ if (parsed.signature.signerDid !== options.expectedSignerDid || parsed.signature.signerDid !== parsed.policy.ownerDid || !verifyEnvelopeV3SignatureOnly(parsed)) return false;
5967
+ const { policy } = parsed;
5968
+ const unsignedPolicy = { ...policy };
5969
+ delete unsignedPolicy.policyId;
5970
+ delete unsignedPolicy.signature;
5971
+ const policySignatureDomain = policy.schema === "xyz.tinycloud.policy/policy/v2" ? POLICY_V2_SIGNATURE_DOMAIN : POLICY_V1_SIGNATURE_DOMAIN;
5972
+ const policyDigest = (0, import_sha256.sha256)(new TextEncoder().encode(`${policySignatureDomain}${canonicalize(unsignedPolicy)}`));
5973
+ const policySignature = policy.signature;
5974
+ if (policySignature.suite !== "Ed25519" || policySignature.signerDid !== policy.ownerDid) return false;
5975
+ let policyPublicKey;
5976
+ let policySignatureBytes;
5977
+ try {
5978
+ policyPublicKey = ed25519PublicKeyFromDidKey(policy.ownerDid);
5979
+ policySignatureBytes = fromBase64Url(policySignature.value);
5980
+ } catch {
5981
+ return false;
5982
+ }
5983
+ if (policySignatureBytes.length !== 64 || !import_ed255193.ed25519.verify(policySignatureBytes, policyDigest, policyPublicKey, ED25519_VERIFY_OPTS2)) return false;
5984
+ const policyIdDigest = policyDigest;
5985
+ if (policy.policyId !== `pol_${base32Lower(policyIdDigest)}`) return false;
5986
+ const policyBytes = new TextEncoder().encode(canonicalize(policy));
5987
+ if (await computeCid(policyBytes) !== parsed.policyCid) return false;
5988
+ const sourceDigest = (0, import_sha256.sha256)(new TextEncoder().encode(`${CONTENT_SOURCE_V1_DOMAIN}${canonicalize(policy.contentSource)}`));
5989
+ if (hex(sourceDigest) !== parsed.contentSourceDigestHex) return false;
5990
+ const sortedCapabilities = [...policy.capabilityCeiling].sort((left, right) => canonicalize(left).localeCompare(canonicalize(right)));
5991
+ const capabilityCeilingHashHex = hex((0, import_sha256.sha256)(new TextEncoder().encode(`${POLICY_CAPABILITY_V1_DOMAIN}${canonicalize(sortedCapabilities)}`)));
5992
+ const nativeProjection = sortedCapabilities.map((capability) => capability.kind === "encryption" ? { service: "tinycloud.encryption", space: capability.resource, path: capability.resource, actions: [capability.action] } : { service: "tinycloud.kv", space: capability.resource.slice(0, capability.resource.indexOf("/kv/")), path: capability.resource.split("/kv/")[1], actions: [...capability.actions], caveat: { type: "xyz.tinycloud.resource/selector", kind: capability.selector, value: capability.resource } }).sort((left, right) => canonicalize(left).localeCompare(canonicalize(right)));
5993
+ const nativeProjectionHashHex = hex((0, import_sha256.sha256)(new TextEncoder().encode(`${NATIVE_PROJECTION_V1_DOMAIN}${canonicalize(nativeProjection)}`)));
5994
+ const expectedAttenuation = Object.fromEntries(sortedCapabilities.map((capability) => capability.kind === "encryption" ? [capability.resource, { [capability.action]: [{}] }] : [capability.resource, Object.fromEntries(capability.actions.map((action) => [action, [{ kind: capability.selector, type: "xyz.tinycloud.resource/selector", value: capability.resource }]]))]));
5995
+ const kv = policy.capabilityCeiling.find((capability) => capability.kind === "kv");
5996
+ const expectedKvActions = parsed.actions.flatMap((action) => action === "read" ? ["tinycloud.kv/get", "tinycloud.kv/metadata"] : action === "list" ? ["tinycloud.kv/list"] : ["tinycloud.kv/put"]);
5997
+ const marker = parsed.contentSource.kvResource.indexOf("/kv/");
5998
+ const resourceSpace = marker < 1 ? "" : parsed.contentSource.kvResource.slice(0, marker);
5999
+ const resourcePath = marker < 1 ? "" : parsed.contentSource.kvResource.slice(marker + 4);
6000
+ if (parsed.shareId !== parsed.contentSource.shareId || resourceSpace !== parsed.target.spaceId || resourcePath !== parsed.resource.path.replace(/\/$/, "") || parsed.resource.kind !== parsed.contentSource.selector || kv?.kind !== "kv" || expectedKvActions.some((action) => !kv.actions.includes(action)) || parsed.encryptionNetwork !== parsed.contentSource.encryptionNetwork || parsed.contentSource.keyVersion <= 0 || policy.expiresAt !== void 0 && Date.parse(parsed.expiry) > Date.parse(policy.expiresAt)) return false;
6001
+ const binding = parsed.attestedEnforcerBinding;
6002
+ const { signature: bindingSignature, ...unsignedBinding } = binding;
6003
+ const expectedBindingDigestHex = hex((0, import_sha256.sha256)(new TextEncoder().encode(canonicalize({ enforcerDid: binding.enforcerDid, nodeAudience: binding.nodeAudience }))));
6004
+ if (binding.enforcerDid !== parsed.target.nodeAudience || binding.attestationBindingDigestHex !== expectedBindingDigestHex || bindingSignature.signerDid !== binding.nodeAudience || bindingSignature.suite !== "Ed25519" || Date.parse(binding.issuedAt) > Date.now() || Date.parse(binding.expiresAt) <= Date.now() || Date.parse(binding.expiresAt) < Date.parse(parsed.expiry)) return false;
6005
+ try {
6006
+ const digest4 = (0, import_sha256.sha256)(new TextEncoder().encode(`${ATTESTED_ENFORCER_V2_DOMAIN}${canonicalize(unsignedBinding)}`));
6007
+ if (!import_ed255193.ed25519.verify(fromBase64Url(bindingSignature.value), digest4, ed25519PublicKeyFromDidKey(binding.nodeAudience), ED25519_VERIFY_OPTS2)) return false;
6008
+ } catch {
6009
+ return false;
6010
+ }
6011
+ if (parsed.policyRoot.role !== "policy-authority" || parsed.enforcementRoot.role !== "policy-enforcement" || parsed.policyRoot.cid === parsed.enforcementRoot.cid) return false;
6012
+ try {
6013
+ const policyRoot = verifyCompactUcanAuthorization(parsed.policyRoot.authorization, parsed.policyRoot.cid);
6014
+ const enforcementRoot = verifyCompactUcanAuthorization(parsed.enforcementRoot.authorization, parsed.enforcementRoot.cid);
6015
+ const policyFact = policyRoot.payload.fct[0];
6016
+ const enforcementFact = enforcementRoot.payload.fct[0];
6017
+ const common = ["ownerDid", "policyId", "policyDigestHex", "policyCid", "contentSourceDigestHex", "capabilityCeilingHashHex", "nativeProjectionHashHex", "nodeAudience"];
6018
+ const policyKeys = [...common, "role", "mode"];
6019
+ const enforcementKeys = [...policyKeys, "enforcerDid"];
6020
+ if (Object.keys(policyFact).length !== policyKeys.length || policyKeys.some((key) => !(key in policyFact)) || Object.keys(enforcementFact).length !== enforcementKeys.length || enforcementKeys.some((key) => !(key in enforcementFact)) || policyRoot.payload.prf.length !== 0 || enforcementRoot.payload.prf.length !== 0 || policyRoot.payload.iss.split("#", 1)[0] !== policy.ownerDid || enforcementRoot.payload.iss.split("#", 1)[0] !== policy.ownerDid || policyRoot.payload.nbf !== enforcementRoot.payload.nbf || policyRoot.payload.exp !== enforcementRoot.payload.exp || canonicalize(policyRoot.payload.att) !== canonicalize(expectedAttenuation) || canonicalize(enforcementRoot.payload.att) !== canonicalize(expectedAttenuation) || policyFact.role !== "policy-authority" || policyFact.mode !== "policy-source" || "enforcerDid" in policyFact || enforcementFact.role !== "policy-enforcement" || enforcementFact.mode !== "conditional-mint" || policyRoot.payload.aud !== `did:tinycloud:policy:${hex(policyIdDigest)}` || enforcementRoot.payload.aud !== binding.enforcerDid || enforcementFact.enforcerDid !== binding.enforcerDid || common.some((key) => policyFact[key] !== enforcementFact[key]) || policyFact.ownerDid !== policy.ownerDid || policyFact.policyId !== policy.policyId || policyFact.policyDigestHex !== hex(policyIdDigest) || policyFact.policyCid !== parsed.policyCid || policyFact.contentSourceDigestHex !== parsed.contentSourceDigestHex || policyFact.capabilityCeilingHashHex !== capabilityCeilingHashHex || policyFact.nativeProjectionHashHex !== nativeProjectionHashHex || policyFact.nodeAudience !== binding.nodeAudience || policyRoot.payload.exp * 1e3 < Date.parse(parsed.expiry)) return false;
6021
+ } catch {
6022
+ return false;
6023
+ }
6024
+ return true;
6025
+ }
6026
+ function hex(bytes3) {
6027
+ return [...bytes3].map((byte) => byte.toString(16).padStart(2, "0")).join("");
6028
+ }
6029
+ function base32Lower(bytes3) {
6030
+ const alphabet = "abcdefghijklmnopqrstuvwxyz234567";
6031
+ let output = "";
6032
+ let buffer = 0;
6033
+ let bits = 0;
6034
+ for (const byte of bytes3) {
6035
+ buffer = buffer << 8 | byte;
6036
+ bits += 8;
6037
+ while (bits >= 5) {
6038
+ output += alphabet[buffer >>> bits - 5 & 31];
6039
+ bits -= 5;
6040
+ }
6041
+ }
6042
+ if (bits > 0) output += alphabet[buffer << 5 - bits & 31];
6043
+ return output;
6044
+ }
5916
6045
  var KEY_LENGTH2 = 32;
5917
6046
  var INLINE_PREFIX = "#v=2&p=";
5918
6047
  var MAX_INLINE_BYTES = 256 * 1024;
@@ -6097,8 +6226,12 @@ async function readResponseBytes(response, limit, tooLargeCode) {
6097
6226
  function registryFetcher(options, limit, tooLargeCode = "max-bytes-exceeded", stage = "envelope") {
6098
6227
  if (options.fetchBlob !== void 0) return async (input) => {
6099
6228
  try {
6100
- return boundedBytes(await options.fetchBlob(input), limit, tooLargeCode);
6229
+ options.signal?.throwIfAborted();
6230
+ const bytes3 = await options.fetchBlob({ ...input, ...options.signal === void 0 ? {} : { signal: options.signal } });
6231
+ options.signal?.throwIfAborted();
6232
+ return boundedBytes(bytes3, limit, tooLargeCode);
6101
6233
  } catch (error) {
6234
+ if (options.signal?.aborted) throw options.signal.reason ?? error;
6102
6235
  if (error instanceof ShareReceiveError) throw error;
6103
6236
  throw new ShareReceiveError("fetch-failed", "registry unavailable", { stage });
6104
6237
  }
@@ -6108,17 +6241,26 @@ function registryFetcher(options, limit, tooLargeCode = "max-bytes-exceeded", st
6108
6241
  const base3 = options.registryBaseUrl.replace(/\/+$/, "");
6109
6242
  return async ({ cid: cid2 }) => {
6110
6243
  try {
6111
- const response = await fetchFn(`${base3}/ipfs/${cid2}?format=raw`, { headers: { accept: "application/vnd.ipld.raw" }, redirect: "error" });
6244
+ options.signal?.throwIfAborted();
6245
+ const response = await fetchFn(`${base3}/ipfs/${cid2}?format=raw`, {
6246
+ headers: { accept: "application/vnd.ipld.raw" },
6247
+ redirect: "error",
6248
+ ...options.signal === void 0 ? {} : { signal: options.signal }
6249
+ });
6250
+ options.signal?.throwIfAborted();
6112
6251
  if (!response.ok) throw new ShareReceiveError("fetch-failed", `registry returned ${response.status}`, { stage });
6113
- return await readResponseBytes(response, limit, tooLargeCode);
6252
+ const bytes3 = await readResponseBytes(response, limit, tooLargeCode);
6253
+ options.signal?.throwIfAborted();
6254
+ return bytes3;
6114
6255
  } catch (error) {
6256
+ if (options.signal?.aborted) throw options.signal.reason ?? error;
6115
6257
  if (error instanceof ShareReceiveError) throw error;
6116
6258
  throw new ShareReceiveError("fetch-failed", "registry unavailable", { stage });
6117
6259
  }
6118
6260
  };
6119
6261
  }
6120
6262
  function metadataFor(envelope, origin) {
6121
- if (envelope.version === 2) {
6263
+ if (envelope.version === 2 || envelope.version === 3) {
6122
6264
  const targetKind3 = envelope.recipientMatcher.kind === "recipientDid" ? "recipientDid" : envelope.recipientMatcher.kind === "exactEmail" ? "email" : envelope.recipientMatcher.kind === "emailDomain" ? "emailDomain" : "bearer";
6123
6265
  return {
6124
6266
  protocol: "tinycloud-share",
@@ -6134,7 +6276,7 @@ function metadataFor(envelope, origin) {
6134
6276
  ...envelope.display.filename === void 0 ? {} : { filename: envelope.display.filename },
6135
6277
  ...envelope.display.mode === void 0 ? {} : { mode: envelope.display.mode }
6136
6278
  },
6137
- ...envelope.content === void 0 ? {} : { content: { cid: envelope.content.cid } }
6279
+ ...envelope.version === 2 && envelope.content !== void 0 ? { content: { cid: envelope.content.cid } } : {}
6138
6280
  };
6139
6281
  }
6140
6282
  return {
@@ -6159,8 +6301,10 @@ function parseEnvelope(bytes3) {
6159
6301
  const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes3);
6160
6302
  const value = JSON.parse(text);
6161
6303
  if (canonicalize(value) !== text) throw new Error("share envelope is not canonical JSON");
6162
- if (typeof value === "object" && value !== null && !Array.isArray(value) && value.version === 2) {
6163
- return shareEnvelopeV2Schema.parse(value);
6304
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
6305
+ const version = value.version;
6306
+ if (version === 3) return shareEnvelopeV3Schema.parse(value);
6307
+ if (version === 2) return shareEnvelopeV2Schema.parse(value);
6164
6308
  }
6165
6309
  return shareEnvelopeSchema.parse(value);
6166
6310
  } catch {
@@ -6232,6 +6376,19 @@ async function verifyV2Envelope(envelope, linkOrigin, options) {
6232
6376
  if (options.trustedSignerDid === void 0 || envelope.signature.signerDid !== options.trustedSignerDid) throw new ShareReceiveError("signature-invalid", "an addressed signer trust root is required");
6233
6377
  }
6234
6378
  }
6379
+ async function verifyV3Envelope(envelope, linkOrigin, options) {
6380
+ if (options.expectedOrigin !== void 0 && linkOrigin !== options.expectedOrigin) {
6381
+ throw new ShareReceiveError("origin-mismatch", "share link origin does not match the trusted origin");
6382
+ }
6383
+ try {
6384
+ if (!await verifyEnvelopeV3(envelope, { expectedSignerDid: envelope.policy.ownerDid })) throw new Error("signature");
6385
+ } catch {
6386
+ throw new ShareReceiveError("signature-invalid", "share signature is invalid");
6387
+ }
6388
+ const expiry = Date.parse(envelope.expiry);
6389
+ if (!Number.isFinite(expiry)) throw new ShareReceiveError("envelope-invalid", "share expiry is invalid");
6390
+ if (expiry <= (options.now?.() ?? Date.now())) throw new ShareReceiveError("expired", "share has expired", { expiresAt: envelope.expiry });
6391
+ }
6235
6392
  async function verifyV1PolicyEnvelope(envelope, linkOrigin, options) {
6236
6393
  if (envelope.authorizationTarget.kind !== "policy") throw new ShareReceiveError("unsupported-target", "share target is not an addressed policy", { reason: "policy-target" });
6237
6394
  if (envelope.target.origin !== linkOrigin || options.expectedOrigin !== void 0 && (linkOrigin !== options.expectedOrigin || envelope.target.origin !== options.expectedOrigin)) {
@@ -6263,12 +6420,18 @@ async function inspectShare(link, options = {}) {
6263
6420
  const resolved = await resolveShareEnvelope(link, options);
6264
6421
  if (resolved.envelope.version === 1 && resolved.envelope.authorizationTarget.kind === "policy") await verifyV1PolicyEnvelope(resolved.envelope, resolved.origin, options);
6265
6422
  else if (resolved.envelope.version === 1) await verifyResolved(resolved.envelope, resolved.origin, options);
6266
- else await verifyV2Envelope(resolved.envelope, resolved.origin, options);
6267
- if (resolved.envelope.version === 2 || resolved.envelope.authorizationTarget.kind === "policy") options.onResolvedAddressedEnvelope?.(resolved.envelope, resolved.cid);
6423
+ else if (resolved.envelope.version === 2) await verifyV2Envelope(resolved.envelope, resolved.origin, options);
6424
+ else await verifyV3Envelope(resolved.envelope, resolved.origin, options);
6425
+ if (resolved.envelope.version === 2 || resolved.envelope.version === 3 || resolved.envelope.authorizationTarget.kind === "policy") options.onResolvedAddressedEnvelope?.(resolved.envelope, resolved.cid);
6268
6426
  return { metadata: metadataFor(resolved.envelope, resolved.origin), link: { origin: resolved.origin, cid: resolved.cid, kind: resolved.kind } };
6269
6427
  }
6270
6428
  async function receiveShare(link, options = {}) {
6271
6429
  const resolved = await resolveShareEnvelope(link, options);
6430
+ if (resolved.envelope.version === 3) {
6431
+ await verifyV3Envelope(resolved.envelope, resolved.origin, options);
6432
+ options.onResolvedAddressedEnvelope?.(resolved.envelope, resolved.cid);
6433
+ return { state: "authorization-required", method: resolved.envelope.recipientMatcher.kind === "recipientDid" ? "openkey-device" : "email-claim" };
6434
+ }
6272
6435
  if (resolved.envelope.version === 2) {
6273
6436
  await verifyV2Envelope(resolved.envelope, resolved.origin, options);
6274
6437
  options.onResolvedAddressedEnvelope?.(resolved.envelope, resolved.cid);
@@ -8811,11 +8974,80 @@ function uiAction(action) {
8811
8974
  function selectedAction(envelope) {
8812
8975
  return envelope.actions.includes("list") ? "tinycloud.kv/list" : envelope.actions.includes("edit") ? "tinycloud.kv/put" : "tinycloud.kv/get";
8813
8976
  }
8814
- function hex(value) {
8977
+ function policyAttenuationForV3(envelope) {
8978
+ const policy = object2(envelope.policy, "v3 policy");
8979
+ if (!Array.isArray(policy.capabilityCeiling)) throw new Error("v3 policy capability ceiling is invalid");
8980
+ const attenuation = {};
8981
+ for (const raw of policy.capabilityCeiling) {
8982
+ const capability = object2(raw, "v3 policy capability");
8983
+ if (typeof capability.resource !== "string" || attenuation[capability.resource] !== void 0) throw new Error("v3 policy capability is invalid");
8984
+ if (capability.kind === "encryption") {
8985
+ if (capability.action !== "tinycloud.encryption/decrypt") throw new Error("v3 policy capability is invalid");
8986
+ attenuation[capability.resource] = { [capability.action]: [{}] };
8987
+ continue;
8988
+ }
8989
+ if (capability.kind !== "kv" || capability.selector !== "exact" && capability.selector !== "prefix" || !Array.isArray(capability.actions) || capability.actions.length === 0 || capability.actions.some((action) => typeof action !== "string")) throw new Error("v3 policy capability is invalid");
8990
+ attenuation[capability.resource] = Object.fromEntries(capability.actions.map((action) => [action, [{
8991
+ type: "xyz.tinycloud.resource/selector",
8992
+ kind: capability.selector,
8993
+ value: capability.resource
8994
+ }]]));
8995
+ }
8996
+ return attenuation;
8997
+ }
8998
+ var POLICY_SESSION_FACT_KEYS = [
8999
+ "profile",
9000
+ "ownerDid",
9001
+ "policyId",
9002
+ "policyDigestHex",
9003
+ "policyCid",
9004
+ "policyDelegationCid",
9005
+ "enforcementDelegationCid",
9006
+ "contentSourceDigestHex",
9007
+ "capabilityCeilingHashHex",
9008
+ "nativeProjectionHashHex",
9009
+ "enforcerDid",
9010
+ "nodeAudience",
9011
+ "recipientDid",
9012
+ "challengeId",
9013
+ "claimDigestHex",
9014
+ "claimJti",
9015
+ "vpDigestHex",
9016
+ "credentialEvidenceDigestHex",
9017
+ "decisionContextDigestHex",
9018
+ "issuanceAuditDigestHex",
9019
+ "remainingRedelegationDepth"
9020
+ ];
9021
+ var POLICY_SESSION_V4_AUDIT_FACT_KEYS = ["credentialIdAuditDigestHex", "presentationJtiAuditDigestHex"];
9022
+ var POLICY_SESSION_DIGEST_FACT_KEYS = [
9023
+ "policyDigestHex",
9024
+ "contentSourceDigestHex",
9025
+ "capabilityCeilingHashHex",
9026
+ "nativeProjectionHashHex",
9027
+ "claimDigestHex",
9028
+ "vpDigestHex",
9029
+ "credentialEvidenceDigestHex",
9030
+ "decisionContextDigestHex",
9031
+ "issuanceAuditDigestHex"
9032
+ ];
9033
+ var LOWER_SHA256_HEX = /^[0-9a-f]{64}$/;
9034
+ function verifyV3PolicyAuthorization(input) {
9035
+ const compact = verifyCompactUcanAuthorization(input.authorization, input.cid);
9036
+ const fact = compact.payload.fct[0];
9037
+ const factKeys = Object.keys(fact);
9038
+ const legacyShape = factKeys.length === POLICY_SESSION_FACT_KEYS.length && factKeys.every((key) => POLICY_SESSION_FACT_KEYS.includes(key)) && POLICY_SESSION_FACT_KEYS.every((key) => key in fact);
9039
+ const v4Shape = factKeys.length === POLICY_SESSION_FACT_KEYS.length + POLICY_SESSION_V4_AUDIT_FACT_KEYS.length && factKeys.every((key) => POLICY_SESSION_FACT_KEYS.includes(key) || POLICY_SESSION_V4_AUDIT_FACT_KEYS.includes(key)) && POLICY_SESSION_FACT_KEYS.every((key) => key in fact) && POLICY_SESSION_V4_AUDIT_FACT_KEYS.every((key) => key in fact);
9040
+ const binding = object2(input.envelope.attestedEnforcerBinding, "v3 attested enforcer binding");
9041
+ const policy = object2(input.envelope.policy, "v3 policy");
9042
+ const now = Math.floor(Date.now() / 1e3);
9043
+ 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");
9044
+ return compact;
9045
+ }
9046
+ function hex2(value) {
8815
9047
  return [...value].map((byte) => byte.toString(16).padStart(2, "0")).join("");
8816
9048
  }
8817
9049
  function canonicalHashHex(value) {
8818
- return hex((0, import_sha2564.sha256)(new TextEncoder().encode(canonicalize(value))));
9050
+ return hex2((0, import_sha2564.sha256)(new TextEncoder().encode(canonicalize(value))));
8819
9051
  }
8820
9052
  async function aesGcmDecrypt(key, blob) {
8821
9053
  if (key.length !== 32 || blob.length < 28) throw new Error("encrypted content is malformed");
@@ -8871,6 +9103,20 @@ var ShareRecipientClient = class {
8871
9103
  this.options = options;
8872
9104
  this.fetchFn = options.fetchFn ?? globalThis.fetch.bind(globalThis);
8873
9105
  this.signer = options.sign;
9106
+ if (options.policyAuthorization !== void 0) {
9107
+ if (options.envelope.version !== 3) throw new Error("policy delegation fast path requires a v3 envelope");
9108
+ const compact = verifyV3PolicyAuthorization({ ...options.policyAuthorization, envelope: options.envelope, holderDid: options.holderDid });
9109
+ const nodeAudience = compact.payload.fct[0].nodeAudience;
9110
+ this.v3Authorization = options.policyAuthorization.authorization;
9111
+ this.v3NodeAudience = nodeAudience;
9112
+ this.nativeSigner = options.sign;
9113
+ this.session = {
9114
+ sessionId: options.policyAuthorization.cid,
9115
+ expiresAt: new Date(compact.payload.exp * 1e3).toISOString(),
9116
+ actions: options.envelope.actions,
9117
+ resource: options.envelope.resource
9118
+ };
9119
+ }
8874
9120
  }
8875
9121
  fetchFn;
8876
9122
  session;
@@ -8878,6 +9124,7 @@ var ShareRecipientClient = class {
8878
9124
  nativeSigner;
8879
9125
  holderProof;
8880
9126
  v3Authorization;
9127
+ v3NodeAudience;
8881
9128
  v3ContentKey;
8882
9129
  v3ContentEnvelope;
8883
9130
  async beginChallenge(envelope) {
@@ -8967,37 +9214,52 @@ var ShareRecipientClient = class {
8967
9214
  }
8968
9215
  async establishV3(envelope) {
8969
9216
  if (this.options.buildPresentation === void 0) throw new Error("share presentation builder is required");
9217
+ const attestedEnforcer = object2(envelope.attestedEnforcerBinding, "v3 attested enforcer binding");
9218
+ if (attestedEnforcer.enforcerDid !== envelope.target.nodeAudience || typeof attestedEnforcer.nodeAudience !== "string") throw new Error("v3 attested enforcer binding mismatch");
9219
+ try {
9220
+ ed25519PublicKeyFromDidKey(attestedEnforcer.nodeAudience);
9221
+ } catch {
9222
+ throw new Error("v3 attested Node audience is invalid");
9223
+ }
8970
9224
  const challengeResponse = await this.fetchFn(new URL("/share/v3/policy/challenges", this.options.nodeOrigin), {
8971
9225
  method: "POST",
8972
9226
  redirect: "error",
8973
9227
  headers: { accept: "application/json", "content-type": "application/json" },
8974
- body: JSON.stringify({ policyCid: envelope.policyCid, recipientDid: this.options.holderDid, requestedCapabilities: envelope.policy.capabilityCeiling })
9228
+ body: JSON.stringify({ policyCid: envelope.policyCid, recipientDid: this.options.holderDid, requestedCapabilities: envelope.policy.capabilityCeiling }),
9229
+ ...this.options.signal === void 0 ? {} : { signal: this.options.signal }
8975
9230
  });
8976
9231
  if (!challengeResponse.ok) throw new Error(`v3 policy challenge rejected (${challengeResponse.status})`);
8977
9232
  const challenge = object2(await challengeResponse.json(), "v3 policy challenge");
8978
- if (typeof challenge.challengeId !== "string" || typeof challenge.nonce !== "string" || challenge.policyCid !== envelope.policyCid || challenge.recipientDid !== this.options.holderDid) throw new Error("v3 policy challenge binding mismatch");
9233
+ if (typeof challenge.challengeId !== "string" || typeof challenge.nonce !== "string" || challenge.policyCid !== envelope.policyCid || challenge.recipientDid !== this.options.holderDid || challenge.nodeAudience !== attestedEnforcer.nodeAudience || challenge.enforcerDid !== void 0 && challenge.enforcerDid !== attestedEnforcer.enforcerDid || typeof challenge.expiresAt !== "string" || !Number.isFinite(Date.parse(challenge.expiresAt)) || Date.parse(challenge.expiresAt) <= Date.now()) throw new Error("v3 policy challenge binding mismatch");
8979
9234
  const material = await this.options.buildPresentation({
8980
9235
  challenge,
8981
9236
  envelope,
8982
9237
  policy: envelope.policy
8983
9238
  });
8984
- if (material.sign === void 0 || material.claim === void 0 || material.presentation === void 0) throw new Error("v3 ceremony requires a recipient signer, claim, and presentation");
9239
+ if (material.sign === void 0 || material.presentation === void 0) throw new Error("v3 ceremony requires a recipient signer and presentation");
9240
+ const accountless = material.presentation.schema === "xyz.tinycloud.policy/presentation/v4";
9241
+ if (accountless ? material.credentialEnvelope === void 0 || material.requirement === void 0 : material.claim === void 0) {
9242
+ throw new Error(accountless ? "v4 ceremony requires a verified credential and requirement" : "v3 ceremony requires a claim");
9243
+ }
9244
+ this.options.onStage?.("policy-admission");
8985
9245
  const delegationResponse = await this.fetchFn(new URL("/share/v3/policy/delegations", this.options.nodeOrigin), {
8986
9246
  method: "POST",
8987
9247
  redirect: "error",
8988
9248
  headers: { accept: "application/json", "content-type": "application/json" },
8989
- body: JSON.stringify({ policyCid: envelope.policyCid, challengeId: challenge.challengeId, nonce: challenge.nonce, claim: material.claim, presentation: material.presentation })
9249
+ 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 }),
9250
+ ...this.options.signal === void 0 ? {} : { signal: this.options.signal }
8990
9251
  });
8991
9252
  if (!delegationResponse.ok) throw new Error(`v3 policy delegation rejected (${delegationResponse.status})`);
8992
9253
  const delegation = object2(await delegationResponse.json(), "v3 policy delegation");
8993
9254
  if (delegation.admitted !== true || typeof delegation.sessionCid !== "string" || typeof delegation.authorization !== "string") throw new Error("v3 policy delegation response is not admitted");
8994
- const compact = verifyCompactUcanAuthorization(delegation.authorization, delegation.sessionCid);
9255
+ const compact = verifyV3PolicyAuthorization({ authorization: delegation.authorization, cid: delegation.sessionCid, envelope, holderDid: this.options.holderDid });
8995
9256
  const fact = compact.payload.fct[0];
8996
- if (compact.payload.aud !== this.options.holderDid || fact.profile !== "policy-session-ucan/v1" || fact.policyCid !== envelope.policyCid || fact.recipientDid !== this.options.holderDid || fact.policyDelegationCid !== envelope.policyRoot.cid || fact.enforcementDelegationCid !== envelope.enforcementRoot.cid || compact.payload.prf.length !== 2 || compact.payload.prf[0] !== envelope.policyRoot.cid || compact.payload.prf[1] !== envelope.enforcementRoot.cid || compact.payload.exp - compact.payload.nbf > 60) throw new Error("v3 policy delegation signed binding mismatch");
8997
- const imported = await this.fetchFn(new URL("/delegate", this.options.nodeOrigin), { method: "POST", redirect: "error", headers: { Authorization: delegation.authorization } });
9257
+ this.options.onStage?.("delegation-import");
9258
+ const imported = await this.fetchFn(new URL("/delegate", this.options.nodeOrigin), { method: "POST", redirect: "error", headers: { Authorization: delegation.authorization }, ...this.options.signal === void 0 ? {} : { signal: this.options.signal } });
8998
9259
  if (!imported.ok) throw new Error(`ordinary delegation import rejected (${imported.status})`);
8999
9260
  this.signer = material.sign;
9000
9261
  this.v3Authorization = delegation.authorization;
9262
+ this.v3NodeAudience = fact.nodeAudience;
9001
9263
  this.session = { sessionId: delegation.sessionCid, expiresAt: new Date(compact.payload.exp * 1e3).toISOString(), actions: envelope.actions, resource: envelope.resource };
9002
9264
  return this.session;
9003
9265
  }
@@ -9005,18 +9267,18 @@ var ShareRecipientClient = class {
9005
9267
  async decryptV3Content(bytes3) {
9006
9268
  const envelope = this.options.envelope;
9007
9269
  const signer = this.nativeSigner ?? this.signer;
9008
- if (envelope.version !== 3 || this.session === void 0 || this.v3Authorization === void 0 || signer === void 0) throw new Error("v3 policy session signer is required");
9270
+ if (envelope.version !== 3 || this.session === void 0 || this.v3Authorization === void 0 || this.v3NodeAudience === void 0 || signer === void 0) throw new Error("v3 policy session signer is required");
9009
9271
  const encrypted = parseV3InlineEncryptedEnvelope(bytes3, envelope);
9010
9272
  const receiverPrivateKey = crypto.getRandomValues(new Uint8Array(32));
9011
9273
  const receiverPublicKey = toBase64Url(import_ed255198.x25519.getPublicKey(receiverPrivateKey));
9012
9274
  const receiverPublicKeyHash = canonicalHashHex(receiverPublicKey);
9013
- const body = { type: "tinycloud.encryption.decrypt/v1", targetNode: envelope.target.nodeAudience, networkId: encrypted.networkId, alg: encrypted.alg, keyVersion: encrypted.keyVersion, encryptedSymmetricKey: encrypted.encryptedSymmetricKey, encryptedSymmetricKeyHash: encrypted.encryptedSymmetricKeyHash, receiverPublicKey, receiverPublicKeyHash };
9014
- const bodyHash = hex((0, import_sha2564.sha256)(new TextEncoder().encode(canonicalize(body))));
9275
+ 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 };
9276
+ const bodyHash = hex2((0, import_sha2564.sha256)(new TextEncoder().encode(canonicalize(body))));
9015
9277
  const session = verifyCompactUcanAuthorization(this.v3Authorization, this.session.sessionId);
9016
9278
  const now = Math.floor(Date.now() / 1e3);
9017
9279
  const invocation = await signCompactUcanAuthorization({
9018
9280
  issuerDid: this.options.holderDid,
9019
- audienceDid: envelope.target.nodeAudience,
9281
+ audienceDid: this.v3NodeAudience,
9020
9282
  attenuation: { [encrypted.networkId]: { "tinycloud.encryption/decrypt": [{}] } },
9021
9283
  facts: [{ type: body.type, targetNode: body.targetNode, networkId: body.networkId, bodyHash, encryptedSymmetricKeyHash: body.encryptedSymmetricKeyHash, receiverPublicKeyHash, alg: body.alg, keyVersion: body.keyVersion }],
9022
9284
  proofs: [session.cid],
@@ -9026,11 +9288,12 @@ var ShareRecipientClient = class {
9026
9288
  sign: signer
9027
9289
  });
9028
9290
  try {
9029
- const response = await this.fetchFn(new URL("/invoke", this.options.nodeOrigin), { method: "POST", redirect: "error", headers: { accept: "application/json", "content-type": "application/json", Authorization: invocation.authorization }, body: JSON.stringify(body) });
9291
+ this.options.onStage?.("decryption");
9292
+ const response = await this.fetchFn(new URL("/invoke", this.options.nodeOrigin), { method: "POST", redirect: "error", headers: { accept: "application/json", "content-type": "application/json", Authorization: invocation.authorization }, body: JSON.stringify(body), ...this.options.signal === void 0 ? {} : { signal: this.options.signal } });
9030
9293
  if (!response.ok) throw new Error(`v3 decrypt invocation rejected (${response.status})`);
9031
9294
  const value = object2(await response.json(), "v3 decrypt response");
9032
9295
  const allowed = ["type", "targetNode", "networkId", "invocationCid", "encryptedSymmetricKeyHash", "receiverPublicKeyHash", "wrappedKey", "alg", "keyVersion", "requestHash", "nodeId", "nodeSignature"];
9033
- if (Object.keys(value).length !== allowed.length || Object.keys(value).some((key) => !allowed.includes(key)) || value.type !== "tinycloud.encryption.decrypt-result/v1" || value.targetNode !== body.targetNode || value.nodeId !== body.targetNode || value.networkId !== body.networkId || value.invocationCid !== invocation.cid || value.encryptedSymmetricKeyHash !== body.encryptedSymmetricKeyHash || value.receiverPublicKeyHash !== receiverPublicKeyHash || value.alg !== body.alg || value.keyVersion !== body.keyVersion || value.requestHash !== hex((0, import_sha2564.sha256)(new TextEncoder().encode(`${invocation.cid}${bodyHash}`))) || typeof value.wrappedKey !== "string" || typeof value.nodeSignature !== "string") throw new Error("v3 decrypt response binding is invalid");
9296
+ if (Object.keys(value).length !== allowed.length || Object.keys(value).some((key) => !allowed.includes(key)) || value.type !== "tinycloud.encryption.decrypt-result/v1" || value.targetNode !== body.targetNode || value.nodeId !== body.targetNode || value.networkId !== body.networkId || value.invocationCid !== invocation.cid || value.encryptedSymmetricKeyHash !== body.encryptedSymmetricKeyHash || value.receiverPublicKeyHash !== receiverPublicKeyHash || value.alg !== body.alg || value.keyVersion !== body.keyVersion || value.requestHash !== hex2((0, import_sha2564.sha256)(new TextEncoder().encode(`${invocation.cid}${bodyHash}`))) || typeof value.wrappedKey !== "string" || typeof value.nodeSignature !== "string") throw new Error("v3 decrypt response binding is invalid");
9034
9297
  const unsigned = { ...value };
9035
9298
  delete unsigned.nodeSignature;
9036
9299
  const signature = fromBase64(value.nodeSignature, "v3 decrypt response signature");
@@ -9100,7 +9363,7 @@ var ShareRecipientClient = class {
9100
9363
  }
9101
9364
  async nativeInvokeV3(request, envelope) {
9102
9365
  const signer = this.nativeSigner ?? this.signer;
9103
- if (this.session === void 0 || this.v3Authorization === void 0 || signer === void 0) throw new Error("v3 policy session signer is required");
9366
+ if (this.session === void 0 || this.v3Authorization === void 0 || this.v3NodeAudience === void 0 || signer === void 0) throw new Error("v3 policy session signer is required");
9104
9367
  const session = verifyCompactUcanAuthorization(this.v3Authorization, this.session.sessionId);
9105
9368
  if (session.payload.aud !== this.options.holderDid) throw new Error("v3 session recipient mismatch");
9106
9369
  const action = request.action === "list" ? "tinycloud.kv/list" : request.action === "put" ? "tinycloud.kv/put" : request.action === "metadata" ? "tinycloud.kv/metadata" : "tinycloud.kv/get";
@@ -9114,7 +9377,7 @@ var ShareRecipientClient = class {
9114
9377
  if (path !== root && !(capability.selector === "prefix" && path.startsWith(`${root}/`))) throw new Error("v3 KV request is outside the signed selector");
9115
9378
  const resource = `${capability.resource.slice(0, split + marker.length)}${path}`;
9116
9379
  const now = Math.floor(Date.now() / 1e3);
9117
- const invocation = await signCompactUcanAuthorization({ issuerDid: this.options.holderDid, audienceDid: envelope.target.nodeAudience, attenuation: { [resource]: { [action]: [{ type: "xyz.tinycloud.resource/selector", kind: capability.selector, value: resource }] } }, facts: [{ type: "tinycloud.policy.invocation/v1", policyCid: envelope.policyCid, sessionCid: session.cid }], proofs: [session.cid], notBefore: now, expiresAt: Math.min(now + 60, session.payload.exp), nonce: toBase64Url(crypto.getRandomValues(new Uint8Array(16))), sign: signer });
9380
+ const invocation = await signCompactUcanAuthorization({ issuerDid: this.options.holderDid, audienceDid: this.v3NodeAudience, attenuation: { [resource]: { [action]: [{ type: "xyz.tinycloud.resource/selector", kind: capability.selector, value: resource }] } }, facts: [{ type: "tinycloud.policy.invocation/v1", policyCid: envelope.policyCid, sessionCid: session.cid }], proofs: [session.cid], notBefore: now, expiresAt: Math.min(now + 60, session.payload.exp), nonce: toBase64Url(crypto.getRandomValues(new Uint8Array(16))), sign: signer });
9118
9381
  const headers = new Headers({ accept: "application/json", Authorization: invocation.authorization });
9119
9382
  let body;
9120
9383
  if (action === "tinycloud.kv/put") {
@@ -9123,7 +9386,8 @@ var ShareRecipientClient = class {
9123
9386
  headers.set("content-type", request.contentType ?? "application/octet-stream");
9124
9387
  if (request.ifMatch !== void 0) headers.set("if-match", request.ifMatch);
9125
9388
  }
9126
- return this.fetchFn(new URL("/invoke", this.options.nodeOrigin), { method: "POST", redirect: "error", headers, ...body === void 0 ? {} : { body } });
9389
+ this.options.onStage?.("invocation");
9390
+ return this.fetchFn(new URL("/invoke", this.options.nodeOrigin), { method: "POST", redirect: "error", headers, ...body === void 0 ? {} : { body }, ...this.options.signal === void 0 ? {} : { signal: this.options.signal } });
9127
9391
  }
9128
9392
  };
9129
9393
  function createAddressedAuthorization(input) {