@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.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
CredentialInvitationError: () => CredentialInvitationError,
|
|
23
24
|
DEFAULT_MAX_CONTENT_BLOB_BYTES: () => DEFAULT_MAX_CONTENT_BLOB_BYTES,
|
|
24
25
|
DEFAULT_MAX_SEALED_BLOB_BYTES: () => DEFAULT_MAX_SEALED_BLOB_BYTES,
|
|
25
26
|
DEFAULT_SHARE_LIFETIME_MS: () => DEFAULT_SHARE_LIFETIME_MS,
|
|
@@ -52,6 +53,7 @@ __export(index_exports, {
|
|
|
52
53
|
createNativeShare: () => createNativeShare,
|
|
53
54
|
createPolicyEnforcementDelegation: () => createPolicyEnforcementDelegation,
|
|
54
55
|
createShareV2HolderBindingArtifact: () => createShareV2HolderBindingArtifact,
|
|
56
|
+
deliverCredentialInvitation: () => deliverCredentialInvitation,
|
|
55
57
|
fromBase64Url: () => fromBase64Url,
|
|
56
58
|
historyRecordForPublishedShare: () => historyRecordForPublishedShare,
|
|
57
59
|
inspectShare: () => inspectShare,
|
|
@@ -229,10 +231,10 @@ var util;
|
|
|
229
231
|
return obj[e];
|
|
230
232
|
});
|
|
231
233
|
};
|
|
232
|
-
util2.objectKeys = typeof Object.keys === "function" ? (obj) => Object.keys(obj) : (
|
|
234
|
+
util2.objectKeys = typeof Object.keys === "function" ? (obj) => Object.keys(obj) : (object4) => {
|
|
233
235
|
const keys = [];
|
|
234
|
-
for (const key in
|
|
235
|
-
if (Object.prototype.hasOwnProperty.call(
|
|
236
|
+
for (const key in object4) {
|
|
237
|
+
if (Object.prototype.hasOwnProperty.call(object4, key)) {
|
|
236
238
|
keys.push(key);
|
|
237
239
|
}
|
|
238
240
|
}
|
|
@@ -5632,7 +5634,74 @@ function checkBearerDelegation(envelope, options = {}) {
|
|
|
5632
5634
|
return { ok: true, delegateeDid: audience };
|
|
5633
5635
|
}
|
|
5634
5636
|
var ENVELOPE_AAD_LABEL = "tinycloud-share-envelope-v1";
|
|
5637
|
+
var SEALED_BLOB_VERSION = 1;
|
|
5635
5638
|
var AAD = utf8Bytes(ENVELOPE_AAD_LABEL);
|
|
5639
|
+
var KEY_LENGTH = 32;
|
|
5640
|
+
var NONCE_LENGTH = 12;
|
|
5641
|
+
var TAG_LENGTH = 16;
|
|
5642
|
+
var HEADER_LENGTH = 1;
|
|
5643
|
+
function assertKey(key32) {
|
|
5644
|
+
if (key32.length !== KEY_LENGTH) {
|
|
5645
|
+
throw new TypeError(`key must be ${KEY_LENGTH} bytes, got ${key32.length}`);
|
|
5646
|
+
}
|
|
5647
|
+
}
|
|
5648
|
+
async function importAesKey(key32, usage) {
|
|
5649
|
+
assertKey(key32);
|
|
5650
|
+
return globalThis.crypto.subtle.importKey(
|
|
5651
|
+
"raw",
|
|
5652
|
+
key32,
|
|
5653
|
+
"AES-GCM",
|
|
5654
|
+
false,
|
|
5655
|
+
[usage]
|
|
5656
|
+
);
|
|
5657
|
+
}
|
|
5658
|
+
function generateKey() {
|
|
5659
|
+
return globalThis.crypto.getRandomValues(new Uint8Array(KEY_LENGTH));
|
|
5660
|
+
}
|
|
5661
|
+
async function seal(plaintextBytes, key32) {
|
|
5662
|
+
const { nonce, ciphertext } = await encryptEnvelope(plaintextBytes, key32);
|
|
5663
|
+
const blob = new Uint8Array(HEADER_LENGTH + nonce.length + ciphertext.length);
|
|
5664
|
+
blob[0] = SEALED_BLOB_VERSION;
|
|
5665
|
+
blob.set(nonce, HEADER_LENGTH);
|
|
5666
|
+
blob.set(ciphertext, HEADER_LENGTH + nonce.length);
|
|
5667
|
+
return { blob, cid: await computeCid(blob) };
|
|
5668
|
+
}
|
|
5669
|
+
async function open(blob, key32) {
|
|
5670
|
+
if (blob.length < HEADER_LENGTH + NONCE_LENGTH + TAG_LENGTH) {
|
|
5671
|
+
throw new TypeError(`sealed blob too short: ${blob.length} bytes`);
|
|
5672
|
+
}
|
|
5673
|
+
if (blob[0] !== SEALED_BLOB_VERSION) {
|
|
5674
|
+
throw new TypeError(`unknown sealed blob version: ${blob[0]}`);
|
|
5675
|
+
}
|
|
5676
|
+
const nonce = blob.subarray(HEADER_LENGTH, HEADER_LENGTH + NONCE_LENGTH);
|
|
5677
|
+
const ciphertext = blob.subarray(HEADER_LENGTH + NONCE_LENGTH);
|
|
5678
|
+
return decryptEnvelope(nonce, ciphertext, key32);
|
|
5679
|
+
}
|
|
5680
|
+
async function encryptEnvelope(plaintextBytes, key32) {
|
|
5681
|
+
const key = await importAesKey(key32, "encrypt");
|
|
5682
|
+
const nonce = globalThis.crypto.getRandomValues(new Uint8Array(NONCE_LENGTH));
|
|
5683
|
+
const ciphertext = new Uint8Array(
|
|
5684
|
+
await globalThis.crypto.subtle.encrypt(
|
|
5685
|
+
{ name: "AES-GCM", iv: nonce, additionalData: AAD },
|
|
5686
|
+
key,
|
|
5687
|
+
plaintextBytes
|
|
5688
|
+
)
|
|
5689
|
+
);
|
|
5690
|
+
return { nonce, ciphertext };
|
|
5691
|
+
}
|
|
5692
|
+
async function decryptEnvelope(nonce, ciphertext, key32) {
|
|
5693
|
+
if (nonce.length !== NONCE_LENGTH) {
|
|
5694
|
+
throw new TypeError(`nonce must be ${NONCE_LENGTH} bytes, got ${nonce.length}`);
|
|
5695
|
+
}
|
|
5696
|
+
const key = await importAesKey(key32, "decrypt");
|
|
5697
|
+
return new Uint8Array(
|
|
5698
|
+
await globalThis.crypto.subtle.decrypt(
|
|
5699
|
+
{ name: "AES-GCM", iv: nonce, additionalData: AAD },
|
|
5700
|
+
key,
|
|
5701
|
+
ciphertext
|
|
5702
|
+
)
|
|
5703
|
+
);
|
|
5704
|
+
}
|
|
5636
5705
|
function isPlainRecord(value) {
|
|
5637
5706
|
const proto = Object.getPrototypeOf(value);
|
|
5638
5707
|
return proto === Object.prototype || proto === null;
|
|
@@ -5865,8 +5934,6 @@ function base32Lower(bytes3) {
|
|
|
5865
5934
|
return output;
|
|
5866
5935
|
}
|
|
5867
5936
|
var KEY_LENGTH2 = 32;
|
|
5868
|
-
var INLINE_PREFIX = "#tc2=";
|
|
5869
|
-
var PUBLIC_INLINE_PARAMETER = "tc2";
|
|
5870
5937
|
var MAX_INLINE_BYTES = 256 * 1024;
|
|
5871
5938
|
function assertCanonicalCid(cidString) {
|
|
5872
5939
|
const cid2 = CID.parse(cidString);
|
|
@@ -5875,62 +5942,61 @@ function assertCanonicalCid(cidString) {
|
|
|
5875
5942
|
throw new TypeError(`not a canonical CIDv1 raw sha2-256 base32 CID: ${cidString}`);
|
|
5876
5943
|
}
|
|
5877
5944
|
}
|
|
5878
|
-
async function
|
|
5945
|
+
async function encodeSealedInlineShareUrl(parts) {
|
|
5879
5946
|
if (!isCanonicalHttpsOrigin(parts.origin)) throw new TypeError("origin must be a canonical https origin");
|
|
5880
|
-
if (parts.
|
|
5881
|
-
|
|
5882
|
-
const
|
|
5947
|
+
if (parts.ciphertext.byteLength === 0 || parts.ciphertext.byteLength > MAX_INLINE_BYTES) throw new RangeError("inline ciphertext is outside the allowed size");
|
|
5948
|
+
if (parts.key32.byteLength !== KEY_LENGTH2) throw new TypeError("inline key must be 32 bytes");
|
|
5949
|
+
const ciphertextCid = await computeCid(parts.ciphertext);
|
|
5950
|
+
const payload = canonicalize({
|
|
5883
5951
|
v: 2,
|
|
5884
|
-
c: toBase64Url(parts.
|
|
5885
|
-
cid: ciphertextCid
|
|
5886
|
-
|
|
5952
|
+
c: toBase64Url(parts.ciphertext),
|
|
5953
|
+
cid: ciphertextCid,
|
|
5954
|
+
k: toBase64Url(parts.key32)
|
|
5955
|
+
});
|
|
5956
|
+
const payloadBytes = new TextEncoder().encode(payload);
|
|
5887
5957
|
if (payloadBytes.byteLength > MAX_INLINE_BYTES * 2) throw new RangeError("inline URL is too large");
|
|
5888
|
-
return `${parts.origin}/
|
|
5958
|
+
return `${parts.origin}/s/inline#v=2&p=${toBase64Url(payloadBytes)}`;
|
|
5889
5959
|
}
|
|
5890
|
-
function
|
|
5960
|
+
async function parseSealedInlineShareUrl(url, options = {}) {
|
|
5891
5961
|
const parsed = new URL(url);
|
|
5892
|
-
if (parsed.protocol !== "https:" || parsed.username !== "" || parsed.password !== "") throw new TypeError("inline share URL must be canonical HTTPS without userinfo");
|
|
5893
|
-
if (!isCanonicalHttpsOrigin(parsed.origin) || options.expectedOrigin !== void 0 && parsed.origin !== options.expectedOrigin) throw new TypeError("inline share URL origin is not trusted");
|
|
5894
|
-
if (parsed.pathname !== "/
|
|
5895
|
-
|
|
5896
|
-
const
|
|
5897
|
-
if (
|
|
5898
|
-
|
|
5962
|
+
if (parsed.protocol !== "https:" || parsed.username !== "" || parsed.password !== "") throw new TypeError("sealed inline share URL must be canonical HTTPS without userinfo");
|
|
5963
|
+
if (!isCanonicalHttpsOrigin(parsed.origin) || options.expectedOrigin !== void 0 && parsed.origin !== options.expectedOrigin) throw new TypeError("sealed inline share URL origin is not trusted");
|
|
5964
|
+
if (parsed.pathname !== "/s/inline" || parsed.search !== "") throw new TypeError("not a Node sealed-inline share URL");
|
|
5965
|
+
if (url !== `${parsed.origin}/s/inline${parsed.hash}`) throw new TypeError("sealed inline share URL is not lexically canonical");
|
|
5966
|
+
const prefix = "#v=2&p=";
|
|
5967
|
+
if (!parsed.hash.startsWith(prefix)) throw new TypeError("sealed inline URL is missing its canonical fragment");
|
|
5968
|
+
const encoded = parsed.hash.slice(prefix.length);
|
|
5969
|
+
if (encoded.length === 0 || parsed.hash !== `${prefix}${encoded}`) throw new TypeError("sealed inline URL fragment is not canonical");
|
|
5899
5970
|
let payloadBytes;
|
|
5900
5971
|
try {
|
|
5901
|
-
payloadBytes = fromBase64Url(
|
|
5972
|
+
payloadBytes = fromBase64Url(encoded);
|
|
5902
5973
|
} catch {
|
|
5903
|
-
throw new TypeError("inline payload is not canonical base64url");
|
|
5974
|
+
throw new TypeError("sealed inline payload is not canonical base64url");
|
|
5904
5975
|
}
|
|
5905
|
-
if (payloadBytes.byteLength === 0 || payloadBytes.byteLength > MAX_INLINE_BYTES * 2) throw new TypeError("inline payload is too large");
|
|
5976
|
+
if (payloadBytes.byteLength === 0 || payloadBytes.byteLength > MAX_INLINE_BYTES * 2) throw new TypeError("sealed inline payload is too large");
|
|
5977
|
+
const payloadText = new TextDecoder("utf-8", { fatal: true }).decode(payloadBytes);
|
|
5906
5978
|
let value;
|
|
5907
5979
|
try {
|
|
5908
|
-
value = JSON.parse(
|
|
5980
|
+
value = JSON.parse(payloadText);
|
|
5909
5981
|
} catch {
|
|
5910
|
-
throw new TypeError("inline payload is not valid JSON");
|
|
5982
|
+
throw new TypeError("sealed inline payload is not valid JSON");
|
|
5911
5983
|
}
|
|
5912
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("inline payload
|
|
5913
|
-
if (canonicalize(value) !== new TextDecoder("utf-8").decode(payloadBytes)) throw new TypeError("inline payload is not canonical JSON");
|
|
5984
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || canonicalize(value) !== payloadText) throw new TypeError("sealed inline payload is not canonical JSON");
|
|
5914
5985
|
const record = value;
|
|
5915
|
-
|
|
5916
|
-
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");
|
|
5986
|
+
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");
|
|
5917
5987
|
let ciphertext;
|
|
5988
|
+
let key32;
|
|
5918
5989
|
try {
|
|
5919
5990
|
ciphertext = fromBase64Url(record.c);
|
|
5991
|
+
key32 = fromBase64Url(record.k);
|
|
5920
5992
|
} catch {
|
|
5921
|
-
throw new TypeError("inline
|
|
5993
|
+
throw new TypeError("sealed inline payload has invalid base64url material");
|
|
5922
5994
|
}
|
|
5923
|
-
if (ciphertext.byteLength === 0 || ciphertext.byteLength > MAX_INLINE_BYTES) throw new TypeError("inline ciphertext is outside the allowed size");
|
|
5995
|
+
if (ciphertext.byteLength === 0 || ciphertext.byteLength > MAX_INLINE_BYTES) throw new TypeError("sealed inline ciphertext is outside the allowed size");
|
|
5996
|
+
if (key32.byteLength !== KEY_LENGTH2) throw new TypeError("sealed inline key must be 32 bytes");
|
|
5924
5997
|
assertCanonicalCid(record.cid);
|
|
5925
|
-
if (
|
|
5926
|
-
|
|
5927
|
-
try {
|
|
5928
|
-
key32 = record.k === void 0 ? void 0 : fromBase64Url(record.k);
|
|
5929
|
-
} catch {
|
|
5930
|
-
throw new TypeError("inline key is not canonical base64url");
|
|
5931
|
-
}
|
|
5932
|
-
if (key32 !== void 0 && key32.length !== KEY_LENGTH2) throw new TypeError("inline key must be 32 bytes");
|
|
5933
|
-
return { kind: "inline", ciphertextCid: record.cid, ciphertext, ...key32 === void 0 ? {} : { key32 } };
|
|
5998
|
+
if (await computeCid(ciphertext) !== record.cid) throw new TypeError("sealed inline ciphertext does not match its CID");
|
|
5999
|
+
return { kind: "inline", ciphertextCid: record.cid, ciphertext, key32 };
|
|
5934
6000
|
}
|
|
5935
6001
|
|
|
5936
6002
|
// src/receive.ts
|
|
@@ -5978,19 +6044,17 @@ async function resolvePolicyShare(link, options) {
|
|
|
5978
6044
|
let parsed;
|
|
5979
6045
|
try {
|
|
5980
6046
|
url = new URL(link);
|
|
5981
|
-
|
|
5982
|
-
parsed = parseInlineShareUrl(link, { ...options.expectedOrigin === void 0 ? {} : { expectedOrigin: options.expectedOrigin } });
|
|
5983
|
-
if (parsed.key32 !== void 0) throw new Error("secret inline transport is retired");
|
|
6047
|
+
parsed = await parseSealedInlineShareUrl(link, { ...options.expectedOrigin === void 0 ? {} : { expectedOrigin: options.expectedOrigin } });
|
|
5984
6048
|
} catch {
|
|
5985
6049
|
throw new ShareReceiveError("invalid-link", "share link format is invalid");
|
|
5986
6050
|
}
|
|
5987
6051
|
const limit = options.maxSealedBlobBytes ?? DEFAULT_MAX_SEALED_BLOB_BYTES;
|
|
5988
|
-
if (!Number.isSafeInteger(limit) || limit <= 0 || parsed.ciphertext.byteLength > limit) throw new ShareReceiveError("max-bytes-exceeded", "
|
|
5989
|
-
if (await computeCid(parsed.ciphertext) !== parsed.ciphertextCid) throw new ShareReceiveError("cid-mismatch", "
|
|
6052
|
+
if (!Number.isSafeInteger(limit) || limit <= 0 || parsed.ciphertext.byteLength > limit) throw new ShareReceiveError("max-bytes-exceeded", "sealed policy envelope exceeds the configured byte limit");
|
|
6053
|
+
if (await computeCid(parsed.ciphertext) !== parsed.ciphertextCid) throw new ShareReceiveError("cid-mismatch", "sealed envelope bytes do not match the link CID");
|
|
5990
6054
|
options.signal?.throwIfAborted();
|
|
5991
6055
|
let envelope;
|
|
5992
6056
|
try {
|
|
5993
|
-
const encoded = new TextDecoder("utf-8", { fatal: true }).decode(parsed.ciphertext);
|
|
6057
|
+
const encoded = new TextDecoder("utf-8", { fatal: true }).decode(await open(parsed.ciphertext, parsed.key32));
|
|
5994
6058
|
const value = JSON.parse(encoded);
|
|
5995
6059
|
if (canonicalize(value) !== encoded) throw new Error("non-canonical envelope");
|
|
5996
6060
|
envelope = shareEnvelopeV3Schema.parse(value);
|
|
@@ -6665,10 +6729,23 @@ function publicationResult(input) {
|
|
|
6665
6729
|
Object.defineProperty(result, "deliveryMaterial", { enumerable: false, value: input.deliveryMaterial });
|
|
6666
6730
|
return result;
|
|
6667
6731
|
}
|
|
6732
|
+
var EMAIL_DOMAIN_PROFILE = { id: "tinycloud.email-domain-proof/v1", version: 1 };
|
|
6733
|
+
var EMAIL_CREDENTIAL_TYPE = { id: "opencredentials.email/v1", version: 1 };
|
|
6734
|
+
var EMAIL_DOMAIN_DESCRIPTOR_DIGEST = "33X5mAkZZgApdD3xh_T-3KS5moop0J2Nloi2nqWsdWY";
|
|
6735
|
+
function assertEmailDomainPolicy(options, domain) {
|
|
6736
|
+
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");
|
|
6737
|
+
if (options.deliveryEmail !== void 0) throw new TypeError("email-domain shares are not emailed");
|
|
6738
|
+
const commitment = options.credentialRequirement;
|
|
6739
|
+
if (commitment === void 0) throw new TypeError("email-domain shares require a credential requirement");
|
|
6740
|
+
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");
|
|
6741
|
+
const expected = { type: "TinyCloudCredentialRequirement", version: 1, profile: EMAIL_DOMAIN_PROFILE, credentialType: EMAIL_CREDENTIAL_TYPE, claims: { emailDomain: domain }, maxAgeSeconds: 300 };
|
|
6742
|
+
if (toBase64Url((0, import_sha2562.sha256)(textEncoder.encode(canonicalize(expected)))) !== commitment.requirementDigest) throw new TypeError("credential requirement is not bound to the email domain");
|
|
6743
|
+
}
|
|
6668
6744
|
async function publishAddressedShare(options) {
|
|
6669
6745
|
assertSafeInput(options);
|
|
6670
6746
|
const target = normalizeShareTarget(options.target);
|
|
6671
6747
|
if (target.kind === "bearer") throw new TypeError("addressed target is required");
|
|
6748
|
+
if (target.kind === "emailDomain") assertEmailDomainPolicy(options, target.domain);
|
|
6672
6749
|
const expiry = rfc3339Seconds(options.expiresAt);
|
|
6673
6750
|
const matcher = targetMatcher(target);
|
|
6674
6751
|
const capabilities = sortCanonical([
|
|
@@ -6741,14 +6818,25 @@ async function publishAddressedShare(options) {
|
|
|
6741
6818
|
const envelope = { ...unsigned, signature: { signerDid: options.authority.ownerDid, algorithm: "Ed25519", value: toBase64Url(envelopeSignature) } };
|
|
6742
6819
|
shareEnvelopeV3Schema.parse(envelope);
|
|
6743
6820
|
const envelopeBytes = textEncoder.encode(canonicalize(envelope));
|
|
6744
|
-
const
|
|
6745
|
-
const
|
|
6746
|
-
const
|
|
6821
|
+
const envelopeKeyBytes = generateKey();
|
|
6822
|
+
const sealed = await seal(envelopeBytes, envelopeKeyBytes);
|
|
6823
|
+
const envelopeKey = toBase64Url(envelopeKeyBytes);
|
|
6824
|
+
const url = await encodeSealedInlineShareUrl({
|
|
6825
|
+
origin: options.shareOrigin,
|
|
6826
|
+
ciphertext: sealed.blob,
|
|
6827
|
+
key32: envelopeKeyBytes
|
|
6828
|
+
});
|
|
6829
|
+
const deliveryMaterial = {
|
|
6830
|
+
envelope,
|
|
6831
|
+
shareCid: sealed.cid,
|
|
6832
|
+
sealedEnvelope: toBase64Url(sealed.blob),
|
|
6833
|
+
envelopeKey
|
|
6834
|
+
};
|
|
6747
6835
|
options.onDeliveryMaterial?.(deliveryMaterial);
|
|
6748
6836
|
return publicationResult({
|
|
6749
6837
|
options,
|
|
6750
6838
|
url,
|
|
6751
|
-
envelopeCid,
|
|
6839
|
+
envelopeCid: sealed.cid,
|
|
6752
6840
|
matcher,
|
|
6753
6841
|
policyCid: created.policyCid,
|
|
6754
6842
|
policyRootCid: policyRoot.cid,
|
|
@@ -7805,6 +7893,119 @@ async function defaultIdempotencyKey(shareId, recipient) {
|
|
|
7805
7893
|
return `tinycloud-share:${shareId}:${toBase64Url(digest3)}`;
|
|
7806
7894
|
}
|
|
7807
7895
|
|
|
7896
|
+
// src/credential-invitation.ts
|
|
7897
|
+
var CredentialInvitationError = class extends Error {
|
|
7898
|
+
constructor(code3, message) {
|
|
7899
|
+
super(message);
|
|
7900
|
+
this.code = code3;
|
|
7901
|
+
this.name = "CredentialInvitationError";
|
|
7902
|
+
}
|
|
7903
|
+
};
|
|
7904
|
+
function canonicalOrigin(value) {
|
|
7905
|
+
let url;
|
|
7906
|
+
try {
|
|
7907
|
+
url = new URL(value);
|
|
7908
|
+
} catch {
|
|
7909
|
+
throw new CredentialInvitationError(
|
|
7910
|
+
"invalid-origin",
|
|
7911
|
+
"credential invitation origin is invalid"
|
|
7912
|
+
);
|
|
7913
|
+
}
|
|
7914
|
+
if (url.origin !== value || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "" || url.protocol !== "https:")
|
|
7915
|
+
throw new CredentialInvitationError(
|
|
7916
|
+
"invalid-origin",
|
|
7917
|
+
"credential invitation origin is invalid"
|
|
7918
|
+
);
|
|
7919
|
+
return url.origin;
|
|
7920
|
+
}
|
|
7921
|
+
function object2(value) {
|
|
7922
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7923
|
+
}
|
|
7924
|
+
async function validateReceipt(receipt, shareUrl) {
|
|
7925
|
+
if (!object2(receipt) || !object2(receipt.request) || !object2(receipt.admission) || !object2(receipt.proof)) {
|
|
7926
|
+
throw new CredentialInvitationError(
|
|
7927
|
+
"invalid-receipt",
|
|
7928
|
+
"credential invitation receipt is invalid"
|
|
7929
|
+
);
|
|
7930
|
+
}
|
|
7931
|
+
if (receipt.request.returnLink !== shareUrl) {
|
|
7932
|
+
throw new CredentialInvitationError(
|
|
7933
|
+
"invalid-receipt",
|
|
7934
|
+
"credential invitation is not bound to the share link"
|
|
7935
|
+
);
|
|
7936
|
+
}
|
|
7937
|
+
let link;
|
|
7938
|
+
try {
|
|
7939
|
+
link = new URL(shareUrl);
|
|
7940
|
+
} catch {
|
|
7941
|
+
throw new CredentialInvitationError(
|
|
7942
|
+
"invalid-receipt",
|
|
7943
|
+
"credential invitation share link is invalid"
|
|
7944
|
+
);
|
|
7945
|
+
}
|
|
7946
|
+
if (link.search !== "" || link.pathname !== "/s/inline") {
|
|
7947
|
+
throw new CredentialInvitationError(
|
|
7948
|
+
"invalid-receipt",
|
|
7949
|
+
"credential invitation share link must be sealed inline"
|
|
7950
|
+
);
|
|
7951
|
+
}
|
|
7952
|
+
try {
|
|
7953
|
+
await parseSealedInlineShareUrl(shareUrl);
|
|
7954
|
+
} catch {
|
|
7955
|
+
throw new CredentialInvitationError(
|
|
7956
|
+
"invalid-receipt",
|
|
7957
|
+
"credential invitation share link is invalid"
|
|
7958
|
+
);
|
|
7959
|
+
}
|
|
7960
|
+
}
|
|
7961
|
+
async function deliverCredentialInvitation(input) {
|
|
7962
|
+
const credentialsOrigin = canonicalOrigin(input.credentialsOrigin);
|
|
7963
|
+
await validateReceipt(input.receipt, input.shareUrl);
|
|
7964
|
+
const fetchFn = input.fetchFn ?? globalThis.fetch;
|
|
7965
|
+
let response;
|
|
7966
|
+
try {
|
|
7967
|
+
response = await fetchFn(`${credentialsOrigin}/v1/credential-invitations`, {
|
|
7968
|
+
method: "POST",
|
|
7969
|
+
credentials: "omit",
|
|
7970
|
+
redirect: "error",
|
|
7971
|
+
referrerPolicy: "no-referrer",
|
|
7972
|
+
headers: {
|
|
7973
|
+
accept: "application/json",
|
|
7974
|
+
"content-type": "application/json"
|
|
7975
|
+
},
|
|
7976
|
+
body: JSON.stringify(input.receipt),
|
|
7977
|
+
...input.signal === void 0 ? {} : { signal: input.signal }
|
|
7978
|
+
});
|
|
7979
|
+
} catch {
|
|
7980
|
+
throw new CredentialInvitationError(
|
|
7981
|
+
"transport",
|
|
7982
|
+
"credential invitation delivery is unavailable"
|
|
7983
|
+
);
|
|
7984
|
+
}
|
|
7985
|
+
if (response.status !== 202) {
|
|
7986
|
+
throw new CredentialInvitationError(
|
|
7987
|
+
"rejected",
|
|
7988
|
+
"credential invitation delivery was not accepted"
|
|
7989
|
+
);
|
|
7990
|
+
}
|
|
7991
|
+
let body;
|
|
7992
|
+
try {
|
|
7993
|
+
body = await response.json();
|
|
7994
|
+
} catch {
|
|
7995
|
+
throw new CredentialInvitationError(
|
|
7996
|
+
"invalid-response",
|
|
7997
|
+
"credential invitation response is invalid"
|
|
7998
|
+
);
|
|
7999
|
+
}
|
|
8000
|
+
if (!object2(body) || Object.keys(body).length !== 1 || body.status !== "accepted") {
|
|
8001
|
+
throw new CredentialInvitationError(
|
|
8002
|
+
"invalid-response",
|
|
8003
|
+
"credential invitation response is invalid"
|
|
8004
|
+
);
|
|
8005
|
+
}
|
|
8006
|
+
return { status: "accepted" };
|
|
8007
|
+
}
|
|
8008
|
+
|
|
7808
8009
|
// src/lifecycle.ts
|
|
7809
8010
|
function targetKind2(record) {
|
|
7810
8011
|
if (record.targetKind !== void 0) return record.targetKind;
|
|
@@ -7911,7 +8112,7 @@ var DOMAIN = SHARE_V2_PROTOCOL.challengeDomain;
|
|
|
7911
8112
|
var PRESENTATION_DOMAIN = SHARE_V2_PROTOCOL.sessionDomain;
|
|
7912
8113
|
var SESSION_DOMAIN = SHARE_V2_PROTOCOL.sessionDomain;
|
|
7913
8114
|
var INVOCATION_DOMAIN = SHARE_V2_PROTOCOL.invocationDomain;
|
|
7914
|
-
function
|
|
8115
|
+
function object3(value, label) {
|
|
7915
8116
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${label} is invalid`);
|
|
7916
8117
|
return value;
|
|
7917
8118
|
}
|
|
@@ -7970,11 +8171,11 @@ function nativeAction(action) {
|
|
|
7970
8171
|
return action === "list" ? "tinycloud.kv/list" : action === "edit" ? "tinycloud.kv/put" : "tinycloud.kv/get";
|
|
7971
8172
|
}
|
|
7972
8173
|
function policyAttenuationForV3(envelope) {
|
|
7973
|
-
const policy =
|
|
8174
|
+
const policy = object3(envelope.policy, "v3 policy");
|
|
7974
8175
|
if (!Array.isArray(policy.capabilityCeiling)) throw new Error("v3 policy capability ceiling is invalid");
|
|
7975
8176
|
const attenuation = {};
|
|
7976
8177
|
for (const raw of policy.capabilityCeiling) {
|
|
7977
|
-
const capability =
|
|
8178
|
+
const capability = object3(raw, "v3 policy capability");
|
|
7978
8179
|
if (typeof capability.resource !== "string" || attenuation[capability.resource] !== void 0) throw new Error("v3 policy capability is invalid");
|
|
7979
8180
|
if (capability.kind === "encryption") {
|
|
7980
8181
|
if (capability.action !== "tinycloud.encryption/decrypt") throw new Error("v3 policy capability is invalid");
|
|
@@ -8032,8 +8233,8 @@ function verifyV3PolicyAuthorization(input) {
|
|
|
8032
8233
|
const factKeys = Object.keys(fact);
|
|
8033
8234
|
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);
|
|
8034
8235
|
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);
|
|
8035
|
-
const binding =
|
|
8036
|
-
const policy =
|
|
8236
|
+
const binding = object3(input.envelope.attestedEnforcerBinding, "v3 attested enforcer binding");
|
|
8237
|
+
const policy = object3(input.envelope.policy, "v3 policy");
|
|
8037
8238
|
const now = Math.floor(Date.now() / 1e3);
|
|
8038
8239
|
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");
|
|
8039
8240
|
return compact;
|
|
@@ -8124,7 +8325,7 @@ var ShareRecipientClient = class {
|
|
|
8124
8325
|
if (this.session === void 0) await this.establish(envelope);
|
|
8125
8326
|
const response = await this.nativeInvoke({ action: "get", resource: envelope.resource });
|
|
8126
8327
|
if (!response.ok) throw new Error("share recipient read was rejected");
|
|
8127
|
-
const value =
|
|
8328
|
+
const value = object3(await response.json(), "share read response");
|
|
8128
8329
|
const expectedAction = nativeAction("read");
|
|
8129
8330
|
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");
|
|
8130
8331
|
const content = fromBase64Url(value.content);
|
|
@@ -8139,7 +8340,7 @@ var ShareRecipientClient = class {
|
|
|
8139
8340
|
}
|
|
8140
8341
|
async establishV3(envelope) {
|
|
8141
8342
|
if (this.options.buildPresentation === void 0) throw new Error("share presentation builder is required");
|
|
8142
|
-
const attestedEnforcer =
|
|
8343
|
+
const attestedEnforcer = object3(envelope.attestedEnforcerBinding, "v3 attested enforcer binding");
|
|
8143
8344
|
if (attestedEnforcer.enforcerDid !== envelope.target.nodeAudience || typeof attestedEnforcer.nodeAudience !== "string") throw new Error("v3 attested enforcer binding mismatch");
|
|
8144
8345
|
try {
|
|
8145
8346
|
ed25519PublicKeyFromDidKey(attestedEnforcer.nodeAudience);
|
|
@@ -8154,7 +8355,7 @@ var ShareRecipientClient = class {
|
|
|
8154
8355
|
...this.options.signal === void 0 ? {} : { signal: this.options.signal }
|
|
8155
8356
|
});
|
|
8156
8357
|
if (!challengeResponse.ok) throw new Error(`v3 policy challenge rejected (${challengeResponse.status})`);
|
|
8157
|
-
const challenge =
|
|
8358
|
+
const challenge = object3(await challengeResponse.json(), "v3 policy challenge");
|
|
8158
8359
|
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");
|
|
8159
8360
|
const material = await this.options.buildPresentation({
|
|
8160
8361
|
challenge,
|
|
@@ -8175,7 +8376,7 @@ var ShareRecipientClient = class {
|
|
|
8175
8376
|
...this.options.signal === void 0 ? {} : { signal: this.options.signal }
|
|
8176
8377
|
});
|
|
8177
8378
|
if (!delegationResponse.ok) throw new Error(`v3 policy delegation rejected (${delegationResponse.status})`);
|
|
8178
|
-
const delegation =
|
|
8379
|
+
const delegation = object3(await delegationResponse.json(), "v3 policy delegation");
|
|
8179
8380
|
if (delegation.admitted !== true || typeof delegation.sessionCid !== "string" || typeof delegation.authorization !== "string") throw new Error("v3 policy delegation response is not admitted");
|
|
8180
8381
|
const compact = verifyV3PolicyAuthorization({ authorization: delegation.authorization, cid: delegation.sessionCid, envelope, holderDid: this.options.holderDid });
|
|
8181
8382
|
const fact = compact.payload.fct[0];
|
|
@@ -8217,7 +8418,7 @@ var ShareRecipientClient = class {
|
|
|
8217
8418
|
this.options.onStage?.("decryption");
|
|
8218
8419
|
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 } });
|
|
8219
8420
|
if (!response.ok) throw new Error(`v3 decrypt invocation rejected (${response.status})`);
|
|
8220
|
-
const value =
|
|
8421
|
+
const value = object3(await response.json(), "v3 decrypt response");
|
|
8221
8422
|
const allowed = ["type", "targetNode", "networkId", "invocationCid", "encryptedSymmetricKeyHash", "receiverPublicKeyHash", "wrappedKey", "alg", "keyVersion", "requestHash", "nodeId", "nodeSignature"];
|
|
8222
8423
|
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((0, import_sha2565.sha256)(new TextEncoder().encode(`${invocation.cid}${bodyHash}`))) || typeof value.wrappedKey !== "string" || typeof value.nodeSignature !== "string") throw new Error("v3 decrypt response binding is invalid");
|
|
8223
8424
|
const unsigned = { ...value };
|
|
@@ -8305,11 +8506,11 @@ function createAddressedAuthorization(input) {
|
|
|
8305
8506
|
},
|
|
8306
8507
|
async verifyResult({ envelope, value, proof }) {
|
|
8307
8508
|
try {
|
|
8308
|
-
const wrapper =
|
|
8309
|
-
const response =
|
|
8509
|
+
const wrapper = object3(proof, "share read proof");
|
|
8510
|
+
const response = object3(wrapper.response, "share read response proof");
|
|
8310
8511
|
const envelopeAction = nativeAction("read");
|
|
8311
8512
|
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;
|
|
8312
|
-
const detached =
|
|
8513
|
+
const detached = object3(wrapper.detached, "share read detached proof");
|
|
8313
8514
|
if (input.trustedNode === void 0 || detached.alg !== "EdDSA" || detached.kid !== input.trustedNode.invitationKid) return false;
|
|
8314
8515
|
const unsigned = { ...response };
|
|
8315
8516
|
delete unsigned.proof;
|
|
@@ -8378,6 +8579,7 @@ async function openNativeShare(sharing, link) {
|
|
|
8378
8579
|
}
|
|
8379
8580
|
// Annotate the CommonJS export names for ESM import in node:
|
|
8380
8581
|
0 && (module.exports = {
|
|
8582
|
+
CredentialInvitationError,
|
|
8381
8583
|
DEFAULT_MAX_CONTENT_BLOB_BYTES,
|
|
8382
8584
|
DEFAULT_MAX_SEALED_BLOB_BYTES,
|
|
8383
8585
|
DEFAULT_SHARE_LIFETIME_MS,
|
|
@@ -8410,6 +8612,7 @@ async function openNativeShare(sharing, link) {
|
|
|
8410
8612
|
createNativeShare,
|
|
8411
8613
|
createPolicyEnforcementDelegation,
|
|
8412
8614
|
createShareV2HolderBindingArtifact,
|
|
8615
|
+
deliverCredentialInvitation,
|
|
8413
8616
|
fromBase64Url,
|
|
8414
8617
|
historyRecordForPublishedShare,
|
|
8415
8618
|
inspectShare,
|