@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.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
|
|
5314
|
-
if (
|
|
5315
|
-
const
|
|
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
|
|
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.
|
|
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) {
|
|
@@ -5778,7 +5806,7 @@ async function signCompactUcanAuthorization(input) {
|
|
|
5778
5806
|
const publicKey = ed25519PublicKeyFromDidKey(principal);
|
|
5779
5807
|
const header = {
|
|
5780
5808
|
alg: "EdDSA",
|
|
5781
|
-
jwk: { alg: "EdDSA", crv: "Ed25519", kty: "OKP", x:
|
|
5809
|
+
jwk: { alg: "EdDSA", crv: "Ed25519", kty: "OKP", x: toBase64Url(publicKey) },
|
|
5782
5810
|
typ: "JWT",
|
|
5783
5811
|
ucv: "0.10.0"
|
|
5784
5812
|
};
|
|
@@ -5792,13 +5820,13 @@ async function signCompactUcanAuthorization(input) {
|
|
|
5792
5820
|
nnc: input.nonce,
|
|
5793
5821
|
prf: input.proofs
|
|
5794
5822
|
};
|
|
5795
|
-
const protectedSegment =
|
|
5796
|
-
const payloadSegment =
|
|
5823
|
+
const protectedSegment = toBase64Url(new TextEncoder().encode(canonicalize(header)));
|
|
5824
|
+
const payloadSegment = toBase64Url(new TextEncoder().encode(canonicalize(payload)));
|
|
5797
5825
|
const signingInput = new TextEncoder().encode(`${protectedSegment}.${payloadSegment}`);
|
|
5798
5826
|
const signature = await input.sign(signingInput);
|
|
5799
5827
|
if (signature.length !== 64) throw new TypeError("compact Authorization signature must be Ed25519");
|
|
5800
5828
|
return verifyCompactUcanAuthorization(
|
|
5801
|
-
`${protectedSegment}.${payloadSegment}.${
|
|
5829
|
+
`${protectedSegment}.${payloadSegment}.${toBase64Url(signature)}`
|
|
5802
5830
|
);
|
|
5803
5831
|
}
|
|
5804
5832
|
function verifyCompactUcanAuthorization(authorization, expectedCid) {
|
|
@@ -5806,9 +5834,9 @@ function verifyCompactUcanAuthorization(authorization, expectedCid) {
|
|
|
5806
5834
|
const segments = authorization.split(".");
|
|
5807
5835
|
if (segments.length !== 3 || segments.some((segment) => segment.length === 0)) throw new TypeError("compact Authorization must contain three segments");
|
|
5808
5836
|
const [headerSegment, payloadSegment, signatureSegment] = segments;
|
|
5809
|
-
const headerBytes =
|
|
5810
|
-
const payloadBytes =
|
|
5811
|
-
const signature =
|
|
5837
|
+
const headerBytes = fromBase64Url(headerSegment);
|
|
5838
|
+
const payloadBytes = fromBase64Url(payloadSegment);
|
|
5839
|
+
const signature = fromBase64Url(signatureSegment);
|
|
5812
5840
|
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
5813
5841
|
const header = JSON.parse(decoder.decode(headerBytes));
|
|
5814
5842
|
const payload = JSON.parse(decoder.decode(payloadBytes));
|
|
@@ -5821,7 +5849,7 @@ function verifyCompactUcanAuthorization(authorization, expectedCid) {
|
|
|
5821
5849
|
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");
|
|
5822
5850
|
const principal = payload.iss.split("#", 1)[0];
|
|
5823
5851
|
const publicKey = ed25519PublicKeyFromDidKey(principal);
|
|
5824
|
-
if (!equal(publicKey,
|
|
5852
|
+
if (!equal(publicKey, fromBase64Url(jwk.x))) throw new TypeError("compact Authorization JWK does not bind issuer");
|
|
5825
5853
|
if (!import_ed255192.ed25519.verify(signature, new TextEncoder().encode(`${headerSegment}.${payloadSegment}`), publicKey, { zip215: false })) throw new TypeError("compact Authorization signature is invalid");
|
|
5826
5854
|
const cid2 = CID.createV1(85, create(30, (0, import_blake3.blake3)(new TextEncoder().encode(authorization)))).toString();
|
|
5827
5855
|
if (expectedCid !== void 0 && cid2 !== expectedCid) throw new TypeError("compact Authorization CID mismatch");
|
|
@@ -5834,22 +5862,19 @@ function object(value, label) {
|
|
|
5834
5862
|
function assertExactKeys(value, keys, label) {
|
|
5835
5863
|
if (Object.keys(value).length !== keys.length || Object.keys(value).some((key) => !keys.includes(key))) throw new TypeError(`${label} has unknown or missing fields`);
|
|
5836
5864
|
}
|
|
5837
|
-
function decodeBase64Url(value) {
|
|
5838
|
-
if (!/^[A-Za-z0-9_-]+$/.test(value) || value.length % 4 === 1) throw new TypeError("compact Authorization segment is not base64url");
|
|
5839
|
-
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));
|
|
5840
|
-
const encoded = typeof Buffer !== "undefined" ? Buffer.from(bytes3).toString("base64url") : btoa(String.fromCharCode(...bytes3)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
5841
|
-
if (encoded !== value) throw new TypeError("compact Authorization segment is not canonical");
|
|
5842
|
-
return bytes3;
|
|
5843
|
-
}
|
|
5844
|
-
function encodeBase64Url(value) {
|
|
5845
|
-
return typeof Buffer !== "undefined" ? Buffer.from(value).toString("base64url") : btoa(String.fromCharCode(...value)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
5846
|
-
}
|
|
5847
5865
|
function equal(left, right) {
|
|
5848
5866
|
return left.length === right.length && left.every((byte, index) => byte === right[index]);
|
|
5849
5867
|
}
|
|
5850
5868
|
var ED25519_VERIFY_OPTS2 = { zip215: false };
|
|
5851
5869
|
var ENVELOPE_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v1\0";
|
|
5852
5870
|
var ENVELOPE_V2_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v2\0";
|
|
5871
|
+
var ENVELOPE_V3_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v3\0";
|
|
5872
|
+
var POLICY_V1_SIGNATURE_DOMAIN = "xyz.tinycloud.policy/policy/v1\0";
|
|
5873
|
+
var POLICY_V2_SIGNATURE_DOMAIN = "xyz.tinycloud.policy/policy/v2\0";
|
|
5874
|
+
var CONTENT_SOURCE_V1_DOMAIN = "xyz.tinycloud.policy/ContentSource/v1\0";
|
|
5875
|
+
var POLICY_CAPABILITY_V1_DOMAIN = "xyz.tinycloud.policy/PolicyCapability/v1\0";
|
|
5876
|
+
var NATIVE_PROJECTION_V1_DOMAIN = "xyz.tinycloud.policy/NativeProjection/v1\0";
|
|
5877
|
+
var ATTESTED_ENFORCER_V2_DOMAIN = "xyz.tinycloud.policy/AttestedEnforcerBinding/v2\0";
|
|
5853
5878
|
function signingBytes(unsigned) {
|
|
5854
5879
|
const domain = utf8Bytes(ENVELOPE_SIGNATURE_DOMAIN);
|
|
5855
5880
|
const body = utf8Bytes(canonicalize(unsigned));
|
|
@@ -5866,6 +5891,14 @@ function signingBytesV2(unsigned) {
|
|
|
5866
5891
|
bytes3.set(body, domain.length);
|
|
5867
5892
|
return bytes3;
|
|
5868
5893
|
}
|
|
5894
|
+
function signingBytesV3(unsigned) {
|
|
5895
|
+
const domain = utf8Bytes(ENVELOPE_V3_SIGNATURE_DOMAIN);
|
|
5896
|
+
const body = utf8Bytes(canonicalize(unsigned));
|
|
5897
|
+
const bytes3 = new Uint8Array(domain.length + body.length);
|
|
5898
|
+
bytes3.set(domain);
|
|
5899
|
+
bytes3.set(body, domain.length);
|
|
5900
|
+
return bytes3;
|
|
5901
|
+
}
|
|
5869
5902
|
function signEnvelope(envelopeWithoutSig, ed25519PrivKey) {
|
|
5870
5903
|
const unsigned = unsignedShareEnvelopeSchema.parse(envelopeWithoutSig);
|
|
5871
5904
|
const publicKey = import_ed255193.ed25519.getPublicKey(ed25519PrivKey);
|
|
@@ -5913,6 +5946,92 @@ async function verifyEnvelopeV2(envelope, options) {
|
|
|
5913
5946
|
if (parsed.authorizationTarget.kind === "policy") return await computeCid(fromBase64Url(parsed.authorizationTarget.policyBytes)) === parsed.authorizationTarget.policyCid;
|
|
5914
5947
|
return true;
|
|
5915
5948
|
}
|
|
5949
|
+
function verifyEnvelopeV3SignatureOnly(envelope) {
|
|
5950
|
+
const parsed = shareEnvelopeV3Schema.parse(envelope);
|
|
5951
|
+
const { signature, ...unsigned } = parsed;
|
|
5952
|
+
return import_ed255193.ed25519.verify(fromBase64Url(signature.value), (0, import_sha256.sha256)(signingBytesV3(unsigned)), ed25519PublicKeyFromDidKey(signature.signerDid), ED25519_VERIFY_OPTS2);
|
|
5953
|
+
}
|
|
5954
|
+
async function verifyEnvelopeV3(envelope, options) {
|
|
5955
|
+
const parsed = shareEnvelopeV3Schema.parse(envelope);
|
|
5956
|
+
if (parsed.signature.signerDid !== options.expectedSignerDid || parsed.signature.signerDid !== parsed.policy.ownerDid || !verifyEnvelopeV3SignatureOnly(parsed)) return false;
|
|
5957
|
+
const { policy } = parsed;
|
|
5958
|
+
const unsignedPolicy = { ...policy };
|
|
5959
|
+
delete unsignedPolicy.policyId;
|
|
5960
|
+
delete unsignedPolicy.signature;
|
|
5961
|
+
const policySignatureDomain = policy.schema === "xyz.tinycloud.policy/policy/v2" ? POLICY_V2_SIGNATURE_DOMAIN : POLICY_V1_SIGNATURE_DOMAIN;
|
|
5962
|
+
const policyDigest = (0, import_sha256.sha256)(new TextEncoder().encode(`${policySignatureDomain}${canonicalize(unsignedPolicy)}`));
|
|
5963
|
+
const policySignature = policy.signature;
|
|
5964
|
+
if (policySignature.suite !== "Ed25519" || policySignature.signerDid !== policy.ownerDid) return false;
|
|
5965
|
+
let policyPublicKey;
|
|
5966
|
+
let policySignatureBytes;
|
|
5967
|
+
try {
|
|
5968
|
+
policyPublicKey = ed25519PublicKeyFromDidKey(policy.ownerDid);
|
|
5969
|
+
policySignatureBytes = fromBase64Url(policySignature.value);
|
|
5970
|
+
} catch {
|
|
5971
|
+
return false;
|
|
5972
|
+
}
|
|
5973
|
+
if (policySignatureBytes.length !== 64 || !import_ed255193.ed25519.verify(policySignatureBytes, policyDigest, policyPublicKey, ED25519_VERIFY_OPTS2)) return false;
|
|
5974
|
+
const policyIdDigest = policyDigest;
|
|
5975
|
+
if (policy.policyId !== `pol_${base32Lower(policyIdDigest)}`) return false;
|
|
5976
|
+
const policyBytes = new TextEncoder().encode(canonicalize(policy));
|
|
5977
|
+
if (await computeCid(policyBytes) !== parsed.policyCid) return false;
|
|
5978
|
+
const sourceDigest = (0, import_sha256.sha256)(new TextEncoder().encode(`${CONTENT_SOURCE_V1_DOMAIN}${canonicalize(policy.contentSource)}`));
|
|
5979
|
+
if (hex(sourceDigest) !== parsed.contentSourceDigestHex) return false;
|
|
5980
|
+
const sortedCapabilities = [...policy.capabilityCeiling].sort((left, right) => canonicalize(left).localeCompare(canonicalize(right)));
|
|
5981
|
+
const capabilityCeilingHashHex = hex((0, import_sha256.sha256)(new TextEncoder().encode(`${POLICY_CAPABILITY_V1_DOMAIN}${canonicalize(sortedCapabilities)}`)));
|
|
5982
|
+
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)));
|
|
5983
|
+
const nativeProjectionHashHex = hex((0, import_sha256.sha256)(new TextEncoder().encode(`${NATIVE_PROJECTION_V1_DOMAIN}${canonicalize(nativeProjection)}`)));
|
|
5984
|
+
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 }]]))]));
|
|
5985
|
+
const kv = policy.capabilityCeiling.find((capability) => capability.kind === "kv");
|
|
5986
|
+
const expectedKvActions = parsed.actions.flatMap((action) => action === "read" ? ["tinycloud.kv/get", "tinycloud.kv/metadata"] : action === "list" ? ["tinycloud.kv/list"] : ["tinycloud.kv/put"]);
|
|
5987
|
+
const marker = parsed.contentSource.kvResource.indexOf("/kv/");
|
|
5988
|
+
const resourceSpace = marker < 1 ? "" : parsed.contentSource.kvResource.slice(0, marker);
|
|
5989
|
+
const resourcePath = marker < 1 ? "" : parsed.contentSource.kvResource.slice(marker + 4);
|
|
5990
|
+
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;
|
|
5991
|
+
const binding = parsed.attestedEnforcerBinding;
|
|
5992
|
+
const { signature: bindingSignature, ...unsignedBinding } = binding;
|
|
5993
|
+
const expectedBindingDigestHex = hex((0, import_sha256.sha256)(new TextEncoder().encode(canonicalize({ enforcerDid: binding.enforcerDid, nodeAudience: binding.nodeAudience }))));
|
|
5994
|
+
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;
|
|
5995
|
+
try {
|
|
5996
|
+
const digest4 = (0, import_sha256.sha256)(new TextEncoder().encode(`${ATTESTED_ENFORCER_V2_DOMAIN}${canonicalize(unsignedBinding)}`));
|
|
5997
|
+
if (!import_ed255193.ed25519.verify(fromBase64Url(bindingSignature.value), digest4, ed25519PublicKeyFromDidKey(binding.nodeAudience), ED25519_VERIFY_OPTS2)) return false;
|
|
5998
|
+
} catch {
|
|
5999
|
+
return false;
|
|
6000
|
+
}
|
|
6001
|
+
if (parsed.policyRoot.role !== "policy-authority" || parsed.enforcementRoot.role !== "policy-enforcement" || parsed.policyRoot.cid === parsed.enforcementRoot.cid) return false;
|
|
6002
|
+
try {
|
|
6003
|
+
const policyRoot = verifyCompactUcanAuthorization(parsed.policyRoot.authorization, parsed.policyRoot.cid);
|
|
6004
|
+
const enforcementRoot = verifyCompactUcanAuthorization(parsed.enforcementRoot.authorization, parsed.enforcementRoot.cid);
|
|
6005
|
+
const policyFact = policyRoot.payload.fct[0];
|
|
6006
|
+
const enforcementFact = enforcementRoot.payload.fct[0];
|
|
6007
|
+
const common = ["ownerDid", "policyId", "policyDigestHex", "policyCid", "contentSourceDigestHex", "capabilityCeilingHashHex", "nativeProjectionHashHex", "nodeAudience"];
|
|
6008
|
+
const policyKeys = [...common, "role", "mode"];
|
|
6009
|
+
const enforcementKeys = [...policyKeys, "enforcerDid"];
|
|
6010
|
+
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;
|
|
6011
|
+
} catch {
|
|
6012
|
+
return false;
|
|
6013
|
+
}
|
|
6014
|
+
return true;
|
|
6015
|
+
}
|
|
6016
|
+
function hex(bytes3) {
|
|
6017
|
+
return [...bytes3].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
6018
|
+
}
|
|
6019
|
+
function base32Lower(bytes3) {
|
|
6020
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz234567";
|
|
6021
|
+
let output = "";
|
|
6022
|
+
let buffer = 0;
|
|
6023
|
+
let bits = 0;
|
|
6024
|
+
for (const byte of bytes3) {
|
|
6025
|
+
buffer = buffer << 8 | byte;
|
|
6026
|
+
bits += 8;
|
|
6027
|
+
while (bits >= 5) {
|
|
6028
|
+
output += alphabet[buffer >>> bits - 5 & 31];
|
|
6029
|
+
bits -= 5;
|
|
6030
|
+
}
|
|
6031
|
+
}
|
|
6032
|
+
if (bits > 0) output += alphabet[buffer << 5 - bits & 31];
|
|
6033
|
+
return output;
|
|
6034
|
+
}
|
|
5916
6035
|
var KEY_LENGTH2 = 32;
|
|
5917
6036
|
var INLINE_PREFIX = "#v=2&p=";
|
|
5918
6037
|
var MAX_INLINE_BYTES = 256 * 1024;
|
|
@@ -6097,8 +6216,12 @@ async function readResponseBytes(response, limit, tooLargeCode) {
|
|
|
6097
6216
|
function registryFetcher(options, limit, tooLargeCode = "max-bytes-exceeded", stage = "envelope") {
|
|
6098
6217
|
if (options.fetchBlob !== void 0) return async (input) => {
|
|
6099
6218
|
try {
|
|
6100
|
-
|
|
6219
|
+
options.signal?.throwIfAborted();
|
|
6220
|
+
const bytes3 = await options.fetchBlob({ ...input, ...options.signal === void 0 ? {} : { signal: options.signal } });
|
|
6221
|
+
options.signal?.throwIfAborted();
|
|
6222
|
+
return boundedBytes(bytes3, limit, tooLargeCode);
|
|
6101
6223
|
} catch (error) {
|
|
6224
|
+
if (options.signal?.aborted) throw options.signal.reason ?? error;
|
|
6102
6225
|
if (error instanceof ShareReceiveError) throw error;
|
|
6103
6226
|
throw new ShareReceiveError("fetch-failed", "registry unavailable", { stage });
|
|
6104
6227
|
}
|
|
@@ -6108,17 +6231,26 @@ function registryFetcher(options, limit, tooLargeCode = "max-bytes-exceeded", st
|
|
|
6108
6231
|
const base3 = options.registryBaseUrl.replace(/\/+$/, "");
|
|
6109
6232
|
return async ({ cid: cid2 }) => {
|
|
6110
6233
|
try {
|
|
6111
|
-
|
|
6234
|
+
options.signal?.throwIfAborted();
|
|
6235
|
+
const response = await fetchFn(`${base3}/ipfs/${cid2}?format=raw`, {
|
|
6236
|
+
headers: { accept: "application/vnd.ipld.raw" },
|
|
6237
|
+
redirect: "error",
|
|
6238
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
6239
|
+
});
|
|
6240
|
+
options.signal?.throwIfAborted();
|
|
6112
6241
|
if (!response.ok) throw new ShareReceiveError("fetch-failed", `registry returned ${response.status}`, { stage });
|
|
6113
|
-
|
|
6242
|
+
const bytes3 = await readResponseBytes(response, limit, tooLargeCode);
|
|
6243
|
+
options.signal?.throwIfAborted();
|
|
6244
|
+
return bytes3;
|
|
6114
6245
|
} catch (error) {
|
|
6246
|
+
if (options.signal?.aborted) throw options.signal.reason ?? error;
|
|
6115
6247
|
if (error instanceof ShareReceiveError) throw error;
|
|
6116
6248
|
throw new ShareReceiveError("fetch-failed", "registry unavailable", { stage });
|
|
6117
6249
|
}
|
|
6118
6250
|
};
|
|
6119
6251
|
}
|
|
6120
6252
|
function metadataFor(envelope, origin) {
|
|
6121
|
-
if (envelope.version === 2) {
|
|
6253
|
+
if (envelope.version === 2 || envelope.version === 3) {
|
|
6122
6254
|
const targetKind3 = envelope.recipientMatcher.kind === "recipientDid" ? "recipientDid" : envelope.recipientMatcher.kind === "exactEmail" ? "email" : envelope.recipientMatcher.kind === "emailDomain" ? "emailDomain" : "bearer";
|
|
6123
6255
|
return {
|
|
6124
6256
|
protocol: "tinycloud-share",
|
|
@@ -6134,7 +6266,7 @@ function metadataFor(envelope, origin) {
|
|
|
6134
6266
|
...envelope.display.filename === void 0 ? {} : { filename: envelope.display.filename },
|
|
6135
6267
|
...envelope.display.mode === void 0 ? {} : { mode: envelope.display.mode }
|
|
6136
6268
|
},
|
|
6137
|
-
...envelope.
|
|
6269
|
+
...envelope.version === 2 && envelope.content !== void 0 ? { content: { cid: envelope.content.cid } } : {}
|
|
6138
6270
|
};
|
|
6139
6271
|
}
|
|
6140
6272
|
return {
|
|
@@ -6159,8 +6291,10 @@ function parseEnvelope(bytes3) {
|
|
|
6159
6291
|
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes3);
|
|
6160
6292
|
const value = JSON.parse(text);
|
|
6161
6293
|
if (canonicalize(value) !== text) throw new Error("share envelope is not canonical JSON");
|
|
6162
|
-
if (typeof value === "object" && value !== null && !Array.isArray(value)
|
|
6163
|
-
|
|
6294
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
6295
|
+
const version = value.version;
|
|
6296
|
+
if (version === 3) return shareEnvelopeV3Schema.parse(value);
|
|
6297
|
+
if (version === 2) return shareEnvelopeV2Schema.parse(value);
|
|
6164
6298
|
}
|
|
6165
6299
|
return shareEnvelopeSchema.parse(value);
|
|
6166
6300
|
} catch {
|
|
@@ -6232,6 +6366,19 @@ async function verifyV2Envelope(envelope, linkOrigin, options) {
|
|
|
6232
6366
|
if (options.trustedSignerDid === void 0 || envelope.signature.signerDid !== options.trustedSignerDid) throw new ShareReceiveError("signature-invalid", "an addressed signer trust root is required");
|
|
6233
6367
|
}
|
|
6234
6368
|
}
|
|
6369
|
+
async function verifyV3Envelope(envelope, linkOrigin, options) {
|
|
6370
|
+
if (options.expectedOrigin !== void 0 && linkOrigin !== options.expectedOrigin) {
|
|
6371
|
+
throw new ShareReceiveError("origin-mismatch", "share link origin does not match the trusted origin");
|
|
6372
|
+
}
|
|
6373
|
+
try {
|
|
6374
|
+
if (!await verifyEnvelopeV3(envelope, { expectedSignerDid: envelope.policy.ownerDid })) throw new Error("signature");
|
|
6375
|
+
} catch {
|
|
6376
|
+
throw new ShareReceiveError("signature-invalid", "share signature is invalid");
|
|
6377
|
+
}
|
|
6378
|
+
const expiry = Date.parse(envelope.expiry);
|
|
6379
|
+
if (!Number.isFinite(expiry)) throw new ShareReceiveError("envelope-invalid", "share expiry is invalid");
|
|
6380
|
+
if (expiry <= (options.now?.() ?? Date.now())) throw new ShareReceiveError("expired", "share has expired", { expiresAt: envelope.expiry });
|
|
6381
|
+
}
|
|
6235
6382
|
async function verifyV1PolicyEnvelope(envelope, linkOrigin, options) {
|
|
6236
6383
|
if (envelope.authorizationTarget.kind !== "policy") throw new ShareReceiveError("unsupported-target", "share target is not an addressed policy", { reason: "policy-target" });
|
|
6237
6384
|
if (envelope.target.origin !== linkOrigin || options.expectedOrigin !== void 0 && (linkOrigin !== options.expectedOrigin || envelope.target.origin !== options.expectedOrigin)) {
|
|
@@ -6263,12 +6410,18 @@ async function inspectShare(link, options = {}) {
|
|
|
6263
6410
|
const resolved = await resolveShareEnvelope(link, options);
|
|
6264
6411
|
if (resolved.envelope.version === 1 && resolved.envelope.authorizationTarget.kind === "policy") await verifyV1PolicyEnvelope(resolved.envelope, resolved.origin, options);
|
|
6265
6412
|
else if (resolved.envelope.version === 1) await verifyResolved(resolved.envelope, resolved.origin, options);
|
|
6266
|
-
else await verifyV2Envelope(resolved.envelope, resolved.origin, options);
|
|
6267
|
-
|
|
6413
|
+
else if (resolved.envelope.version === 2) await verifyV2Envelope(resolved.envelope, resolved.origin, options);
|
|
6414
|
+
else await verifyV3Envelope(resolved.envelope, resolved.origin, options);
|
|
6415
|
+
if (resolved.envelope.version === 2 || resolved.envelope.version === 3 || resolved.envelope.authorizationTarget.kind === "policy") options.onResolvedAddressedEnvelope?.(resolved.envelope, resolved.cid);
|
|
6268
6416
|
return { metadata: metadataFor(resolved.envelope, resolved.origin), link: { origin: resolved.origin, cid: resolved.cid, kind: resolved.kind } };
|
|
6269
6417
|
}
|
|
6270
6418
|
async function receiveShare(link, options = {}) {
|
|
6271
6419
|
const resolved = await resolveShareEnvelope(link, options);
|
|
6420
|
+
if (resolved.envelope.version === 3) {
|
|
6421
|
+
await verifyV3Envelope(resolved.envelope, resolved.origin, options);
|
|
6422
|
+
options.onResolvedAddressedEnvelope?.(resolved.envelope, resolved.cid);
|
|
6423
|
+
return { state: "authorization-required", method: resolved.envelope.recipientMatcher.kind === "recipientDid" ? "openkey-device" : "email-claim" };
|
|
6424
|
+
}
|
|
6272
6425
|
if (resolved.envelope.version === 2) {
|
|
6273
6426
|
await verifyV2Envelope(resolved.envelope, resolved.origin, options);
|
|
6274
6427
|
options.onResolvedAddressedEnvelope?.(resolved.envelope, resolved.cid);
|
|
@@ -8811,11 +8964,80 @@ function uiAction(action) {
|
|
|
8811
8964
|
function selectedAction(envelope) {
|
|
8812
8965
|
return envelope.actions.includes("list") ? "tinycloud.kv/list" : envelope.actions.includes("edit") ? "tinycloud.kv/put" : "tinycloud.kv/get";
|
|
8813
8966
|
}
|
|
8814
|
-
function
|
|
8967
|
+
function policyAttenuationForV3(envelope) {
|
|
8968
|
+
const policy = object2(envelope.policy, "v3 policy");
|
|
8969
|
+
if (!Array.isArray(policy.capabilityCeiling)) throw new Error("v3 policy capability ceiling is invalid");
|
|
8970
|
+
const attenuation = {};
|
|
8971
|
+
for (const raw of policy.capabilityCeiling) {
|
|
8972
|
+
const capability = object2(raw, "v3 policy capability");
|
|
8973
|
+
if (typeof capability.resource !== "string" || attenuation[capability.resource] !== void 0) throw new Error("v3 policy capability is invalid");
|
|
8974
|
+
if (capability.kind === "encryption") {
|
|
8975
|
+
if (capability.action !== "tinycloud.encryption/decrypt") throw new Error("v3 policy capability is invalid");
|
|
8976
|
+
attenuation[capability.resource] = { [capability.action]: [{}] };
|
|
8977
|
+
continue;
|
|
8978
|
+
}
|
|
8979
|
+
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");
|
|
8980
|
+
attenuation[capability.resource] = Object.fromEntries(capability.actions.map((action) => [action, [{
|
|
8981
|
+
type: "xyz.tinycloud.resource/selector",
|
|
8982
|
+
kind: capability.selector,
|
|
8983
|
+
value: capability.resource
|
|
8984
|
+
}]]));
|
|
8985
|
+
}
|
|
8986
|
+
return attenuation;
|
|
8987
|
+
}
|
|
8988
|
+
var POLICY_SESSION_FACT_KEYS = [
|
|
8989
|
+
"profile",
|
|
8990
|
+
"ownerDid",
|
|
8991
|
+
"policyId",
|
|
8992
|
+
"policyDigestHex",
|
|
8993
|
+
"policyCid",
|
|
8994
|
+
"policyDelegationCid",
|
|
8995
|
+
"enforcementDelegationCid",
|
|
8996
|
+
"contentSourceDigestHex",
|
|
8997
|
+
"capabilityCeilingHashHex",
|
|
8998
|
+
"nativeProjectionHashHex",
|
|
8999
|
+
"enforcerDid",
|
|
9000
|
+
"nodeAudience",
|
|
9001
|
+
"recipientDid",
|
|
9002
|
+
"challengeId",
|
|
9003
|
+
"claimDigestHex",
|
|
9004
|
+
"claimJti",
|
|
9005
|
+
"vpDigestHex",
|
|
9006
|
+
"credentialEvidenceDigestHex",
|
|
9007
|
+
"decisionContextDigestHex",
|
|
9008
|
+
"issuanceAuditDigestHex",
|
|
9009
|
+
"remainingRedelegationDepth"
|
|
9010
|
+
];
|
|
9011
|
+
var POLICY_SESSION_V4_AUDIT_FACT_KEYS = ["credentialIdAuditDigestHex", "presentationJtiAuditDigestHex"];
|
|
9012
|
+
var POLICY_SESSION_DIGEST_FACT_KEYS = [
|
|
9013
|
+
"policyDigestHex",
|
|
9014
|
+
"contentSourceDigestHex",
|
|
9015
|
+
"capabilityCeilingHashHex",
|
|
9016
|
+
"nativeProjectionHashHex",
|
|
9017
|
+
"claimDigestHex",
|
|
9018
|
+
"vpDigestHex",
|
|
9019
|
+
"credentialEvidenceDigestHex",
|
|
9020
|
+
"decisionContextDigestHex",
|
|
9021
|
+
"issuanceAuditDigestHex"
|
|
9022
|
+
];
|
|
9023
|
+
var LOWER_SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
9024
|
+
function verifyV3PolicyAuthorization(input) {
|
|
9025
|
+
const compact = verifyCompactUcanAuthorization(input.authorization, input.cid);
|
|
9026
|
+
const fact = compact.payload.fct[0];
|
|
9027
|
+
const factKeys = Object.keys(fact);
|
|
9028
|
+
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);
|
|
9029
|
+
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);
|
|
9030
|
+
const binding = object2(input.envelope.attestedEnforcerBinding, "v3 attested enforcer binding");
|
|
9031
|
+
const policy = object2(input.envelope.policy, "v3 policy");
|
|
9032
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
9033
|
+
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");
|
|
9034
|
+
return compact;
|
|
9035
|
+
}
|
|
9036
|
+
function hex2(value) {
|
|
8815
9037
|
return [...value].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
8816
9038
|
}
|
|
8817
9039
|
function canonicalHashHex(value) {
|
|
8818
|
-
return
|
|
9040
|
+
return hex2((0, import_sha2564.sha256)(new TextEncoder().encode(canonicalize(value))));
|
|
8819
9041
|
}
|
|
8820
9042
|
async function aesGcmDecrypt(key, blob) {
|
|
8821
9043
|
if (key.length !== 32 || blob.length < 28) throw new Error("encrypted content is malformed");
|
|
@@ -8871,6 +9093,20 @@ var ShareRecipientClient = class {
|
|
|
8871
9093
|
this.options = options;
|
|
8872
9094
|
this.fetchFn = options.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
8873
9095
|
this.signer = options.sign;
|
|
9096
|
+
if (options.policyAuthorization !== void 0) {
|
|
9097
|
+
if (options.envelope.version !== 3) throw new Error("policy delegation fast path requires a v3 envelope");
|
|
9098
|
+
const compact = verifyV3PolicyAuthorization({ ...options.policyAuthorization, envelope: options.envelope, holderDid: options.holderDid });
|
|
9099
|
+
const nodeAudience = compact.payload.fct[0].nodeAudience;
|
|
9100
|
+
this.v3Authorization = options.policyAuthorization.authorization;
|
|
9101
|
+
this.v3NodeAudience = nodeAudience;
|
|
9102
|
+
this.nativeSigner = options.sign;
|
|
9103
|
+
this.session = {
|
|
9104
|
+
sessionId: options.policyAuthorization.cid,
|
|
9105
|
+
expiresAt: new Date(compact.payload.exp * 1e3).toISOString(),
|
|
9106
|
+
actions: options.envelope.actions,
|
|
9107
|
+
resource: options.envelope.resource
|
|
9108
|
+
};
|
|
9109
|
+
}
|
|
8874
9110
|
}
|
|
8875
9111
|
fetchFn;
|
|
8876
9112
|
session;
|
|
@@ -8878,6 +9114,7 @@ var ShareRecipientClient = class {
|
|
|
8878
9114
|
nativeSigner;
|
|
8879
9115
|
holderProof;
|
|
8880
9116
|
v3Authorization;
|
|
9117
|
+
v3NodeAudience;
|
|
8881
9118
|
v3ContentKey;
|
|
8882
9119
|
v3ContentEnvelope;
|
|
8883
9120
|
async beginChallenge(envelope) {
|
|
@@ -8967,37 +9204,52 @@ var ShareRecipientClient = class {
|
|
|
8967
9204
|
}
|
|
8968
9205
|
async establishV3(envelope) {
|
|
8969
9206
|
if (this.options.buildPresentation === void 0) throw new Error("share presentation builder is required");
|
|
9207
|
+
const attestedEnforcer = object2(envelope.attestedEnforcerBinding, "v3 attested enforcer binding");
|
|
9208
|
+
if (attestedEnforcer.enforcerDid !== envelope.target.nodeAudience || typeof attestedEnforcer.nodeAudience !== "string") throw new Error("v3 attested enforcer binding mismatch");
|
|
9209
|
+
try {
|
|
9210
|
+
ed25519PublicKeyFromDidKey(attestedEnforcer.nodeAudience);
|
|
9211
|
+
} catch {
|
|
9212
|
+
throw new Error("v3 attested Node audience is invalid");
|
|
9213
|
+
}
|
|
8970
9214
|
const challengeResponse = await this.fetchFn(new URL("/share/v3/policy/challenges", this.options.nodeOrigin), {
|
|
8971
9215
|
method: "POST",
|
|
8972
9216
|
redirect: "error",
|
|
8973
9217
|
headers: { accept: "application/json", "content-type": "application/json" },
|
|
8974
|
-
body: JSON.stringify({ policyCid: envelope.policyCid, recipientDid: this.options.holderDid, requestedCapabilities: envelope.policy.capabilityCeiling })
|
|
9218
|
+
body: JSON.stringify({ policyCid: envelope.policyCid, recipientDid: this.options.holderDid, requestedCapabilities: envelope.policy.capabilityCeiling }),
|
|
9219
|
+
...this.options.signal === void 0 ? {} : { signal: this.options.signal }
|
|
8975
9220
|
});
|
|
8976
9221
|
if (!challengeResponse.ok) throw new Error(`v3 policy challenge rejected (${challengeResponse.status})`);
|
|
8977
9222
|
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");
|
|
9223
|
+
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
9224
|
const material = await this.options.buildPresentation({
|
|
8980
9225
|
challenge,
|
|
8981
9226
|
envelope,
|
|
8982
9227
|
policy: envelope.policy
|
|
8983
9228
|
});
|
|
8984
|
-
if (material.sign === void 0 || material.
|
|
9229
|
+
if (material.sign === void 0 || material.presentation === void 0) throw new Error("v3 ceremony requires a recipient signer and presentation");
|
|
9230
|
+
const accountless = material.presentation.schema === "xyz.tinycloud.policy/presentation/v4";
|
|
9231
|
+
if (accountless ? material.credentialEnvelope === void 0 || material.requirement === void 0 : material.claim === void 0) {
|
|
9232
|
+
throw new Error(accountless ? "v4 ceremony requires a verified credential and requirement" : "v3 ceremony requires a claim");
|
|
9233
|
+
}
|
|
9234
|
+
this.options.onStage?.("policy-admission");
|
|
8985
9235
|
const delegationResponse = await this.fetchFn(new URL("/share/v3/policy/delegations", this.options.nodeOrigin), {
|
|
8986
9236
|
method: "POST",
|
|
8987
9237
|
redirect: "error",
|
|
8988
9238
|
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 })
|
|
9239
|
+
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 }),
|
|
9240
|
+
...this.options.signal === void 0 ? {} : { signal: this.options.signal }
|
|
8990
9241
|
});
|
|
8991
9242
|
if (!delegationResponse.ok) throw new Error(`v3 policy delegation rejected (${delegationResponse.status})`);
|
|
8992
9243
|
const delegation = object2(await delegationResponse.json(), "v3 policy delegation");
|
|
8993
9244
|
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 =
|
|
9245
|
+
const compact = verifyV3PolicyAuthorization({ authorization: delegation.authorization, cid: delegation.sessionCid, envelope, holderDid: this.options.holderDid });
|
|
8995
9246
|
const fact = compact.payload.fct[0];
|
|
8996
|
-
|
|
8997
|
-
const imported = await this.fetchFn(new URL("/delegate", this.options.nodeOrigin), { method: "POST", redirect: "error", headers: { Authorization: delegation.authorization } });
|
|
9247
|
+
this.options.onStage?.("delegation-import");
|
|
9248
|
+
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
9249
|
if (!imported.ok) throw new Error(`ordinary delegation import rejected (${imported.status})`);
|
|
8999
9250
|
this.signer = material.sign;
|
|
9000
9251
|
this.v3Authorization = delegation.authorization;
|
|
9252
|
+
this.v3NodeAudience = fact.nodeAudience;
|
|
9001
9253
|
this.session = { sessionId: delegation.sessionCid, expiresAt: new Date(compact.payload.exp * 1e3).toISOString(), actions: envelope.actions, resource: envelope.resource };
|
|
9002
9254
|
return this.session;
|
|
9003
9255
|
}
|
|
@@ -9005,18 +9257,18 @@ var ShareRecipientClient = class {
|
|
|
9005
9257
|
async decryptV3Content(bytes3) {
|
|
9006
9258
|
const envelope = this.options.envelope;
|
|
9007
9259
|
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");
|
|
9260
|
+
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
9261
|
const encrypted = parseV3InlineEncryptedEnvelope(bytes3, envelope);
|
|
9010
9262
|
const receiverPrivateKey = crypto.getRandomValues(new Uint8Array(32));
|
|
9011
|
-
const receiverPublicKey =
|
|
9263
|
+
const receiverPublicKey = toBase64(import_ed255198.x25519.getPublicKey(receiverPrivateKey));
|
|
9012
9264
|
const receiverPublicKeyHash = canonicalHashHex(receiverPublicKey);
|
|
9013
|
-
const body = { type: "tinycloud.encryption.decrypt/v1", targetNode:
|
|
9014
|
-
const bodyHash =
|
|
9265
|
+
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 };
|
|
9266
|
+
const bodyHash = hex2((0, import_sha2564.sha256)(new TextEncoder().encode(canonicalize(body))));
|
|
9015
9267
|
const session = verifyCompactUcanAuthorization(this.v3Authorization, this.session.sessionId);
|
|
9016
9268
|
const now = Math.floor(Date.now() / 1e3);
|
|
9017
9269
|
const invocation = await signCompactUcanAuthorization({
|
|
9018
9270
|
issuerDid: this.options.holderDid,
|
|
9019
|
-
audienceDid:
|
|
9271
|
+
audienceDid: this.v3NodeAudience,
|
|
9020
9272
|
attenuation: { [encrypted.networkId]: { "tinycloud.encryption/decrypt": [{}] } },
|
|
9021
9273
|
facts: [{ type: body.type, targetNode: body.targetNode, networkId: body.networkId, bodyHash, encryptedSymmetricKeyHash: body.encryptedSymmetricKeyHash, receiverPublicKeyHash, alg: body.alg, keyVersion: body.keyVersion }],
|
|
9022
9274
|
proofs: [session.cid],
|
|
@@ -9026,22 +9278,23 @@ var ShareRecipientClient = class {
|
|
|
9026
9278
|
sign: signer
|
|
9027
9279
|
});
|
|
9028
9280
|
try {
|
|
9029
|
-
|
|
9281
|
+
this.options.onStage?.("decryption");
|
|
9282
|
+
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
9283
|
if (!response.ok) throw new Error(`v3 decrypt invocation rejected (${response.status})`);
|
|
9031
9284
|
const value = object2(await response.json(), "v3 decrypt response");
|
|
9032
9285
|
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 !==
|
|
9286
|
+
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
9287
|
const unsigned = { ...value };
|
|
9035
9288
|
delete unsigned.nodeSignature;
|
|
9036
9289
|
const signature = fromBase64(value.nodeSignature, "v3 decrypt response signature");
|
|
9037
9290
|
if (signature.length !== 64 || !import_ed255198.ed25519.verify(signature, new TextEncoder().encode(canonicalize(unsigned)), ed25519PublicKeyFromDidKey(body.targetNode), { zip215: false })) throw new Error("v3 decrypt response signature is invalid");
|
|
9038
9291
|
const wrapped = fromBase64(value.wrappedKey, "v3 wrapped content key");
|
|
9039
|
-
if (wrapped.length <
|
|
9292
|
+
if (wrapped.length < 61 || wrapped[32] !== 1) throw new Error("v3 wrapped content key is malformed");
|
|
9040
9293
|
const shared = import_ed255198.x25519.getSharedSecret(receiverPrivateKey, wrapped.slice(0, 32));
|
|
9041
|
-
const symmetricKey = await aesGcmDecrypt(shared, wrapped.slice(
|
|
9294
|
+
const symmetricKey = await aesGcmDecrypt(shared, wrapped.slice(33));
|
|
9042
9295
|
shared.fill(0);
|
|
9043
9296
|
if (symmetricKey.length !== 32) throw new Error("v3 content key is malformed");
|
|
9044
|
-
const plaintext = await aesGcmDecrypt(symmetricKey,
|
|
9297
|
+
const plaintext = await aesGcmDecrypt(symmetricKey, fromBase64(encrypted.ciphertext, "v3 encrypted content"));
|
|
9045
9298
|
this.v3ContentKey?.fill(0);
|
|
9046
9299
|
this.v3ContentKey = symmetricKey;
|
|
9047
9300
|
this.v3ContentEnvelope = encrypted;
|
|
@@ -9100,7 +9353,7 @@ var ShareRecipientClient = class {
|
|
|
9100
9353
|
}
|
|
9101
9354
|
async nativeInvokeV3(request, envelope) {
|
|
9102
9355
|
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");
|
|
9356
|
+
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
9357
|
const session = verifyCompactUcanAuthorization(this.v3Authorization, this.session.sessionId);
|
|
9105
9358
|
if (session.payload.aud !== this.options.holderDid) throw new Error("v3 session recipient mismatch");
|
|
9106
9359
|
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 +9367,7 @@ var ShareRecipientClient = class {
|
|
|
9114
9367
|
if (path !== root && !(capability.selector === "prefix" && path.startsWith(`${root}/`))) throw new Error("v3 KV request is outside the signed selector");
|
|
9115
9368
|
const resource = `${capability.resource.slice(0, split + marker.length)}${path}`;
|
|
9116
9369
|
const now = Math.floor(Date.now() / 1e3);
|
|
9117
|
-
const invocation = await signCompactUcanAuthorization({ issuerDid: this.options.holderDid, audienceDid:
|
|
9370
|
+
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
9371
|
const headers = new Headers({ accept: "application/json", Authorization: invocation.authorization });
|
|
9119
9372
|
let body;
|
|
9120
9373
|
if (action === "tinycloud.kv/put") {
|
|
@@ -9123,7 +9376,8 @@ var ShareRecipientClient = class {
|
|
|
9123
9376
|
headers.set("content-type", request.contentType ?? "application/octet-stream");
|
|
9124
9377
|
if (request.ifMatch !== void 0) headers.set("if-match", request.ifMatch);
|
|
9125
9378
|
}
|
|
9126
|
-
|
|
9379
|
+
this.options.onStage?.("invocation");
|
|
9380
|
+
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
9381
|
}
|
|
9128
9382
|
};
|
|
9129
9383
|
function createAddressedAuthorization(input) {
|