@tinycloud/share-sdk 1.0.0-beta.3 → 1.0.0-beta.5
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 +267 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +45 -7
- package/dist/index.d.ts +45 -7
- package/dist/index.js +265 -64
- 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
|
}
|
|
@@ -5555,7 +5555,74 @@ function checkBearerDelegation(envelope, options = {}) {
|
|
|
5555
5555
|
return { ok: true, delegateeDid: audience };
|
|
5556
5556
|
}
|
|
5557
5557
|
var ENVELOPE_AAD_LABEL = "tinycloud-share-envelope-v1";
|
|
5558
|
+
var SEALED_BLOB_VERSION = 1;
|
|
5558
5559
|
var AAD = utf8Bytes(ENVELOPE_AAD_LABEL);
|
|
5560
|
+
var KEY_LENGTH = 32;
|
|
5561
|
+
var NONCE_LENGTH = 12;
|
|
5562
|
+
var TAG_LENGTH = 16;
|
|
5563
|
+
var HEADER_LENGTH = 1;
|
|
5564
|
+
function assertKey(key32) {
|
|
5565
|
+
if (key32.length !== KEY_LENGTH) {
|
|
5566
|
+
throw new TypeError(`key must be ${KEY_LENGTH} bytes, got ${key32.length}`);
|
|
5567
|
+
}
|
|
5568
|
+
}
|
|
5569
|
+
async function importAesKey(key32, usage) {
|
|
5570
|
+
assertKey(key32);
|
|
5571
|
+
return globalThis.crypto.subtle.importKey(
|
|
5572
|
+
"raw",
|
|
5573
|
+
key32,
|
|
5574
|
+
"AES-GCM",
|
|
5575
|
+
false,
|
|
5576
|
+
[usage]
|
|
5577
|
+
);
|
|
5578
|
+
}
|
|
5579
|
+
function generateKey() {
|
|
5580
|
+
return globalThis.crypto.getRandomValues(new Uint8Array(KEY_LENGTH));
|
|
5581
|
+
}
|
|
5582
|
+
async function seal(plaintextBytes, key32) {
|
|
5583
|
+
const { nonce, ciphertext } = await encryptEnvelope(plaintextBytes, key32);
|
|
5584
|
+
const blob = new Uint8Array(HEADER_LENGTH + nonce.length + ciphertext.length);
|
|
5585
|
+
blob[0] = SEALED_BLOB_VERSION;
|
|
5586
|
+
blob.set(nonce, HEADER_LENGTH);
|
|
5587
|
+
blob.set(ciphertext, HEADER_LENGTH + nonce.length);
|
|
5588
|
+
return { blob, cid: await computeCid(blob) };
|
|
5589
|
+
}
|
|
5590
|
+
async function open(blob, key32) {
|
|
5591
|
+
if (blob.length < HEADER_LENGTH + NONCE_LENGTH + TAG_LENGTH) {
|
|
5592
|
+
throw new TypeError(`sealed blob too short: ${blob.length} bytes`);
|
|
5593
|
+
}
|
|
5594
|
+
if (blob[0] !== SEALED_BLOB_VERSION) {
|
|
5595
|
+
throw new TypeError(`unknown sealed blob version: ${blob[0]}`);
|
|
5596
|
+
}
|
|
5597
|
+
const nonce = blob.subarray(HEADER_LENGTH, HEADER_LENGTH + NONCE_LENGTH);
|
|
5598
|
+
const ciphertext = blob.subarray(HEADER_LENGTH + NONCE_LENGTH);
|
|
5599
|
+
return decryptEnvelope(nonce, ciphertext, key32);
|
|
5600
|
+
}
|
|
5601
|
+
async function encryptEnvelope(plaintextBytes, key32) {
|
|
5602
|
+
const key = await importAesKey(key32, "encrypt");
|
|
5603
|
+
const nonce = globalThis.crypto.getRandomValues(new Uint8Array(NONCE_LENGTH));
|
|
5604
|
+
const ciphertext = new Uint8Array(
|
|
5605
|
+
await globalThis.crypto.subtle.encrypt(
|
|
5606
|
+
{ name: "AES-GCM", iv: nonce, additionalData: AAD },
|
|
5607
|
+
key,
|
|
5608
|
+
plaintextBytes
|
|
5609
|
+
)
|
|
5610
|
+
);
|
|
5611
|
+
return { nonce, ciphertext };
|
|
5612
|
+
}
|
|
5613
|
+
async function decryptEnvelope(nonce, ciphertext, key32) {
|
|
5614
|
+
if (nonce.length !== NONCE_LENGTH) {
|
|
5615
|
+
throw new TypeError(`nonce must be ${NONCE_LENGTH} bytes, got ${nonce.length}`);
|
|
5616
|
+
}
|
|
5617
|
+
const key = await importAesKey(key32, "decrypt");
|
|
5618
|
+
return new Uint8Array(
|
|
5619
|
+
await globalThis.crypto.subtle.decrypt(
|
|
5620
|
+
{ name: "AES-GCM", iv: nonce, additionalData: AAD },
|
|
5621
|
+
key,
|
|
5622
|
+
ciphertext
|
|
5623
|
+
)
|
|
5624
|
+
);
|
|
5625
|
+
}
|
|
5559
5626
|
function isPlainRecord(value) {
|
|
5560
5627
|
const proto = Object.getPrototypeOf(value);
|
|
5561
5628
|
return proto === Object.prototype || proto === null;
|
|
@@ -5788,8 +5855,6 @@ function base32Lower(bytes3) {
|
|
|
5788
5855
|
return output;
|
|
5789
5856
|
}
|
|
5790
5857
|
var KEY_LENGTH2 = 32;
|
|
5791
|
-
var INLINE_PREFIX = "#tc2=";
|
|
5792
|
-
var PUBLIC_INLINE_PARAMETER = "tc2";
|
|
5793
5858
|
var MAX_INLINE_BYTES = 256 * 1024;
|
|
5794
5859
|
function assertCanonicalCid(cidString) {
|
|
5795
5860
|
const cid2 = CID.parse(cidString);
|
|
@@ -5798,62 +5863,61 @@ function assertCanonicalCid(cidString) {
|
|
|
5798
5863
|
throw new TypeError(`not a canonical CIDv1 raw sha2-256 base32 CID: ${cidString}`);
|
|
5799
5864
|
}
|
|
5800
5865
|
}
|
|
5801
|
-
async function
|
|
5866
|
+
async function encodeSealedInlineShareUrl(parts) {
|
|
5802
5867
|
if (!isCanonicalHttpsOrigin(parts.origin)) throw new TypeError("origin must be a canonical https origin");
|
|
5803
|
-
if (parts.
|
|
5804
|
-
|
|
5805
|
-
const
|
|
5868
|
+
if (parts.ciphertext.byteLength === 0 || parts.ciphertext.byteLength > MAX_INLINE_BYTES) throw new RangeError("inline ciphertext is outside the allowed size");
|
|
5869
|
+
if (parts.key32.byteLength !== KEY_LENGTH2) throw new TypeError("inline key must be 32 bytes");
|
|
5870
|
+
const ciphertextCid = await computeCid(parts.ciphertext);
|
|
5871
|
+
const payload = canonicalize({
|
|
5806
5872
|
v: 2,
|
|
5807
|
-
c: toBase64Url(parts.
|
|
5808
|
-
cid: ciphertextCid
|
|
5809
|
-
|
|
5873
|
+
c: toBase64Url(parts.ciphertext),
|
|
5874
|
+
cid: ciphertextCid,
|
|
5875
|
+
k: toBase64Url(parts.key32)
|
|
5876
|
+
});
|
|
5877
|
+
const payloadBytes = new TextEncoder().encode(payload);
|
|
5810
5878
|
if (payloadBytes.byteLength > MAX_INLINE_BYTES * 2) throw new RangeError("inline URL is too large");
|
|
5811
|
-
return `${parts.origin}/
|
|
5879
|
+
return `${parts.origin}/s/inline#v=2&p=${toBase64Url(payloadBytes)}`;
|
|
5812
5880
|
}
|
|
5813
|
-
function
|
|
5881
|
+
async function parseSealedInlineShareUrl(url, options = {}) {
|
|
5814
5882
|
const parsed = new URL(url);
|
|
5815
|
-
if (parsed.protocol !== "https:" || parsed.username !== "" || parsed.password !== "") throw new TypeError("inline share URL must be canonical HTTPS without userinfo");
|
|
5816
|
-
if (!isCanonicalHttpsOrigin(parsed.origin) || options.expectedOrigin !== void 0 && parsed.origin !== options.expectedOrigin) throw new TypeError("inline share URL origin is not trusted");
|
|
5817
|
-
if (parsed.pathname !== "/
|
|
5818
|
-
|
|
5819
|
-
const
|
|
5820
|
-
if (
|
|
5821
|
-
|
|
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");
|
|
5822
5891
|
let payloadBytes;
|
|
5823
5892
|
try {
|
|
5824
|
-
payloadBytes = fromBase64Url(
|
|
5893
|
+
payloadBytes = fromBase64Url(encoded);
|
|
5825
5894
|
} catch {
|
|
5826
|
-
throw new TypeError("inline payload is not canonical base64url");
|
|
5895
|
+
throw new TypeError("sealed inline payload is not canonical base64url");
|
|
5827
5896
|
}
|
|
5828
|
-
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);
|
|
5829
5899
|
let value;
|
|
5830
5900
|
try {
|
|
5831
|
-
value = JSON.parse(
|
|
5901
|
+
value = JSON.parse(payloadText);
|
|
5832
5902
|
} catch {
|
|
5833
|
-
throw new TypeError("inline payload is not valid JSON");
|
|
5903
|
+
throw new TypeError("sealed inline payload is not valid JSON");
|
|
5834
5904
|
}
|
|
5835
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("inline payload
|
|
5836
|
-
if (canonicalize(value) !== new TextDecoder("utf-8").decode(payloadBytes)) throw new TypeError("inline payload is not canonical JSON");
|
|
5905
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || canonicalize(value) !== payloadText) throw new TypeError("sealed inline payload is not canonical JSON");
|
|
5837
5906
|
const record = value;
|
|
5838
|
-
|
|
5839
|
-
if (!keys.every((key) => key === "v" || key === "c" || key === "cid" || key === "k") || record.v !== 2 || typeof record.c !== "string" || typeof record.cid !== "string" || publicPayload !== void 0 && (keys.length !== 3 || record.k !== void 0)) throw new TypeError("inline payload has invalid fields");
|
|
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");
|
|
5840
5908
|
let ciphertext;
|
|
5909
|
+
let key32;
|
|
5841
5910
|
try {
|
|
5842
5911
|
ciphertext = fromBase64Url(record.c);
|
|
5912
|
+
key32 = fromBase64Url(record.k);
|
|
5843
5913
|
} catch {
|
|
5844
|
-
throw new TypeError("inline
|
|
5914
|
+
throw new TypeError("sealed inline payload has invalid base64url material");
|
|
5845
5915
|
}
|
|
5846
|
-
if (ciphertext.byteLength === 0 || ciphertext.byteLength > MAX_INLINE_BYTES) throw new TypeError("inline ciphertext is outside the allowed size");
|
|
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");
|
|
5847
5918
|
assertCanonicalCid(record.cid);
|
|
5848
|
-
if (
|
|
5849
|
-
|
|
5850
|
-
try {
|
|
5851
|
-
key32 = record.k === void 0 ? void 0 : fromBase64Url(record.k);
|
|
5852
|
-
} catch {
|
|
5853
|
-
throw new TypeError("inline key is not canonical base64url");
|
|
5854
|
-
}
|
|
5855
|
-
if (key32 !== void 0 && key32.length !== KEY_LENGTH2) throw new TypeError("inline key must be 32 bytes");
|
|
5856
|
-
return { kind: "inline", ciphertextCid: record.cid, ciphertext, ...key32 === void 0 ? {} : { key32 } };
|
|
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 };
|
|
5857
5921
|
}
|
|
5858
5922
|
|
|
5859
5923
|
// src/receive.ts
|
|
@@ -5901,19 +5965,17 @@ async function resolvePolicyShare(link, options) {
|
|
|
5901
5965
|
let parsed;
|
|
5902
5966
|
try {
|
|
5903
5967
|
url = new URL(link);
|
|
5904
|
-
|
|
5905
|
-
parsed = parseInlineShareUrl(link, { ...options.expectedOrigin === void 0 ? {} : { expectedOrigin: options.expectedOrigin } });
|
|
5906
|
-
if (parsed.key32 !== void 0) throw new Error("secret inline transport is retired");
|
|
5968
|
+
parsed = await parseSealedInlineShareUrl(link, { ...options.expectedOrigin === void 0 ? {} : { expectedOrigin: options.expectedOrigin } });
|
|
5907
5969
|
} catch {
|
|
5908
5970
|
throw new ShareReceiveError("invalid-link", "share link format is invalid");
|
|
5909
5971
|
}
|
|
5910
5972
|
const limit = options.maxSealedBlobBytes ?? DEFAULT_MAX_SEALED_BLOB_BYTES;
|
|
5911
|
-
if (!Number.isSafeInteger(limit) || limit <= 0 || parsed.ciphertext.byteLength > limit) throw new ShareReceiveError("max-bytes-exceeded", "
|
|
5912
|
-
if (await computeCid(parsed.ciphertext) !== parsed.ciphertextCid) throw new ShareReceiveError("cid-mismatch", "
|
|
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");
|
|
5913
5975
|
options.signal?.throwIfAborted();
|
|
5914
5976
|
let envelope;
|
|
5915
5977
|
try {
|
|
5916
|
-
const encoded = new TextDecoder("utf-8", { fatal: true }).decode(parsed.ciphertext);
|
|
5978
|
+
const encoded = new TextDecoder("utf-8", { fatal: true }).decode(await open(parsed.ciphertext, parsed.key32));
|
|
5917
5979
|
const value = JSON.parse(encoded);
|
|
5918
5980
|
if (canonicalize(value) !== encoded) throw new Error("non-canonical envelope");
|
|
5919
5981
|
envelope = shareEnvelopeV3Schema.parse(value);
|
|
@@ -6588,10 +6650,23 @@ function publicationResult(input) {
|
|
|
6588
6650
|
Object.defineProperty(result, "deliveryMaterial", { enumerable: false, value: input.deliveryMaterial });
|
|
6589
6651
|
return result;
|
|
6590
6652
|
}
|
|
6653
|
+
var EMAIL_DOMAIN_PROFILE = { id: "tinycloud.email-domain-proof/v1", version: 1 };
|
|
6654
|
+
var EMAIL_CREDENTIAL_TYPE = { id: "opencredentials.email/v1", version: 1 };
|
|
6655
|
+
var EMAIL_DOMAIN_DESCRIPTOR_DIGEST = "33X5mAkZZgApdD3xh_T-3KS5moop0J2Nloi2nqWsdWY";
|
|
6656
|
+
function assertEmailDomainPolicy(options, domain) {
|
|
6657
|
+
if (!options.actions.every((action) => action === "read" || action === "list") || !options.policyActions.every((action) => action === "tinycloud.kv/get" || action === "tinycloud.kv/list" || action === "tinycloud.kv/metadata")) throw new TypeError("email-domain shares are view-only");
|
|
6658
|
+
if (options.deliveryEmail !== void 0) throw new TypeError("email-domain shares are not emailed");
|
|
6659
|
+
const commitment = options.credentialRequirement;
|
|
6660
|
+
if (commitment === void 0) throw new TypeError("email-domain shares require a credential requirement");
|
|
6661
|
+
if (canonicalize(commitment.profile) !== canonicalize(EMAIL_DOMAIN_PROFILE) || canonicalize(commitment.credentialType) !== canonicalize(EMAIL_CREDENTIAL_TYPE) || commitment.descriptorDigest !== EMAIL_DOMAIN_DESCRIPTOR_DIGEST) throw new TypeError("email-domain shares require the email-domain credential profile");
|
|
6662
|
+
const expected = { type: "TinyCloudCredentialRequirement", version: 1, profile: EMAIL_DOMAIN_PROFILE, credentialType: EMAIL_CREDENTIAL_TYPE, claims: { emailDomain: domain }, maxAgeSeconds: 300 };
|
|
6663
|
+
if (toBase64Url(sha2563(textEncoder.encode(canonicalize(expected)))) !== commitment.requirementDigest) throw new TypeError("credential requirement is not bound to the email domain");
|
|
6664
|
+
}
|
|
6591
6665
|
async function publishAddressedShare(options) {
|
|
6592
6666
|
assertSafeInput(options);
|
|
6593
6667
|
const target = normalizeShareTarget(options.target);
|
|
6594
6668
|
if (target.kind === "bearer") throw new TypeError("addressed target is required");
|
|
6669
|
+
if (target.kind === "emailDomain") assertEmailDomainPolicy(options, target.domain);
|
|
6595
6670
|
const expiry = rfc3339Seconds(options.expiresAt);
|
|
6596
6671
|
const matcher = targetMatcher(target);
|
|
6597
6672
|
const capabilities = sortCanonical([
|
|
@@ -6664,14 +6739,25 @@ async function publishAddressedShare(options) {
|
|
|
6664
6739
|
const envelope = { ...unsigned, signature: { signerDid: options.authority.ownerDid, algorithm: "Ed25519", value: toBase64Url(envelopeSignature) } };
|
|
6665
6740
|
shareEnvelopeV3Schema.parse(envelope);
|
|
6666
6741
|
const envelopeBytes = textEncoder.encode(canonicalize(envelope));
|
|
6667
|
-
const
|
|
6668
|
-
const
|
|
6669
|
-
const
|
|
6742
|
+
const envelopeKeyBytes = generateKey();
|
|
6743
|
+
const sealed = await seal(envelopeBytes, envelopeKeyBytes);
|
|
6744
|
+
const envelopeKey = toBase64Url(envelopeKeyBytes);
|
|
6745
|
+
const url = await encodeSealedInlineShareUrl({
|
|
6746
|
+
origin: options.shareOrigin,
|
|
6747
|
+
ciphertext: sealed.blob,
|
|
6748
|
+
key32: envelopeKeyBytes
|
|
6749
|
+
});
|
|
6750
|
+
const deliveryMaterial = {
|
|
6751
|
+
envelope,
|
|
6752
|
+
shareCid: sealed.cid,
|
|
6753
|
+
sealedEnvelope: toBase64Url(sealed.blob),
|
|
6754
|
+
envelopeKey
|
|
6755
|
+
};
|
|
6670
6756
|
options.onDeliveryMaterial?.(deliveryMaterial);
|
|
6671
6757
|
return publicationResult({
|
|
6672
6758
|
options,
|
|
6673
6759
|
url,
|
|
6674
|
-
envelopeCid,
|
|
6760
|
+
envelopeCid: sealed.cid,
|
|
6675
6761
|
matcher,
|
|
6676
6762
|
policyCid: created.policyCid,
|
|
6677
6763
|
policyRootCid: policyRoot.cid,
|
|
@@ -7728,6 +7814,119 @@ async function defaultIdempotencyKey(shareId, recipient) {
|
|
|
7728
7814
|
return `tinycloud-share:${shareId}:${toBase64Url(digest3)}`;
|
|
7729
7815
|
}
|
|
7730
7816
|
|
|
7817
|
+
// src/credential-invitation.ts
|
|
7818
|
+
var CredentialInvitationError = class extends Error {
|
|
7819
|
+
constructor(code3, message) {
|
|
7820
|
+
super(message);
|
|
7821
|
+
this.code = code3;
|
|
7822
|
+
this.name = "CredentialInvitationError";
|
|
7823
|
+
}
|
|
7824
|
+
};
|
|
7825
|
+
function canonicalOrigin(value) {
|
|
7826
|
+
let url;
|
|
7827
|
+
try {
|
|
7828
|
+
url = new URL(value);
|
|
7829
|
+
} catch {
|
|
7830
|
+
throw new CredentialInvitationError(
|
|
7831
|
+
"invalid-origin",
|
|
7832
|
+
"credential invitation origin is invalid"
|
|
7833
|
+
);
|
|
7834
|
+
}
|
|
7835
|
+
if (url.origin !== value || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "" || url.protocol !== "https:")
|
|
7836
|
+
throw new CredentialInvitationError(
|
|
7837
|
+
"invalid-origin",
|
|
7838
|
+
"credential invitation origin is invalid"
|
|
7839
|
+
);
|
|
7840
|
+
return url.origin;
|
|
7841
|
+
}
|
|
7842
|
+
function object2(value) {
|
|
7843
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7844
|
+
}
|
|
7845
|
+
async function validateReceipt(receipt, shareUrl) {
|
|
7846
|
+
if (!object2(receipt) || !object2(receipt.request) || !object2(receipt.admission) || !object2(receipt.proof)) {
|
|
7847
|
+
throw new CredentialInvitationError(
|
|
7848
|
+
"invalid-receipt",
|
|
7849
|
+
"credential invitation receipt is invalid"
|
|
7850
|
+
);
|
|
7851
|
+
}
|
|
7852
|
+
if (receipt.request.returnLink !== shareUrl) {
|
|
7853
|
+
throw new CredentialInvitationError(
|
|
7854
|
+
"invalid-receipt",
|
|
7855
|
+
"credential invitation is not bound to the share link"
|
|
7856
|
+
);
|
|
7857
|
+
}
|
|
7858
|
+
let link;
|
|
7859
|
+
try {
|
|
7860
|
+
link = new URL(shareUrl);
|
|
7861
|
+
} catch {
|
|
7862
|
+
throw new CredentialInvitationError(
|
|
7863
|
+
"invalid-receipt",
|
|
7864
|
+
"credential invitation share link is invalid"
|
|
7865
|
+
);
|
|
7866
|
+
}
|
|
7867
|
+
if (link.search !== "" || link.pathname !== "/s/inline") {
|
|
7868
|
+
throw new CredentialInvitationError(
|
|
7869
|
+
"invalid-receipt",
|
|
7870
|
+
"credential invitation share link must be sealed inline"
|
|
7871
|
+
);
|
|
7872
|
+
}
|
|
7873
|
+
try {
|
|
7874
|
+
await parseSealedInlineShareUrl(shareUrl);
|
|
7875
|
+
} catch {
|
|
7876
|
+
throw new CredentialInvitationError(
|
|
7877
|
+
"invalid-receipt",
|
|
7878
|
+
"credential invitation share link is invalid"
|
|
7879
|
+
);
|
|
7880
|
+
}
|
|
7881
|
+
}
|
|
7882
|
+
async function deliverCredentialInvitation(input) {
|
|
7883
|
+
const credentialsOrigin = canonicalOrigin(input.credentialsOrigin);
|
|
7884
|
+
await validateReceipt(input.receipt, input.shareUrl);
|
|
7885
|
+
const fetchFn = input.fetchFn ?? globalThis.fetch;
|
|
7886
|
+
let response;
|
|
7887
|
+
try {
|
|
7888
|
+
response = await fetchFn(`${credentialsOrigin}/v1/credential-invitations`, {
|
|
7889
|
+
method: "POST",
|
|
7890
|
+
credentials: "omit",
|
|
7891
|
+
redirect: "error",
|
|
7892
|
+
referrerPolicy: "no-referrer",
|
|
7893
|
+
headers: {
|
|
7894
|
+
accept: "application/json",
|
|
7895
|
+
"content-type": "application/json"
|
|
7896
|
+
},
|
|
7897
|
+
body: JSON.stringify(input.receipt),
|
|
7898
|
+
...input.signal === void 0 ? {} : { signal: input.signal }
|
|
7899
|
+
});
|
|
7900
|
+
} catch {
|
|
7901
|
+
throw new CredentialInvitationError(
|
|
7902
|
+
"transport",
|
|
7903
|
+
"credential invitation delivery is unavailable"
|
|
7904
|
+
);
|
|
7905
|
+
}
|
|
7906
|
+
if (response.status !== 202) {
|
|
7907
|
+
throw new CredentialInvitationError(
|
|
7908
|
+
"rejected",
|
|
7909
|
+
"credential invitation delivery was not accepted"
|
|
7910
|
+
);
|
|
7911
|
+
}
|
|
7912
|
+
let body;
|
|
7913
|
+
try {
|
|
7914
|
+
body = await response.json();
|
|
7915
|
+
} catch {
|
|
7916
|
+
throw new CredentialInvitationError(
|
|
7917
|
+
"invalid-response",
|
|
7918
|
+
"credential invitation response is invalid"
|
|
7919
|
+
);
|
|
7920
|
+
}
|
|
7921
|
+
if (!object2(body) || Object.keys(body).length !== 1 || body.status !== "accepted") {
|
|
7922
|
+
throw new CredentialInvitationError(
|
|
7923
|
+
"invalid-response",
|
|
7924
|
+
"credential invitation response is invalid"
|
|
7925
|
+
);
|
|
7926
|
+
}
|
|
7927
|
+
return { status: "accepted" };
|
|
7928
|
+
}
|
|
7929
|
+
|
|
7731
7930
|
// src/lifecycle.ts
|
|
7732
7931
|
function targetKind2(record) {
|
|
7733
7932
|
if (record.targetKind !== void 0) return record.targetKind;
|
|
@@ -7834,7 +8033,7 @@ var DOMAIN = SHARE_V2_PROTOCOL.challengeDomain;
|
|
|
7834
8033
|
var PRESENTATION_DOMAIN = SHARE_V2_PROTOCOL.sessionDomain;
|
|
7835
8034
|
var SESSION_DOMAIN = SHARE_V2_PROTOCOL.sessionDomain;
|
|
7836
8035
|
var INVOCATION_DOMAIN = SHARE_V2_PROTOCOL.invocationDomain;
|
|
7837
|
-
function
|
|
8036
|
+
function object3(value, label) {
|
|
7838
8037
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${label} is invalid`);
|
|
7839
8038
|
return value;
|
|
7840
8039
|
}
|
|
@@ -7893,11 +8092,11 @@ function nativeAction(action) {
|
|
|
7893
8092
|
return action === "list" ? "tinycloud.kv/list" : action === "edit" ? "tinycloud.kv/put" : "tinycloud.kv/get";
|
|
7894
8093
|
}
|
|
7895
8094
|
function policyAttenuationForV3(envelope) {
|
|
7896
|
-
const policy =
|
|
8095
|
+
const policy = object3(envelope.policy, "v3 policy");
|
|
7897
8096
|
if (!Array.isArray(policy.capabilityCeiling)) throw new Error("v3 policy capability ceiling is invalid");
|
|
7898
8097
|
const attenuation = {};
|
|
7899
8098
|
for (const raw of policy.capabilityCeiling) {
|
|
7900
|
-
const capability =
|
|
8099
|
+
const capability = object3(raw, "v3 policy capability");
|
|
7901
8100
|
if (typeof capability.resource !== "string" || attenuation[capability.resource] !== void 0) throw new Error("v3 policy capability is invalid");
|
|
7902
8101
|
if (capability.kind === "encryption") {
|
|
7903
8102
|
if (capability.action !== "tinycloud.encryption/decrypt") throw new Error("v3 policy capability is invalid");
|
|
@@ -7955,8 +8154,8 @@ function verifyV3PolicyAuthorization(input) {
|
|
|
7955
8154
|
const factKeys = Object.keys(fact);
|
|
7956
8155
|
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);
|
|
7957
8156
|
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);
|
|
7958
|
-
const binding =
|
|
7959
|
-
const policy =
|
|
8157
|
+
const binding = object3(input.envelope.attestedEnforcerBinding, "v3 attested enforcer binding");
|
|
8158
|
+
const policy = object3(input.envelope.policy, "v3 policy");
|
|
7960
8159
|
const now = Math.floor(Date.now() / 1e3);
|
|
7961
8160
|
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");
|
|
7962
8161
|
return compact;
|
|
@@ -8047,7 +8246,7 @@ var ShareRecipientClient = class {
|
|
|
8047
8246
|
if (this.session === void 0) await this.establish(envelope);
|
|
8048
8247
|
const response = await this.nativeInvoke({ action: "get", resource: envelope.resource });
|
|
8049
8248
|
if (!response.ok) throw new Error("share recipient read was rejected");
|
|
8050
|
-
const value =
|
|
8249
|
+
const value = object3(await response.json(), "share read response");
|
|
8051
8250
|
const expectedAction = nativeAction("read");
|
|
8052
8251
|
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");
|
|
8053
8252
|
const content = fromBase64Url(value.content);
|
|
@@ -8062,7 +8261,7 @@ var ShareRecipientClient = class {
|
|
|
8062
8261
|
}
|
|
8063
8262
|
async establishV3(envelope) {
|
|
8064
8263
|
if (this.options.buildPresentation === void 0) throw new Error("share presentation builder is required");
|
|
8065
|
-
const attestedEnforcer =
|
|
8264
|
+
const attestedEnforcer = object3(envelope.attestedEnforcerBinding, "v3 attested enforcer binding");
|
|
8066
8265
|
if (attestedEnforcer.enforcerDid !== envelope.target.nodeAudience || typeof attestedEnforcer.nodeAudience !== "string") throw new Error("v3 attested enforcer binding mismatch");
|
|
8067
8266
|
try {
|
|
8068
8267
|
ed25519PublicKeyFromDidKey(attestedEnforcer.nodeAudience);
|
|
@@ -8077,7 +8276,7 @@ var ShareRecipientClient = class {
|
|
|
8077
8276
|
...this.options.signal === void 0 ? {} : { signal: this.options.signal }
|
|
8078
8277
|
});
|
|
8079
8278
|
if (!challengeResponse.ok) throw new Error(`v3 policy challenge rejected (${challengeResponse.status})`);
|
|
8080
|
-
const challenge =
|
|
8279
|
+
const challenge = object3(await challengeResponse.json(), "v3 policy challenge");
|
|
8081
8280
|
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");
|
|
8082
8281
|
const material = await this.options.buildPresentation({
|
|
8083
8282
|
challenge,
|
|
@@ -8098,7 +8297,7 @@ var ShareRecipientClient = class {
|
|
|
8098
8297
|
...this.options.signal === void 0 ? {} : { signal: this.options.signal }
|
|
8099
8298
|
});
|
|
8100
8299
|
if (!delegationResponse.ok) throw new Error(`v3 policy delegation rejected (${delegationResponse.status})`);
|
|
8101
|
-
const delegation =
|
|
8300
|
+
const delegation = object3(await delegationResponse.json(), "v3 policy delegation");
|
|
8102
8301
|
if (delegation.admitted !== true || typeof delegation.sessionCid !== "string" || typeof delegation.authorization !== "string") throw new Error("v3 policy delegation response is not admitted");
|
|
8103
8302
|
const compact = verifyV3PolicyAuthorization({ authorization: delegation.authorization, cid: delegation.sessionCid, envelope, holderDid: this.options.holderDid });
|
|
8104
8303
|
const fact = compact.payload.fct[0];
|
|
@@ -8140,7 +8339,7 @@ var ShareRecipientClient = class {
|
|
|
8140
8339
|
this.options.onStage?.("decryption");
|
|
8141
8340
|
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 } });
|
|
8142
8341
|
if (!response.ok) throw new Error(`v3 decrypt invocation rejected (${response.status})`);
|
|
8143
|
-
const value =
|
|
8342
|
+
const value = object3(await response.json(), "v3 decrypt response");
|
|
8144
8343
|
const allowed = ["type", "targetNode", "networkId", "invocationCid", "encryptedSymmetricKeyHash", "receiverPublicKeyHash", "wrappedKey", "alg", "keyVersion", "requestHash", "nodeId", "nodeSignature"];
|
|
8145
8344
|
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");
|
|
8146
8345
|
const unsigned = { ...value };
|
|
@@ -8228,11 +8427,11 @@ function createAddressedAuthorization(input) {
|
|
|
8228
8427
|
},
|
|
8229
8428
|
async verifyResult({ envelope, value, proof }) {
|
|
8230
8429
|
try {
|
|
8231
|
-
const wrapper =
|
|
8232
|
-
const response =
|
|
8430
|
+
const wrapper = object3(proof, "share read proof");
|
|
8431
|
+
const response = object3(wrapper.response, "share read response proof");
|
|
8233
8432
|
const envelopeAction = nativeAction("read");
|
|
8234
8433
|
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;
|
|
8235
|
-
const detached =
|
|
8434
|
+
const detached = object3(wrapper.detached, "share read detached proof");
|
|
8236
8435
|
if (input.trustedNode === void 0 || detached.alg !== "EdDSA" || detached.kid !== input.trustedNode.invitationKid) return false;
|
|
8237
8436
|
const unsigned = { ...response };
|
|
8238
8437
|
delete unsigned.proof;
|
|
@@ -8300,6 +8499,7 @@ async function openNativeShare(sharing, link) {
|
|
|
8300
8499
|
return sharing.receive(parseNativeShareUrl(link), { autoSubdelegate: false, useSessionKey: false });
|
|
8301
8500
|
}
|
|
8302
8501
|
export {
|
|
8502
|
+
CredentialInvitationError,
|
|
8303
8503
|
DEFAULT_MAX_CONTENT_BLOB_BYTES,
|
|
8304
8504
|
DEFAULT_MAX_SEALED_BLOB_BYTES,
|
|
8305
8505
|
DEFAULT_SHARE_LIFETIME_MS,
|
|
@@ -8332,6 +8532,7 @@ export {
|
|
|
8332
8532
|
createNativeShare,
|
|
8333
8533
|
createPolicyEnforcementDelegation,
|
|
8334
8534
|
createShareV2HolderBindingArtifact,
|
|
8535
|
+
deliverCredentialInvitation,
|
|
8335
8536
|
fromBase64Url,
|
|
8336
8537
|
historyRecordForPublishedShare,
|
|
8337
8538
|
inspectShare,
|