@tinycloud/share-sdk 0.3.0-beta.2 → 1.0.0-beta.4
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 +426 -1166
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +120 -291
- package/dist/index.d.ts +120 -291
- package/dist/index.js +417 -1153
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -152,10 +152,10 @@ var util;
|
|
|
152
152
|
return obj[e];
|
|
153
153
|
});
|
|
154
154
|
};
|
|
155
|
-
util2.objectKeys = typeof Object.keys === "function" ? (obj) => Object.keys(obj) : (
|
|
155
|
+
util2.objectKeys = typeof Object.keys === "function" ? (obj) => Object.keys(obj) : (object4) => {
|
|
156
156
|
const keys = [];
|
|
157
|
-
for (const key in
|
|
158
|
-
if (Object.prototype.hasOwnProperty.call(
|
|
157
|
+
for (const key in object4) {
|
|
158
|
+
if (Object.prototype.hasOwnProperty.call(object4, key)) {
|
|
159
159
|
keys.push(key);
|
|
160
160
|
}
|
|
161
161
|
}
|
|
@@ -5359,7 +5359,7 @@ function validateV3Invariants(value, ctx) {
|
|
|
5359
5359
|
if (value.contentSource.encryptionNetwork !== value.encryptionNetwork) {
|
|
5360
5360
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["encryptionNetwork"], message: "encryption network is not bound to the source" });
|
|
5361
5361
|
}
|
|
5362
|
-
if (value.attestedEnforcerBinding.
|
|
5362
|
+
if (value.attestedEnforcerBinding.nodeAudience !== value.target.nodeAudience || value.attestedEnforcerBinding.signature.signerDid !== value.attestedEnforcerBinding.nodeAudience || Date.parse(value.attestedEnforcerBinding.expiresAt) < Date.parse(value.expiry)) {
|
|
5363
5363
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["attestedEnforcerBinding"], message: "enforcer binding does not cover the target and share lifetime" });
|
|
5364
5364
|
}
|
|
5365
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) {
|
|
@@ -5554,36 +5554,6 @@ function checkBearerDelegation(envelope, options = {}) {
|
|
|
5554
5554
|
}
|
|
5555
5555
|
return { ok: true, delegateeDid: audience };
|
|
5556
5556
|
}
|
|
5557
|
-
function mintBearerDelegation(options) {
|
|
5558
|
-
const { issuerPrivateKey, audienceDid, resourceUri, expiresAtSeconds } = options;
|
|
5559
|
-
if (!audienceDid.startsWith("did:")) {
|
|
5560
|
-
throw new TypeError(`audienceDid must be a DID, got ${audienceDid}`);
|
|
5561
|
-
}
|
|
5562
|
-
if (resourceUri.length === 0) {
|
|
5563
|
-
throw new TypeError("resourceUri must be non-empty");
|
|
5564
|
-
}
|
|
5565
|
-
if (!Number.isSafeInteger(expiresAtSeconds) || expiresAtSeconds <= 0) {
|
|
5566
|
-
throw new TypeError(
|
|
5567
|
-
`expiresAtSeconds must be a positive integer (epoch seconds), got ${expiresAtSeconds}`
|
|
5568
|
-
);
|
|
5569
|
-
}
|
|
5570
|
-
const issuerDid = didKeyFromEd25519PublicKey(
|
|
5571
|
-
ed25519.getPublicKey(issuerPrivateKey)
|
|
5572
|
-
);
|
|
5573
|
-
const header = { alg: "EdDSA", typ: "JWT", ucv: "0.9.1" };
|
|
5574
|
-
const payload = {
|
|
5575
|
-
iss: issuerDid,
|
|
5576
|
-
aud: audienceDid,
|
|
5577
|
-
att: [{ with: resourceUri, can: BEARER_READ_ABILITY }],
|
|
5578
|
-
prf: [],
|
|
5579
|
-
exp: expiresAtSeconds
|
|
5580
|
-
};
|
|
5581
|
-
const signingInput = `${toBase64Url(utf8Bytes(JSON.stringify(header)))}.${toBase64Url(
|
|
5582
|
-
utf8Bytes(JSON.stringify(payload))
|
|
5583
|
-
)}`;
|
|
5584
|
-
const signature = ed25519.sign(utf8Bytes(signingInput), issuerPrivateKey);
|
|
5585
|
-
return `${signingInput}.${toBase64Url(signature)}`;
|
|
5586
|
-
}
|
|
5587
5557
|
var ENVELOPE_AAD_LABEL = "tinycloud-share-envelope-v1";
|
|
5588
5558
|
var SEALED_BLOB_VERSION = 1;
|
|
5589
5559
|
var AAD = utf8Bytes(ENVELOPE_AAD_LABEL);
|
|
@@ -5783,8 +5753,6 @@ function equal(left, right) {
|
|
|
5783
5753
|
return left.length === right.length && left.every((byte, index) => byte === right[index]);
|
|
5784
5754
|
}
|
|
5785
5755
|
var ED25519_VERIFY_OPTS2 = { zip215: false };
|
|
5786
|
-
var ENVELOPE_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v1\0";
|
|
5787
|
-
var ENVELOPE_V2_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v2\0";
|
|
5788
5756
|
var ENVELOPE_V3_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v3\0";
|
|
5789
5757
|
var POLICY_V1_SIGNATURE_DOMAIN = "xyz.tinycloud.policy/policy/v1\0";
|
|
5790
5758
|
var POLICY_V2_SIGNATURE_DOMAIN = "xyz.tinycloud.policy/policy/v2\0";
|
|
@@ -5792,22 +5760,6 @@ var CONTENT_SOURCE_V1_DOMAIN = "xyz.tinycloud.policy/ContentSource/v1\0";
|
|
|
5792
5760
|
var POLICY_CAPABILITY_V1_DOMAIN = "xyz.tinycloud.policy/PolicyCapability/v1\0";
|
|
5793
5761
|
var NATIVE_PROJECTION_V1_DOMAIN = "xyz.tinycloud.policy/NativeProjection/v1\0";
|
|
5794
5762
|
var ATTESTED_ENFORCER_V2_DOMAIN = "xyz.tinycloud.policy/AttestedEnforcerBinding/v2\0";
|
|
5795
|
-
function signingBytes(unsigned) {
|
|
5796
|
-
const domain = utf8Bytes(ENVELOPE_SIGNATURE_DOMAIN);
|
|
5797
|
-
const body = utf8Bytes(canonicalize(unsigned));
|
|
5798
|
-
const bytes3 = new Uint8Array(domain.length + body.length);
|
|
5799
|
-
bytes3.set(domain);
|
|
5800
|
-
bytes3.set(body, domain.length);
|
|
5801
|
-
return bytes3;
|
|
5802
|
-
}
|
|
5803
|
-
function signingBytesV2(unsigned) {
|
|
5804
|
-
const domain = utf8Bytes(ENVELOPE_V2_SIGNATURE_DOMAIN);
|
|
5805
|
-
const body = utf8Bytes(canonicalize(unsigned));
|
|
5806
|
-
const bytes3 = new Uint8Array(domain.length + body.length);
|
|
5807
|
-
bytes3.set(domain);
|
|
5808
|
-
bytes3.set(body, domain.length);
|
|
5809
|
-
return bytes3;
|
|
5810
|
-
}
|
|
5811
5763
|
function signingBytesV3(unsigned) {
|
|
5812
5764
|
const domain = utf8Bytes(ENVELOPE_V3_SIGNATURE_DOMAIN);
|
|
5813
5765
|
const body = utf8Bytes(canonicalize(unsigned));
|
|
@@ -5816,53 +5768,6 @@ function signingBytesV3(unsigned) {
|
|
|
5816
5768
|
bytes3.set(body, domain.length);
|
|
5817
5769
|
return bytes3;
|
|
5818
5770
|
}
|
|
5819
|
-
function signEnvelope(envelopeWithoutSig, ed25519PrivKey) {
|
|
5820
|
-
const unsigned = unsignedShareEnvelopeSchema.parse(envelopeWithoutSig);
|
|
5821
|
-
const publicKey = ed255193.getPublicKey(ed25519PrivKey);
|
|
5822
|
-
const signature = ed255193.sign(signingBytes(unsigned), ed25519PrivKey);
|
|
5823
|
-
return {
|
|
5824
|
-
...unsigned,
|
|
5825
|
-
signature: {
|
|
5826
|
-
signerDid: didKeyFromEd25519PublicKey(publicKey),
|
|
5827
|
-
algorithm: "Ed25519",
|
|
5828
|
-
value: toBase64Url(signature)
|
|
5829
|
-
}
|
|
5830
|
-
};
|
|
5831
|
-
}
|
|
5832
|
-
function verifyEnvelopeSignatureOnly(envelope) {
|
|
5833
|
-
const parsed = shareEnvelopeSchema.parse(envelope);
|
|
5834
|
-
const { signature, ...unsigned } = parsed;
|
|
5835
|
-
const publicKey = ed25519PublicKeyFromDidKey(signature.signerDid);
|
|
5836
|
-
return ed255193.verify(
|
|
5837
|
-
fromBase64Url(signature.value),
|
|
5838
|
-
signingBytes(unsigned),
|
|
5839
|
-
publicKey,
|
|
5840
|
-
ED25519_VERIFY_OPTS2
|
|
5841
|
-
);
|
|
5842
|
-
}
|
|
5843
|
-
async function verifyEnvelope(envelope, options) {
|
|
5844
|
-
const parsed = shareEnvelopeSchema.parse(envelope);
|
|
5845
|
-
if (parsed.signature.signerDid !== options.expectedSignerDid) return false;
|
|
5846
|
-
if (!verifyEnvelopeSignatureOnly(parsed)) return false;
|
|
5847
|
-
if (parsed.authorizationTarget.kind === "policy") {
|
|
5848
|
-
const policyBytes = fromBase64Url(parsed.authorizationTarget.policyBytes);
|
|
5849
|
-
if (await computeCid(policyBytes) !== parsed.authorizationTarget.policyCid) {
|
|
5850
|
-
return false;
|
|
5851
|
-
}
|
|
5852
|
-
}
|
|
5853
|
-
return true;
|
|
5854
|
-
}
|
|
5855
|
-
function verifyEnvelopeV2SignatureOnly(envelope) {
|
|
5856
|
-
const parsed = shareEnvelopeV2Schema.parse(envelope);
|
|
5857
|
-
const { signature, ...unsigned } = parsed;
|
|
5858
|
-
return ed255193.verify(fromBase64Url(signature.value), signingBytesV2(unsigned), ed25519PublicKeyFromDidKey(signature.signerDid), ED25519_VERIFY_OPTS2);
|
|
5859
|
-
}
|
|
5860
|
-
async function verifyEnvelopeV2(envelope, options) {
|
|
5861
|
-
const parsed = shareEnvelopeV2Schema.parse(envelope);
|
|
5862
|
-
if (parsed.signature.signerDid !== options.expectedSignerDid || !verifyEnvelopeV2SignatureOnly(parsed)) return false;
|
|
5863
|
-
if (parsed.authorizationTarget.kind === "policy") return await computeCid(fromBase64Url(parsed.authorizationTarget.policyBytes)) === parsed.authorizationTarget.policyCid;
|
|
5864
|
-
return true;
|
|
5865
|
-
}
|
|
5866
5771
|
function verifyEnvelopeV3SignatureOnly(envelope) {
|
|
5867
5772
|
const parsed = shareEnvelopeV3Schema.parse(envelope);
|
|
5868
5773
|
const { signature, ...unsigned } = parsed;
|
|
@@ -5908,7 +5813,7 @@ async function verifyEnvelopeV3(envelope, options) {
|
|
|
5908
5813
|
const binding = parsed.attestedEnforcerBinding;
|
|
5909
5814
|
const { signature: bindingSignature, ...unsignedBinding } = binding;
|
|
5910
5815
|
const expectedBindingDigestHex = hex(sha2562(new TextEncoder().encode(canonicalize({ enforcerDid: binding.enforcerDid, nodeAudience: binding.nodeAudience }))));
|
|
5911
|
-
if (binding.
|
|
5816
|
+
if (binding.nodeAudience !== 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
5817
|
try {
|
|
5913
5818
|
const digest3 = sha2562(new TextEncoder().encode(`${ATTESTED_ENFORCER_V2_DOMAIN}${canonicalize(unsignedBinding)}`));
|
|
5914
5819
|
if (!ed255193.verify(fromBase64Url(bindingSignature.value), digest3, ed25519PublicKeyFromDidKey(binding.nodeAudience), ED25519_VERIFY_OPTS2)) return false;
|
|
@@ -5950,7 +5855,6 @@ function base32Lower(bytes3) {
|
|
|
5950
5855
|
return output;
|
|
5951
5856
|
}
|
|
5952
5857
|
var KEY_LENGTH2 = 32;
|
|
5953
|
-
var INLINE_PREFIX = "#v=2&p=";
|
|
5954
5858
|
var MAX_INLINE_BYTES = 256 * 1024;
|
|
5955
5859
|
function assertCanonicalCid(cidString) {
|
|
5956
5860
|
const cid2 = CID.parse(cidString);
|
|
@@ -5959,120 +5863,67 @@ function assertCanonicalCid(cidString) {
|
|
|
5959
5863
|
throw new TypeError(`not a canonical CIDv1 raw sha2-256 base32 CID: ${cidString}`);
|
|
5960
5864
|
}
|
|
5961
5865
|
}
|
|
5962
|
-
function
|
|
5963
|
-
if (key32.length !== KEY_LENGTH2) {
|
|
5964
|
-
throw new TypeError(`key must be ${KEY_LENGTH2} bytes, got ${key32.length}`);
|
|
5965
|
-
}
|
|
5966
|
-
assertCanonicalCid(ciphertextCid);
|
|
5967
|
-
if (!isCanonicalHttpsOrigin(origin)) {
|
|
5968
|
-
throw new TypeError(`origin must be a canonical https origin, got ${origin}`);
|
|
5969
|
-
}
|
|
5970
|
-
return `${origin}/s/${ciphertextCid}#k=${toBase64Url(key32)}`;
|
|
5971
|
-
}
|
|
5972
|
-
function parseShareUrl(url, options = {}) {
|
|
5973
|
-
const parsed = new URL(url);
|
|
5974
|
-
if (parsed.protocol !== "https:") {
|
|
5975
|
-
throw new TypeError(`share URL must be https, got ${parsed.protocol}`);
|
|
5976
|
-
}
|
|
5977
|
-
if (parsed.username !== "" || parsed.password !== "") {
|
|
5978
|
-
throw new TypeError("share URL must not carry userinfo");
|
|
5979
|
-
}
|
|
5980
|
-
if (parsed.search !== "") {
|
|
5981
|
-
throw new TypeError("share URL must not have a query string");
|
|
5982
|
-
}
|
|
5983
|
-
if (options.expectedOrigin !== void 0) {
|
|
5984
|
-
if (!isCanonicalHttpsOrigin(options.expectedOrigin)) {
|
|
5985
|
-
throw new TypeError(
|
|
5986
|
-
`expectedOrigin must be a canonical https origin, got ${options.expectedOrigin}`
|
|
5987
|
-
);
|
|
5988
|
-
}
|
|
5989
|
-
if (parsed.origin !== options.expectedOrigin) {
|
|
5990
|
-
throw new TypeError(
|
|
5991
|
-
`share URL origin ${parsed.origin} does not match expected ${options.expectedOrigin}`
|
|
5992
|
-
);
|
|
5993
|
-
}
|
|
5994
|
-
}
|
|
5995
|
-
const match = /^\/s\/([a-z2-7]+)$/.exec(parsed.pathname);
|
|
5996
|
-
if (!match || match[1] === void 0) {
|
|
5997
|
-
throw new TypeError(`not a share URL path: ${parsed.pathname}`);
|
|
5998
|
-
}
|
|
5999
|
-
const ciphertextCid = match[1];
|
|
6000
|
-
assertCanonicalCid(ciphertextCid);
|
|
6001
|
-
if (!parsed.hash.startsWith("#k=")) {
|
|
6002
|
-
throw new TypeError("share URL is missing the #k= key fragment");
|
|
6003
|
-
}
|
|
6004
|
-
const key32 = fromBase64Url(parsed.hash.slice("#k=".length));
|
|
6005
|
-
if (key32.length !== KEY_LENGTH2) {
|
|
6006
|
-
throw new TypeError(`fragment key must be ${KEY_LENGTH2} bytes, got ${key32.length}`);
|
|
6007
|
-
}
|
|
6008
|
-
return { ciphertextCid, key32 };
|
|
6009
|
-
}
|
|
6010
|
-
async function encodeInlineShareUrl(parts) {
|
|
5866
|
+
async function encodeSealedInlineShareUrl(parts) {
|
|
6011
5867
|
if (!isCanonicalHttpsOrigin(parts.origin)) throw new TypeError("origin must be a canonical https origin");
|
|
6012
5868
|
if (parts.ciphertext.byteLength === 0 || parts.ciphertext.byteLength > MAX_INLINE_BYTES) throw new RangeError("inline ciphertext is outside the allowed size");
|
|
6013
|
-
if (parts.key32
|
|
5869
|
+
if (parts.key32.byteLength !== KEY_LENGTH2) throw new TypeError("inline key must be 32 bytes");
|
|
6014
5870
|
const ciphertextCid = await computeCid(parts.ciphertext);
|
|
6015
5871
|
const payload = canonicalize({
|
|
6016
5872
|
v: 2,
|
|
6017
5873
|
c: toBase64Url(parts.ciphertext),
|
|
6018
5874
|
cid: ciphertextCid,
|
|
6019
|
-
|
|
5875
|
+
k: toBase64Url(parts.key32)
|
|
6020
5876
|
});
|
|
6021
5877
|
const payloadBytes = new TextEncoder().encode(payload);
|
|
6022
5878
|
if (payloadBytes.byteLength > MAX_INLINE_BYTES * 2) throw new RangeError("inline URL is too large");
|
|
6023
|
-
return `${parts.origin}/s/inline
|
|
5879
|
+
return `${parts.origin}/s/inline#v=2&p=${toBase64Url(payloadBytes)}`;
|
|
6024
5880
|
}
|
|
6025
|
-
function
|
|
5881
|
+
async function parseSealedInlineShareUrl(url, options = {}) {
|
|
6026
5882
|
const parsed = new URL(url);
|
|
6027
|
-
if (parsed.protocol !== "https:" || parsed.username !== "" || parsed.password !== ""
|
|
6028
|
-
if (!isCanonicalHttpsOrigin(parsed.origin) || options.expectedOrigin !== void 0 && parsed.origin !== options.expectedOrigin) throw new TypeError("inline share URL origin is not trusted");
|
|
6029
|
-
if (parsed.pathname !== "/s/inline" ||
|
|
5883
|
+
if (parsed.protocol !== "https:" || parsed.username !== "" || parsed.password !== "") throw new TypeError("sealed inline share URL must be canonical HTTPS without userinfo");
|
|
5884
|
+
if (!isCanonicalHttpsOrigin(parsed.origin) || options.expectedOrigin !== void 0 && parsed.origin !== options.expectedOrigin) throw new TypeError("sealed inline share URL origin is not trusted");
|
|
5885
|
+
if (parsed.pathname !== "/s/inline" || parsed.search !== "") throw new TypeError("not a Node sealed-inline share URL");
|
|
5886
|
+
if (url !== `${parsed.origin}/s/inline${parsed.hash}`) throw new TypeError("sealed inline share URL is not lexically canonical");
|
|
5887
|
+
const prefix = "#v=2&p=";
|
|
5888
|
+
if (!parsed.hash.startsWith(prefix)) throw new TypeError("sealed inline URL is missing its canonical fragment");
|
|
5889
|
+
const encoded = parsed.hash.slice(prefix.length);
|
|
5890
|
+
if (encoded.length === 0 || parsed.hash !== `${prefix}${encoded}`) throw new TypeError("sealed inline URL fragment is not canonical");
|
|
6030
5891
|
let payloadBytes;
|
|
6031
5892
|
try {
|
|
6032
|
-
payloadBytes = fromBase64Url(
|
|
5893
|
+
payloadBytes = fromBase64Url(encoded);
|
|
6033
5894
|
} catch {
|
|
6034
|
-
throw new TypeError("inline payload is not canonical base64url");
|
|
5895
|
+
throw new TypeError("sealed inline payload is not canonical base64url");
|
|
6035
5896
|
}
|
|
6036
|
-
if (payloadBytes.byteLength === 0 || payloadBytes.byteLength > MAX_INLINE_BYTES * 2) throw new TypeError("inline payload is too large");
|
|
5897
|
+
if (payloadBytes.byteLength === 0 || payloadBytes.byteLength > MAX_INLINE_BYTES * 2) throw new TypeError("sealed inline payload is too large");
|
|
5898
|
+
const payloadText = new TextDecoder("utf-8", { fatal: true }).decode(payloadBytes);
|
|
6037
5899
|
let value;
|
|
6038
5900
|
try {
|
|
6039
|
-
value = JSON.parse(
|
|
5901
|
+
value = JSON.parse(payloadText);
|
|
6040
5902
|
} catch {
|
|
6041
|
-
throw new TypeError("inline payload is not valid JSON");
|
|
5903
|
+
throw new TypeError("sealed inline payload is not valid JSON");
|
|
6042
5904
|
}
|
|
6043
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("inline payload
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
const keys = Object.keys(record2);
|
|
6047
|
-
if (!keys.every((key) => key === "v" || key === "c" || key === "cid" || key === "k") || record2.v !== 2 || typeof record2.c !== "string" || typeof record2.cid !== "string") throw new TypeError("inline payload has invalid fields");
|
|
5905
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || canonicalize(value) !== payloadText) throw new TypeError("sealed inline payload is not canonical JSON");
|
|
5906
|
+
const record = value;
|
|
5907
|
+
if (Object.keys(record).length !== 4 || record.v !== 2 || typeof record.c !== "string" || typeof record.cid !== "string" || typeof record.k !== "string") throw new TypeError("sealed inline payload has invalid fields");
|
|
6048
5908
|
let ciphertext;
|
|
6049
|
-
try {
|
|
6050
|
-
ciphertext = fromBase64Url(record2.c);
|
|
6051
|
-
} catch {
|
|
6052
|
-
throw new TypeError("inline ciphertext is not canonical base64url");
|
|
6053
|
-
}
|
|
6054
|
-
if (ciphertext.byteLength === 0 || ciphertext.byteLength > MAX_INLINE_BYTES) throw new TypeError("inline ciphertext is outside the allowed size");
|
|
6055
|
-
assertCanonicalCid(record2.cid);
|
|
6056
|
-
if (record2.k !== void 0 && typeof record2.k !== "string") throw new TypeError("inline key must be base64url");
|
|
6057
5909
|
let key32;
|
|
6058
5910
|
try {
|
|
6059
|
-
|
|
5911
|
+
ciphertext = fromBase64Url(record.c);
|
|
5912
|
+
key32 = fromBase64Url(record.k);
|
|
6060
5913
|
} catch {
|
|
6061
|
-
throw new TypeError("inline
|
|
5914
|
+
throw new TypeError("sealed inline payload has invalid base64url material");
|
|
6062
5915
|
}
|
|
6063
|
-
if (
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
return { kind: "compact", ...parseShareUrl(url, options) };
|
|
5916
|
+
if (ciphertext.byteLength === 0 || ciphertext.byteLength > MAX_INLINE_BYTES) throw new TypeError("sealed inline ciphertext is outside the allowed size");
|
|
5917
|
+
if (key32.byteLength !== KEY_LENGTH2) throw new TypeError("sealed inline key must be 32 bytes");
|
|
5918
|
+
assertCanonicalCid(record.cid);
|
|
5919
|
+
if (await computeCid(ciphertext) !== record.cid) throw new TypeError("sealed inline ciphertext does not match its CID");
|
|
5920
|
+
return { kind: "inline", ciphertextCid: record.cid, ciphertext, key32 };
|
|
6069
5921
|
}
|
|
6070
5922
|
|
|
6071
5923
|
// src/receive.ts
|
|
6072
5924
|
var SHARE_RESULT_VERSION = 1;
|
|
6073
5925
|
var DEFAULT_MAX_SEALED_BLOB_BYTES = 100 * 1024 * 1024 + 29;
|
|
6074
5926
|
var DEFAULT_MAX_CONTENT_BLOB_BYTES = DEFAULT_MAX_SEALED_BLOB_BYTES;
|
|
6075
|
-
var CONTENT_SEALED_OVERHEAD = 1 + 12 + 16;
|
|
6076
5927
|
var ShareReceiveError = class extends Error {
|
|
6077
5928
|
code;
|
|
6078
5929
|
details;
|
|
@@ -6082,7 +5933,6 @@ var ShareReceiveError = class extends Error {
|
|
|
6082
5933
|
this.code = code3;
|
|
6083
5934
|
this.details = details;
|
|
6084
5935
|
}
|
|
6085
|
-
/** Machine output is deliberately code-only; diagnostics belong on stderr. */
|
|
6086
5936
|
toJSON() {
|
|
6087
5937
|
return { protocol: "tinycloud-share", version: SHARE_RESULT_VERSION, error: { code: this.code } };
|
|
6088
5938
|
}
|
|
@@ -6091,338 +5941,77 @@ function toShareErrorInfo(error) {
|
|
|
6091
5941
|
if (error instanceof ShareReceiveError) return error.toJSON();
|
|
6092
5942
|
return { protocol: "tinycloud-share", version: SHARE_RESULT_VERSION, error: { code: "fetch-failed" } };
|
|
6093
5943
|
}
|
|
6094
|
-
function boundedBytes(value, limit, code3) {
|
|
6095
|
-
if (!Number.isSafeInteger(limit) || limit <= 0) throw new ShareReceiveError("fetch-failed", "share byte limit is invalid");
|
|
6096
|
-
if (!(value instanceof Uint8Array)) throw new ShareReceiveError("fetch-failed", "registry returned invalid bytes");
|
|
6097
|
-
if (value.byteLength > limit) throw new ShareReceiveError(code3, "share blob exceeds the configured byte limit");
|
|
6098
|
-
return value;
|
|
6099
|
-
}
|
|
6100
|
-
async function readResponseBytes(response, limit, tooLargeCode) {
|
|
6101
|
-
const contentLength = response.headers.get("content-length");
|
|
6102
|
-
if (contentLength !== null && Number.isSafeInteger(Number(contentLength)) && Number(contentLength) > limit) {
|
|
6103
|
-
throw new ShareReceiveError(tooLargeCode, "share blob exceeds the configured byte limit");
|
|
6104
|
-
}
|
|
6105
|
-
if (response.body === null) {
|
|
6106
|
-
return boundedBytes(new Uint8Array(await response.arrayBuffer()), limit, tooLargeCode);
|
|
6107
|
-
}
|
|
6108
|
-
const reader = response.body.getReader();
|
|
6109
|
-
const chunks = [];
|
|
6110
|
-
let total = 0;
|
|
6111
|
-
try {
|
|
6112
|
-
for (; ; ) {
|
|
6113
|
-
const part = await reader.read();
|
|
6114
|
-
if (part.done) break;
|
|
6115
|
-
total += part.value.byteLength;
|
|
6116
|
-
if (total > limit) {
|
|
6117
|
-
await reader.cancel();
|
|
6118
|
-
throw new ShareReceiveError(tooLargeCode, "share blob exceeds the configured byte limit");
|
|
6119
|
-
}
|
|
6120
|
-
chunks.push(part.value);
|
|
6121
|
-
}
|
|
6122
|
-
} finally {
|
|
6123
|
-
reader.releaseLock();
|
|
6124
|
-
}
|
|
6125
|
-
const bytes3 = new Uint8Array(total);
|
|
6126
|
-
let offset = 0;
|
|
6127
|
-
for (const chunk of chunks) {
|
|
6128
|
-
bytes3.set(chunk, offset);
|
|
6129
|
-
offset += chunk.byteLength;
|
|
6130
|
-
}
|
|
6131
|
-
return bytes3;
|
|
6132
|
-
}
|
|
6133
|
-
function registryFetcher(options, limit, tooLargeCode = "max-bytes-exceeded", stage = "envelope") {
|
|
6134
|
-
if (options.fetchBlob !== void 0) return async (input) => {
|
|
6135
|
-
try {
|
|
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);
|
|
6140
|
-
} catch (error) {
|
|
6141
|
-
if (options.signal?.aborted) throw options.signal.reason ?? error;
|
|
6142
|
-
if (error instanceof ShareReceiveError) throw error;
|
|
6143
|
-
throw new ShareReceiveError("fetch-failed", "registry unavailable", { stage });
|
|
6144
|
-
}
|
|
6145
|
-
};
|
|
6146
|
-
if (options.registryBaseUrl === void 0) throw new ShareReceiveError("fetch-failed", "a registry fetch adapter is required");
|
|
6147
|
-
const fetchFn = options.fetchFn ?? globalThis.fetch;
|
|
6148
|
-
const base3 = options.registryBaseUrl.replace(/\/+$/, "");
|
|
6149
|
-
return async ({ cid: cid2 }) => {
|
|
6150
|
-
try {
|
|
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();
|
|
6158
|
-
if (!response.ok) throw new ShareReceiveError("fetch-failed", `registry returned ${response.status}`, { stage });
|
|
6159
|
-
const bytes3 = await readResponseBytes(response, limit, tooLargeCode);
|
|
6160
|
-
options.signal?.throwIfAborted();
|
|
6161
|
-
return bytes3;
|
|
6162
|
-
} catch (error) {
|
|
6163
|
-
if (options.signal?.aborted) throw options.signal.reason ?? error;
|
|
6164
|
-
if (error instanceof ShareReceiveError) throw error;
|
|
6165
|
-
throw new ShareReceiveError("fetch-failed", "registry unavailable", { stage });
|
|
6166
|
-
}
|
|
6167
|
-
};
|
|
6168
|
-
}
|
|
6169
5944
|
function metadataFor(envelope, origin) {
|
|
6170
|
-
|
|
6171
|
-
const targetKind3 = envelope.recipientMatcher.kind === "recipientDid" ? "recipientDid" : envelope.recipientMatcher.kind === "exactEmail" ? "email" : envelope.recipientMatcher.kind === "emailDomain" ? "emailDomain" : "bearer";
|
|
6172
|
-
return {
|
|
6173
|
-
protocol: "tinycloud-share",
|
|
6174
|
-
version: 1,
|
|
6175
|
-
shareId: envelope.shareId,
|
|
6176
|
-
origin,
|
|
6177
|
-
target: { ...envelope.target, kind: targetKind3 },
|
|
6178
|
-
resource: envelope.resource,
|
|
6179
|
-
actions: envelope.actions,
|
|
6180
|
-
expiresAt: envelope.expiry,
|
|
6181
|
-
display: {
|
|
6182
|
-
...envelope.display.senderName === void 0 ? {} : { senderName: envelope.display.senderName },
|
|
6183
|
-
...envelope.display.filename === void 0 ? {} : { filename: envelope.display.filename },
|
|
6184
|
-
...envelope.display.mode === void 0 ? {} : { mode: envelope.display.mode }
|
|
6185
|
-
},
|
|
6186
|
-
...envelope.version === 2 && envelope.content !== void 0 ? { content: { cid: envelope.content.cid } } : {}
|
|
6187
|
-
};
|
|
6188
|
-
}
|
|
5945
|
+
const kind = envelope.recipientMatcher.kind === "recipientDid" ? "recipientDid" : envelope.recipientMatcher.kind === "exactEmail" ? "email" : "emailDomain";
|
|
6189
5946
|
return {
|
|
6190
5947
|
protocol: "tinycloud-share",
|
|
6191
5948
|
version: 1,
|
|
6192
5949
|
shareId: envelope.shareId,
|
|
6193
5950
|
origin,
|
|
6194
|
-
target: { ...envelope.target, kind
|
|
6195
|
-
resource: envelope.
|
|
6196
|
-
actions: [
|
|
5951
|
+
target: { ...envelope.target, kind },
|
|
5952
|
+
resource: { ...envelope.resource },
|
|
5953
|
+
actions: [...envelope.actions],
|
|
6197
5954
|
expiresAt: envelope.expiry,
|
|
6198
5955
|
display: {
|
|
6199
5956
|
...envelope.display.senderName === void 0 ? {} : { senderName: envelope.display.senderName },
|
|
6200
5957
|
...envelope.display.filename === void 0 ? {} : { filename: envelope.display.filename },
|
|
6201
5958
|
...envelope.display.mode === void 0 ? {} : { mode: envelope.display.mode }
|
|
6202
|
-
}
|
|
6203
|
-
...envelope.content === void 0 ? {} : { content: { cid: envelope.content.cid } }
|
|
5959
|
+
}
|
|
6204
5960
|
};
|
|
6205
5961
|
}
|
|
6206
|
-
function
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
const value = JSON.parse(text);
|
|
6210
|
-
if (canonicalize(value) !== text) throw new Error("share envelope is not canonical JSON");
|
|
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);
|
|
6215
|
-
}
|
|
6216
|
-
return shareEnvelopeSchema.parse(value);
|
|
6217
|
-
} catch {
|
|
6218
|
-
throw new ShareReceiveError("envelope-invalid", "share envelope is invalid");
|
|
6219
|
-
}
|
|
6220
|
-
}
|
|
6221
|
-
async function resolveShareEnvelope(link, options = {}) {
|
|
5962
|
+
async function resolvePolicyShare(link, options) {
|
|
5963
|
+
options.signal?.throwIfAborted();
|
|
5964
|
+
let url;
|
|
6222
5965
|
let parsed;
|
|
6223
5966
|
try {
|
|
6224
|
-
|
|
5967
|
+
url = new URL(link);
|
|
5968
|
+
parsed = await parseSealedInlineShareUrl(link, { ...options.expectedOrigin === void 0 ? {} : { expectedOrigin: options.expectedOrigin } });
|
|
6225
5969
|
} catch {
|
|
6226
5970
|
throw new ShareReceiveError("invalid-link", "share link format is invalid");
|
|
6227
5971
|
}
|
|
5972
|
+
const limit = options.maxSealedBlobBytes ?? DEFAULT_MAX_SEALED_BLOB_BYTES;
|
|
5973
|
+
if (!Number.isSafeInteger(limit) || limit <= 0 || parsed.ciphertext.byteLength > limit) throw new ShareReceiveError("max-bytes-exceeded", "sealed policy envelope exceeds the configured byte limit");
|
|
5974
|
+
if (await computeCid(parsed.ciphertext) !== parsed.ciphertextCid) throw new ShareReceiveError("cid-mismatch", "sealed envelope bytes do not match the link CID");
|
|
5975
|
+
options.signal?.throwIfAborted();
|
|
5976
|
+
let envelope;
|
|
6228
5977
|
try {
|
|
6229
|
-
|
|
6230
|
-
const
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
let plaintext;
|
|
6234
|
-
try {
|
|
6235
|
-
plaintext = parsed.key32 === void 0 ? sealed : await open(sealed, parsed.key32);
|
|
6236
|
-
} catch {
|
|
6237
|
-
throw new ShareReceiveError("decrypt-failed", "share envelope could not be opened");
|
|
6238
|
-
}
|
|
6239
|
-
return { envelope: parseEnvelope(plaintext), origin: url.origin, cid: parsed.ciphertextCid, kind: parsed.kind };
|
|
6240
|
-
} finally {
|
|
6241
|
-
parsed.key32?.fill(0);
|
|
6242
|
-
}
|
|
6243
|
-
}
|
|
6244
|
-
function actionName(value) {
|
|
6245
|
-
return value === "read" ? "tinycloud.kv/get" : value === "list" ? "tinycloud.kv/list" : value === "edit" ? "tinycloud.kv/put" : value;
|
|
6246
|
-
}
|
|
6247
|
-
function resourceContains(outer, inner) {
|
|
6248
|
-
if (outer.kind === "exact") return inner.kind === "exact" && outer.path === inner.path;
|
|
6249
|
-
const prefix = outer.path.endsWith("/") ? outer.path : `${outer.path}/`;
|
|
6250
|
-
return inner.path === outer.path || inner.path.startsWith(prefix);
|
|
6251
|
-
}
|
|
6252
|
-
function policyEvidenceMatches(envelope, evidence) {
|
|
6253
|
-
const registeredActions = new Set(evidence.actions.map(actionName));
|
|
6254
|
-
return evidence.policyCid === (envelope.authorizationTarget.kind === "policy" ? envelope.authorizationTarget.policyCid : "") && evidence.signerDid === envelope.signature.signerDid && evidence.registrationCid.length > 0 && evidence.shareId === envelope.shareId && canonicalize(evidence.recipientMatcher) === canonicalize(envelope.recipientMatcher) && canonicalize(evidence.target) === canonicalize(envelope.target) && resourceContains(evidence.resource, envelope.resource) && envelope.actions.every((action) => registeredActions.has(actionName(action))) && canonicalize(evidence.contentSource) === canonicalize(envelope.contentSource) && evidence.contentSourceDigest === envelope.contentSourceDigest && evidence.delegationCid === envelope.delegationCid && evidence.authorityMaterialHandle === envelope.authorityMaterialHandle && evidence.authorityMaterialDigest === envelope.authorityMaterialDigest && evidence.expiresAt === envelope.expiry && Number.isFinite(Date.parse(evidence.expiresAt)) && Date.parse(evidence.expiresAt) === Date.parse(envelope.expiry);
|
|
6255
|
-
}
|
|
6256
|
-
async function verifyV2Envelope(envelope, linkOrigin, options) {
|
|
6257
|
-
if (options.expectedOrigin !== void 0 && linkOrigin !== options.expectedOrigin) {
|
|
6258
|
-
throw new ShareReceiveError("origin-mismatch", "share link origin does not match the trusted origin");
|
|
6259
|
-
}
|
|
6260
|
-
try {
|
|
6261
|
-
if (!await verifyEnvelopeV2(envelope, { expectedSignerDid: envelope.signature.signerDid })) throw new Error("signature");
|
|
5978
|
+
const encoded = new TextDecoder("utf-8", { fatal: true }).decode(await open(parsed.ciphertext, parsed.key32));
|
|
5979
|
+
const value = JSON.parse(encoded);
|
|
5980
|
+
if (canonicalize(value) !== encoded) throw new Error("non-canonical envelope");
|
|
5981
|
+
envelope = shareEnvelopeV3Schema.parse(value);
|
|
6262
5982
|
} catch {
|
|
6263
|
-
throw new ShareReceiveError("
|
|
6264
|
-
}
|
|
6265
|
-
const expiry = Date.parse(envelope.expiry);
|
|
6266
|
-
if (!Number.isFinite(expiry)) throw new ShareReceiveError("envelope-invalid", "share expiry is invalid");
|
|
6267
|
-
if (expiry <= (options.now?.() ?? Date.now())) throw new ShareReceiveError("expired", "share has expired", { expiresAt: envelope.expiry });
|
|
6268
|
-
if (envelope.authorizationTarget.kind === "policy") {
|
|
6269
|
-
if (options.trustedPolicyAuthority === void 0) throw new ShareReceiveError("envelope-invalid", "an external policy authority is required");
|
|
6270
|
-
let evidence;
|
|
6271
|
-
try {
|
|
6272
|
-
evidence = await options.trustedPolicyAuthority.resolve({ policyCid: envelope.authorizationTarget.policyCid, envelope });
|
|
6273
|
-
} catch {
|
|
6274
|
-
evidence = void 0;
|
|
6275
|
-
}
|
|
6276
|
-
if (evidence === void 0 || !policyEvidenceMatches(envelope, evidence)) throw new ShareReceiveError("capability-invalid", "share policy authority evidence is not bound to the envelope");
|
|
6277
|
-
if (options.trustedSignerDid !== void 0 && (evidence.signerDid !== options.trustedSignerDid || envelope.signature.signerDid !== options.trustedSignerDid)) {
|
|
6278
|
-
throw new ShareReceiveError("signature-invalid", "share signer is not trusted");
|
|
6279
|
-
}
|
|
6280
|
-
} else if (envelope.authorizationTarget.kind === "recipientDid" && envelope.recipientMatcher.kind === "recipientDid" && envelope.authorizationTarget.did !== envelope.recipientMatcher.value) {
|
|
6281
|
-
throw new ShareReceiveError("capability-invalid", "recipient authorization does not match the signed recipient");
|
|
6282
|
-
} else {
|
|
6283
|
-
if (options.trustedSignerDid === void 0 || envelope.signature.signerDid !== options.trustedSignerDid) throw new ShareReceiveError("signature-invalid", "an addressed signer trust root is required");
|
|
6284
|
-
}
|
|
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");
|
|
5983
|
+
throw new ShareReceiveError("envelope-invalid", "share envelope is invalid");
|
|
6289
5984
|
}
|
|
6290
5985
|
try {
|
|
6291
5986
|
if (!await verifyEnvelopeV3(envelope, { expectedSignerDid: envelope.policy.ownerDid })) throw new Error("signature");
|
|
6292
5987
|
} catch {
|
|
6293
5988
|
throw new ShareReceiveError("signature-invalid", "share signature is invalid");
|
|
6294
5989
|
}
|
|
5990
|
+
options.signal?.throwIfAborted();
|
|
6295
5991
|
const expiry = Date.parse(envelope.expiry);
|
|
6296
5992
|
if (!Number.isFinite(expiry)) throw new ShareReceiveError("envelope-invalid", "share expiry is invalid");
|
|
6297
5993
|
if (expiry <= (options.now?.() ?? Date.now())) throw new ShareReceiveError("expired", "share has expired", { expiresAt: envelope.expiry });
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
if (envelope.authorizationTarget.kind !== "policy") throw new ShareReceiveError("unsupported-target", "share target is not an addressed policy", { reason: "policy-target" });
|
|
6301
|
-
if (envelope.target.origin !== linkOrigin || options.expectedOrigin !== void 0 && (linkOrigin !== options.expectedOrigin || envelope.target.origin !== options.expectedOrigin)) {
|
|
6302
|
-
throw new ShareReceiveError("origin-mismatch", "share origin does not match the trusted origin");
|
|
6303
|
-
}
|
|
6304
|
-
throw new ShareReceiveError("unsupported-target", "share policy target is not supported", { reason: "policy-target" });
|
|
6305
|
-
}
|
|
6306
|
-
async function verifyBearerEnvelope(envelope, linkOrigin, options = {}) {
|
|
6307
|
-
if (envelope.target.origin !== linkOrigin || options.expectedOrigin !== void 0 && (linkOrigin !== options.expectedOrigin || envelope.target.origin !== options.expectedOrigin)) {
|
|
6308
|
-
throw new ShareReceiveError("origin-mismatch", "share origin does not match the trusted origin");
|
|
6309
|
-
}
|
|
6310
|
-
if (envelope.authorizationTarget.kind !== "bearerKey") throw new ShareReceiveError("unsupported-target", "this receive path only handles bearer shares", { reason: envelope.authorizationTarget.kind === "policy" ? "policy-target" : "recipient-did-target" });
|
|
6311
|
-
if (envelope.target.resource.kind !== "exact") throw new ShareReceiveError("unsupported-target", "this receive path only handles exact resources", { reason: "prefix-resource" });
|
|
6312
|
-
const expiry = Date.parse(envelope.expiry);
|
|
6313
|
-
if (!Number.isFinite(expiry)) throw new ShareReceiveError("envelope-invalid", "share expiry is invalid");
|
|
6314
|
-
try {
|
|
6315
|
-
if (!await verifyEnvelope(envelope, { expectedSignerDid: envelope.signature.signerDid })) throw new Error("signature");
|
|
6316
|
-
} catch {
|
|
6317
|
-
throw new ShareReceiveError("signature-invalid", "share signature is invalid");
|
|
6318
|
-
}
|
|
6319
|
-
if (expiry <= (options.now?.() ?? Date.now())) throw new ShareReceiveError("expired", "share has expired", { expiresAt: envelope.expiry });
|
|
6320
|
-
const capability = checkBearerDelegation(envelope, options.now === void 0 ? {} : { now: options.now });
|
|
6321
|
-
if (!capability.ok) throw new ShareReceiveError("capability-invalid", "share delegation does not authorize the target");
|
|
6322
|
-
}
|
|
6323
|
-
async function verifyResolved(envelope, linkOrigin, options) {
|
|
6324
|
-
await verifyBearerEnvelope(envelope, linkOrigin, options);
|
|
5994
|
+
options.onResolvedAddressedEnvelope?.(envelope, parsed.ciphertextCid);
|
|
5995
|
+
return { envelope, origin: url.origin, cid: parsed.ciphertextCid };
|
|
6325
5996
|
}
|
|
6326
5997
|
async function inspectShare(link, options = {}) {
|
|
6327
|
-
const resolved = await
|
|
6328
|
-
|
|
6329
|
-
else if (resolved.envelope.version === 1) await verifyResolved(resolved.envelope, resolved.origin, options);
|
|
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);
|
|
6333
|
-
return { metadata: metadataFor(resolved.envelope, resolved.origin), link: { origin: resolved.origin, cid: resolved.cid, kind: resolved.kind } };
|
|
5998
|
+
const resolved = await resolvePolicyShare(link, options);
|
|
5999
|
+
return { metadata: metadataFor(resolved.envelope, resolved.origin), link: { origin: resolved.origin, cid: resolved.cid, kind: "policy" } };
|
|
6334
6000
|
}
|
|
6335
6001
|
async function receiveShare(link, options = {}) {
|
|
6336
|
-
const resolved = await
|
|
6337
|
-
|
|
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
|
-
}
|
|
6342
|
-
if (resolved.envelope.version === 2) {
|
|
6343
|
-
await verifyV2Envelope(resolved.envelope, resolved.origin, options);
|
|
6344
|
-
options.onResolvedAddressedEnvelope?.(resolved.envelope, resolved.cid);
|
|
6345
|
-
const method = resolved.envelope.recipientMatcher.kind === "recipientDid" ? "openkey-device" : "email-claim";
|
|
6346
|
-
if (options.authorization === void 0) return { state: "authorization-required", method };
|
|
6347
|
-
const result = options.authorizationResumeToken === void 0 ? await options.authorization.begin({ envelope: resolved.envelope, method }) : await options.authorization.resume({ envelope: resolved.envelope, method, resumeToken: options.authorizationResumeToken, ...options.authorizationProof === void 0 ? {} : { proof: options.authorizationProof } });
|
|
6348
|
-
if (result.state === "authorization-required") return result;
|
|
6349
|
-
if (result.state === "denied") throw new ShareReceiveError("authorization-denied", "share authorization was denied");
|
|
6350
|
-
if (result.value === null || typeof result.value !== "object" || !(result.value.bytes instanceof Uint8Array) || result.value.proof === void 0 || options.authorization.verifyResult === void 0) {
|
|
6351
|
-
throw new ShareReceiveError("content-integrity-failed", "share authorization returned an unsigned content result");
|
|
6352
|
-
}
|
|
6353
|
-
const authorized = result.value;
|
|
6354
|
-
try {
|
|
6355
|
-
if (!await options.authorization.verifyResult({ envelope: resolved.envelope, value: authorized, proof: authorized.proof })) throw new Error("node proof");
|
|
6356
|
-
} catch {
|
|
6357
|
-
throw new ShareReceiveError("content-integrity-failed", "node authorization proof is invalid");
|
|
6358
|
-
}
|
|
6359
|
-
const bytes4 = authorized.bytes.slice();
|
|
6360
|
-
const maxBytes = options.maxContentBlobBytes ?? DEFAULT_MAX_CONTENT_BLOB_BYTES;
|
|
6361
|
-
if (bytes4.byteLength > maxBytes) throw new ShareReceiveError("max-bytes-exceeded", "shared content exceeds the configured byte limit");
|
|
6362
|
-
const digestBytes3 = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes4));
|
|
6363
|
-
const bodyDigest = toBase64Url(digestBytes3);
|
|
6364
|
-
if (authorized.bodyDigest !== bodyDigest) throw new ShareReceiveError("content-integrity-failed", "authorized content digest does not match");
|
|
6365
|
-
if (authorized.contentSourceDigest !== resolved.envelope.contentSourceDigest) throw new ShareReceiveError("content-integrity-failed", "authorized content source does not match");
|
|
6366
|
-
const binding = authorized.binding;
|
|
6367
|
-
if (binding.shareId !== resolved.envelope.shareId || binding.delegationCid !== resolved.envelope.delegationCid || binding.authorityMaterialHandle !== resolved.envelope.authorityMaterialHandle || binding.authorityMaterialDigest !== resolved.envelope.authorityMaterialDigest || binding.resource.kind !== resolved.envelope.resource.kind || binding.resource.path !== resolved.envelope.resource.path || binding.action !== void 0 && !resolved.envelope.actions.some((action) => action === binding.action || `tinycloud.kv/${action}` === binding.action)) throw new ShareReceiveError("content-integrity-failed", "authorized content binding does not match");
|
|
6368
|
-
if (resolved.envelope.metadata.byteLength !== void 0 && resolved.envelope.metadata.byteLength !== bytes4.byteLength) {
|
|
6369
|
-
throw new ShareReceiveError("content-integrity-failed", "authorized content length does not match");
|
|
6370
|
-
}
|
|
6371
|
-
let text2;
|
|
6372
|
-
try {
|
|
6373
|
-
text2 = new TextDecoder("utf-8", { fatal: true }).decode(bytes4);
|
|
6374
|
-
} catch {
|
|
6375
|
-
}
|
|
6376
|
-
return { metadata: metadataFor(resolved.envelope, resolved.origin), link: { origin: resolved.origin, cid: resolved.cid, kind: resolved.kind }, bytes: bytes4, ...text2 === void 0 ? {} : { text: text2 } };
|
|
6377
|
-
}
|
|
6378
|
-
if (resolved.envelope.authorizationTarget.kind === "policy") {
|
|
6379
|
-
await verifyV1PolicyEnvelope(resolved.envelope, resolved.origin, options);
|
|
6380
|
-
options.onResolvedAddressedEnvelope?.(resolved.envelope, resolved.cid);
|
|
6381
|
-
return { state: "authorization-required", method: "email-claim" };
|
|
6382
|
-
}
|
|
6383
|
-
await verifyResolved(resolved.envelope, resolved.origin, options);
|
|
6384
|
-
let bytes3 = new Uint8Array();
|
|
6385
|
-
if (resolved.envelope.content !== void 0) {
|
|
6386
|
-
bytes3 = await registryFetcher(
|
|
6387
|
-
options,
|
|
6388
|
-
(options.maxContentBlobBytes ?? DEFAULT_MAX_CONTENT_BLOB_BYTES) + CONTENT_SEALED_OVERHEAD,
|
|
6389
|
-
"max-bytes-exceeded",
|
|
6390
|
-
"content"
|
|
6391
|
-
)({ origin: resolved.origin, cid: resolved.envelope.content.cid });
|
|
6392
|
-
if (await computeCid(bytes3) !== resolved.envelope.content.cid) throw new ShareReceiveError("content-integrity-failed", "shared content CID does not match");
|
|
6393
|
-
const key = fromBase64Url(resolved.envelope.content.key);
|
|
6394
|
-
try {
|
|
6395
|
-
bytes3 = await open(bytes3, key);
|
|
6396
|
-
} catch {
|
|
6397
|
-
throw new ShareReceiveError("content-integrity-failed", "shared content could not be opened");
|
|
6398
|
-
} finally {
|
|
6399
|
-
key.fill(0);
|
|
6400
|
-
}
|
|
6401
|
-
if (bytes3.byteLength > (options.maxContentBlobBytes ?? DEFAULT_MAX_CONTENT_BLOB_BYTES)) {
|
|
6402
|
-
throw new ShareReceiveError("max-bytes-exceeded", "shared content exceeds the configured byte limit");
|
|
6403
|
-
}
|
|
6404
|
-
}
|
|
6405
|
-
let text;
|
|
6406
|
-
if (bytes3.byteLength !== 0) {
|
|
6407
|
-
try {
|
|
6408
|
-
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes3);
|
|
6409
|
-
} catch {
|
|
6410
|
-
}
|
|
6411
|
-
}
|
|
6412
|
-
return { metadata: metadataFor(resolved.envelope, resolved.origin), link: { origin: resolved.origin, cid: resolved.cid, kind: resolved.kind }, bytes: bytes3, ...text === void 0 ? {} : { text } };
|
|
6002
|
+
const resolved = await resolvePolicyShare(link, options);
|
|
6003
|
+
return { state: "authorization-required", method: resolved.envelope.recipientMatcher.kind === "recipientDid" ? "openkey-device" : "email-claim" };
|
|
6413
6004
|
}
|
|
6414
6005
|
|
|
6415
6006
|
// src/publish.ts
|
|
6416
|
-
import { ed25519 as ed255194 } from "@noble/curves/ed25519";
|
|
6417
6007
|
var SHARE_CONTENT_LIMIT = 100 * 1024 * 1024;
|
|
6418
|
-
var SHARE_SEALED_OVERHEAD = 1 + 12 + 16;
|
|
6419
6008
|
var SHARE_PUBLISH_RESULT_VERSION = 1;
|
|
6420
6009
|
var DEFAULT_SHARE_LIFETIME_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
6421
6010
|
function redactPublishedShare(result) {
|
|
6422
6011
|
return {
|
|
6423
6012
|
protocol: "tinycloud-share",
|
|
6424
6013
|
version: SHARE_PUBLISH_RESULT_VERSION,
|
|
6425
|
-
link: {
|
|
6014
|
+
link: { ...result.link },
|
|
6426
6015
|
metadata: {
|
|
6427
6016
|
protocol: "tinycloud-share",
|
|
6428
6017
|
version: 1,
|
|
@@ -6432,10 +6021,8 @@ function redactPublishedShare(result) {
|
|
|
6432
6021
|
resource: { ...result.metadata.resource },
|
|
6433
6022
|
actions: [...result.metadata.actions],
|
|
6434
6023
|
expiresAt: result.metadata.expiresAt,
|
|
6435
|
-
display: { ...result.metadata.display }
|
|
6436
|
-
|
|
6437
|
-
},
|
|
6438
|
-
registryDeleteAfter: result.registryDeleteAfter
|
|
6024
|
+
display: { ...result.metadata.display }
|
|
6025
|
+
}
|
|
6439
6026
|
};
|
|
6440
6027
|
}
|
|
6441
6028
|
var SharePublishError = class extends Error {
|
|
@@ -6446,237 +6033,6 @@ var SharePublishError = class extends Error {
|
|
|
6446
6033
|
this.code = code3;
|
|
6447
6034
|
}
|
|
6448
6035
|
};
|
|
6449
|
-
function assertSafeFilename(filename) {
|
|
6450
|
-
if (filename.length === 0 || filename === "." || filename === ".." || /[/\\\u0000-\u001f\u007f]/.test(filename)) {
|
|
6451
|
-
throw new SharePublishError("invalid-argument", "filename must be one safe path segment");
|
|
6452
|
-
}
|
|
6453
|
-
}
|
|
6454
|
-
function assertOrigin(origin, label) {
|
|
6455
|
-
if (!isCanonicalHttpsOrigin(origin)) {
|
|
6456
|
-
throw new SharePublishError("invalid-argument", `${label} must be a canonical HTTPS origin`);
|
|
6457
|
-
}
|
|
6458
|
-
}
|
|
6459
|
-
async function snapshotSource(source, limit) {
|
|
6460
|
-
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
6461
|
-
throw new SharePublishError("invalid-argument", "maxBytes must be a positive safe integer");
|
|
6462
|
-
}
|
|
6463
|
-
if (source instanceof Uint8Array) {
|
|
6464
|
-
if (source.byteLength > limit) throw new SharePublishError("max-bytes-exceeded", "share content exceeds the configured byte limit");
|
|
6465
|
-
return source.slice();
|
|
6466
|
-
}
|
|
6467
|
-
const chunks = [];
|
|
6468
|
-
let total = 0;
|
|
6469
|
-
try {
|
|
6470
|
-
for await (const chunk of source) {
|
|
6471
|
-
if (!(chunk instanceof Uint8Array)) throw new SharePublishError("invalid-argument", "share source yielded invalid bytes");
|
|
6472
|
-
total += chunk.byteLength;
|
|
6473
|
-
if (total > limit) throw new SharePublishError("max-bytes-exceeded", "share content exceeds the configured byte limit");
|
|
6474
|
-
chunks.push(chunk);
|
|
6475
|
-
}
|
|
6476
|
-
} catch (error) {
|
|
6477
|
-
if (error instanceof SharePublishError) throw error;
|
|
6478
|
-
throw new SharePublishError("upload-failed", "share source could not be read");
|
|
6479
|
-
}
|
|
6480
|
-
const bytes3 = new Uint8Array(total);
|
|
6481
|
-
let offset = 0;
|
|
6482
|
-
for (const chunk of chunks) {
|
|
6483
|
-
bytes3.set(chunk, offset);
|
|
6484
|
-
offset += chunk.byteLength;
|
|
6485
|
-
}
|
|
6486
|
-
return bytes3;
|
|
6487
|
-
}
|
|
6488
|
-
function assertTarget(target) {
|
|
6489
|
-
if (target.kind !== "bearer") {
|
|
6490
|
-
throw new SharePublishError("unsupported-target", "this release publishes bearer shares only");
|
|
6491
|
-
}
|
|
6492
|
-
}
|
|
6493
|
-
function assertRegistryUrl(value, allowInsecure) {
|
|
6494
|
-
let url;
|
|
6495
|
-
try {
|
|
6496
|
-
url = new URL(value);
|
|
6497
|
-
} catch {
|
|
6498
|
-
throw new SharePublishError("invalid-argument", "registry URL is invalid");
|
|
6499
|
-
}
|
|
6500
|
-
if (url.username || url.password || url.search || url.hash) throw new SharePublishError("invalid-argument", "registry URL must not contain credentials, query, or fragment");
|
|
6501
|
-
if (!allowInsecure && url.protocol !== "https:") throw new SharePublishError("invalid-argument", "production registry uploads require HTTPS");
|
|
6502
|
-
if (url.protocol !== "https:" && url.hostname !== "127.0.0.1" && url.hostname !== "localhost") throw new SharePublishError("invalid-argument", "insecure registry uploads are limited to localhost");
|
|
6503
|
-
return url;
|
|
6504
|
-
}
|
|
6505
|
-
async function defaultUpload(options, input) {
|
|
6506
|
-
if (options.registryBaseUrl === void 0) throw new SharePublishError("upload-auth-required", "an authenticated share registry is required");
|
|
6507
|
-
const base3 = assertRegistryUrl(options.registryBaseUrl, options.allowInsecureRegistry === true).toString().replace(/\/$/, "");
|
|
6508
|
-
const localInsecure = options.allowInsecureRegistry === true && (new URL(base3).hostname === "127.0.0.1" || new URL(base3).hostname === "localhost");
|
|
6509
|
-
if (options.authorizeUpload === void 0 && !localInsecure) {
|
|
6510
|
-
throw new SharePublishError("upload-auth-required", "share upload authorization is required");
|
|
6511
|
-
}
|
|
6512
|
-
let authorization;
|
|
6513
|
-
if (options.authorizeUpload !== void 0) {
|
|
6514
|
-
try {
|
|
6515
|
-
authorization = await options.authorizeUpload(input);
|
|
6516
|
-
} catch {
|
|
6517
|
-
throw new SharePublishError("upload-auth-required", "share upload authorization was rejected");
|
|
6518
|
-
}
|
|
6519
|
-
}
|
|
6520
|
-
if (authorization !== void 0 && options.authorizationOrigin !== void 0) {
|
|
6521
|
-
let authorizationUrl;
|
|
6522
|
-
try {
|
|
6523
|
-
authorizationUrl = new URL(options.authorizationOrigin);
|
|
6524
|
-
} catch {
|
|
6525
|
-
throw new SharePublishError("invalid-argument", "upload authorization origin is invalid");
|
|
6526
|
-
}
|
|
6527
|
-
if (authorizationUrl.origin !== new URL(base3).origin) throw new SharePublishError("upload-auth-required", "upload authorization is scoped to another origin");
|
|
6528
|
-
}
|
|
6529
|
-
const headers = new Headers({
|
|
6530
|
-
"content-type": "application/vnd.ipld.raw",
|
|
6531
|
-
"if-none-match": "*",
|
|
6532
|
-
"x-delete-after": input.deleteAfter
|
|
6533
|
-
});
|
|
6534
|
-
if (typeof authorization === "string") {
|
|
6535
|
-
headers.set("x-tinycloud-upload-attestation", authorization);
|
|
6536
|
-
try {
|
|
6537
|
-
const attestation = JSON.parse(authorization);
|
|
6538
|
-
if (attestation.retention === void 0) throw new Error("retention");
|
|
6539
|
-
headers.set("x-tinycloud-retention", canonicalize(attestation.retention));
|
|
6540
|
-
} catch {
|
|
6541
|
-
throw new SharePublishError("upload-auth-required", "share upload authorization was rejected");
|
|
6542
|
-
}
|
|
6543
|
-
} else if (authorization !== void 0) {
|
|
6544
|
-
new Headers(authorization).forEach((value, key) => headers.set(key, value));
|
|
6545
|
-
}
|
|
6546
|
-
let response;
|
|
6547
|
-
try {
|
|
6548
|
-
response = await (options.fetchFn ?? globalThis.fetch)(`${base3}/blobs`, {
|
|
6549
|
-
method: "POST",
|
|
6550
|
-
headers,
|
|
6551
|
-
body: input.blob,
|
|
6552
|
-
...options.credentials === void 0 ? {} : { credentials: options.credentials },
|
|
6553
|
-
redirect: "error",
|
|
6554
|
-
referrerPolicy: "no-referrer"
|
|
6555
|
-
});
|
|
6556
|
-
} catch {
|
|
6557
|
-
throw new SharePublishError("upload-failed", "share registry is unavailable");
|
|
6558
|
-
}
|
|
6559
|
-
if (response.status === 401 || response.status === 403) throw new SharePublishError("upload-auth-required", "share upload authorization was rejected");
|
|
6560
|
-
if (!response.ok) throw new SharePublishError("upload-failed", "share registry rejected the encrypted blob");
|
|
6561
|
-
let body;
|
|
6562
|
-
try {
|
|
6563
|
-
body = await response.json();
|
|
6564
|
-
} catch {
|
|
6565
|
-
throw new SharePublishError("upload-failed", "share registry returned an invalid response");
|
|
6566
|
-
}
|
|
6567
|
-
const record2 = typeof body === "object" && body !== null ? body : {};
|
|
6568
|
-
if (record2.cid !== input.cid || typeof record2.deleteAfter !== "string") throw new SharePublishError("upload-failed", "share registry returned an unexpected CID or retention value");
|
|
6569
|
-
return { cid: input.cid, deleteAfter: record2.deleteAfter };
|
|
6570
|
-
}
|
|
6571
|
-
async function uploadShareBlob(options, input) {
|
|
6572
|
-
const result = await (options.uploadBlob ?? ((value) => defaultUpload(options, value)))(input);
|
|
6573
|
-
if (result.cid !== input.cid || typeof result.deleteAfter !== "string") throw new SharePublishError("upload-failed", "share uploader returned an invalid result");
|
|
6574
|
-
return result;
|
|
6575
|
-
}
|
|
6576
|
-
async function publishShare(options) {
|
|
6577
|
-
const target = options.target ?? { kind: "bearer" };
|
|
6578
|
-
assertTarget(target);
|
|
6579
|
-
assertSafeFilename(options.filename);
|
|
6580
|
-
assertOrigin(options.origin, "share origin");
|
|
6581
|
-
if (options.mediaType !== void 0 && options.mediaType.length === 0) throw new SharePublishError("invalid-argument", "mediaType must not be empty");
|
|
6582
|
-
const maxBytes = options.maxBytes ?? SHARE_CONTENT_LIMIT;
|
|
6583
|
-
if (maxBytes > SHARE_CONTENT_LIMIT) throw new SharePublishError("max-bytes-exceeded", "share content exceeds the maximum 100 MiB limit");
|
|
6584
|
-
const content = await snapshotSource(options.source, maxBytes);
|
|
6585
|
-
if (content.byteLength === 0) throw new SharePublishError("invalid-argument", "share content is empty");
|
|
6586
|
-
if (options.allowBinary !== true) {
|
|
6587
|
-
try {
|
|
6588
|
-
new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
6589
|
-
} catch {
|
|
6590
|
-
throw new SharePublishError("invalid-argument", "Markdown input must be valid UTF-8");
|
|
6591
|
-
}
|
|
6592
|
-
}
|
|
6593
|
-
const nowMs = options.now?.() ?? Date.now();
|
|
6594
|
-
const expiresAt = options.expiresAt ?? new Date(nowMs + DEFAULT_SHARE_LIFETIME_MS);
|
|
6595
|
-
if (!Number.isFinite(expiresAt.getTime()) || expiresAt.getTime() <= nowMs) throw new SharePublishError("invalid-argument", "expiresAt must be a valid future time");
|
|
6596
|
-
const expiry = expiresAt.toISOString();
|
|
6597
|
-
const nodeAudience = options.nodeAudience ?? "did:web:node.tinycloud.xyz";
|
|
6598
|
-
const spaceId = options.spaceId ?? "bearer";
|
|
6599
|
-
const shareId = toBase64Url(globalThis.crypto.getRandomValues(new Uint8Array(16)));
|
|
6600
|
-
const path = `shares/${shareId}/${options.filename}`;
|
|
6601
|
-
const sessionPrivateKey = ed255194.utils.randomPrivateKey();
|
|
6602
|
-
const senderPrivateKey = ed255194.utils.randomPrivateKey();
|
|
6603
|
-
let contentKey;
|
|
6604
|
-
let envelopeKey;
|
|
6605
|
-
try {
|
|
6606
|
-
const sessionPublicKey = ed255194.getPublicKey(sessionPrivateKey);
|
|
6607
|
-
const sessionDid = didKeyFromEd25519PublicKey(sessionPublicKey);
|
|
6608
|
-
const delegation = mintBearerDelegation({
|
|
6609
|
-
issuerPrivateKey: senderPrivateKey,
|
|
6610
|
-
audienceDid: sessionDid,
|
|
6611
|
-
resourceUri: bearerResourceUri(options.origin, spaceId, path),
|
|
6612
|
-
expiresAtSeconds: Math.ceil(expiresAt.getTime() / 1e3)
|
|
6613
|
-
});
|
|
6614
|
-
contentKey = generateKey();
|
|
6615
|
-
const sealedContent = await seal(content, contentKey);
|
|
6616
|
-
const unsigned = {
|
|
6617
|
-
version: 1,
|
|
6618
|
-
shareId,
|
|
6619
|
-
delegation,
|
|
6620
|
-
authorizationTarget: {
|
|
6621
|
-
kind: "bearerKey",
|
|
6622
|
-
sessionJwk: { kty: "OKP", crv: "Ed25519", x: toBase64Url(sessionPublicKey), d: toBase64Url(sessionPrivateKey) }
|
|
6623
|
-
},
|
|
6624
|
-
target: { origin: options.origin, nodeAudience, spaceId, resource: { kind: "exact", path } },
|
|
6625
|
-
display: { filename: options.filename },
|
|
6626
|
-
expiry,
|
|
6627
|
-
content: { cid: sealedContent.cid, key: toBase64Url(contentKey) }
|
|
6628
|
-
};
|
|
6629
|
-
const envelope = signEnvelope(unsigned, senderPrivateKey);
|
|
6630
|
-
envelopeKey = generateKey();
|
|
6631
|
-
const sealedEnvelope = await seal(new TextEncoder().encode(canonicalize(envelope)), envelopeKey);
|
|
6632
|
-
let inlineUrl;
|
|
6633
|
-
if (options.inline === true) {
|
|
6634
|
-
try {
|
|
6635
|
-
inlineUrl = await encodeInlineShareUrl({ origin: options.origin, ciphertext: sealedEnvelope.blob, key32: envelopeKey });
|
|
6636
|
-
} catch (error) {
|
|
6637
|
-
throw new SharePublishError("inline-too-large", error instanceof Error ? error.message : "inline share is too large");
|
|
6638
|
-
}
|
|
6639
|
-
}
|
|
6640
|
-
const contentUpload = await uploadShareBlob(options, { blob: sealedContent.blob, cid: sealedContent.cid, deleteAfter: expiry, contentLength: sealedContent.blob.byteLength });
|
|
6641
|
-
let url;
|
|
6642
|
-
let kind;
|
|
6643
|
-
let retention = contentUpload.deleteAfter;
|
|
6644
|
-
if (options.inline === true) {
|
|
6645
|
-
if (inlineUrl === void 0) throw new SharePublishError("inline-too-large", "inline share is too large");
|
|
6646
|
-
url = inlineUrl;
|
|
6647
|
-
kind = "inline";
|
|
6648
|
-
} else {
|
|
6649
|
-
const envelopeUpload = await uploadShareBlob(options, { blob: sealedEnvelope.blob, cid: sealedEnvelope.cid, deleteAfter: expiry, contentLength: sealedEnvelope.blob.byteLength });
|
|
6650
|
-
url = encodeShareUrl({ origin: options.origin, ciphertextCid: envelopeUpload.cid, key32: envelopeKey });
|
|
6651
|
-
kind = "compact";
|
|
6652
|
-
retention = envelopeUpload.deleteAfter;
|
|
6653
|
-
}
|
|
6654
|
-
const metadata = {
|
|
6655
|
-
protocol: "tinycloud-share",
|
|
6656
|
-
version: 1,
|
|
6657
|
-
shareId,
|
|
6658
|
-
origin: options.origin,
|
|
6659
|
-
target: { kind: "bearer", origin: options.origin, nodeAudience, spaceId },
|
|
6660
|
-
resource: { kind: "exact", path },
|
|
6661
|
-
actions: ["read"],
|
|
6662
|
-
expiresAt: expiry,
|
|
6663
|
-
display: { filename: options.filename },
|
|
6664
|
-
content: { cid: sealedContent.cid }
|
|
6665
|
-
};
|
|
6666
|
-
const result = { protocol: "tinycloud-share", version: SHARE_PUBLISH_RESULT_VERSION, link: { kind, cid: sealedEnvelope.cid }, metadata, registryDeleteAfter: retention };
|
|
6667
|
-
Object.defineProperty(result, "toJSON", {
|
|
6668
|
-
enumerable: false,
|
|
6669
|
-
value: () => redactPublishedShare(result)
|
|
6670
|
-
});
|
|
6671
|
-
Object.defineProperty(result, "url", { enumerable: false, value: url });
|
|
6672
|
-
return result;
|
|
6673
|
-
} finally {
|
|
6674
|
-
sessionPrivateKey.fill(0);
|
|
6675
|
-
senderPrivateKey.fill(0);
|
|
6676
|
-
contentKey?.fill(0);
|
|
6677
|
-
envelopeKey?.fill(0);
|
|
6678
|
-
}
|
|
6679
|
-
}
|
|
6680
6036
|
|
|
6681
6037
|
// src/addressed.ts
|
|
6682
6038
|
async function parseAddressedEnvelope(value) {
|
|
@@ -7120,8 +6476,8 @@ function targetAuthorizationMethod(target) {
|
|
|
7120
6476
|
}
|
|
7121
6477
|
async function publishTargetShare(input) {
|
|
7122
6478
|
const target = normalizeShareTarget(input.target);
|
|
7123
|
-
if (target.kind === "bearer") return publishShare(input);
|
|
7124
6479
|
if (input.targetAdapter === void 0) {
|
|
6480
|
+
if (target.kind === "bearer") throw new SharePublishError("authority-required", "native bearer publication requires an authenticated TinyCloud node");
|
|
7125
6481
|
return {
|
|
7126
6482
|
state: "authorization-required",
|
|
7127
6483
|
method: targetAuthorizationMethod(target)
|
|
@@ -7131,9 +6487,9 @@ async function publishTargetShare(input) {
|
|
|
7131
6487
|
const chunks = [];
|
|
7132
6488
|
let size = 0;
|
|
7133
6489
|
for await (const chunk of input.source) {
|
|
7134
|
-
if (!(chunk instanceof Uint8Array)) throw new TypeError("
|
|
6490
|
+
if (!(chunk instanceof Uint8Array)) throw new TypeError("share publication source yielded invalid bytes");
|
|
7135
6491
|
size += chunk.byteLength;
|
|
7136
|
-
if (size > (input.maxBytes ??
|
|
6492
|
+
if (size > (input.maxBytes ?? SHARE_CONTENT_LIMIT)) throw new SharePublishError("max-bytes-exceeded", "share publication exceeds maxBytes");
|
|
7137
6493
|
chunks.push(chunk.slice());
|
|
7138
6494
|
}
|
|
7139
6495
|
const bytes3 = new Uint8Array(size);
|
|
@@ -7144,15 +6500,29 @@ async function publishTargetShare(input) {
|
|
|
7144
6500
|
}
|
|
7145
6501
|
return bytes3;
|
|
7146
6502
|
})();
|
|
6503
|
+
if (source.byteLength === 0) throw new SharePublishError("invalid-argument", "share content is empty");
|
|
6504
|
+
if (!input.filename || input.filename === "." || input.filename === ".." || /[/\\\u0000-\u001f\u007f]/.test(input.filename)) {
|
|
6505
|
+
throw new SharePublishError("invalid-argument", "filename must be one safe path segment");
|
|
6506
|
+
}
|
|
6507
|
+
if (target.kind === "bearer" && input.allowBinary !== true) {
|
|
6508
|
+
try {
|
|
6509
|
+
new TextDecoder("utf-8", { fatal: true }).decode(source);
|
|
6510
|
+
} catch {
|
|
6511
|
+
throw new SharePublishError("invalid-argument", "Markdown input must be valid UTF-8");
|
|
6512
|
+
}
|
|
6513
|
+
}
|
|
7147
6514
|
const files = input.files === void 0 || input.files.length === 0 ? [{ bytes: source }] : input.files;
|
|
7148
6515
|
const limit = input.maxBytes ?? SHARE_CONTENT_LIMIT;
|
|
7149
6516
|
let totalBytes = 0;
|
|
7150
6517
|
for (const file of files) {
|
|
7151
6518
|
totalBytes += file.bytes.byteLength;
|
|
7152
6519
|
if (!Number.isSafeInteger(totalBytes) || totalBytes > limit) {
|
|
7153
|
-
throw new SharePublishError("max-bytes-exceeded", "
|
|
6520
|
+
throw new SharePublishError("max-bytes-exceeded", "share publication exceeds the combined byte limit");
|
|
7154
6521
|
}
|
|
7155
6522
|
}
|
|
6523
|
+
const nowMs = input.now?.() ?? Date.now();
|
|
6524
|
+
const expiresAt = input.expiresAt ?? new Date(nowMs + DEFAULT_SHARE_LIFETIME_MS);
|
|
6525
|
+
if (!Number.isFinite(expiresAt.getTime()) || expiresAt.getTime() <= nowMs) throw new SharePublishError("invalid-argument", "expiresAt must be a valid future time");
|
|
7156
6526
|
return input.targetAdapter.publish({
|
|
7157
6527
|
source,
|
|
7158
6528
|
filename: input.filename,
|
|
@@ -7161,18 +6531,8 @@ async function publishTargetShare(input) {
|
|
|
7161
6531
|
...input.resourceKind === void 0 ? {} : { resourceKind: input.resourceKind },
|
|
7162
6532
|
...input.actions === void 0 ? {} : { actions: input.actions },
|
|
7163
6533
|
target,
|
|
7164
|
-
expiresAt
|
|
6534
|
+
expiresAt,
|
|
7165
6535
|
origin: input.origin,
|
|
7166
|
-
...input.inline === void 0 ? {} : { inline: input.inline },
|
|
7167
|
-
upload: {
|
|
7168
|
-
...input.registryBaseUrl === void 0 ? {} : { registryBaseUrl: input.registryBaseUrl },
|
|
7169
|
-
...input.fetchFn === void 0 ? {} : { fetchFn: input.fetchFn },
|
|
7170
|
-
...input.authorizeUpload === void 0 ? {} : { authorizeUpload: input.authorizeUpload },
|
|
7171
|
-
...input.authorizationOrigin === void 0 ? {} : { authorizationOrigin: input.authorizationOrigin },
|
|
7172
|
-
...input.credentials === void 0 ? {} : { credentials: input.credentials },
|
|
7173
|
-
...input.allowInsecureRegistry === void 0 ? {} : { allowInsecureRegistry: input.allowInsecureRegistry },
|
|
7174
|
-
...input.uploadBlob === void 0 ? {} : { uploadBlob: input.uploadBlob }
|
|
7175
|
-
},
|
|
7176
6536
|
...input.notify === void 0 ? {} : { notify: input.notify }
|
|
7177
6537
|
});
|
|
7178
6538
|
}
|
|
@@ -7264,7 +6624,7 @@ function publicationResult(input) {
|
|
|
7264
6624
|
protocol: "tinycloud-share",
|
|
7265
6625
|
version: SHARE_PUBLISH_RESULT_VERSION,
|
|
7266
6626
|
url: input.url,
|
|
7267
|
-
link: { kind:
|
|
6627
|
+
link: { kind: "policy", cid: input.envelopeCid },
|
|
7268
6628
|
metadata: {
|
|
7269
6629
|
protocol: "tinycloud-share",
|
|
7270
6630
|
version: 1,
|
|
@@ -7283,8 +6643,7 @@ function publicationResult(input) {
|
|
|
7283
6643
|
enforcerDid: input.enforcerDid,
|
|
7284
6644
|
envelopeCid: input.envelopeCid,
|
|
7285
6645
|
shareCid: input.envelopeCid
|
|
7286
|
-
}
|
|
7287
|
-
registryDeleteAfter: input.retention
|
|
6646
|
+
}
|
|
7288
6647
|
};
|
|
7289
6648
|
Object.defineProperty(result, "toJSON", { enumerable: false, value: () => redactPublishedShare(result) });
|
|
7290
6649
|
Object.defineProperty(result, "url", { enumerable: false, value: input.url });
|
|
@@ -7366,40 +6725,100 @@ async function publishAddressedShare(options) {
|
|
|
7366
6725
|
if (envelopeSignature.byteLength !== 64) throw new TypeError("v3 envelope signature must be Ed25519");
|
|
7367
6726
|
const envelope = { ...unsigned, signature: { signerDid: options.authority.ownerDid, algorithm: "Ed25519", value: toBase64Url(envelopeSignature) } };
|
|
7368
6727
|
shareEnvelopeV3Schema.parse(envelope);
|
|
7369
|
-
const
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
6728
|
+
const envelopeBytes = textEncoder.encode(canonicalize(envelope));
|
|
6729
|
+
const envelopeKeyBytes = generateKey();
|
|
6730
|
+
const sealed = await seal(envelopeBytes, envelopeKeyBytes);
|
|
6731
|
+
const envelopeKey = toBase64Url(envelopeKeyBytes);
|
|
6732
|
+
const url = await encodeSealedInlineShareUrl({
|
|
6733
|
+
origin: options.shareOrigin,
|
|
6734
|
+
ciphertext: sealed.blob,
|
|
6735
|
+
key32: envelopeKeyBytes
|
|
6736
|
+
});
|
|
6737
|
+
const deliveryMaterial = {
|
|
6738
|
+
envelope,
|
|
6739
|
+
shareCid: sealed.cid,
|
|
6740
|
+
sealedEnvelope: toBase64Url(sealed.blob),
|
|
6741
|
+
envelopeKey
|
|
6742
|
+
};
|
|
6743
|
+
options.onDeliveryMaterial?.(deliveryMaterial);
|
|
6744
|
+
return publicationResult({
|
|
6745
|
+
options,
|
|
6746
|
+
url,
|
|
6747
|
+
envelopeCid: sealed.cid,
|
|
6748
|
+
matcher,
|
|
6749
|
+
policyCid: created.policyCid,
|
|
6750
|
+
policyRootCid: policyRoot.cid,
|
|
6751
|
+
enforcementRootCid: enforcementRoot.cid,
|
|
6752
|
+
enforcerDid: registration.attestedEnforcerBinding.enforcerDid,
|
|
6753
|
+
expiry,
|
|
6754
|
+
deliveryMaterial
|
|
6755
|
+
});
|
|
7395
6756
|
}
|
|
7396
6757
|
|
|
7397
|
-
// src/
|
|
7398
|
-
import {
|
|
6758
|
+
// src/cache.ts
|
|
6759
|
+
import { sha256 as sha2564 } from "@noble/hashes/sha256";
|
|
6760
|
+
function cacheKey(key) {
|
|
6761
|
+
if (!key.cid || !key.expiresAt) throw new Error("share cache keys require CID and expiry");
|
|
6762
|
+
return `${key.cid}\0${key.expiresAt}`;
|
|
6763
|
+
}
|
|
6764
|
+
function digest(bytes3) {
|
|
6765
|
+
return sha2564(bytes3);
|
|
6766
|
+
}
|
|
6767
|
+
function equalBytes(left, right) {
|
|
6768
|
+
if (left.byteLength !== right.byteLength) return false;
|
|
6769
|
+
return left.every((byte, index) => byte === right[index]);
|
|
6770
|
+
}
|
|
6771
|
+
var MemoryShareCache = class {
|
|
6772
|
+
constructor(now = () => /* @__PURE__ */ new Date()) {
|
|
6773
|
+
this.now = now;
|
|
6774
|
+
}
|
|
6775
|
+
entries = /* @__PURE__ */ new Map();
|
|
6776
|
+
async get(key, now = /* @__PURE__ */ new Date()) {
|
|
6777
|
+
const mapKey = cacheKey(key);
|
|
6778
|
+
const entry = this.entries.get(mapKey);
|
|
6779
|
+
if (entry === void 0) return void 0;
|
|
6780
|
+
if (Date.parse(entry.key.expiresAt) <= now.getTime() || !equalBytes(entry.digest, digest(entry.bytes))) {
|
|
6781
|
+
this.entries.delete(mapKey);
|
|
6782
|
+
return void 0;
|
|
6783
|
+
}
|
|
6784
|
+
return entry.bytes.slice();
|
|
6785
|
+
}
|
|
6786
|
+
async getByCid(cid2, now = /* @__PURE__ */ new Date()) {
|
|
6787
|
+
const entries = [...this.entries.values()].filter((entry) => entry.key.cid === cid2);
|
|
6788
|
+
for (const entry of entries) {
|
|
6789
|
+
if (Date.parse(entry.key.expiresAt) <= now.getTime() || !equalBytes(entry.digest, digest(entry.bytes))) {
|
|
6790
|
+
this.entries.delete(cacheKey(entry.key));
|
|
6791
|
+
continue;
|
|
6792
|
+
}
|
|
6793
|
+
return {
|
|
6794
|
+
key: { ...entry.key },
|
|
6795
|
+
bytes: entry.bytes.slice(),
|
|
6796
|
+
digest: entry.digest.slice(),
|
|
6797
|
+
metadata: { ...entry.metadata }
|
|
6798
|
+
};
|
|
6799
|
+
}
|
|
6800
|
+
return void 0;
|
|
6801
|
+
}
|
|
6802
|
+
async set(key, bytes3, metadata) {
|
|
6803
|
+
if (Date.parse(key.expiresAt) <= this.now().getTime()) return;
|
|
6804
|
+
this.entries.set(cacheKey(key), {
|
|
6805
|
+
key: { cid: key.cid, expiresAt: key.expiresAt },
|
|
6806
|
+
bytes: bytes3.slice(),
|
|
6807
|
+
digest: digest(bytes3),
|
|
6808
|
+
metadata: { ...metadata }
|
|
6809
|
+
});
|
|
6810
|
+
}
|
|
6811
|
+
async delete(key) {
|
|
6812
|
+
this.entries.delete(cacheKey(key));
|
|
6813
|
+
}
|
|
6814
|
+
async clear() {
|
|
6815
|
+
this.entries.clear();
|
|
6816
|
+
}
|
|
6817
|
+
};
|
|
7399
6818
|
|
|
7400
6819
|
// src/owner-policy.ts
|
|
7401
|
-
import { ed25519 as
|
|
7402
|
-
import { sha256 as
|
|
6820
|
+
import { ed25519 as ed255194 } from "@noble/curves/ed25519";
|
|
6821
|
+
import { sha256 as sha2565 } from "@noble/hashes/sha256";
|
|
7403
6822
|
|
|
7404
6823
|
// ../../node_modules/multiformats/dist/src/bases/base32.js
|
|
7405
6824
|
var base322 = rfc46482({
|
|
@@ -7953,10 +7372,10 @@ function fromB64(value) {
|
|
|
7953
7372
|
throw new Error("base64url decoding is unavailable");
|
|
7954
7373
|
}
|
|
7955
7374
|
function cid(bytesValue) {
|
|
7956
|
-
return CID2.createV1(code2, create2(SHA256_CODE2,
|
|
7375
|
+
return CID2.createV1(code2, create2(SHA256_CODE2, sha2565(bytesValue))).toString(base322.encoder);
|
|
7957
7376
|
}
|
|
7958
|
-
function
|
|
7959
|
-
return b64(
|
|
7377
|
+
function digest2(bytesValue) {
|
|
7378
|
+
return b64(sha2565(bytesValue));
|
|
7960
7379
|
}
|
|
7961
7380
|
function didKeyFromEd25519PublicKey2(publicKey) {
|
|
7962
7381
|
if (publicKey.length !== 32) throw new Error("share key public key must be 32 bytes");
|
|
@@ -8030,9 +7449,9 @@ function dagCborEncode(value) {
|
|
|
8030
7449
|
}
|
|
8031
7450
|
function assertObject(value, keys, label) {
|
|
8032
7451
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${label} is invalid`);
|
|
8033
|
-
const
|
|
8034
|
-
if (Object.keys(
|
|
8035
|
-
return
|
|
7452
|
+
const record = value;
|
|
7453
|
+
if (Object.keys(record).length !== keys.length || keys.some((key) => !Object.prototype.hasOwnProperty.call(record, key))) throw new Error(`${label} has unknown or missing fields`);
|
|
7454
|
+
return record;
|
|
8036
7455
|
}
|
|
8037
7456
|
async function createDelegatedShareKey(options) {
|
|
8038
7457
|
if (!globalThis.crypto?.subtle) throw new Error("WebCrypto Ed25519 is required for delegated share keys");
|
|
@@ -8095,7 +7514,7 @@ async function createPolicyEnforcementDelegation(input) {
|
|
|
8095
7514
|
async function canonicalOwnerSharePolicy(policy) {
|
|
8096
7515
|
const text = canonicalize({ domain: POLICY_DOMAIN, policy });
|
|
8097
7516
|
const policyBytes = new TextEncoder().encode(text);
|
|
8098
|
-
return { bytes: policyBytes, cid: cid(policyBytes), digest:
|
|
7517
|
+
return { bytes: policyBytes, cid: cid(policyBytes), digest: digest2(policyBytes) };
|
|
8099
7518
|
}
|
|
8100
7519
|
function computeOwnerShareRegistrationCid(registration) {
|
|
8101
7520
|
return cid(new TextEncoder().encode(canonicalize(registration)));
|
|
@@ -8134,7 +7553,7 @@ function validateOwnerSharePolicyRegistration(value, expected) {
|
|
|
8134
7553
|
const publicKey = encodedKid.length === 34 ? encodedKid.slice(2) : encodedKid;
|
|
8135
7554
|
const signatureBytes = fromB64(proof.signature);
|
|
8136
7555
|
const signedBytes = new TextEncoder().encode(`${OWNER_SHARE_REGISTRATION_DOMAIN}${canonicalize(registrationCore)}`);
|
|
8137
|
-
if (signatureBytes.length !== 64 || !
|
|
7556
|
+
if (signatureBytes.length !== 64 || !ed255194.verify(signatureBytes, signedBytes, publicKey)) throw new Error("owner-share registration proof signature is invalid");
|
|
8138
7557
|
return { registration, proof };
|
|
8139
7558
|
}
|
|
8140
7559
|
function validateOwnerSharePolicyRegistrationBytes(bytesValue, expected) {
|
|
@@ -8147,318 +7566,6 @@ function validateOwnerSharePolicyRegistrationBytes(bytesValue, expected) {
|
|
|
8147
7566
|
return validateOwnerSharePolicyRegistration(value, expected);
|
|
8148
7567
|
}
|
|
8149
7568
|
|
|
8150
|
-
// src/policy-authority.ts
|
|
8151
|
-
var ENVELOPE_DOMAIN = "xyz.tinycloud.share/envelope/v2\0";
|
|
8152
|
-
function record(value, keys, label) {
|
|
8153
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${label} is invalid`);
|
|
8154
|
-
const output = value;
|
|
8155
|
-
if (Object.keys(output).length !== keys.length || keys.some((key) => !Object.hasOwn(output, key))) throw new Error(`${label} has unknown or missing fields`);
|
|
8156
|
-
return output;
|
|
8157
|
-
}
|
|
8158
|
-
function requiredString(value, label) {
|
|
8159
|
-
if (typeof value !== "string" || value.length === 0) throw new Error(`${label} is invalid`);
|
|
8160
|
-
return value;
|
|
8161
|
-
}
|
|
8162
|
-
function stringArray(value, label) {
|
|
8163
|
-
if (!Array.isArray(value) || value.length === 0 || !value.every((item) => typeof item === "string" && item.length > 0)) throw new Error(`${label} is invalid`);
|
|
8164
|
-
return value;
|
|
8165
|
-
}
|
|
8166
|
-
function parseEnforcement(value) {
|
|
8167
|
-
const delegation = record(value, ["cid", "dagCbor", "issuerDid", "audienceDid", "facts", "signature"], "policy enforcement delegation");
|
|
8168
|
-
const facts = record(delegation.facts, ["ownerDelegationCid", "policyCid", "shareId", "shareKeyDid", "enforcerDid", "nodeAudience", "spaceId", "path", "actions", "contentSourceDigest", "expiresAt"], "policy enforcement facts");
|
|
8169
|
-
return {
|
|
8170
|
-
cid: requiredString(delegation.cid, "policy enforcement CID"),
|
|
8171
|
-
dagCbor: requiredString(delegation.dagCbor, "policy enforcement bytes"),
|
|
8172
|
-
issuerDid: requiredString(delegation.issuerDid, "policy enforcement issuer"),
|
|
8173
|
-
audienceDid: requiredString(delegation.audienceDid, "policy enforcement audience"),
|
|
8174
|
-
facts: {
|
|
8175
|
-
ownerDelegationCid: requiredString(facts.ownerDelegationCid, "owner delegation CID"),
|
|
8176
|
-
policyCid: requiredString(facts.policyCid, "policy CID"),
|
|
8177
|
-
shareId: requiredString(facts.shareId, "share id"),
|
|
8178
|
-
shareKeyDid: requiredString(facts.shareKeyDid, "share key DID"),
|
|
8179
|
-
enforcerDid: requiredString(facts.enforcerDid, "enforcer DID"),
|
|
8180
|
-
nodeAudience: requiredString(facts.nodeAudience, "node audience"),
|
|
8181
|
-
spaceId: requiredString(facts.spaceId, "space id"),
|
|
8182
|
-
path: requiredString(facts.path, "resource path"),
|
|
8183
|
-
actions: stringArray(facts.actions, "policy actions"),
|
|
8184
|
-
contentSourceDigest: requiredString(facts.contentSourceDigest, "content source digest"),
|
|
8185
|
-
expiresAt: requiredString(facts.expiresAt, "policy expiry")
|
|
8186
|
-
},
|
|
8187
|
-
signature: requiredString(delegation.signature, "policy enforcement signature")
|
|
8188
|
-
};
|
|
8189
|
-
}
|
|
8190
|
-
async function verifyEnforcement(delegation) {
|
|
8191
|
-
try {
|
|
8192
|
-
const dagCbor = fromBase64Url(delegation.dagCbor);
|
|
8193
|
-
if (await computeCid(dagCbor) !== delegation.cid) return false;
|
|
8194
|
-
const expectedDagCbor = dagCborEncode({
|
|
8195
|
-
domain: POLICY_ENFORCEMENT_DOMAIN,
|
|
8196
|
-
unsigned: {
|
|
8197
|
-
type: "TinyCloudSharePolicyEnforcement",
|
|
8198
|
-
version: 2,
|
|
8199
|
-
issuerDid: delegation.issuerDid,
|
|
8200
|
-
audienceDid: delegation.audienceDid,
|
|
8201
|
-
facts: delegation.facts
|
|
8202
|
-
}
|
|
8203
|
-
});
|
|
8204
|
-
if (expectedDagCbor.length !== dagCbor.length || expectedDagCbor.some((byte, index) => byte !== dagCbor[index])) return false;
|
|
8205
|
-
const signature = fromBase64Url(delegation.signature);
|
|
8206
|
-
return signature.length === 64 && ed255196.verify(
|
|
8207
|
-
signature,
|
|
8208
|
-
dagCbor,
|
|
8209
|
-
ed25519PublicKeyFromDidKey(delegation.issuerDid),
|
|
8210
|
-
{ zip215: false }
|
|
8211
|
-
);
|
|
8212
|
-
} catch {
|
|
8213
|
-
return false;
|
|
8214
|
-
}
|
|
8215
|
-
}
|
|
8216
|
-
async function verifyOuterEnvelope(value, envelope, policyCid, expectedEnforcerDid, expectedActions) {
|
|
8217
|
-
try {
|
|
8218
|
-
const hasDecryption = typeof value === "object" && value !== null && Object.hasOwn(value, "decryption");
|
|
8219
|
-
const outer = record(value, ["schema", "version", "shareId", "delegationCid", "policyCid", "target", "resource", "actions", ...hasDecryption ? ["decryption"] : [], "contentSource", "contentSourceDigest", "expiresAt", "envelopeCid", "shareCid", "signature"], "outer envelope");
|
|
8220
|
-
const target = record(outer.target, ["origin", "nodeAudience", "enforcerDid", "spaceId"], "outer target");
|
|
8221
|
-
const signature = record(outer.signature, ["signerDid", "algorithm", "value"], "outer envelope signature");
|
|
8222
|
-
const envelopeCid = requiredString(outer.envelopeCid, "outer envelope CID");
|
|
8223
|
-
const shareCid = requiredString(outer.shareCid, "outer share CID");
|
|
8224
|
-
const { signature: _signature, envelopeCid: _envelopeCid, shareCid: _shareCid, ...identity } = outer;
|
|
8225
|
-
if (outer.schema !== "xyz.tinycloud.share/envelope/v2" || outer.version !== 2 || outer.shareId !== envelope.shareId || outer.delegationCid !== envelope.delegationCid || outer.policyCid !== policyCid || target.origin !== envelope.target.origin || target.nodeAudience !== envelope.target.nodeAudience || target.enforcerDid !== expectedEnforcerDid || target.spaceId !== envelope.target.spaceId || canonicalize(outer.resource) !== canonicalize(envelope.resource) || canonicalize(outer.actions) !== canonicalize(expectedActions) || (hasDecryption ? envelope.decryption === void 0 || canonicalize(outer.decryption) !== canonicalize(envelope.decryption) : envelope.decryption !== void 0) || canonicalize(outer.contentSource) !== canonicalize(envelope.contentSource) || outer.contentSourceDigest !== envelope.contentSourceDigest || outer.expiresAt !== envelope.expiry || signature.signerDid !== envelope.signature.signerDid || signature.algorithm !== "Ed25519") return void 0;
|
|
8226
|
-
if (envelopeCid !== await computeCid(new TextEncoder().encode(canonicalize(identity)))) return void 0;
|
|
8227
|
-
if (shareCid !== await computeCid(new TextEncoder().encode(canonicalize({ version: 2, shareId: envelope.shareId, policyCid, envelopeCid })))) return void 0;
|
|
8228
|
-
const unsigned = { ...identity, envelopeCid, shareCid };
|
|
8229
|
-
const signatureBytes = fromBase64Url(requiredString(signature.value, "outer signature"));
|
|
8230
|
-
if (signatureBytes.length !== 64 || !ed255196.verify(
|
|
8231
|
-
signatureBytes,
|
|
8232
|
-
new TextEncoder().encode(`${ENVELOPE_DOMAIN}${canonicalize(unsigned)}`),
|
|
8233
|
-
ed25519PublicKeyFromDidKey(envelope.signature.signerDid),
|
|
8234
|
-
{ zip215: false }
|
|
8235
|
-
)) return void 0;
|
|
8236
|
-
return { envelopeCid, shareCid };
|
|
8237
|
-
} catch {
|
|
8238
|
-
return void 0;
|
|
8239
|
-
}
|
|
8240
|
-
}
|
|
8241
|
-
function createRegisteredPolicyAuthority(options) {
|
|
8242
|
-
const proofKey = options.nodeProof.publicKey.slice();
|
|
8243
|
-
return {
|
|
8244
|
-
async resolve({ policyCid, envelope }) {
|
|
8245
|
-
try {
|
|
8246
|
-
if (envelope.authorizationTarget.kind !== "policy" || envelope.authorizationTarget.policyCid !== policyCid) return void 0;
|
|
8247
|
-
const ownerAuthority = envelope.ownerAuthority;
|
|
8248
|
-
if (ownerAuthority?.registrationReceipt === void 0) return void 0;
|
|
8249
|
-
const enforcement = parseEnforcement(ownerAuthority.enforcementDelegation);
|
|
8250
|
-
if (!await verifyEnforcement(enforcement)) return void 0;
|
|
8251
|
-
const policyBytes = fromBase64Url(envelope.authorizationTarget.policyBytes);
|
|
8252
|
-
const ownerDelegation = {
|
|
8253
|
-
delegationCid: envelope.delegationCid,
|
|
8254
|
-
signedDagCbor: new Uint8Array(),
|
|
8255
|
-
delegation: {
|
|
8256
|
-
delegateDID: envelope.signature.signerDid,
|
|
8257
|
-
spaceId: envelope.target.spaceId,
|
|
8258
|
-
path: envelope.resource.path,
|
|
8259
|
-
actions: envelope.actions,
|
|
8260
|
-
expiry: new Date(envelope.expiry)
|
|
8261
|
-
}
|
|
8262
|
-
};
|
|
8263
|
-
const receipt = validateOwnerSharePolicyRegistration(ownerAuthority.registrationReceipt, {
|
|
8264
|
-
policy: { bytes: policyBytes, cid: policyCid, proof: "" },
|
|
8265
|
-
ownerDelegation,
|
|
8266
|
-
enforcementDelegation: enforcement,
|
|
8267
|
-
contentSourceDigest: envelope.contentSourceDigest,
|
|
8268
|
-
nodeProof: { kid: options.nodeProof.kid, publicKey: proofKey }
|
|
8269
|
-
});
|
|
8270
|
-
const registration = receipt.registration;
|
|
8271
|
-
const outer = await verifyOuterEnvelope(ownerAuthority.outerEnvelope, envelope, policyCid, options.expectedTarget.enforcerDid, registration.actions);
|
|
8272
|
-
if (outer === void 0 || ownerAuthority.registrationCid !== registration.registrationCid || ownerAuthority.envelopeCid !== outer.envelopeCid || ownerAuthority.shareCid !== outer.shareCid || envelope.authorityMaterialHandle !== registration.registrationCid || envelope.signature.signerDid !== registration.shareKeyDid || registration.target.origin !== options.expectedTarget.origin || registration.target.nodeAudience !== options.expectedTarget.nodeAudience || registration.enforcerDid !== options.expectedTarget.enforcerDid || enforcement.issuerDid !== registration.shareKeyDid || enforcement.audienceDid !== registration.enforcerDid) return void 0;
|
|
8273
|
-
return {
|
|
8274
|
-
policyCid: registration.policyCid,
|
|
8275
|
-
signerDid: registration.shareKeyDid,
|
|
8276
|
-
registrationCid: registration.registrationCid,
|
|
8277
|
-
shareId: registration.shareId,
|
|
8278
|
-
recipientMatcher: registration.recipientMatcher,
|
|
8279
|
-
target: registration.target,
|
|
8280
|
-
resource: registration.resource,
|
|
8281
|
-
actions: registration.actions,
|
|
8282
|
-
contentSource: registration.contentSource,
|
|
8283
|
-
contentSourceDigest: registration.contentSourceDigest,
|
|
8284
|
-
delegationCid: registration.ownerDelegationCid,
|
|
8285
|
-
authorityMaterialHandle: registration.registrationCid,
|
|
8286
|
-
authorityMaterialDigest: envelope.authorityMaterialDigest,
|
|
8287
|
-
expiresAt: registration.expiresAt
|
|
8288
|
-
};
|
|
8289
|
-
} catch {
|
|
8290
|
-
return void 0;
|
|
8291
|
-
}
|
|
8292
|
-
}
|
|
8293
|
-
};
|
|
8294
|
-
}
|
|
8295
|
-
|
|
8296
|
-
// src/cache.ts
|
|
8297
|
-
import { sha256 as sha2565 } from "@noble/hashes/sha256";
|
|
8298
|
-
function cacheKey(key) {
|
|
8299
|
-
if (!key.cid || !key.expiresAt) throw new Error("share cache keys require CID and expiry");
|
|
8300
|
-
return `${key.cid}\0${key.expiresAt}`;
|
|
8301
|
-
}
|
|
8302
|
-
function digest2(bytes3) {
|
|
8303
|
-
return sha2565(bytes3);
|
|
8304
|
-
}
|
|
8305
|
-
function equalBytes(left, right) {
|
|
8306
|
-
if (left.byteLength !== right.byteLength) return false;
|
|
8307
|
-
return left.every((byte, index) => byte === right[index]);
|
|
8308
|
-
}
|
|
8309
|
-
var MemoryShareCache = class {
|
|
8310
|
-
constructor(now = () => /* @__PURE__ */ new Date()) {
|
|
8311
|
-
this.now = now;
|
|
8312
|
-
}
|
|
8313
|
-
entries = /* @__PURE__ */ new Map();
|
|
8314
|
-
async get(key, now = /* @__PURE__ */ new Date()) {
|
|
8315
|
-
const mapKey = cacheKey(key);
|
|
8316
|
-
const entry = this.entries.get(mapKey);
|
|
8317
|
-
if (entry === void 0) return void 0;
|
|
8318
|
-
if (Date.parse(entry.key.expiresAt) <= now.getTime() || !equalBytes(entry.digest, digest2(entry.bytes))) {
|
|
8319
|
-
this.entries.delete(mapKey);
|
|
8320
|
-
return void 0;
|
|
8321
|
-
}
|
|
8322
|
-
return entry.bytes.slice();
|
|
8323
|
-
}
|
|
8324
|
-
async getByCid(cid2, now = /* @__PURE__ */ new Date()) {
|
|
8325
|
-
const entries = [...this.entries.values()].filter((entry) => entry.key.cid === cid2);
|
|
8326
|
-
for (const entry of entries) {
|
|
8327
|
-
if (Date.parse(entry.key.expiresAt) <= now.getTime() || !equalBytes(entry.digest, digest2(entry.bytes))) {
|
|
8328
|
-
this.entries.delete(cacheKey(entry.key));
|
|
8329
|
-
continue;
|
|
8330
|
-
}
|
|
8331
|
-
return {
|
|
8332
|
-
key: { ...entry.key },
|
|
8333
|
-
bytes: entry.bytes.slice(),
|
|
8334
|
-
digest: entry.digest.slice(),
|
|
8335
|
-
metadata: { ...entry.metadata }
|
|
8336
|
-
};
|
|
8337
|
-
}
|
|
8338
|
-
return void 0;
|
|
8339
|
-
}
|
|
8340
|
-
async set(key, bytes3, metadata) {
|
|
8341
|
-
if (Date.parse(key.expiresAt) <= this.now().getTime()) return;
|
|
8342
|
-
this.entries.set(cacheKey(key), {
|
|
8343
|
-
key: { cid: key.cid, expiresAt: key.expiresAt },
|
|
8344
|
-
bytes: bytes3.slice(),
|
|
8345
|
-
digest: digest2(bytes3),
|
|
8346
|
-
metadata: { ...metadata }
|
|
8347
|
-
});
|
|
8348
|
-
}
|
|
8349
|
-
async delete(key) {
|
|
8350
|
-
this.entries.delete(cacheKey(key));
|
|
8351
|
-
}
|
|
8352
|
-
async clear() {
|
|
8353
|
-
this.entries.clear();
|
|
8354
|
-
}
|
|
8355
|
-
};
|
|
8356
|
-
|
|
8357
|
-
// src/delivery.ts
|
|
8358
|
-
import { ed25519 as ed255197 } from "@noble/curves/ed25519";
|
|
8359
|
-
var SHARE_DELIVERY_AUTHORIZATION_DOMAIN = "xyz.tinycloud.share/delivery-authorization/v2\0";
|
|
8360
|
-
function isCanonicalHttpsOrigin2(value) {
|
|
8361
|
-
let url;
|
|
8362
|
-
try {
|
|
8363
|
-
url = new URL(value);
|
|
8364
|
-
} catch {
|
|
8365
|
-
return false;
|
|
8366
|
-
}
|
|
8367
|
-
return url.protocol === "https:" && url.origin === value && url.pathname === "/" && url.search === "" && url.hash === "" && url.username === "" && url.password === "";
|
|
8368
|
-
}
|
|
8369
|
-
var AUTHORIZATION_KEYS = [
|
|
8370
|
-
"type",
|
|
8371
|
-
"version",
|
|
8372
|
-
"jti",
|
|
8373
|
-
"shareCid",
|
|
8374
|
-
"shareId",
|
|
8375
|
-
"registrationCid",
|
|
8376
|
-
"delegationCid",
|
|
8377
|
-
"enforcementDelegationCid",
|
|
8378
|
-
"envelopeCid",
|
|
8379
|
-
"policyCid",
|
|
8380
|
-
"nodeAudience",
|
|
8381
|
-
"targetOrigin",
|
|
8382
|
-
"openCredentialsAudience",
|
|
8383
|
-
"holder",
|
|
8384
|
-
"recipientMatcher",
|
|
8385
|
-
"deliveryEmail",
|
|
8386
|
-
"shareUrl",
|
|
8387
|
-
"returnOrigin",
|
|
8388
|
-
"documentName",
|
|
8389
|
-
"senderDid",
|
|
8390
|
-
"senderTrust",
|
|
8391
|
-
"contentSource",
|
|
8392
|
-
"contentSourceDigest",
|
|
8393
|
-
"shareExpiresAt",
|
|
8394
|
-
"issuedAt",
|
|
8395
|
-
"reportAbuseToken",
|
|
8396
|
-
"actions",
|
|
8397
|
-
"resource",
|
|
8398
|
-
"authorityMaterialHandle",
|
|
8399
|
-
"authorityMaterialDigest",
|
|
8400
|
-
"requestBodyDigest",
|
|
8401
|
-
"idempotencyKey",
|
|
8402
|
-
"expiresAt",
|
|
8403
|
-
"dataAuthority"
|
|
8404
|
-
];
|
|
8405
|
-
function assertObject2(value, keys, label) {
|
|
8406
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${label} is invalid`);
|
|
8407
|
-
const record2 = value;
|
|
8408
|
-
if (Object.keys(record2).length !== keys.length || keys.some((key) => !Object.prototype.hasOwnProperty.call(record2, key))) throw new Error(`${label} has unknown or missing fields`);
|
|
8409
|
-
return record2;
|
|
8410
|
-
}
|
|
8411
|
-
function fromB642(value) {
|
|
8412
|
-
if (!/^[A-Za-z0-9_-]+$/.test(value)) throw new Error("share delivery value is not canonical base64url");
|
|
8413
|
-
if (typeof atob === "function") {
|
|
8414
|
-
const padded = value.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - value.length % 4) % 4);
|
|
8415
|
-
return Uint8Array.from(atob(padded), (character) => character.charCodeAt(0));
|
|
8416
|
-
}
|
|
8417
|
-
if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(value, "base64url"));
|
|
8418
|
-
throw new Error("base64url decoding is unavailable");
|
|
8419
|
-
}
|
|
8420
|
-
function validateShareDeliveryAuthorizationBytes(bytesValue, expected) {
|
|
8421
|
-
let value;
|
|
8422
|
-
try {
|
|
8423
|
-
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytesValue));
|
|
8424
|
-
} catch {
|
|
8425
|
-
throw new Error("share delivery authorization response is not valid UTF-8 JSON");
|
|
8426
|
-
}
|
|
8427
|
-
const root = assertObject2(value, ["authorization", "proof"], "share delivery authorization response");
|
|
8428
|
-
const authorization = assertObject2(root.authorization, AUTHORIZATION_KEYS, "share delivery authorization");
|
|
8429
|
-
const proof = assertObject2(root.proof, ["alg", "kid", "signature"], "share delivery authorization proof");
|
|
8430
|
-
const request = expected.request;
|
|
8431
|
-
if (authorization.type !== "TinyCloudShareDeliveryAuthorization" || authorization.version !== 2 || authorization.dataAuthority !== false || authorization.jti !== request.jti || authorization.shareCid !== request.shareCid || authorization.shareId !== request.shareId || authorization.registrationCid !== request.registrationCid || authorization.delegationCid !== request.delegationCid || authorization.enforcementDelegationCid !== request.enforcementDelegationCid || authorization.envelopeCid !== request.envelopeCid || authorization.policyCid !== request.policyCid || authorization.deliveryEmail !== request.recipientEmail || authorization.shareUrl !== request.shareUrl || authorization.documentName !== request.documentName || authorization.requestBodyDigest !== request.requestBodyDigest || authorization.expiresAt !== request.expiresAt || authorization.idempotencyKey !== request.idempotencyKey || authorization.reportAbuseToken !== request.jti || authorization.authorityMaterialHandle !== request.registrationCid) {
|
|
8432
|
-
throw new Error("share delivery authorization is not bound to the submitted request");
|
|
8433
|
-
}
|
|
8434
|
-
const now = Date.now();
|
|
8435
|
-
const issuedAt = typeof authorization.issuedAt === "string" ? Date.parse(authorization.issuedAt) : Number.NaN;
|
|
8436
|
-
const expiresAt = typeof authorization.expiresAt === "string" ? Date.parse(authorization.expiresAt) : Number.NaN;
|
|
8437
|
-
if (!Number.isFinite(issuedAt) || issuedAt > now + 6e4 || issuedAt > expiresAt) throw new Error("share delivery authorization issuedAt is invalid");
|
|
8438
|
-
if (!Number.isFinite(expiresAt) || expiresAt <= now) throw new Error("share delivery authorization is expired");
|
|
8439
|
-
if (new Date(expiresAt).toISOString() !== authorization.expiresAt) throw new Error("share delivery authorization expiresAt is non-canonical");
|
|
8440
|
-
if (typeof authorization.openCredentialsAudience !== "string" || !isCanonicalHttpsOrigin2(authorization.openCredentialsAudience) || authorization.openCredentialsAudience !== expected.credentialsAudience || authorization.openCredentialsAudience === authorization.nodeAudience || authorization.openCredentialsAudience === authorization.returnOrigin) {
|
|
8441
|
-
throw new Error("share delivery authorization openCredentialsAudience is missing or untrusted");
|
|
8442
|
-
}
|
|
8443
|
-
if (proof.alg !== "EdDSA" || typeof proof.kid !== "string" || typeof proof.signature !== "string") throw new Error("share delivery authorization proof is invalid");
|
|
8444
|
-
if (proof.kid !== expected.nodeProof.kid) throw new Error("share delivery authorization proof key is not trusted");
|
|
8445
|
-
const encodedKid = expected.nodeProof.publicKey;
|
|
8446
|
-
const publicKey = encodedKid.length === 34 && encodedKid[0] === 237 && encodedKid[1] === 1 ? encodedKid.slice(2) : encodedKid;
|
|
8447
|
-
if (publicKey.length !== 32) throw new Error("share delivery authorization proof key is invalid");
|
|
8448
|
-
const signatureBytes = fromB642(proof.signature);
|
|
8449
|
-
const signedBytes = new TextEncoder().encode(`${SHARE_DELIVERY_AUTHORIZATION_DOMAIN}${canonicalize(authorization)}`);
|
|
8450
|
-
if (signatureBytes.length !== 64 || !ed255197.verify(signatureBytes, signedBytes, publicKey)) throw new Error("share delivery authorization proof signature is invalid");
|
|
8451
|
-
return { authorization, proof };
|
|
8452
|
-
}
|
|
8453
|
-
function shareDeliveryTrustedKid(publicKey) {
|
|
8454
|
-
if (publicKey.length !== 32) throw new Error("share delivery node key must be a raw 32-byte Ed25519 public key");
|
|
8455
|
-
const prefixed = new Uint8Array(34);
|
|
8456
|
-
prefixed.set([237, 1]);
|
|
8457
|
-
prefixed.set(publicKey, 2);
|
|
8458
|
-
const did = `did:key:${base58btc2.encode(prefixed)}`;
|
|
8459
|
-
return `${did}#${did.slice("did:key:".length)}`;
|
|
8460
|
-
}
|
|
8461
|
-
|
|
8462
7569
|
// src/history.ts
|
|
8463
7570
|
var MemoryEncryptedShareHistoryStorage = class {
|
|
8464
7571
|
values = [];
|
|
@@ -8508,8 +7615,8 @@ function historyRecordForPublishedShare(result, now = /* @__PURE__ */ new Date()
|
|
|
8508
7615
|
}
|
|
8509
7616
|
var MemorySenderShareRecordStorage = class {
|
|
8510
7617
|
records = /* @__PURE__ */ new Map();
|
|
8511
|
-
async put(
|
|
8512
|
-
this.records.set(
|
|
7618
|
+
async put(record) {
|
|
7619
|
+
this.records.set(record.shareId, record);
|
|
8513
7620
|
}
|
|
8514
7621
|
async list() {
|
|
8515
7622
|
return [...this.records.values()];
|
|
@@ -8546,7 +7653,7 @@ var SenderShareStore = class {
|
|
|
8546
7653
|
async save(receipt, key) {
|
|
8547
7654
|
const registration = receipt.registration;
|
|
8548
7655
|
if (registration.shareKeyDid !== key.did) throw new Error("share key does not match the registered receipt");
|
|
8549
|
-
const
|
|
7656
|
+
const record = {
|
|
8550
7657
|
shareId: registration.shareId,
|
|
8551
7658
|
registrationCid: registration.registrationCid,
|
|
8552
7659
|
policyCid: registration.policyCid,
|
|
@@ -8563,15 +7670,15 @@ var SenderShareStore = class {
|
|
|
8563
7670
|
registeredAt: registration.registeredAt,
|
|
8564
7671
|
expiresAt: registration.expiresAt
|
|
8565
7672
|
};
|
|
8566
|
-
await this.records.put(
|
|
8567
|
-
await this.keys.put(
|
|
8568
|
-
return
|
|
7673
|
+
await this.records.put(record);
|
|
7674
|
+
await this.keys.put(record.shareId, { cryptoKey: key.cryptoKey, publicKey: key.publicKey, extractable: key.extractable });
|
|
7675
|
+
return record;
|
|
8569
7676
|
}
|
|
8570
7677
|
/** List shares created in any session, most recent first. */
|
|
8571
7678
|
async list(options) {
|
|
8572
7679
|
const now = this.now().getTime();
|
|
8573
7680
|
const records = await this.records.list();
|
|
8574
|
-
return records.filter((
|
|
7681
|
+
return records.filter((record) => (options?.includeRevoked ?? false) || record.revokedAt === void 0).filter((record) => (options?.includeExpired ?? false) || Date.parse(record.expiresAt) > now).sort((left, right) => Date.parse(right.registeredAt) - Date.parse(left.registeredAt));
|
|
8575
7682
|
}
|
|
8576
7683
|
async get(shareId) {
|
|
8577
7684
|
return this.records.get(shareId);
|
|
@@ -8594,17 +7701,17 @@ var SenderShareStore = class {
|
|
|
8594
7701
|
* returned CID, then call {@link markRevoked} once that succeeds.
|
|
8595
7702
|
*/
|
|
8596
7703
|
async revocationTarget(shareId, scope) {
|
|
8597
|
-
const
|
|
8598
|
-
if (
|
|
8599
|
-
const delegationCid = scope === "direct" ?
|
|
7704
|
+
const record = await this.records.get(shareId);
|
|
7705
|
+
if (record === void 0) throw new Error(`Unknown share: ${shareId}`);
|
|
7706
|
+
const delegationCid = scope === "direct" ? record.enforcementDelegationCid : record.ownerDelegationCid;
|
|
8600
7707
|
if (delegationCid === void 0) throw new Error("share has no node-enforced delegation");
|
|
8601
7708
|
return delegationCid;
|
|
8602
7709
|
}
|
|
8603
7710
|
/** Mark a share revoked locally after the caller's node-side revoke call succeeds. */
|
|
8604
7711
|
async markRevoked(shareId, revokedAt = this.now().toISOString()) {
|
|
8605
|
-
const
|
|
8606
|
-
if (
|
|
8607
|
-
const revoked = { ...
|
|
7712
|
+
const record = await this.records.get(shareId);
|
|
7713
|
+
if (record === void 0) throw new Error(`Unknown share: ${shareId}`);
|
|
7714
|
+
const revoked = { ...record, revokedAt };
|
|
8608
7715
|
await this.records.put(revoked);
|
|
8609
7716
|
return revoked;
|
|
8610
7717
|
}
|
|
@@ -8620,9 +7727,9 @@ var EncryptedSenderShareHistory = class {
|
|
|
8620
7727
|
this.key = key;
|
|
8621
7728
|
this.now = now;
|
|
8622
7729
|
}
|
|
8623
|
-
async put(
|
|
7730
|
+
async put(record) {
|
|
8624
7731
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
8625
|
-
const payload = new TextEncoder().encode(JSON.stringify(
|
|
7732
|
+
const payload = new TextEncoder().encode(JSON.stringify(record));
|
|
8626
7733
|
const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv }, this.key, payload));
|
|
8627
7734
|
const encoded = new Uint8Array(iv.length + ciphertext.length);
|
|
8628
7735
|
encoded.set(iv);
|
|
@@ -8655,8 +7762,8 @@ var ShareNotifyError = class extends Error {
|
|
|
8655
7762
|
this.name = "ShareNotifyError";
|
|
8656
7763
|
}
|
|
8657
7764
|
};
|
|
8658
|
-
function recipientMatchesShareRecord(
|
|
8659
|
-
const matcher =
|
|
7765
|
+
function recipientMatchesShareRecord(record, recipient) {
|
|
7766
|
+
const matcher = record.recipientMatcher;
|
|
8660
7767
|
if (matcher.kind === "bearer" || matcher.kind === "recipientDid") return false;
|
|
8661
7768
|
if (matcher.kind === "exactEmail") return matcher.value === recipient;
|
|
8662
7769
|
const at = recipient.lastIndexOf("@");
|
|
@@ -8694,60 +7801,180 @@ async function defaultIdempotencyKey(shareId, recipient) {
|
|
|
8694
7801
|
return `tinycloud-share:${shareId}:${toBase64Url(digest3)}`;
|
|
8695
7802
|
}
|
|
8696
7803
|
|
|
7804
|
+
// src/credential-invitation.ts
|
|
7805
|
+
var CredentialInvitationError = class extends Error {
|
|
7806
|
+
constructor(code3, message) {
|
|
7807
|
+
super(message);
|
|
7808
|
+
this.code = code3;
|
|
7809
|
+
this.name = "CredentialInvitationError";
|
|
7810
|
+
}
|
|
7811
|
+
};
|
|
7812
|
+
function canonicalOrigin(value) {
|
|
7813
|
+
let url;
|
|
7814
|
+
try {
|
|
7815
|
+
url = new URL(value);
|
|
7816
|
+
} catch {
|
|
7817
|
+
throw new CredentialInvitationError(
|
|
7818
|
+
"invalid-origin",
|
|
7819
|
+
"credential invitation origin is invalid"
|
|
7820
|
+
);
|
|
7821
|
+
}
|
|
7822
|
+
if (url.origin !== value || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "" || url.protocol !== "https:")
|
|
7823
|
+
throw new CredentialInvitationError(
|
|
7824
|
+
"invalid-origin",
|
|
7825
|
+
"credential invitation origin is invalid"
|
|
7826
|
+
);
|
|
7827
|
+
return url.origin;
|
|
7828
|
+
}
|
|
7829
|
+
function object2(value) {
|
|
7830
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7831
|
+
}
|
|
7832
|
+
async function validateReceipt(receipt, shareUrl) {
|
|
7833
|
+
if (!object2(receipt) || !object2(receipt.request) || !object2(receipt.admission) || !object2(receipt.proof)) {
|
|
7834
|
+
throw new CredentialInvitationError(
|
|
7835
|
+
"invalid-receipt",
|
|
7836
|
+
"credential invitation receipt is invalid"
|
|
7837
|
+
);
|
|
7838
|
+
}
|
|
7839
|
+
if (receipt.request.returnLink !== shareUrl) {
|
|
7840
|
+
throw new CredentialInvitationError(
|
|
7841
|
+
"invalid-receipt",
|
|
7842
|
+
"credential invitation is not bound to the share link"
|
|
7843
|
+
);
|
|
7844
|
+
}
|
|
7845
|
+
let link;
|
|
7846
|
+
try {
|
|
7847
|
+
link = new URL(shareUrl);
|
|
7848
|
+
} catch {
|
|
7849
|
+
throw new CredentialInvitationError(
|
|
7850
|
+
"invalid-receipt",
|
|
7851
|
+
"credential invitation share link is invalid"
|
|
7852
|
+
);
|
|
7853
|
+
}
|
|
7854
|
+
if (link.search !== "" || link.pathname !== "/s/inline") {
|
|
7855
|
+
throw new CredentialInvitationError(
|
|
7856
|
+
"invalid-receipt",
|
|
7857
|
+
"credential invitation share link must be sealed inline"
|
|
7858
|
+
);
|
|
7859
|
+
}
|
|
7860
|
+
try {
|
|
7861
|
+
await parseSealedInlineShareUrl(shareUrl);
|
|
7862
|
+
} catch {
|
|
7863
|
+
throw new CredentialInvitationError(
|
|
7864
|
+
"invalid-receipt",
|
|
7865
|
+
"credential invitation share link is invalid"
|
|
7866
|
+
);
|
|
7867
|
+
}
|
|
7868
|
+
}
|
|
7869
|
+
async function deliverCredentialInvitation(input) {
|
|
7870
|
+
const credentialsOrigin = canonicalOrigin(input.credentialsOrigin);
|
|
7871
|
+
await validateReceipt(input.receipt, input.shareUrl);
|
|
7872
|
+
const fetchFn = input.fetchFn ?? globalThis.fetch;
|
|
7873
|
+
let response;
|
|
7874
|
+
try {
|
|
7875
|
+
response = await fetchFn(`${credentialsOrigin}/v1/credential-invitations`, {
|
|
7876
|
+
method: "POST",
|
|
7877
|
+
credentials: "omit",
|
|
7878
|
+
redirect: "error",
|
|
7879
|
+
referrerPolicy: "no-referrer",
|
|
7880
|
+
headers: {
|
|
7881
|
+
accept: "application/json",
|
|
7882
|
+
"content-type": "application/json"
|
|
7883
|
+
},
|
|
7884
|
+
body: JSON.stringify(input.receipt),
|
|
7885
|
+
...input.signal === void 0 ? {} : { signal: input.signal }
|
|
7886
|
+
});
|
|
7887
|
+
} catch {
|
|
7888
|
+
throw new CredentialInvitationError(
|
|
7889
|
+
"transport",
|
|
7890
|
+
"credential invitation delivery is unavailable"
|
|
7891
|
+
);
|
|
7892
|
+
}
|
|
7893
|
+
if (response.status !== 202) {
|
|
7894
|
+
throw new CredentialInvitationError(
|
|
7895
|
+
"rejected",
|
|
7896
|
+
"credential invitation delivery was not accepted"
|
|
7897
|
+
);
|
|
7898
|
+
}
|
|
7899
|
+
let body;
|
|
7900
|
+
try {
|
|
7901
|
+
body = await response.json();
|
|
7902
|
+
} catch {
|
|
7903
|
+
throw new CredentialInvitationError(
|
|
7904
|
+
"invalid-response",
|
|
7905
|
+
"credential invitation response is invalid"
|
|
7906
|
+
);
|
|
7907
|
+
}
|
|
7908
|
+
if (!object2(body) || Object.keys(body).length !== 1 || body.status !== "accepted") {
|
|
7909
|
+
throw new CredentialInvitationError(
|
|
7910
|
+
"invalid-response",
|
|
7911
|
+
"credential invitation response is invalid"
|
|
7912
|
+
);
|
|
7913
|
+
}
|
|
7914
|
+
return { status: "accepted" };
|
|
7915
|
+
}
|
|
7916
|
+
|
|
8697
7917
|
// src/lifecycle.ts
|
|
8698
|
-
function targetKind2(
|
|
8699
|
-
if (
|
|
8700
|
-
return
|
|
7918
|
+
function targetKind2(record) {
|
|
7919
|
+
if (record.targetKind !== void 0) return record.targetKind;
|
|
7920
|
+
return record.recipientMatcher.kind === "exactEmail" ? "email" : record.recipientMatcher.kind === "emailDomain" ? "emailDomain" : record.recipientMatcher.kind === "recipientDid" ? "recipientDid" : "bearer";
|
|
7921
|
+
}
|
|
7922
|
+
function policyNodeAudience(record) {
|
|
7923
|
+
const envelope = record.deliveryMaterial?.envelope;
|
|
7924
|
+
if (typeof envelope !== "object" || envelope === null || Array.isArray(envelope)) return record.target.nodeAudience;
|
|
7925
|
+
const binding = envelope.attestedEnforcerBinding;
|
|
7926
|
+
if (typeof binding !== "object" || binding === null || Array.isArray(binding)) return record.target.nodeAudience;
|
|
7927
|
+
const nodeAudience = binding.nodeAudience;
|
|
7928
|
+
return typeof nodeAudience === "string" ? nodeAudience : record.target.nodeAudience;
|
|
8701
7929
|
}
|
|
8702
7930
|
async function revokeShare(input) {
|
|
8703
7931
|
const target = targetKind2(input.record);
|
|
8704
|
-
if (target === "bearer") return { state: "retention-only", target, reason: "bearer-capability-cannot-be-revoked" };
|
|
8705
7932
|
if (input.adapter === void 0) return { state: "unsupported", target, reason: "node revocation authority is required", code: "unsupported-target" };
|
|
8706
7933
|
const scope = input.scope ?? "direct";
|
|
8707
7934
|
const delegationCid = scope === "ancestor" ? input.record.ownerDelegationCid : input.record.enforcementDelegationCid;
|
|
8708
7935
|
if (delegationCid === void 0) return { state: "unsupported", target, reason: "share has no node-enforced delegation receipt", code: "unsupported-target" };
|
|
8709
|
-
|
|
7936
|
+
if (target === "bearer") {
|
|
7937
|
+
if (input.adapter.revokeDelegation === void 0) return { state: "unsupported", target, reason: "native delegation revocation authority is required", code: "unsupported-target" };
|
|
7938
|
+
await input.adapter.revokeDelegation({ delegationCid, scope });
|
|
7939
|
+
} else {
|
|
7940
|
+
if (input.record.ownerDid === void 0) return { state: "unsupported", target, reason: "share has no Policy/v3 owner receipt", code: "unsupported-target" };
|
|
7941
|
+
if (input.adapter.revokePolicyRoot === void 0) return { state: "unsupported", target, reason: "Policy/v3 root revocation authority is required", code: "unsupported-target" };
|
|
7942
|
+
await input.adapter.revokePolicyRoot({
|
|
7943
|
+
rootCid: delegationCid,
|
|
7944
|
+
targetRole: scope === "ancestor" ? "policy-authority" : "policy-enforcement",
|
|
7945
|
+
ownerDid: input.record.ownerDid,
|
|
7946
|
+
nodeOrigin: input.record.target.origin,
|
|
7947
|
+
nodeAudience: policyNodeAudience(input.record)
|
|
7948
|
+
});
|
|
7949
|
+
}
|
|
8710
7950
|
const revokedAt = (input.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
8711
7951
|
if (input.records !== void 0) await input.records.put({ ...input.record, revokedAt });
|
|
8712
7952
|
return { state: "revoked", target, delegationCid, revokedAt };
|
|
8713
7953
|
}
|
|
8714
|
-
function redactRecord(
|
|
8715
|
-
const matcher =
|
|
8716
|
-
const revealedLink = revealLink ? link ??
|
|
7954
|
+
function redactRecord(record, revealLink, link) {
|
|
7955
|
+
const matcher = record.recipientMatcher;
|
|
7956
|
+
const revealedLink = revealLink ? link ?? record.link : void 0;
|
|
8717
7957
|
return {
|
|
8718
|
-
shareId:
|
|
7958
|
+
shareId: record.shareId,
|
|
8719
7959
|
target: matcher.kind === "exactEmail" ? "email" : matcher.kind === "emailDomain" ? "email-domain" : matcher.kind === "recipientDid" ? "recipient-did" : "bearer",
|
|
8720
7960
|
...matcher.kind === "exactEmail" ? { recipient: matcher.value } : matcher.kind === "emailDomain" ? { recipient: `*@${matcher.value}` } : matcher.kind === "recipientDid" ? { recipient: matcher.value } : {},
|
|
8721
|
-
expiresAt:
|
|
8722
|
-
revoked:
|
|
7961
|
+
expiresAt: record.expiresAt,
|
|
7962
|
+
revoked: record.revokedAt !== void 0,
|
|
8723
7963
|
...revealedLink === void 0 ? {} : { link: revealedLink }
|
|
8724
7964
|
};
|
|
8725
7965
|
}
|
|
8726
7966
|
async function listShares(storage) {
|
|
8727
7967
|
const records = await storage.list();
|
|
8728
|
-
return records.map((
|
|
7968
|
+
return records.map((record) => redactRecord(record, false));
|
|
8729
7969
|
}
|
|
8730
7970
|
async function showShare(input) {
|
|
8731
|
-
const
|
|
8732
|
-
if (
|
|
8733
|
-
return redactRecord(
|
|
8734
|
-
}
|
|
8735
|
-
|
|
8736
|
-
// src/legacy.ts
|
|
8737
|
-
function isLegacyShareLink(value) {
|
|
8738
|
-
return value.startsWith("tc1:");
|
|
8739
|
-
}
|
|
8740
|
-
async function receiveLegacyShare(link, reader) {
|
|
8741
|
-
if (!isLegacyShareLink(link)) throw new Error("legacy link must use tc1:");
|
|
8742
|
-
return reader.read(link);
|
|
8743
|
-
}
|
|
8744
|
-
async function migrateShare(input) {
|
|
8745
|
-
const value = await receiveLegacyShare(input.link, input.reader);
|
|
8746
|
-
return { protocol: "tinycloud-share", version: 1, legacy: true, value, migrated: await input.publish(value) };
|
|
7971
|
+
const record = await input.storage.get(input.shareId);
|
|
7972
|
+
if (record === void 0) throw new Error("share not found");
|
|
7973
|
+
return redactRecord(record, input.revealLink === true, input.link);
|
|
8747
7974
|
}
|
|
8748
7975
|
|
|
8749
7976
|
// src/recipient.ts
|
|
8750
|
-
import { ed25519 as
|
|
7977
|
+
import { ed25519 as ed255195, x25519 } from "@noble/curves/ed25519";
|
|
8751
7978
|
import { sha256 as sha2566 } from "@noble/hashes/sha256";
|
|
8752
7979
|
|
|
8753
7980
|
// src/protocol.ts
|
|
@@ -8793,7 +8020,7 @@ var DOMAIN = SHARE_V2_PROTOCOL.challengeDomain;
|
|
|
8793
8020
|
var PRESENTATION_DOMAIN = SHARE_V2_PROTOCOL.sessionDomain;
|
|
8794
8021
|
var SESSION_DOMAIN = SHARE_V2_PROTOCOL.sessionDomain;
|
|
8795
8022
|
var INVOCATION_DOMAIN = SHARE_V2_PROTOCOL.invocationDomain;
|
|
8796
|
-
function
|
|
8023
|
+
function object3(value, label) {
|
|
8797
8024
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${label} is invalid`);
|
|
8798
8025
|
return value;
|
|
8799
8026
|
}
|
|
@@ -8852,11 +8079,11 @@ function nativeAction(action) {
|
|
|
8852
8079
|
return action === "list" ? "tinycloud.kv/list" : action === "edit" ? "tinycloud.kv/put" : "tinycloud.kv/get";
|
|
8853
8080
|
}
|
|
8854
8081
|
function policyAttenuationForV3(envelope) {
|
|
8855
|
-
const policy =
|
|
8082
|
+
const policy = object3(envelope.policy, "v3 policy");
|
|
8856
8083
|
if (!Array.isArray(policy.capabilityCeiling)) throw new Error("v3 policy capability ceiling is invalid");
|
|
8857
8084
|
const attenuation = {};
|
|
8858
8085
|
for (const raw of policy.capabilityCeiling) {
|
|
8859
|
-
const capability =
|
|
8086
|
+
const capability = object3(raw, "v3 policy capability");
|
|
8860
8087
|
if (typeof capability.resource !== "string" || attenuation[capability.resource] !== void 0) throw new Error("v3 policy capability is invalid");
|
|
8861
8088
|
if (capability.kind === "encryption") {
|
|
8862
8089
|
if (capability.action !== "tinycloud.encryption/decrypt") throw new Error("v3 policy capability is invalid");
|
|
@@ -8914,8 +8141,8 @@ function verifyV3PolicyAuthorization(input) {
|
|
|
8914
8141
|
const factKeys = Object.keys(fact);
|
|
8915
8142
|
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);
|
|
8916
8143
|
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);
|
|
8917
|
-
const binding =
|
|
8918
|
-
const policy =
|
|
8144
|
+
const binding = object3(input.envelope.attestedEnforcerBinding, "v3 attested enforcer binding");
|
|
8145
|
+
const policy = object3(input.envelope.policy, "v3 policy");
|
|
8919
8146
|
const now = Math.floor(Date.now() / 1e3);
|
|
8920
8147
|
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");
|
|
8921
8148
|
return compact;
|
|
@@ -8923,6 +8150,15 @@ function verifyV3PolicyAuthorization(input) {
|
|
|
8923
8150
|
function hex3(value) {
|
|
8924
8151
|
return [...value].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
8925
8152
|
}
|
|
8153
|
+
function matchesInitialCiphertextDigest(bytes3, expected) {
|
|
8154
|
+
if (typeof expected !== "string" || !LOWER_SHA256_HEX.test(expected)) return false;
|
|
8155
|
+
const actual = sha2566(bytes3);
|
|
8156
|
+
let mismatch = 0;
|
|
8157
|
+
for (let index = 0; index < actual.length; index += 1) {
|
|
8158
|
+
mismatch |= actual[index] ^ Number.parseInt(expected.slice(index * 2, index * 2 + 2), 16);
|
|
8159
|
+
}
|
|
8160
|
+
return mismatch === 0;
|
|
8161
|
+
}
|
|
8926
8162
|
function canonicalHashHex(value) {
|
|
8927
8163
|
return hex3(sha2566(new TextEncoder().encode(canonicalize(value))));
|
|
8928
8164
|
}
|
|
@@ -8949,12 +8185,12 @@ function parseV3InlineEncryptedEnvelope(bytes3, expected) {
|
|
|
8949
8185
|
throw new Error("encrypted content envelope is malformed");
|
|
8950
8186
|
}
|
|
8951
8187
|
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("encrypted content envelope is malformed");
|
|
8952
|
-
const
|
|
8188
|
+
const record = value;
|
|
8953
8189
|
const allowed = ["v", "networkId", "alg", "keyVersion", "encryptedSymmetricKey", "encryptedSymmetricKeyHash", "ciphertext", "metadata"];
|
|
8954
|
-
if (Object.keys(
|
|
8190
|
+
if (Object.keys(record).some((key) => !allowed.includes(key)) || record.v !== 1 || record.networkId !== expected.encryptionNetwork || record.alg !== "x25519-aes256gcm/v1" || record.keyVersion !== expected.contentSource.keyVersion || typeof record.encryptedSymmetricKey !== "string" || typeof record.encryptedSymmetricKeyHash !== "string" || record.encryptedSymmetricKeyHash !== expected.contentSource.encryptedSymmetricKeyDigestHex || record.encryptedSymmetricKeyHash !== canonicalHashHex(record.encryptedSymmetricKey) || typeof record.ciphertext !== "string" || record.metadata !== void 0 && (record.metadata === null || typeof record.metadata !== "object" || Array.isArray(record.metadata) || Object.values(record.metadata).some((entry) => typeof entry !== "string"))) {
|
|
8955
8191
|
throw new Error("encrypted content envelope binding is invalid");
|
|
8956
8192
|
}
|
|
8957
|
-
return
|
|
8193
|
+
return record;
|
|
8958
8194
|
}
|
|
8959
8195
|
var ShareRecipientClient = class {
|
|
8960
8196
|
constructor(options) {
|
|
@@ -8997,7 +8233,7 @@ var ShareRecipientClient = class {
|
|
|
8997
8233
|
if (this.session === void 0) await this.establish(envelope);
|
|
8998
8234
|
const response = await this.nativeInvoke({ action: "get", resource: envelope.resource });
|
|
8999
8235
|
if (!response.ok) throw new Error("share recipient read was rejected");
|
|
9000
|
-
const value =
|
|
8236
|
+
const value = object3(await response.json(), "share read response");
|
|
9001
8237
|
const expectedAction = nativeAction("read");
|
|
9002
8238
|
if (value.type !== "TinyCloudShareInvokeResponse" || value.version !== 2 || value.action !== expectedAction || value.resource !== envelope.resource.path || typeof value.content !== "string" || typeof value.bodyDigest !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(value.bodyDigest) || !Object.hasOwn(value, "proof")) throw new Error("share read response is invalid");
|
|
9003
8239
|
const content = fromBase64Url(value.content);
|
|
@@ -9012,7 +8248,7 @@ var ShareRecipientClient = class {
|
|
|
9012
8248
|
}
|
|
9013
8249
|
async establishV3(envelope) {
|
|
9014
8250
|
if (this.options.buildPresentation === void 0) throw new Error("share presentation builder is required");
|
|
9015
|
-
const attestedEnforcer =
|
|
8251
|
+
const attestedEnforcer = object3(envelope.attestedEnforcerBinding, "v3 attested enforcer binding");
|
|
9016
8252
|
if (attestedEnforcer.enforcerDid !== envelope.target.nodeAudience || typeof attestedEnforcer.nodeAudience !== "string") throw new Error("v3 attested enforcer binding mismatch");
|
|
9017
8253
|
try {
|
|
9018
8254
|
ed25519PublicKeyFromDidKey(attestedEnforcer.nodeAudience);
|
|
@@ -9027,7 +8263,7 @@ var ShareRecipientClient = class {
|
|
|
9027
8263
|
...this.options.signal === void 0 ? {} : { signal: this.options.signal }
|
|
9028
8264
|
});
|
|
9029
8265
|
if (!challengeResponse.ok) throw new Error(`v3 policy challenge rejected (${challengeResponse.status})`);
|
|
9030
|
-
const challenge =
|
|
8266
|
+
const challenge = object3(await challengeResponse.json(), "v3 policy challenge");
|
|
9031
8267
|
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");
|
|
9032
8268
|
const material = await this.options.buildPresentation({
|
|
9033
8269
|
challenge,
|
|
@@ -9048,7 +8284,7 @@ var ShareRecipientClient = class {
|
|
|
9048
8284
|
...this.options.signal === void 0 ? {} : { signal: this.options.signal }
|
|
9049
8285
|
});
|
|
9050
8286
|
if (!delegationResponse.ok) throw new Error(`v3 policy delegation rejected (${delegationResponse.status})`);
|
|
9051
|
-
const delegation =
|
|
8287
|
+
const delegation = object3(await delegationResponse.json(), "v3 policy delegation");
|
|
9052
8288
|
if (delegation.admitted !== true || typeof delegation.sessionCid !== "string" || typeof delegation.authorization !== "string") throw new Error("v3 policy delegation response is not admitted");
|
|
9053
8289
|
const compact = verifyV3PolicyAuthorization({ authorization: delegation.authorization, cid: delegation.sessionCid, envelope, holderDid: this.options.holderDid });
|
|
9054
8290
|
const fact = compact.payload.fct[0];
|
|
@@ -9066,6 +8302,7 @@ var ShareRecipientClient = class {
|
|
|
9066
8302
|
const envelope = this.options.envelope;
|
|
9067
8303
|
const signer = this.nativeSigner ?? this.signer;
|
|
9068
8304
|
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");
|
|
8305
|
+
if (!matchesInitialCiphertextDigest(bytes3, envelope.contentSource.initialCiphertextDigestHex)) throw new Error("v3 owner KV ciphertext digest mismatch");
|
|
9069
8306
|
const encrypted = parseV3InlineEncryptedEnvelope(bytes3, envelope);
|
|
9070
8307
|
const receiverPrivateKey = crypto.getRandomValues(new Uint8Array(32));
|
|
9071
8308
|
const receiverPublicKey = toBase64(x25519.getPublicKey(receiverPrivateKey));
|
|
@@ -9089,13 +8326,13 @@ var ShareRecipientClient = class {
|
|
|
9089
8326
|
this.options.onStage?.("decryption");
|
|
9090
8327
|
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 } });
|
|
9091
8328
|
if (!response.ok) throw new Error(`v3 decrypt invocation rejected (${response.status})`);
|
|
9092
|
-
const value =
|
|
8329
|
+
const value = object3(await response.json(), "v3 decrypt response");
|
|
9093
8330
|
const allowed = ["type", "targetNode", "networkId", "invocationCid", "encryptedSymmetricKeyHash", "receiverPublicKeyHash", "wrappedKey", "alg", "keyVersion", "requestHash", "nodeId", "nodeSignature"];
|
|
9094
8331
|
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 !== hex3(sha2566(new TextEncoder().encode(`${invocation.cid}${bodyHash}`))) || typeof value.wrappedKey !== "string" || typeof value.nodeSignature !== "string") throw new Error("v3 decrypt response binding is invalid");
|
|
9095
8332
|
const unsigned = { ...value };
|
|
9096
8333
|
delete unsigned.nodeSignature;
|
|
9097
8334
|
const signature = fromBase64(value.nodeSignature, "v3 decrypt response signature");
|
|
9098
|
-
if (signature.length !== 64 || !
|
|
8335
|
+
if (signature.length !== 64 || !ed255195.verify(signature, new TextEncoder().encode(canonicalize(unsigned)), ed25519PublicKeyFromDidKey(body.targetNode), { zip215: false })) throw new Error("v3 decrypt response signature is invalid");
|
|
9099
8336
|
const wrapped = fromBase64(value.wrappedKey, "v3 wrapped content key");
|
|
9100
8337
|
if (wrapped.length < 61 || wrapped[32] !== 1) throw new Error("v3 wrapped content key is malformed");
|
|
9101
8338
|
const shared = x25519.getSharedSecret(receiverPrivateKey, wrapped.slice(0, 32));
|
|
@@ -9177,15 +8414,15 @@ function createAddressedAuthorization(input) {
|
|
|
9177
8414
|
},
|
|
9178
8415
|
async verifyResult({ envelope, value, proof }) {
|
|
9179
8416
|
try {
|
|
9180
|
-
const wrapper =
|
|
9181
|
-
const response =
|
|
8417
|
+
const wrapper = object3(proof, "share read proof");
|
|
8418
|
+
const response = object3(wrapper.response, "share read response proof");
|
|
9182
8419
|
const envelopeAction = nativeAction("read");
|
|
9183
8420
|
if (response.type !== "TinyCloudShareInvokeResponse" || response.version !== 2 || response.action !== envelopeAction || response.resource !== envelope.resource.path || typeof response.bodyDigest !== "string" || response.bodyDigest !== value.bodyDigest || response.bodyDigest !== await digestBytes2(value.bytes)) return false;
|
|
9184
|
-
const detached =
|
|
9185
|
-
if (detached.alg !== "EdDSA" || detached.kid !== input.trustedNode.invitationKid) return false;
|
|
8421
|
+
const detached = object3(wrapper.detached, "share read detached proof");
|
|
8422
|
+
if (input.trustedNode === void 0 || detached.alg !== "EdDSA" || detached.kid !== input.trustedNode.invitationKid) return false;
|
|
9186
8423
|
const unsigned = { ...response };
|
|
9187
8424
|
delete unsigned.proof;
|
|
9188
|
-
return
|
|
8425
|
+
return ed255195.verify(bytes2(detached.signature, "share read signature"), new TextEncoder().encode(`${SHARE_V2_PROTOCOL.readResponseDomain}${canonicalize(unsigned)}`), trustedPublicKey(input.trustedNode));
|
|
9189
8426
|
} catch {
|
|
9190
8427
|
return false;
|
|
9191
8428
|
}
|
|
@@ -9217,7 +8454,39 @@ async function claimShare(input) {
|
|
|
9217
8454
|
async function resumeShareAuthorization(input) {
|
|
9218
8455
|
return authorizeShare(input);
|
|
9219
8456
|
}
|
|
8457
|
+
|
|
8458
|
+
// src/native.ts
|
|
8459
|
+
var NATIVE_SHARE_FRAGMENT_PARAMETER = "tc1";
|
|
8460
|
+
function canonicalViewerUrl(viewerOrigin) {
|
|
8461
|
+
const url = new URL(viewerOrigin);
|
|
8462
|
+
if (url.protocol !== "https:" || url.origin !== viewerOrigin || url.pathname !== "/" || url.search || url.hash) throw new TypeError("viewer origin must be a canonical HTTPS origin");
|
|
8463
|
+
url.pathname = "/viewer";
|
|
8464
|
+
return url;
|
|
8465
|
+
}
|
|
8466
|
+
async function createNativeShare(sharing, input) {
|
|
8467
|
+
if (!input.path || input.path.startsWith("/") || input.path.endsWith("/") || input.path.includes("..") || input.path.includes("//")) throw new TypeError("native share path must be one canonical TinyCloud KV key");
|
|
8468
|
+
const generated = await sharing.generate({ path: input.path, actions: ["tinycloud.kv/get"], expiry: input.expiresAt });
|
|
8469
|
+
if (generated.ok !== true) throw new Error(typeof generated.error?.message === "string" ? generated.error.message : "TinyCloud sharing service rejected delegation generation");
|
|
8470
|
+
if (typeof generated.data?.token !== "string" || typeof generated.data.delegation?.cid !== "string" || !(generated.data.expiresAt instanceof Date)) throw new Error("TinyCloud sharing service returned incomplete delegation metadata");
|
|
8471
|
+
const delegation = sharing.decodeLink(generated.data.token);
|
|
8472
|
+
if (typeof delegation.spaceId !== "string" || delegation.spaceId.length === 0 || typeof delegation.path !== "string" || delegation.path.length === 0) throw new Error("TinyCloud sharing service returned a non-canonical delegation authority");
|
|
8473
|
+
const url = canonicalViewerUrl(input.viewerOrigin);
|
|
8474
|
+
url.hash = `${NATIVE_SHARE_FRAGMENT_PARAMETER}=${encodeURIComponent(generated.data.token)}`;
|
|
8475
|
+
return { url: url.toString(), delegationCid: generated.data.delegation.cid, expiresAt: generated.data.expiresAt, spaceId: delegation.spaceId };
|
|
8476
|
+
}
|
|
8477
|
+
function parseNativeShareUrl(value) {
|
|
8478
|
+
const url = new URL(value);
|
|
8479
|
+
if (url.search || url.pathname !== "/viewer") throw new TypeError("native shares must carry tc1 only in the /viewer URL fragment");
|
|
8480
|
+
const fragment = new URLSearchParams(url.hash.slice(1));
|
|
8481
|
+
const token = fragment.get(NATIVE_SHARE_FRAGMENT_PARAMETER);
|
|
8482
|
+
if (!token || fragment.size !== 1) throw new TypeError("missing native share fragment");
|
|
8483
|
+
return token;
|
|
8484
|
+
}
|
|
8485
|
+
async function openNativeShare(sharing, link) {
|
|
8486
|
+
return sharing.receive(parseNativeShareUrl(link), { autoSubdelegate: false, useSessionKey: false });
|
|
8487
|
+
}
|
|
9220
8488
|
export {
|
|
8489
|
+
CredentialInvitationError,
|
|
9221
8490
|
DEFAULT_MAX_CONTENT_BLOB_BYTES,
|
|
9222
8491
|
DEFAULT_MAX_SEALED_BLOB_BYTES,
|
|
9223
8492
|
DEFAULT_SHARE_LIFETIME_MS,
|
|
@@ -9227,12 +8496,11 @@ export {
|
|
|
9227
8496
|
MemorySenderShareKeyStorage,
|
|
9228
8497
|
MemorySenderShareRecordStorage,
|
|
9229
8498
|
MemoryShareCache,
|
|
8499
|
+
NATIVE_SHARE_FRAGMENT_PARAMETER,
|
|
9230
8500
|
OWNER_SHARE_REGISTRATION_DOMAIN,
|
|
9231
8501
|
SHARE_CONTENT_LIMIT,
|
|
9232
|
-
SHARE_DELIVERY_AUTHORIZATION_DOMAIN,
|
|
9233
8502
|
SHARE_PUBLISH_RESULT_VERSION,
|
|
9234
8503
|
SHARE_RESULT_VERSION,
|
|
9235
|
-
SHARE_SEALED_OVERHEAD,
|
|
9236
8504
|
SHARE_V2_PROTOCOL,
|
|
9237
8505
|
SenderShareStore,
|
|
9238
8506
|
ShareNotifyError,
|
|
@@ -9248,23 +8516,22 @@ export {
|
|
|
9248
8516
|
computeOwnerShareRegistrationCid,
|
|
9249
8517
|
createAddressedAuthorization,
|
|
9250
8518
|
createDelegatedShareKey,
|
|
8519
|
+
createNativeShare,
|
|
9251
8520
|
createPolicyEnforcementDelegation,
|
|
9252
|
-
createRegisteredPolicyAuthority,
|
|
9253
8521
|
createShareV2HolderBindingArtifact,
|
|
8522
|
+
deliverCredentialInvitation,
|
|
9254
8523
|
fromBase64Url,
|
|
9255
8524
|
historyRecordForPublishedShare,
|
|
9256
8525
|
inspectShare,
|
|
9257
|
-
isLegacyShareLink,
|
|
9258
8526
|
listShares,
|
|
9259
|
-
migrateShare,
|
|
9260
8527
|
normalizeShareTarget,
|
|
9261
8528
|
notifyShare,
|
|
8529
|
+
openNativeShare,
|
|
9262
8530
|
parseAddressedEnvelope,
|
|
8531
|
+
parseNativeShareUrl,
|
|
9263
8532
|
publishAddressedShare,
|
|
9264
8533
|
publishPolicyShare,
|
|
9265
|
-
publishShare,
|
|
9266
8534
|
publishTargetShare,
|
|
9267
|
-
receiveLegacyShare,
|
|
9268
8535
|
receiveShare,
|
|
9269
8536
|
recipientMatchesShareRecord,
|
|
9270
8537
|
redactPublishedShare,
|
|
@@ -9273,14 +8540,11 @@ export {
|
|
|
9273
8540
|
restoreDelegatedShareKey,
|
|
9274
8541
|
resumeShareAuthorization,
|
|
9275
8542
|
revokeShare,
|
|
9276
|
-
shareDeliveryTrustedKid,
|
|
9277
8543
|
showShare,
|
|
9278
8544
|
targetAuthorizationMethod,
|
|
9279
8545
|
toBase64Url,
|
|
9280
8546
|
toShareErrorInfo,
|
|
9281
8547
|
validateOwnerSharePolicyRegistration,
|
|
9282
|
-
validateOwnerSharePolicyRegistrationBytes
|
|
9283
|
-
validateShareDeliveryAuthorizationBytes,
|
|
9284
|
-
verifyBearerEnvelope
|
|
8548
|
+
validateOwnerSharePolicyRegistrationBytes
|
|
9285
8549
|
};
|
|
9286
8550
|
//# sourceMappingURL=index.js.map
|