@tinycloud/share-sdk 0.2.0 → 0.3.0-beta.1
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 +308 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -2
- package/dist/index.d.ts +16 -2
- package/dist/index.js +308 -54
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -5227,9 +5227,12 @@ function validateV2Invariants(value, ctx) {
|
|
|
5227
5227
|
var unsignedShareEnvelopeV2Schema = unsignedShareEnvelopeV2BaseSchema.superRefine(validateV2Invariants);
|
|
5228
5228
|
var shareEnvelopeV2Schema = unsignedShareEnvelopeV2BaseSchema.extend({ signature: signatureSchema }).strict().superRefine(validateV2Invariants);
|
|
5229
5229
|
var unifiedResourceSchema = external_exports.string().refine((value) => {
|
|
5230
|
-
const
|
|
5231
|
-
if (
|
|
5232
|
-
const
|
|
5230
|
+
const marker = value.indexOf("/kv/");
|
|
5231
|
+
if (marker < 1) return false;
|
|
5232
|
+
const space = value.slice(0, marker);
|
|
5233
|
+
const legacy = space.startsWith("tinycloud://");
|
|
5234
|
+
if (legacy ? /[:/?#%]/.test(space.slice("tinycloud://".length)) : !space.startsWith("tinycloud:") || /[/?#%]/.test(space)) return false;
|
|
5235
|
+
const path = value.slice(marker + 4);
|
|
5233
5236
|
return !path.startsWith("/") && !path.endsWith("/") && !path.includes("//") && path.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
|
|
5234
5237
|
}, { message: "expected canonical TinyCloud KV resource" });
|
|
5235
5238
|
var unifiedEncryptionNetworkSchema = external_exports.string().refine((value) => {
|
|
@@ -5267,7 +5270,7 @@ var unifiedContentSourceSchema = external_exports.object({
|
|
|
5267
5270
|
mode: external_exports.union([external_exports.literal("mutable"), external_exports.literal("immutable")]),
|
|
5268
5271
|
initialCiphertextDigestHex: external_exports.string().regex(/^[0-9a-f]{64}$/).optional()
|
|
5269
5272
|
}).strict();
|
|
5270
|
-
var
|
|
5273
|
+
var unifiedPolicyV1Schema = external_exports.object({
|
|
5271
5274
|
schema: external_exports.literal("xyz.tinycloud.policy/policy/v1"),
|
|
5272
5275
|
policyId: external_exports.string().regex(/^pol_[a-z2-7]+$/),
|
|
5273
5276
|
ownerDid: external_exports.string().min(1),
|
|
@@ -5277,6 +5280,31 @@ var unifiedPolicySchema = external_exports.object({
|
|
|
5277
5280
|
capabilityCeiling: external_exports.array(unifiedCapabilitySchema).min(2),
|
|
5278
5281
|
signature: external_exports.object({ suite: external_exports.string().min(1), signerDid: external_exports.string().min(1), value: external_exports.string().min(1) }).strict()
|
|
5279
5282
|
}).strict();
|
|
5283
|
+
var policyCredentialRequirementV1Schema = external_exports.object({
|
|
5284
|
+
type: external_exports.literal("TinyCloudPolicyCredentialRequirement"),
|
|
5285
|
+
version: external_exports.literal(1),
|
|
5286
|
+
requirementDigest: external_exports.string().regex(/^[A-Za-z0-9_-]{43}$/),
|
|
5287
|
+
descriptorDigest: external_exports.string().regex(/^[A-Za-z0-9_-]{43}$/),
|
|
5288
|
+
issuerDid: external_exports.string().min(1),
|
|
5289
|
+
issuerKid: external_exports.string().min(1),
|
|
5290
|
+
profile: external_exports.object({ id: external_exports.string().min(1), version: external_exports.literal(1) }).strict(),
|
|
5291
|
+
credentialType: external_exports.object({ id: external_exports.string().min(1), version: external_exports.literal(1) }).strict()
|
|
5292
|
+
}).strict();
|
|
5293
|
+
var unifiedPolicyV2Schema = external_exports.object({
|
|
5294
|
+
schema: external_exports.literal("xyz.tinycloud.policy/policy/v2"),
|
|
5295
|
+
policyId: external_exports.string().regex(/^pol_[a-z2-7]+$/),
|
|
5296
|
+
ownerDid: external_exports.string().min(1),
|
|
5297
|
+
createdAt: external_exports.string().datetime({ offset: true }),
|
|
5298
|
+
expiresAt: external_exports.string().datetime({ offset: true }).optional(),
|
|
5299
|
+
contentSource: unifiedContentSourceSchema,
|
|
5300
|
+
capabilityCeiling: external_exports.array(unifiedCapabilitySchema).min(2),
|
|
5301
|
+
credentialRequirement: policyCredentialRequirementV1Schema,
|
|
5302
|
+
signature: external_exports.object({ suite: external_exports.literal("Ed25519"), signerDid: external_exports.string().min(1), value: external_exports.string().min(1) }).strict()
|
|
5303
|
+
}).strict();
|
|
5304
|
+
var unifiedPolicySchema = external_exports.discriminatedUnion("schema", [
|
|
5305
|
+
unifiedPolicyV1Schema,
|
|
5306
|
+
unifiedPolicyV2Schema
|
|
5307
|
+
]);
|
|
5280
5308
|
var unifiedRootSchema = external_exports.object({
|
|
5281
5309
|
cid: external_exports.string().min(1),
|
|
5282
5310
|
authorization: external_exports.string().regex(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/),
|
|
@@ -5331,7 +5359,7 @@ function validateV3Invariants(value, ctx) {
|
|
|
5331
5359
|
if (value.contentSource.encryptionNetwork !== value.encryptionNetwork) {
|
|
5332
5360
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["encryptionNetwork"], message: "encryption network is not bound to the source" });
|
|
5333
5361
|
}
|
|
5334
|
-
if (value.attestedEnforcerBinding.enforcerDid !== value.target.nodeAudience || value.attestedEnforcerBinding.
|
|
5362
|
+
if (value.attestedEnforcerBinding.enforcerDid !== value.target.nodeAudience || value.attestedEnforcerBinding.signature.signerDid !== value.attestedEnforcerBinding.nodeAudience || Date.parse(value.attestedEnforcerBinding.expiresAt) < Date.parse(value.expiry)) {
|
|
5335
5363
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["attestedEnforcerBinding"], message: "enforcer binding does not cover the target and share lifetime" });
|
|
5336
5364
|
}
|
|
5337
5365
|
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) {
|
|
@@ -5695,7 +5723,7 @@ async function signCompactUcanAuthorization(input) {
|
|
|
5695
5723
|
const publicKey = ed25519PublicKeyFromDidKey(principal);
|
|
5696
5724
|
const header = {
|
|
5697
5725
|
alg: "EdDSA",
|
|
5698
|
-
jwk: { alg: "EdDSA", crv: "Ed25519", kty: "OKP", x:
|
|
5726
|
+
jwk: { alg: "EdDSA", crv: "Ed25519", kty: "OKP", x: toBase64Url(publicKey) },
|
|
5699
5727
|
typ: "JWT",
|
|
5700
5728
|
ucv: "0.10.0"
|
|
5701
5729
|
};
|
|
@@ -5709,13 +5737,13 @@ async function signCompactUcanAuthorization(input) {
|
|
|
5709
5737
|
nnc: input.nonce,
|
|
5710
5738
|
prf: input.proofs
|
|
5711
5739
|
};
|
|
5712
|
-
const protectedSegment =
|
|
5713
|
-
const payloadSegment =
|
|
5740
|
+
const protectedSegment = toBase64Url(new TextEncoder().encode(canonicalize(header)));
|
|
5741
|
+
const payloadSegment = toBase64Url(new TextEncoder().encode(canonicalize(payload)));
|
|
5714
5742
|
const signingInput = new TextEncoder().encode(`${protectedSegment}.${payloadSegment}`);
|
|
5715
5743
|
const signature = await input.sign(signingInput);
|
|
5716
5744
|
if (signature.length !== 64) throw new TypeError("compact Authorization signature must be Ed25519");
|
|
5717
5745
|
return verifyCompactUcanAuthorization(
|
|
5718
|
-
`${protectedSegment}.${payloadSegment}.${
|
|
5746
|
+
`${protectedSegment}.${payloadSegment}.${toBase64Url(signature)}`
|
|
5719
5747
|
);
|
|
5720
5748
|
}
|
|
5721
5749
|
function verifyCompactUcanAuthorization(authorization, expectedCid) {
|
|
@@ -5723,9 +5751,9 @@ function verifyCompactUcanAuthorization(authorization, expectedCid) {
|
|
|
5723
5751
|
const segments = authorization.split(".");
|
|
5724
5752
|
if (segments.length !== 3 || segments.some((segment) => segment.length === 0)) throw new TypeError("compact Authorization must contain three segments");
|
|
5725
5753
|
const [headerSegment, payloadSegment, signatureSegment] = segments;
|
|
5726
|
-
const headerBytes =
|
|
5727
|
-
const payloadBytes =
|
|
5728
|
-
const signature =
|
|
5754
|
+
const headerBytes = fromBase64Url(headerSegment);
|
|
5755
|
+
const payloadBytes = fromBase64Url(payloadSegment);
|
|
5756
|
+
const signature = fromBase64Url(signatureSegment);
|
|
5729
5757
|
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
5730
5758
|
const header = JSON.parse(decoder.decode(headerBytes));
|
|
5731
5759
|
const payload = JSON.parse(decoder.decode(payloadBytes));
|
|
@@ -5738,7 +5766,7 @@ function verifyCompactUcanAuthorization(authorization, expectedCid) {
|
|
|
5738
5766
|
if (typeof payload.iss !== "string" || typeof payload.aud !== "string" || typeof payload.nnc !== "string" || !Number.isInteger(payload.nbf) || !Number.isInteger(payload.exp) || payload.nbf >= payload.exp || !Array.isArray(payload.prf) || payload.prf.some((proof) => typeof proof !== "string") || !Array.isArray(payload.fct) || payload.fct.length !== 1) throw new TypeError("compact Authorization payload is invalid");
|
|
5739
5767
|
const principal = payload.iss.split("#", 1)[0];
|
|
5740
5768
|
const publicKey = ed25519PublicKeyFromDidKey(principal);
|
|
5741
|
-
if (!equal(publicKey,
|
|
5769
|
+
if (!equal(publicKey, fromBase64Url(jwk.x))) throw new TypeError("compact Authorization JWK does not bind issuer");
|
|
5742
5770
|
if (!ed255192.verify(signature, new TextEncoder().encode(`${headerSegment}.${payloadSegment}`), publicKey, { zip215: false })) throw new TypeError("compact Authorization signature is invalid");
|
|
5743
5771
|
const cid2 = CID.createV1(85, create(30, blake3(new TextEncoder().encode(authorization)))).toString();
|
|
5744
5772
|
if (expectedCid !== void 0 && cid2 !== expectedCid) throw new TypeError("compact Authorization CID mismatch");
|
|
@@ -5751,22 +5779,19 @@ function object(value, label) {
|
|
|
5751
5779
|
function assertExactKeys(value, keys, label) {
|
|
5752
5780
|
if (Object.keys(value).length !== keys.length || Object.keys(value).some((key) => !keys.includes(key))) throw new TypeError(`${label} has unknown or missing fields`);
|
|
5753
5781
|
}
|
|
5754
|
-
function decodeBase64Url(value) {
|
|
5755
|
-
if (!/^[A-Za-z0-9_-]+$/.test(value) || value.length % 4 === 1) throw new TypeError("compact Authorization segment is not base64url");
|
|
5756
|
-
const bytes3 = typeof Buffer !== "undefined" ? new Uint8Array(Buffer.from(value, "base64url")) : Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=")), (character) => character.charCodeAt(0));
|
|
5757
|
-
const encoded = typeof Buffer !== "undefined" ? Buffer.from(bytes3).toString("base64url") : btoa(String.fromCharCode(...bytes3)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
5758
|
-
if (encoded !== value) throw new TypeError("compact Authorization segment is not canonical");
|
|
5759
|
-
return bytes3;
|
|
5760
|
-
}
|
|
5761
|
-
function encodeBase64Url(value) {
|
|
5762
|
-
return typeof Buffer !== "undefined" ? Buffer.from(value).toString("base64url") : btoa(String.fromCharCode(...value)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
5763
|
-
}
|
|
5764
5782
|
function equal(left, right) {
|
|
5765
5783
|
return left.length === right.length && left.every((byte, index) => byte === right[index]);
|
|
5766
5784
|
}
|
|
5767
5785
|
var ED25519_VERIFY_OPTS2 = { zip215: false };
|
|
5768
5786
|
var ENVELOPE_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v1\0";
|
|
5769
5787
|
var ENVELOPE_V2_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v2\0";
|
|
5788
|
+
var ENVELOPE_V3_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v3\0";
|
|
5789
|
+
var POLICY_V1_SIGNATURE_DOMAIN = "xyz.tinycloud.policy/policy/v1\0";
|
|
5790
|
+
var POLICY_V2_SIGNATURE_DOMAIN = "xyz.tinycloud.policy/policy/v2\0";
|
|
5791
|
+
var CONTENT_SOURCE_V1_DOMAIN = "xyz.tinycloud.policy/ContentSource/v1\0";
|
|
5792
|
+
var POLICY_CAPABILITY_V1_DOMAIN = "xyz.tinycloud.policy/PolicyCapability/v1\0";
|
|
5793
|
+
var NATIVE_PROJECTION_V1_DOMAIN = "xyz.tinycloud.policy/NativeProjection/v1\0";
|
|
5794
|
+
var ATTESTED_ENFORCER_V2_DOMAIN = "xyz.tinycloud.policy/AttestedEnforcerBinding/v2\0";
|
|
5770
5795
|
function signingBytes(unsigned) {
|
|
5771
5796
|
const domain = utf8Bytes(ENVELOPE_SIGNATURE_DOMAIN);
|
|
5772
5797
|
const body = utf8Bytes(canonicalize(unsigned));
|
|
@@ -5783,6 +5808,14 @@ function signingBytesV2(unsigned) {
|
|
|
5783
5808
|
bytes3.set(body, domain.length);
|
|
5784
5809
|
return bytes3;
|
|
5785
5810
|
}
|
|
5811
|
+
function signingBytesV3(unsigned) {
|
|
5812
|
+
const domain = utf8Bytes(ENVELOPE_V3_SIGNATURE_DOMAIN);
|
|
5813
|
+
const body = utf8Bytes(canonicalize(unsigned));
|
|
5814
|
+
const bytes3 = new Uint8Array(domain.length + body.length);
|
|
5815
|
+
bytes3.set(domain);
|
|
5816
|
+
bytes3.set(body, domain.length);
|
|
5817
|
+
return bytes3;
|
|
5818
|
+
}
|
|
5786
5819
|
function signEnvelope(envelopeWithoutSig, ed25519PrivKey) {
|
|
5787
5820
|
const unsigned = unsignedShareEnvelopeSchema.parse(envelopeWithoutSig);
|
|
5788
5821
|
const publicKey = ed255193.getPublicKey(ed25519PrivKey);
|
|
@@ -5830,6 +5863,92 @@ async function verifyEnvelopeV2(envelope, options) {
|
|
|
5830
5863
|
if (parsed.authorizationTarget.kind === "policy") return await computeCid(fromBase64Url(parsed.authorizationTarget.policyBytes)) === parsed.authorizationTarget.policyCid;
|
|
5831
5864
|
return true;
|
|
5832
5865
|
}
|
|
5866
|
+
function verifyEnvelopeV3SignatureOnly(envelope) {
|
|
5867
|
+
const parsed = shareEnvelopeV3Schema.parse(envelope);
|
|
5868
|
+
const { signature, ...unsigned } = parsed;
|
|
5869
|
+
return ed255193.verify(fromBase64Url(signature.value), sha2562(signingBytesV3(unsigned)), ed25519PublicKeyFromDidKey(signature.signerDid), ED25519_VERIFY_OPTS2);
|
|
5870
|
+
}
|
|
5871
|
+
async function verifyEnvelopeV3(envelope, options) {
|
|
5872
|
+
const parsed = shareEnvelopeV3Schema.parse(envelope);
|
|
5873
|
+
if (parsed.signature.signerDid !== options.expectedSignerDid || parsed.signature.signerDid !== parsed.policy.ownerDid || !verifyEnvelopeV3SignatureOnly(parsed)) return false;
|
|
5874
|
+
const { policy } = parsed;
|
|
5875
|
+
const unsignedPolicy = { ...policy };
|
|
5876
|
+
delete unsignedPolicy.policyId;
|
|
5877
|
+
delete unsignedPolicy.signature;
|
|
5878
|
+
const policySignatureDomain = policy.schema === "xyz.tinycloud.policy/policy/v2" ? POLICY_V2_SIGNATURE_DOMAIN : POLICY_V1_SIGNATURE_DOMAIN;
|
|
5879
|
+
const policyDigest = sha2562(new TextEncoder().encode(`${policySignatureDomain}${canonicalize(unsignedPolicy)}`));
|
|
5880
|
+
const policySignature = policy.signature;
|
|
5881
|
+
if (policySignature.suite !== "Ed25519" || policySignature.signerDid !== policy.ownerDid) return false;
|
|
5882
|
+
let policyPublicKey;
|
|
5883
|
+
let policySignatureBytes;
|
|
5884
|
+
try {
|
|
5885
|
+
policyPublicKey = ed25519PublicKeyFromDidKey(policy.ownerDid);
|
|
5886
|
+
policySignatureBytes = fromBase64Url(policySignature.value);
|
|
5887
|
+
} catch {
|
|
5888
|
+
return false;
|
|
5889
|
+
}
|
|
5890
|
+
if (policySignatureBytes.length !== 64 || !ed255193.verify(policySignatureBytes, policyDigest, policyPublicKey, ED25519_VERIFY_OPTS2)) return false;
|
|
5891
|
+
const policyIdDigest = policyDigest;
|
|
5892
|
+
if (policy.policyId !== `pol_${base32Lower(policyIdDigest)}`) return false;
|
|
5893
|
+
const policyBytes = new TextEncoder().encode(canonicalize(policy));
|
|
5894
|
+
if (await computeCid(policyBytes) !== parsed.policyCid) return false;
|
|
5895
|
+
const sourceDigest = sha2562(new TextEncoder().encode(`${CONTENT_SOURCE_V1_DOMAIN}${canonicalize(policy.contentSource)}`));
|
|
5896
|
+
if (hex(sourceDigest) !== parsed.contentSourceDigestHex) return false;
|
|
5897
|
+
const sortedCapabilities = [...policy.capabilityCeiling].sort((left, right) => canonicalize(left).localeCompare(canonicalize(right)));
|
|
5898
|
+
const capabilityCeilingHashHex = hex(sha2562(new TextEncoder().encode(`${POLICY_CAPABILITY_V1_DOMAIN}${canonicalize(sortedCapabilities)}`)));
|
|
5899
|
+
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)));
|
|
5900
|
+
const nativeProjectionHashHex = hex(sha2562(new TextEncoder().encode(`${NATIVE_PROJECTION_V1_DOMAIN}${canonicalize(nativeProjection)}`)));
|
|
5901
|
+
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 }]]))]));
|
|
5902
|
+
const kv = policy.capabilityCeiling.find((capability) => capability.kind === "kv");
|
|
5903
|
+
const expectedKvActions = parsed.actions.flatMap((action) => action === "read" ? ["tinycloud.kv/get", "tinycloud.kv/metadata"] : action === "list" ? ["tinycloud.kv/list"] : ["tinycloud.kv/put"]);
|
|
5904
|
+
const marker = parsed.contentSource.kvResource.indexOf("/kv/");
|
|
5905
|
+
const resourceSpace = marker < 1 ? "" : parsed.contentSource.kvResource.slice(0, marker);
|
|
5906
|
+
const resourcePath = marker < 1 ? "" : parsed.contentSource.kvResource.slice(marker + 4);
|
|
5907
|
+
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;
|
|
5908
|
+
const binding = parsed.attestedEnforcerBinding;
|
|
5909
|
+
const { signature: bindingSignature, ...unsignedBinding } = binding;
|
|
5910
|
+
const expectedBindingDigestHex = hex(sha2562(new TextEncoder().encode(canonicalize({ enforcerDid: binding.enforcerDid, nodeAudience: binding.nodeAudience }))));
|
|
5911
|
+
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;
|
|
5912
|
+
try {
|
|
5913
|
+
const digest4 = sha2562(new TextEncoder().encode(`${ATTESTED_ENFORCER_V2_DOMAIN}${canonicalize(unsignedBinding)}`));
|
|
5914
|
+
if (!ed255193.verify(fromBase64Url(bindingSignature.value), digest4, ed25519PublicKeyFromDidKey(binding.nodeAudience), ED25519_VERIFY_OPTS2)) return false;
|
|
5915
|
+
} catch {
|
|
5916
|
+
return false;
|
|
5917
|
+
}
|
|
5918
|
+
if (parsed.policyRoot.role !== "policy-authority" || parsed.enforcementRoot.role !== "policy-enforcement" || parsed.policyRoot.cid === parsed.enforcementRoot.cid) return false;
|
|
5919
|
+
try {
|
|
5920
|
+
const policyRoot = verifyCompactUcanAuthorization(parsed.policyRoot.authorization, parsed.policyRoot.cid);
|
|
5921
|
+
const enforcementRoot = verifyCompactUcanAuthorization(parsed.enforcementRoot.authorization, parsed.enforcementRoot.cid);
|
|
5922
|
+
const policyFact = policyRoot.payload.fct[0];
|
|
5923
|
+
const enforcementFact = enforcementRoot.payload.fct[0];
|
|
5924
|
+
const common = ["ownerDid", "policyId", "policyDigestHex", "policyCid", "contentSourceDigestHex", "capabilityCeilingHashHex", "nativeProjectionHashHex", "nodeAudience"];
|
|
5925
|
+
const policyKeys = [...common, "role", "mode"];
|
|
5926
|
+
const enforcementKeys = [...policyKeys, "enforcerDid"];
|
|
5927
|
+
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;
|
|
5928
|
+
} catch {
|
|
5929
|
+
return false;
|
|
5930
|
+
}
|
|
5931
|
+
return true;
|
|
5932
|
+
}
|
|
5933
|
+
function hex(bytes3) {
|
|
5934
|
+
return [...bytes3].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
5935
|
+
}
|
|
5936
|
+
function base32Lower(bytes3) {
|
|
5937
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz234567";
|
|
5938
|
+
let output = "";
|
|
5939
|
+
let buffer = 0;
|
|
5940
|
+
let bits = 0;
|
|
5941
|
+
for (const byte of bytes3) {
|
|
5942
|
+
buffer = buffer << 8 | byte;
|
|
5943
|
+
bits += 8;
|
|
5944
|
+
while (bits >= 5) {
|
|
5945
|
+
output += alphabet[buffer >>> bits - 5 & 31];
|
|
5946
|
+
bits -= 5;
|
|
5947
|
+
}
|
|
5948
|
+
}
|
|
5949
|
+
if (bits > 0) output += alphabet[buffer << 5 - bits & 31];
|
|
5950
|
+
return output;
|
|
5951
|
+
}
|
|
5833
5952
|
var KEY_LENGTH2 = 32;
|
|
5834
5953
|
var INLINE_PREFIX = "#v=2&p=";
|
|
5835
5954
|
var MAX_INLINE_BYTES = 256 * 1024;
|
|
@@ -6014,8 +6133,12 @@ async function readResponseBytes(response, limit, tooLargeCode) {
|
|
|
6014
6133
|
function registryFetcher(options, limit, tooLargeCode = "max-bytes-exceeded", stage = "envelope") {
|
|
6015
6134
|
if (options.fetchBlob !== void 0) return async (input) => {
|
|
6016
6135
|
try {
|
|
6017
|
-
|
|
6136
|
+
options.signal?.throwIfAborted();
|
|
6137
|
+
const bytes3 = await options.fetchBlob({ ...input, ...options.signal === void 0 ? {} : { signal: options.signal } });
|
|
6138
|
+
options.signal?.throwIfAborted();
|
|
6139
|
+
return boundedBytes(bytes3, limit, tooLargeCode);
|
|
6018
6140
|
} catch (error) {
|
|
6141
|
+
if (options.signal?.aborted) throw options.signal.reason ?? error;
|
|
6019
6142
|
if (error instanceof ShareReceiveError) throw error;
|
|
6020
6143
|
throw new ShareReceiveError("fetch-failed", "registry unavailable", { stage });
|
|
6021
6144
|
}
|
|
@@ -6025,17 +6148,26 @@ function registryFetcher(options, limit, tooLargeCode = "max-bytes-exceeded", st
|
|
|
6025
6148
|
const base3 = options.registryBaseUrl.replace(/\/+$/, "");
|
|
6026
6149
|
return async ({ cid: cid2 }) => {
|
|
6027
6150
|
try {
|
|
6028
|
-
|
|
6151
|
+
options.signal?.throwIfAborted();
|
|
6152
|
+
const response = await fetchFn(`${base3}/ipfs/${cid2}?format=raw`, {
|
|
6153
|
+
headers: { accept: "application/vnd.ipld.raw" },
|
|
6154
|
+
redirect: "error",
|
|
6155
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
6156
|
+
});
|
|
6157
|
+
options.signal?.throwIfAborted();
|
|
6029
6158
|
if (!response.ok) throw new ShareReceiveError("fetch-failed", `registry returned ${response.status}`, { stage });
|
|
6030
|
-
|
|
6159
|
+
const bytes3 = await readResponseBytes(response, limit, tooLargeCode);
|
|
6160
|
+
options.signal?.throwIfAborted();
|
|
6161
|
+
return bytes3;
|
|
6031
6162
|
} catch (error) {
|
|
6163
|
+
if (options.signal?.aborted) throw options.signal.reason ?? error;
|
|
6032
6164
|
if (error instanceof ShareReceiveError) throw error;
|
|
6033
6165
|
throw new ShareReceiveError("fetch-failed", "registry unavailable", { stage });
|
|
6034
6166
|
}
|
|
6035
6167
|
};
|
|
6036
6168
|
}
|
|
6037
6169
|
function metadataFor(envelope, origin) {
|
|
6038
|
-
if (envelope.version === 2) {
|
|
6170
|
+
if (envelope.version === 2 || envelope.version === 3) {
|
|
6039
6171
|
const targetKind3 = envelope.recipientMatcher.kind === "recipientDid" ? "recipientDid" : envelope.recipientMatcher.kind === "exactEmail" ? "email" : envelope.recipientMatcher.kind === "emailDomain" ? "emailDomain" : "bearer";
|
|
6040
6172
|
return {
|
|
6041
6173
|
protocol: "tinycloud-share",
|
|
@@ -6051,7 +6183,7 @@ function metadataFor(envelope, origin) {
|
|
|
6051
6183
|
...envelope.display.filename === void 0 ? {} : { filename: envelope.display.filename },
|
|
6052
6184
|
...envelope.display.mode === void 0 ? {} : { mode: envelope.display.mode }
|
|
6053
6185
|
},
|
|
6054
|
-
...envelope.
|
|
6186
|
+
...envelope.version === 2 && envelope.content !== void 0 ? { content: { cid: envelope.content.cid } } : {}
|
|
6055
6187
|
};
|
|
6056
6188
|
}
|
|
6057
6189
|
return {
|
|
@@ -6076,8 +6208,10 @@ function parseEnvelope(bytes3) {
|
|
|
6076
6208
|
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes3);
|
|
6077
6209
|
const value = JSON.parse(text);
|
|
6078
6210
|
if (canonicalize(value) !== text) throw new Error("share envelope is not canonical JSON");
|
|
6079
|
-
if (typeof value === "object" && value !== null && !Array.isArray(value)
|
|
6080
|
-
|
|
6211
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
6212
|
+
const version = value.version;
|
|
6213
|
+
if (version === 3) return shareEnvelopeV3Schema.parse(value);
|
|
6214
|
+
if (version === 2) return shareEnvelopeV2Schema.parse(value);
|
|
6081
6215
|
}
|
|
6082
6216
|
return shareEnvelopeSchema.parse(value);
|
|
6083
6217
|
} catch {
|
|
@@ -6149,6 +6283,19 @@ async function verifyV2Envelope(envelope, linkOrigin, options) {
|
|
|
6149
6283
|
if (options.trustedSignerDid === void 0 || envelope.signature.signerDid !== options.trustedSignerDid) throw new ShareReceiveError("signature-invalid", "an addressed signer trust root is required");
|
|
6150
6284
|
}
|
|
6151
6285
|
}
|
|
6286
|
+
async function verifyV3Envelope(envelope, linkOrigin, options) {
|
|
6287
|
+
if (options.expectedOrigin !== void 0 && linkOrigin !== options.expectedOrigin) {
|
|
6288
|
+
throw new ShareReceiveError("origin-mismatch", "share link origin does not match the trusted origin");
|
|
6289
|
+
}
|
|
6290
|
+
try {
|
|
6291
|
+
if (!await verifyEnvelopeV3(envelope, { expectedSignerDid: envelope.policy.ownerDid })) throw new Error("signature");
|
|
6292
|
+
} catch {
|
|
6293
|
+
throw new ShareReceiveError("signature-invalid", "share signature is invalid");
|
|
6294
|
+
}
|
|
6295
|
+
const expiry = Date.parse(envelope.expiry);
|
|
6296
|
+
if (!Number.isFinite(expiry)) throw new ShareReceiveError("envelope-invalid", "share expiry is invalid");
|
|
6297
|
+
if (expiry <= (options.now?.() ?? Date.now())) throw new ShareReceiveError("expired", "share has expired", { expiresAt: envelope.expiry });
|
|
6298
|
+
}
|
|
6152
6299
|
async function verifyV1PolicyEnvelope(envelope, linkOrigin, options) {
|
|
6153
6300
|
if (envelope.authorizationTarget.kind !== "policy") throw new ShareReceiveError("unsupported-target", "share target is not an addressed policy", { reason: "policy-target" });
|
|
6154
6301
|
if (envelope.target.origin !== linkOrigin || options.expectedOrigin !== void 0 && (linkOrigin !== options.expectedOrigin || envelope.target.origin !== options.expectedOrigin)) {
|
|
@@ -6180,12 +6327,18 @@ async function inspectShare(link, options = {}) {
|
|
|
6180
6327
|
const resolved = await resolveShareEnvelope(link, options);
|
|
6181
6328
|
if (resolved.envelope.version === 1 && resolved.envelope.authorizationTarget.kind === "policy") await verifyV1PolicyEnvelope(resolved.envelope, resolved.origin, options);
|
|
6182
6329
|
else if (resolved.envelope.version === 1) await verifyResolved(resolved.envelope, resolved.origin, options);
|
|
6183
|
-
else await verifyV2Envelope(resolved.envelope, resolved.origin, options);
|
|
6184
|
-
|
|
6330
|
+
else if (resolved.envelope.version === 2) await verifyV2Envelope(resolved.envelope, resolved.origin, options);
|
|
6331
|
+
else await verifyV3Envelope(resolved.envelope, resolved.origin, options);
|
|
6332
|
+
if (resolved.envelope.version === 2 || resolved.envelope.version === 3 || resolved.envelope.authorizationTarget.kind === "policy") options.onResolvedAddressedEnvelope?.(resolved.envelope, resolved.cid);
|
|
6185
6333
|
return { metadata: metadataFor(resolved.envelope, resolved.origin), link: { origin: resolved.origin, cid: resolved.cid, kind: resolved.kind } };
|
|
6186
6334
|
}
|
|
6187
6335
|
async function receiveShare(link, options = {}) {
|
|
6188
6336
|
const resolved = await resolveShareEnvelope(link, options);
|
|
6337
|
+
if (resolved.envelope.version === 3) {
|
|
6338
|
+
await verifyV3Envelope(resolved.envelope, resolved.origin, options);
|
|
6339
|
+
options.onResolvedAddressedEnvelope?.(resolved.envelope, resolved.cid);
|
|
6340
|
+
return { state: "authorization-required", method: resolved.envelope.recipientMatcher.kind === "recipientDid" ? "openkey-device" : "email-claim" };
|
|
6341
|
+
}
|
|
6189
6342
|
if (resolved.envelope.version === 2) {
|
|
6190
6343
|
await verifyV2Envelope(resolved.envelope, resolved.origin, options);
|
|
6191
6344
|
options.onResolvedAddressedEnvelope?.(resolved.envelope, resolved.cid);
|
|
@@ -8728,11 +8881,80 @@ function uiAction(action) {
|
|
|
8728
8881
|
function selectedAction(envelope) {
|
|
8729
8882
|
return envelope.actions.includes("list") ? "tinycloud.kv/list" : envelope.actions.includes("edit") ? "tinycloud.kv/put" : "tinycloud.kv/get";
|
|
8730
8883
|
}
|
|
8731
|
-
function
|
|
8884
|
+
function policyAttenuationForV3(envelope) {
|
|
8885
|
+
const policy = object2(envelope.policy, "v3 policy");
|
|
8886
|
+
if (!Array.isArray(policy.capabilityCeiling)) throw new Error("v3 policy capability ceiling is invalid");
|
|
8887
|
+
const attenuation = {};
|
|
8888
|
+
for (const raw of policy.capabilityCeiling) {
|
|
8889
|
+
const capability = object2(raw, "v3 policy capability");
|
|
8890
|
+
if (typeof capability.resource !== "string" || attenuation[capability.resource] !== void 0) throw new Error("v3 policy capability is invalid");
|
|
8891
|
+
if (capability.kind === "encryption") {
|
|
8892
|
+
if (capability.action !== "tinycloud.encryption/decrypt") throw new Error("v3 policy capability is invalid");
|
|
8893
|
+
attenuation[capability.resource] = { [capability.action]: [{}] };
|
|
8894
|
+
continue;
|
|
8895
|
+
}
|
|
8896
|
+
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");
|
|
8897
|
+
attenuation[capability.resource] = Object.fromEntries(capability.actions.map((action) => [action, [{
|
|
8898
|
+
type: "xyz.tinycloud.resource/selector",
|
|
8899
|
+
kind: capability.selector,
|
|
8900
|
+
value: capability.resource
|
|
8901
|
+
}]]));
|
|
8902
|
+
}
|
|
8903
|
+
return attenuation;
|
|
8904
|
+
}
|
|
8905
|
+
var POLICY_SESSION_FACT_KEYS = [
|
|
8906
|
+
"profile",
|
|
8907
|
+
"ownerDid",
|
|
8908
|
+
"policyId",
|
|
8909
|
+
"policyDigestHex",
|
|
8910
|
+
"policyCid",
|
|
8911
|
+
"policyDelegationCid",
|
|
8912
|
+
"enforcementDelegationCid",
|
|
8913
|
+
"contentSourceDigestHex",
|
|
8914
|
+
"capabilityCeilingHashHex",
|
|
8915
|
+
"nativeProjectionHashHex",
|
|
8916
|
+
"enforcerDid",
|
|
8917
|
+
"nodeAudience",
|
|
8918
|
+
"recipientDid",
|
|
8919
|
+
"challengeId",
|
|
8920
|
+
"claimDigestHex",
|
|
8921
|
+
"claimJti",
|
|
8922
|
+
"vpDigestHex",
|
|
8923
|
+
"credentialEvidenceDigestHex",
|
|
8924
|
+
"decisionContextDigestHex",
|
|
8925
|
+
"issuanceAuditDigestHex",
|
|
8926
|
+
"remainingRedelegationDepth"
|
|
8927
|
+
];
|
|
8928
|
+
var POLICY_SESSION_V4_AUDIT_FACT_KEYS = ["credentialIdAuditDigestHex", "presentationJtiAuditDigestHex"];
|
|
8929
|
+
var POLICY_SESSION_DIGEST_FACT_KEYS = [
|
|
8930
|
+
"policyDigestHex",
|
|
8931
|
+
"contentSourceDigestHex",
|
|
8932
|
+
"capabilityCeilingHashHex",
|
|
8933
|
+
"nativeProjectionHashHex",
|
|
8934
|
+
"claimDigestHex",
|
|
8935
|
+
"vpDigestHex",
|
|
8936
|
+
"credentialEvidenceDigestHex",
|
|
8937
|
+
"decisionContextDigestHex",
|
|
8938
|
+
"issuanceAuditDigestHex"
|
|
8939
|
+
];
|
|
8940
|
+
var LOWER_SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
8941
|
+
function verifyV3PolicyAuthorization(input) {
|
|
8942
|
+
const compact = verifyCompactUcanAuthorization(input.authorization, input.cid);
|
|
8943
|
+
const fact = compact.payload.fct[0];
|
|
8944
|
+
const factKeys = Object.keys(fact);
|
|
8945
|
+
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);
|
|
8946
|
+
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);
|
|
8947
|
+
const binding = object2(input.envelope.attestedEnforcerBinding, "v3 attested enforcer binding");
|
|
8948
|
+
const policy = object2(input.envelope.policy, "v3 policy");
|
|
8949
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
8950
|
+
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");
|
|
8951
|
+
return compact;
|
|
8952
|
+
}
|
|
8953
|
+
function hex2(value) {
|
|
8732
8954
|
return [...value].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
8733
8955
|
}
|
|
8734
8956
|
function canonicalHashHex(value) {
|
|
8735
|
-
return
|
|
8957
|
+
return hex2(sha2566(new TextEncoder().encode(canonicalize(value))));
|
|
8736
8958
|
}
|
|
8737
8959
|
async function aesGcmDecrypt(key, blob) {
|
|
8738
8960
|
if (key.length !== 32 || blob.length < 28) throw new Error("encrypted content is malformed");
|
|
@@ -8788,6 +9010,20 @@ var ShareRecipientClient = class {
|
|
|
8788
9010
|
this.options = options;
|
|
8789
9011
|
this.fetchFn = options.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
8790
9012
|
this.signer = options.sign;
|
|
9013
|
+
if (options.policyAuthorization !== void 0) {
|
|
9014
|
+
if (options.envelope.version !== 3) throw new Error("policy delegation fast path requires a v3 envelope");
|
|
9015
|
+
const compact = verifyV3PolicyAuthorization({ ...options.policyAuthorization, envelope: options.envelope, holderDid: options.holderDid });
|
|
9016
|
+
const nodeAudience = compact.payload.fct[0].nodeAudience;
|
|
9017
|
+
this.v3Authorization = options.policyAuthorization.authorization;
|
|
9018
|
+
this.v3NodeAudience = nodeAudience;
|
|
9019
|
+
this.nativeSigner = options.sign;
|
|
9020
|
+
this.session = {
|
|
9021
|
+
sessionId: options.policyAuthorization.cid,
|
|
9022
|
+
expiresAt: new Date(compact.payload.exp * 1e3).toISOString(),
|
|
9023
|
+
actions: options.envelope.actions,
|
|
9024
|
+
resource: options.envelope.resource
|
|
9025
|
+
};
|
|
9026
|
+
}
|
|
8791
9027
|
}
|
|
8792
9028
|
fetchFn;
|
|
8793
9029
|
session;
|
|
@@ -8795,6 +9031,7 @@ var ShareRecipientClient = class {
|
|
|
8795
9031
|
nativeSigner;
|
|
8796
9032
|
holderProof;
|
|
8797
9033
|
v3Authorization;
|
|
9034
|
+
v3NodeAudience;
|
|
8798
9035
|
v3ContentKey;
|
|
8799
9036
|
v3ContentEnvelope;
|
|
8800
9037
|
async beginChallenge(envelope) {
|
|
@@ -8884,37 +9121,52 @@ var ShareRecipientClient = class {
|
|
|
8884
9121
|
}
|
|
8885
9122
|
async establishV3(envelope) {
|
|
8886
9123
|
if (this.options.buildPresentation === void 0) throw new Error("share presentation builder is required");
|
|
9124
|
+
const attestedEnforcer = object2(envelope.attestedEnforcerBinding, "v3 attested enforcer binding");
|
|
9125
|
+
if (attestedEnforcer.enforcerDid !== envelope.target.nodeAudience || typeof attestedEnforcer.nodeAudience !== "string") throw new Error("v3 attested enforcer binding mismatch");
|
|
9126
|
+
try {
|
|
9127
|
+
ed25519PublicKeyFromDidKey(attestedEnforcer.nodeAudience);
|
|
9128
|
+
} catch {
|
|
9129
|
+
throw new Error("v3 attested Node audience is invalid");
|
|
9130
|
+
}
|
|
8887
9131
|
const challengeResponse = await this.fetchFn(new URL("/share/v3/policy/challenges", this.options.nodeOrigin), {
|
|
8888
9132
|
method: "POST",
|
|
8889
9133
|
redirect: "error",
|
|
8890
9134
|
headers: { accept: "application/json", "content-type": "application/json" },
|
|
8891
|
-
body: JSON.stringify({ policyCid: envelope.policyCid, recipientDid: this.options.holderDid, requestedCapabilities: envelope.policy.capabilityCeiling })
|
|
9135
|
+
body: JSON.stringify({ policyCid: envelope.policyCid, recipientDid: this.options.holderDid, requestedCapabilities: envelope.policy.capabilityCeiling }),
|
|
9136
|
+
...this.options.signal === void 0 ? {} : { signal: this.options.signal }
|
|
8892
9137
|
});
|
|
8893
9138
|
if (!challengeResponse.ok) throw new Error(`v3 policy challenge rejected (${challengeResponse.status})`);
|
|
8894
9139
|
const challenge = object2(await challengeResponse.json(), "v3 policy challenge");
|
|
8895
|
-
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");
|
|
9140
|
+
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");
|
|
8896
9141
|
const material = await this.options.buildPresentation({
|
|
8897
9142
|
challenge,
|
|
8898
9143
|
envelope,
|
|
8899
9144
|
policy: envelope.policy
|
|
8900
9145
|
});
|
|
8901
|
-
if (material.sign === void 0 || material.
|
|
9146
|
+
if (material.sign === void 0 || material.presentation === void 0) throw new Error("v3 ceremony requires a recipient signer and presentation");
|
|
9147
|
+
const accountless = material.presentation.schema === "xyz.tinycloud.policy/presentation/v4";
|
|
9148
|
+
if (accountless ? material.credentialEnvelope === void 0 || material.requirement === void 0 : material.claim === void 0) {
|
|
9149
|
+
throw new Error(accountless ? "v4 ceremony requires a verified credential and requirement" : "v3 ceremony requires a claim");
|
|
9150
|
+
}
|
|
9151
|
+
this.options.onStage?.("policy-admission");
|
|
8902
9152
|
const delegationResponse = await this.fetchFn(new URL("/share/v3/policy/delegations", this.options.nodeOrigin), {
|
|
8903
9153
|
method: "POST",
|
|
8904
9154
|
redirect: "error",
|
|
8905
9155
|
headers: { accept: "application/json", "content-type": "application/json" },
|
|
8906
|
-
body: JSON.stringify({ policyCid: envelope.policyCid, challengeId: challenge.challengeId, nonce: challenge.nonce, claim: material.claim, presentation: material.presentation })
|
|
9156
|
+
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 }),
|
|
9157
|
+
...this.options.signal === void 0 ? {} : { signal: this.options.signal }
|
|
8907
9158
|
});
|
|
8908
9159
|
if (!delegationResponse.ok) throw new Error(`v3 policy delegation rejected (${delegationResponse.status})`);
|
|
8909
9160
|
const delegation = object2(await delegationResponse.json(), "v3 policy delegation");
|
|
8910
9161
|
if (delegation.admitted !== true || typeof delegation.sessionCid !== "string" || typeof delegation.authorization !== "string") throw new Error("v3 policy delegation response is not admitted");
|
|
8911
|
-
const compact =
|
|
9162
|
+
const compact = verifyV3PolicyAuthorization({ authorization: delegation.authorization, cid: delegation.sessionCid, envelope, holderDid: this.options.holderDid });
|
|
8912
9163
|
const fact = compact.payload.fct[0];
|
|
8913
|
-
|
|
8914
|
-
const imported = await this.fetchFn(new URL("/delegate", this.options.nodeOrigin), { method: "POST", redirect: "error", headers: { Authorization: delegation.authorization } });
|
|
9164
|
+
this.options.onStage?.("delegation-import");
|
|
9165
|
+
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 } });
|
|
8915
9166
|
if (!imported.ok) throw new Error(`ordinary delegation import rejected (${imported.status})`);
|
|
8916
9167
|
this.signer = material.sign;
|
|
8917
9168
|
this.v3Authorization = delegation.authorization;
|
|
9169
|
+
this.v3NodeAudience = fact.nodeAudience;
|
|
8918
9170
|
this.session = { sessionId: delegation.sessionCid, expiresAt: new Date(compact.payload.exp * 1e3).toISOString(), actions: envelope.actions, resource: envelope.resource };
|
|
8919
9171
|
return this.session;
|
|
8920
9172
|
}
|
|
@@ -8922,18 +9174,18 @@ var ShareRecipientClient = class {
|
|
|
8922
9174
|
async decryptV3Content(bytes3) {
|
|
8923
9175
|
const envelope = this.options.envelope;
|
|
8924
9176
|
const signer = this.nativeSigner ?? this.signer;
|
|
8925
|
-
if (envelope.version !== 3 || this.session === void 0 || this.v3Authorization === void 0 || signer === void 0) throw new Error("v3 policy session signer is required");
|
|
9177
|
+
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");
|
|
8926
9178
|
const encrypted = parseV3InlineEncryptedEnvelope(bytes3, envelope);
|
|
8927
9179
|
const receiverPrivateKey = crypto.getRandomValues(new Uint8Array(32));
|
|
8928
|
-
const receiverPublicKey =
|
|
9180
|
+
const receiverPublicKey = toBase64(x25519.getPublicKey(receiverPrivateKey));
|
|
8929
9181
|
const receiverPublicKeyHash = canonicalHashHex(receiverPublicKey);
|
|
8930
|
-
const body = { type: "tinycloud.encryption.decrypt/v1", targetNode:
|
|
8931
|
-
const bodyHash =
|
|
9182
|
+
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 };
|
|
9183
|
+
const bodyHash = hex2(sha2566(new TextEncoder().encode(canonicalize(body))));
|
|
8932
9184
|
const session = verifyCompactUcanAuthorization(this.v3Authorization, this.session.sessionId);
|
|
8933
9185
|
const now = Math.floor(Date.now() / 1e3);
|
|
8934
9186
|
const invocation = await signCompactUcanAuthorization({
|
|
8935
9187
|
issuerDid: this.options.holderDid,
|
|
8936
|
-
audienceDid:
|
|
9188
|
+
audienceDid: this.v3NodeAudience,
|
|
8937
9189
|
attenuation: { [encrypted.networkId]: { "tinycloud.encryption/decrypt": [{}] } },
|
|
8938
9190
|
facts: [{ type: body.type, targetNode: body.targetNode, networkId: body.networkId, bodyHash, encryptedSymmetricKeyHash: body.encryptedSymmetricKeyHash, receiverPublicKeyHash, alg: body.alg, keyVersion: body.keyVersion }],
|
|
8939
9191
|
proofs: [session.cid],
|
|
@@ -8943,22 +9195,23 @@ var ShareRecipientClient = class {
|
|
|
8943
9195
|
sign: signer
|
|
8944
9196
|
});
|
|
8945
9197
|
try {
|
|
8946
|
-
|
|
9198
|
+
this.options.onStage?.("decryption");
|
|
9199
|
+
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 } });
|
|
8947
9200
|
if (!response.ok) throw new Error(`v3 decrypt invocation rejected (${response.status})`);
|
|
8948
9201
|
const value = object2(await response.json(), "v3 decrypt response");
|
|
8949
9202
|
const allowed = ["type", "targetNode", "networkId", "invocationCid", "encryptedSymmetricKeyHash", "receiverPublicKeyHash", "wrappedKey", "alg", "keyVersion", "requestHash", "nodeId", "nodeSignature"];
|
|
8950
|
-
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 !==
|
|
9203
|
+
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(sha2566(new TextEncoder().encode(`${invocation.cid}${bodyHash}`))) || typeof value.wrappedKey !== "string" || typeof value.nodeSignature !== "string") throw new Error("v3 decrypt response binding is invalid");
|
|
8951
9204
|
const unsigned = { ...value };
|
|
8952
9205
|
delete unsigned.nodeSignature;
|
|
8953
9206
|
const signature = fromBase64(value.nodeSignature, "v3 decrypt response signature");
|
|
8954
9207
|
if (signature.length !== 64 || !ed255198.verify(signature, new TextEncoder().encode(canonicalize(unsigned)), ed25519PublicKeyFromDidKey(body.targetNode), { zip215: false })) throw new Error("v3 decrypt response signature is invalid");
|
|
8955
9208
|
const wrapped = fromBase64(value.wrappedKey, "v3 wrapped content key");
|
|
8956
|
-
if (wrapped.length <
|
|
9209
|
+
if (wrapped.length < 61 || wrapped[32] !== 1) throw new Error("v3 wrapped content key is malformed");
|
|
8957
9210
|
const shared = x25519.getSharedSecret(receiverPrivateKey, wrapped.slice(0, 32));
|
|
8958
|
-
const symmetricKey = await aesGcmDecrypt(shared, wrapped.slice(
|
|
9211
|
+
const symmetricKey = await aesGcmDecrypt(shared, wrapped.slice(33));
|
|
8959
9212
|
shared.fill(0);
|
|
8960
9213
|
if (symmetricKey.length !== 32) throw new Error("v3 content key is malformed");
|
|
8961
|
-
const plaintext = await aesGcmDecrypt(symmetricKey,
|
|
9214
|
+
const plaintext = await aesGcmDecrypt(symmetricKey, fromBase64(encrypted.ciphertext, "v3 encrypted content"));
|
|
8962
9215
|
this.v3ContentKey?.fill(0);
|
|
8963
9216
|
this.v3ContentKey = symmetricKey;
|
|
8964
9217
|
this.v3ContentEnvelope = encrypted;
|
|
@@ -9017,7 +9270,7 @@ var ShareRecipientClient = class {
|
|
|
9017
9270
|
}
|
|
9018
9271
|
async nativeInvokeV3(request, envelope) {
|
|
9019
9272
|
const signer = this.nativeSigner ?? this.signer;
|
|
9020
|
-
if (this.session === void 0 || this.v3Authorization === void 0 || signer === void 0) throw new Error("v3 policy session signer is required");
|
|
9273
|
+
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");
|
|
9021
9274
|
const session = verifyCompactUcanAuthorization(this.v3Authorization, this.session.sessionId);
|
|
9022
9275
|
if (session.payload.aud !== this.options.holderDid) throw new Error("v3 session recipient mismatch");
|
|
9023
9276
|
const action = request.action === "list" ? "tinycloud.kv/list" : request.action === "put" ? "tinycloud.kv/put" : request.action === "metadata" ? "tinycloud.kv/metadata" : "tinycloud.kv/get";
|
|
@@ -9031,7 +9284,7 @@ var ShareRecipientClient = class {
|
|
|
9031
9284
|
if (path !== root && !(capability.selector === "prefix" && path.startsWith(`${root}/`))) throw new Error("v3 KV request is outside the signed selector");
|
|
9032
9285
|
const resource = `${capability.resource.slice(0, split + marker.length)}${path}`;
|
|
9033
9286
|
const now = Math.floor(Date.now() / 1e3);
|
|
9034
|
-
const invocation = await signCompactUcanAuthorization({ issuerDid: this.options.holderDid, audienceDid:
|
|
9287
|
+
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 });
|
|
9035
9288
|
const headers = new Headers({ accept: "application/json", Authorization: invocation.authorization });
|
|
9036
9289
|
let body;
|
|
9037
9290
|
if (action === "tinycloud.kv/put") {
|
|
@@ -9040,7 +9293,8 @@ var ShareRecipientClient = class {
|
|
|
9040
9293
|
headers.set("content-type", request.contentType ?? "application/octet-stream");
|
|
9041
9294
|
if (request.ifMatch !== void 0) headers.set("if-match", request.ifMatch);
|
|
9042
9295
|
}
|
|
9043
|
-
|
|
9296
|
+
this.options.onStage?.("invocation");
|
|
9297
|
+
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 } });
|
|
9044
9298
|
}
|
|
9045
9299
|
};
|
|
9046
9300
|
function createAddressedAuthorization(input) {
|