@tinycloud/share-sdk 1.4.1-beta.0 → 1.4.1-beta.2
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/{addressed-publish-BP6nsgRq.d.cts → addressed-publish-BcvisG4a.d.cts} +7 -0
- package/dist/{addressed-publish-CpkwV-Au.d.ts → addressed-publish-CwTTn9Jn.d.ts} +7 -0
- package/dist/artifact.cjs +27 -4
- package/dist/artifact.cjs.map +1 -1
- package/dist/artifact.js +27 -4
- package/dist/artifact.js.map +1 -1
- package/dist/index.cjs +302 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +302 -49
- package/dist/index.js.map +1 -1
- package/dist/share-index.cjs +27 -4
- package/dist/share-index.cjs.map +1 -1
- package/dist/share-index.js +27 -4
- package/dist/share-index.js.map +1 -1
- package/dist/share-root.cjs +27 -4
- package/dist/share-root.cjs.map +1 -1
- package/dist/share-root.d.cts +1 -1
- package/dist/share-root.d.ts +1 -1
- package/dist/share-root.js +27 -4
- package/dist/share-root.js.map +1 -1
- package/dist/{web-recipient-entry-CiUzdSQs.d.cts → web-recipient-entry-Bn3yRB7g.d.cts} +47 -6
- package/dist/{web-recipient-entry-DVyTQBl0.d.ts → web-recipient-entry-CTFoIH8l.d.ts} +47 -6
- package/dist/web-recipient.cjs +277 -47
- package/dist/web-recipient.cjs.map +1 -1
- package/dist/web-recipient.d.cts +1 -1
- package/dist/web-recipient.d.ts +1 -1
- package/dist/web-recipient.js +277 -47
- package/dist/web-recipient.js.map +1 -1
- package/package.json +2 -2
package/dist/web-recipient.cjs
CHANGED
|
@@ -5256,6 +5256,23 @@ var unifiedEncryptionNetworkSchema = external_exports.string().refine((value) =>
|
|
|
5256
5256
|
const network = separator < 0 ? "" : rest.slice(separator + 1);
|
|
5257
5257
|
return owner.startsWith("did:") && owner.length > 4 && network.length > 0 && !/[:/%?#\s]/.test(network);
|
|
5258
5258
|
}, { message: "expected canonical TinyCloud encryption network" });
|
|
5259
|
+
var unifiedEncryptionResourceSchema = external_exports.string().refine((value) => {
|
|
5260
|
+
const prefix = "urn:tinycloud:encryption:";
|
|
5261
|
+
if (!value.startsWith(prefix)) return false;
|
|
5262
|
+
const match = /^(did:.+):([^/:?#\s]+)(?:\/(.+))?$/.exec(value.slice(prefix.length));
|
|
5263
|
+
if (match === null) return false;
|
|
5264
|
+
const path = match[3];
|
|
5265
|
+
return path === void 0 || path.split("/").every(
|
|
5266
|
+
(segment) => segment.length > 0 && segment !== "." && segment !== ".." && !/[\\:%?#\s]/.test(segment)
|
|
5267
|
+
);
|
|
5268
|
+
}, { message: "expected a canonical TinyCloud encryption resource" });
|
|
5269
|
+
var unifiedEncryptionKeyPathSchema = external_exports.string().regex(/^share\/[^/]+$/).refine(
|
|
5270
|
+
(value) => {
|
|
5271
|
+
const segment = value.slice("share/".length);
|
|
5272
|
+
return segment !== "." && segment !== ".." && !/[\\:%?#\s]/.test(segment);
|
|
5273
|
+
},
|
|
5274
|
+
{ message: "expected an exact share key path" }
|
|
5275
|
+
);
|
|
5259
5276
|
var unifiedKvCapabilitySchema = external_exports.object({
|
|
5260
5277
|
kind: external_exports.literal("kv"),
|
|
5261
5278
|
resource: unifiedResourceSchema,
|
|
@@ -5269,7 +5286,7 @@ var unifiedKvCapabilitySchema = external_exports.object({
|
|
|
5269
5286
|
}).strict();
|
|
5270
5287
|
var unifiedEncryptionCapabilitySchema = external_exports.object({
|
|
5271
5288
|
kind: external_exports.literal("encryption"),
|
|
5272
|
-
resource:
|
|
5289
|
+
resource: unifiedEncryptionResourceSchema,
|
|
5273
5290
|
action: external_exports.literal("tinycloud.encryption/decrypt")
|
|
5274
5291
|
}).strict();
|
|
5275
5292
|
var unifiedCapabilitySchema = external_exports.union([unifiedKvCapabilitySchema, unifiedEncryptionCapabilitySchema]);
|
|
@@ -5278,6 +5295,7 @@ var unifiedContentSourceSchema = external_exports.object({
|
|
|
5278
5295
|
kvResource: unifiedResourceSchema,
|
|
5279
5296
|
selector: external_exports.union([external_exports.literal("exact"), external_exports.literal("prefix")]),
|
|
5280
5297
|
encryptionNetwork: unifiedEncryptionNetworkSchema,
|
|
5298
|
+
encryptionKeyPath: unifiedEncryptionKeyPathSchema.optional(),
|
|
5281
5299
|
encryptedSymmetricKeyDigestHex: external_exports.string().regex(/^[0-9a-f]{64}$/),
|
|
5282
5300
|
keyVersion: external_exports.number().int().positive(),
|
|
5283
5301
|
mode: external_exports.union([external_exports.literal("mutable"), external_exports.literal("immutable")]),
|
|
@@ -5395,22 +5413,71 @@ function validateV3Invariants(value, ctx) {
|
|
|
5395
5413
|
if (value.attestedEnforcerBinding.nodeAudience !== value.target.nodeAudience || value.attestedEnforcerBinding.signature.signerDid !== value.attestedEnforcerBinding.nodeAudience || Date.parse(value.attestedEnforcerBinding.expiresAt) < Date.parse(value.expiry)) {
|
|
5396
5414
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["attestedEnforcerBinding"], message: "enforcer binding does not cover the target and share lifetime" });
|
|
5397
5415
|
}
|
|
5398
|
-
|
|
5416
|
+
const encryptionKeyPath = value.contentSource.encryptionKeyPath;
|
|
5417
|
+
if (encryptionKeyPath !== void 0 && encryptionKeyPath !== `share/${value.contentSource.shareId}`) {
|
|
5418
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["contentSource", "encryptionKeyPath"], message: "share key path must name this share" });
|
|
5419
|
+
}
|
|
5420
|
+
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.encryptionKeyPath !== encryptionKeyPath || 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) {
|
|
5399
5421
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["contentSource"], message: "content source is not bound to policy" });
|
|
5400
5422
|
}
|
|
5401
5423
|
if (value.policy.signature.suite !== "Ed25519" || value.policy.signature.signerDid !== value.policy.ownerDid) {
|
|
5402
5424
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["policy", "signature"], message: "policy must be signed by its owner" });
|
|
5403
5425
|
}
|
|
5404
5426
|
const kvCapabilities = value.policy.capabilityCeiling.filter((capability) => capability.kind === "kv");
|
|
5405
|
-
const
|
|
5427
|
+
const expectedEncryptionResource = encryptionKeyPath === void 0 ? value.encryptionNetwork : `${value.encryptionNetwork}/${encryptionKeyPath}`;
|
|
5428
|
+
const decryptCapabilities = value.policy.capabilityCeiling.filter((capability) => capability.kind === "encryption" && capability.resource === expectedEncryptionResource && capability.action === "tinycloud.encryption/decrypt");
|
|
5406
5429
|
if (value.policy.capabilityCeiling.length !== 2 || kvCapabilities.length !== 1 || decryptCapabilities.length !== 1 || kvCapabilities[0]?.resource !== value.contentSource.kvResource || kvCapabilities[0]?.selector !== value.contentSource.selector) {
|
|
5407
|
-
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["policy", "capabilityCeiling"], message: "policy ceiling must contain exact decrypt
|
|
5430
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["policy", "capabilityCeiling"], message: "policy ceiling must contain the exact decrypt resource" });
|
|
5408
5431
|
}
|
|
5409
5432
|
}
|
|
5410
5433
|
var unsignedShareEnvelopeV3Schema = unsignedShareEnvelopeV3BaseSchema.superRefine(validateV3Invariants);
|
|
5411
5434
|
var shareEnvelopeV3Schema = unsignedShareEnvelopeV3BaseSchema.extend({ signature: signatureSchema }).strict().superRefine(validateV3Invariants);
|
|
5412
5435
|
var ENVELOPE_AAD_LABEL = "tinycloud-share-envelope-v1";
|
|
5436
|
+
var SEALED_BLOB_VERSION = 1;
|
|
5413
5437
|
var AAD = utf8Bytes(ENVELOPE_AAD_LABEL);
|
|
5438
|
+
var KEY_LENGTH = 32;
|
|
5439
|
+
var NONCE_LENGTH = 12;
|
|
5440
|
+
var TAG_LENGTH = 16;
|
|
5441
|
+
var HEADER_LENGTH = 1;
|
|
5442
|
+
function assertKey(key32) {
|
|
5443
|
+
if (key32.length !== KEY_LENGTH) {
|
|
5444
|
+
throw new TypeError(`key must be ${KEY_LENGTH} bytes, got ${key32.length}`);
|
|
5445
|
+
}
|
|
5446
|
+
}
|
|
5447
|
+
async function importAesKey(key32, usage) {
|
|
5448
|
+
assertKey(key32);
|
|
5449
|
+
return globalThis.crypto.subtle.importKey(
|
|
5450
|
+
"raw",
|
|
5451
|
+
key32,
|
|
5452
|
+
"AES-GCM",
|
|
5453
|
+
false,
|
|
5454
|
+
[usage]
|
|
5455
|
+
);
|
|
5456
|
+
}
|
|
5457
|
+
async function open(blob, key32) {
|
|
5458
|
+
if (blob.length < HEADER_LENGTH + NONCE_LENGTH + TAG_LENGTH) {
|
|
5459
|
+
throw new TypeError(`sealed blob too short: ${blob.length} bytes`);
|
|
5460
|
+
}
|
|
5461
|
+
if (blob[0] !== SEALED_BLOB_VERSION) {
|
|
5462
|
+
throw new TypeError(`unknown sealed blob version: ${blob[0]}`);
|
|
5463
|
+
}
|
|
5464
|
+
const nonce = blob.subarray(HEADER_LENGTH, HEADER_LENGTH + NONCE_LENGTH);
|
|
5465
|
+
const ciphertext = blob.subarray(HEADER_LENGTH + NONCE_LENGTH);
|
|
5466
|
+
return decryptEnvelope(nonce, ciphertext, key32);
|
|
5467
|
+
}
|
|
5468
|
+
async function decryptEnvelope(nonce, ciphertext, key32) {
|
|
5469
|
+
if (nonce.length !== NONCE_LENGTH) {
|
|
5470
|
+
throw new TypeError(`nonce must be ${NONCE_LENGTH} bytes, got ${nonce.length}`);
|
|
5471
|
+
}
|
|
5472
|
+
const key = await importAesKey(key32, "decrypt");
|
|
5473
|
+
return new Uint8Array(
|
|
5474
|
+
await globalThis.crypto.subtle.decrypt(
|
|
5475
|
+
{ name: "AES-GCM", iv: nonce, additionalData: AAD },
|
|
5476
|
+
key,
|
|
5477
|
+
ciphertext
|
|
5478
|
+
)
|
|
5479
|
+
);
|
|
5480
|
+
}
|
|
5414
5481
|
function isPlainRecord(value) {
|
|
5415
5482
|
const proto = Object.getPrototypeOf(value);
|
|
5416
5483
|
return proto === Object.prototype || proto === null;
|
|
@@ -5541,6 +5608,7 @@ function equal(left, right) {
|
|
|
5541
5608
|
return left.length === right.length && left.every((byte, index) => byte === right[index]);
|
|
5542
5609
|
}
|
|
5543
5610
|
var ED25519_VERIFY_OPTS2 = { zip215: false };
|
|
5611
|
+
var ENVELOPE_V2_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v2\0";
|
|
5544
5612
|
var ENVELOPE_V3_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v3\0";
|
|
5545
5613
|
var POLICY_V1_SIGNATURE_DOMAIN = "xyz.tinycloud.policy/policy/v1\0";
|
|
5546
5614
|
var POLICY_V2_SIGNATURE_DOMAIN = "xyz.tinycloud.policy/policy/v2\0";
|
|
@@ -5548,6 +5616,14 @@ var CONTENT_SOURCE_V1_DOMAIN = "xyz.tinycloud.policy/ContentSource/v1\0";
|
|
|
5548
5616
|
var POLICY_CAPABILITY_V1_DOMAIN = "xyz.tinycloud.policy/PolicyCapability/v1\0";
|
|
5549
5617
|
var NATIVE_PROJECTION_V1_DOMAIN = "xyz.tinycloud.policy/NativeProjection/v1\0";
|
|
5550
5618
|
var ATTESTED_ENFORCER_V2_DOMAIN = "xyz.tinycloud.policy/AttestedEnforcerBinding/v2\0";
|
|
5619
|
+
function signingBytesV2(unsigned) {
|
|
5620
|
+
const domain = utf8Bytes(ENVELOPE_V2_SIGNATURE_DOMAIN);
|
|
5621
|
+
const body = utf8Bytes(canonicalize(unsigned));
|
|
5622
|
+
const bytes = new Uint8Array(domain.length + body.length);
|
|
5623
|
+
bytes.set(domain);
|
|
5624
|
+
bytes.set(body, domain.length);
|
|
5625
|
+
return bytes;
|
|
5626
|
+
}
|
|
5551
5627
|
function signingBytesV3(unsigned) {
|
|
5552
5628
|
const domain = utf8Bytes(ENVELOPE_V3_SIGNATURE_DOMAIN);
|
|
5553
5629
|
const body = utf8Bytes(canonicalize(unsigned));
|
|
@@ -5556,6 +5632,17 @@ function signingBytesV3(unsigned) {
|
|
|
5556
5632
|
bytes.set(body, domain.length);
|
|
5557
5633
|
return bytes;
|
|
5558
5634
|
}
|
|
5635
|
+
function verifyEnvelopeV2SignatureOnly(envelope) {
|
|
5636
|
+
const parsed = shareEnvelopeV2Schema.parse(envelope);
|
|
5637
|
+
const { signature, ...unsigned } = parsed;
|
|
5638
|
+
return import_ed255193.ed25519.verify(fromBase64Url(signature.value), signingBytesV2(unsigned), ed25519PublicKeyFromDidKey(signature.signerDid), ED25519_VERIFY_OPTS2);
|
|
5639
|
+
}
|
|
5640
|
+
async function verifyEnvelopeV2(envelope, options) {
|
|
5641
|
+
const parsed = shareEnvelopeV2Schema.parse(envelope);
|
|
5642
|
+
if (parsed.signature.signerDid !== options.expectedSignerDid || !verifyEnvelopeV2SignatureOnly(parsed)) return false;
|
|
5643
|
+
if (parsed.authorizationTarget.kind === "policy") return await computeCid(fromBase64Url(parsed.authorizationTarget.policyBytes)) === parsed.authorizationTarget.policyCid;
|
|
5644
|
+
return true;
|
|
5645
|
+
}
|
|
5559
5646
|
function verifyEnvelopeV3SignatureOnly(envelope) {
|
|
5560
5647
|
const parsed = shareEnvelopeV3Schema.parse(envelope);
|
|
5561
5648
|
const { signature, ...unsigned } = parsed;
|
|
@@ -5642,6 +5729,52 @@ function base32Lower(bytes) {
|
|
|
5642
5729
|
if (bits > 0) output += alphabet[buffer << 5 - bits & 31];
|
|
5643
5730
|
return output;
|
|
5644
5731
|
}
|
|
5732
|
+
var KEY_LENGTH2 = 32;
|
|
5733
|
+
function assertCanonicalCid(cidString) {
|
|
5734
|
+
const cid = CID.parse(cidString);
|
|
5735
|
+
if (cid.version !== 1 || cid.code !== code || cid.multihash.code !== 18 || // sha2-256 (0x12) only, at the link layer too
|
|
5736
|
+
cid.toString() !== cidString) {
|
|
5737
|
+
throw new TypeError(`not a canonical CIDv1 raw sha2-256 base32 CID: ${cidString}`);
|
|
5738
|
+
}
|
|
5739
|
+
}
|
|
5740
|
+
function parseShareUrl(url, options = {}) {
|
|
5741
|
+
const parsed = new URL(url);
|
|
5742
|
+
if (parsed.protocol !== "https:") {
|
|
5743
|
+
throw new TypeError(`share URL must be https, got ${parsed.protocol}`);
|
|
5744
|
+
}
|
|
5745
|
+
if (parsed.username !== "" || parsed.password !== "") {
|
|
5746
|
+
throw new TypeError("share URL must not carry userinfo");
|
|
5747
|
+
}
|
|
5748
|
+
if (parsed.search !== "") {
|
|
5749
|
+
throw new TypeError("share URL must not have a query string");
|
|
5750
|
+
}
|
|
5751
|
+
if (options.expectedOrigin !== void 0) {
|
|
5752
|
+
if (!isCanonicalHttpsOrigin(options.expectedOrigin)) {
|
|
5753
|
+
throw new TypeError(
|
|
5754
|
+
`expectedOrigin must be a canonical https origin, got ${options.expectedOrigin}`
|
|
5755
|
+
);
|
|
5756
|
+
}
|
|
5757
|
+
if (parsed.origin !== options.expectedOrigin) {
|
|
5758
|
+
throw new TypeError(
|
|
5759
|
+
`share URL origin ${parsed.origin} does not match expected ${options.expectedOrigin}`
|
|
5760
|
+
);
|
|
5761
|
+
}
|
|
5762
|
+
}
|
|
5763
|
+
const match = /^\/s\/([a-z2-7]+)$/.exec(parsed.pathname);
|
|
5764
|
+
if (!match || match[1] === void 0) {
|
|
5765
|
+
throw new TypeError(`not a share URL path: ${parsed.pathname}`);
|
|
5766
|
+
}
|
|
5767
|
+
const ciphertextCid = match[1];
|
|
5768
|
+
assertCanonicalCid(ciphertextCid);
|
|
5769
|
+
if (!parsed.hash.startsWith("#k=")) {
|
|
5770
|
+
throw new TypeError("share URL is missing the #k= key fragment");
|
|
5771
|
+
}
|
|
5772
|
+
const key32 = fromBase64Url(parsed.hash.slice("#k=".length));
|
|
5773
|
+
if (key32.length !== KEY_LENGTH2) {
|
|
5774
|
+
throw new TypeError(`fragment key must be ${KEY_LENGTH2} bytes, got ${key32.length}`);
|
|
5775
|
+
}
|
|
5776
|
+
return { ciphertextCid, key32 };
|
|
5777
|
+
}
|
|
5645
5778
|
|
|
5646
5779
|
// src/artifact-core.ts
|
|
5647
5780
|
var ARTIFACT_ROOT = "artifacts";
|
|
@@ -5738,7 +5871,7 @@ var ShareReceiveError = class extends Error {
|
|
|
5738
5871
|
}
|
|
5739
5872
|
};
|
|
5740
5873
|
function metadataFor(envelope, origin) {
|
|
5741
|
-
const kind = envelope.recipientMatcher.kind === "recipientDid" ? "recipientDid" : envelope.recipientMatcher.kind === "exactEmail" ? "email" : "emailDomain";
|
|
5874
|
+
const kind = envelope.recipientMatcher.kind === "recipientDid" ? "recipientDid" : envelope.recipientMatcher.kind === "exactEmail" ? "email" : envelope.recipientMatcher.kind === "emailDomain" ? "emailDomain" : "bearer";
|
|
5742
5875
|
return {
|
|
5743
5876
|
protocol: "tinycloud-share",
|
|
5744
5877
|
version: 1,
|
|
@@ -5753,7 +5886,7 @@ function metadataFor(envelope, origin) {
|
|
|
5753
5886
|
...envelope.display.filename === void 0 ? {} : { filename: envelope.display.filename },
|
|
5754
5887
|
...envelope.display.mode === void 0 ? {} : { mode: envelope.display.mode }
|
|
5755
5888
|
},
|
|
5756
|
-
...envelope.
|
|
5889
|
+
...envelope.version === 3 && envelope.artifactPin !== void 0 ? { artifact: { id: envelope.artifactPin.artifactId, version: envelope.artifactPin.version } } : {}
|
|
5757
5890
|
};
|
|
5758
5891
|
}
|
|
5759
5892
|
function receiveErrorFor(error) {
|
|
@@ -5851,10 +5984,123 @@ async function resolvePolicyShare(link, options) {
|
|
|
5851
5984
|
options.onResolvedAddressedEnvelope?.(envelope, cid);
|
|
5852
5985
|
return { envelope, origin: url.origin, cid };
|
|
5853
5986
|
}
|
|
5987
|
+
async function resolveLegacyV2(link, options) {
|
|
5988
|
+
let parsed;
|
|
5989
|
+
try {
|
|
5990
|
+
parsed = parseShareUrl(link, { ...options.expectedOrigin === void 0 ? {} : { expectedOrigin: options.expectedOrigin } });
|
|
5991
|
+
} catch {
|
|
5992
|
+
throw new ShareReceiveError("invalid-link", "legacy share link format is invalid");
|
|
5993
|
+
}
|
|
5994
|
+
try {
|
|
5995
|
+
options.signal?.throwIfAborted();
|
|
5996
|
+
const origin = new URL(link).origin;
|
|
5997
|
+
const limit = options.maxSealedBlobBytes ?? DEFAULT_MAX_SEALED_BLOB_BYTES;
|
|
5998
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope limit is invalid");
|
|
5999
|
+
let sealed;
|
|
6000
|
+
if (options.fetchBlob !== void 0) {
|
|
6001
|
+
try {
|
|
6002
|
+
sealed = await options.fetchBlob({ origin, cid: parsed.ciphertextCid, ...options.signal === void 0 ? {} : { signal: options.signal } });
|
|
6003
|
+
} catch {
|
|
6004
|
+
throw new ShareReceiveError("fetch-failed", "legacy registry unavailable");
|
|
6005
|
+
}
|
|
6006
|
+
} else {
|
|
6007
|
+
if (options.registryBaseUrl === void 0) throw new ShareReceiveError("fetch-failed", "legacy registry is required");
|
|
6008
|
+
const response = await (options.fetch ?? globalThis.fetch)(`${options.registryBaseUrl.replace(/\/+$/, "")}/ipfs/${parsed.ciphertextCid}?format=raw`, {
|
|
6009
|
+
headers: { accept: "application/vnd.ipld.raw" },
|
|
6010
|
+
redirect: "error",
|
|
6011
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
6012
|
+
});
|
|
6013
|
+
if (!response.ok) throw new ShareReceiveError("fetch-failed", "legacy registry unavailable");
|
|
6014
|
+
if (Number(response.headers.get("content-length")) > limit) throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope exceeds the size limit");
|
|
6015
|
+
const reader = response.body?.getReader();
|
|
6016
|
+
if (reader === void 0) throw new ShareReceiveError("fetch-failed", "legacy registry returned no body");
|
|
6017
|
+
const chunks = [];
|
|
6018
|
+
let length2 = 0;
|
|
6019
|
+
for (; ; ) {
|
|
6020
|
+
const part = await reader.read();
|
|
6021
|
+
if (part.done) break;
|
|
6022
|
+
length2 += part.value.length;
|
|
6023
|
+
if (length2 > limit) {
|
|
6024
|
+
await reader.cancel();
|
|
6025
|
+
throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope exceeds the size limit");
|
|
6026
|
+
}
|
|
6027
|
+
chunks.push(part.value);
|
|
6028
|
+
}
|
|
6029
|
+
sealed = new Uint8Array(length2);
|
|
6030
|
+
let offset = 0;
|
|
6031
|
+
for (const chunk of chunks) {
|
|
6032
|
+
sealed.set(chunk, offset);
|
|
6033
|
+
offset += chunk.length;
|
|
6034
|
+
}
|
|
6035
|
+
}
|
|
6036
|
+
options.signal?.throwIfAborted();
|
|
6037
|
+
if (!(sealed instanceof Uint8Array) || sealed.length > limit) throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope exceeds the size limit");
|
|
6038
|
+
if (await computeCid(sealed) !== parsed.ciphertextCid) throw new ShareReceiveError("cid-mismatch", "legacy envelope CID does not match");
|
|
6039
|
+
let plaintext;
|
|
6040
|
+
try {
|
|
6041
|
+
plaintext = await open(sealed, parsed.key32);
|
|
6042
|
+
} catch {
|
|
6043
|
+
throw new ShareReceiveError("decrypt-failed", "legacy envelope could not be opened");
|
|
6044
|
+
}
|
|
6045
|
+
let envelope;
|
|
6046
|
+
try {
|
|
6047
|
+
const encoded = new TextDecoder("utf-8", { fatal: true }).decode(plaintext);
|
|
6048
|
+
const value = JSON.parse(encoded);
|
|
6049
|
+
if (canonicalize(value) !== encoded) throw new Error("non-canonical envelope");
|
|
6050
|
+
envelope = shareEnvelopeV2Schema.parse(value);
|
|
6051
|
+
} catch {
|
|
6052
|
+
throw new ShareReceiveError("envelope-invalid", "legacy envelope is invalid");
|
|
6053
|
+
}
|
|
6054
|
+
return { envelope, origin, cid: parsed.ciphertextCid };
|
|
6055
|
+
} finally {
|
|
6056
|
+
parsed.key32.fill(0);
|
|
6057
|
+
}
|
|
6058
|
+
}
|
|
6059
|
+
function evidenceMatchesV2(envelope, evidence) {
|
|
6060
|
+
const actions = new Set(evidence.actions.map((action) => action === "read" ? "tinycloud.kv/get" : action === "list" ? "tinycloud.kv/list" : action === "edit" ? "tinycloud.kv/put" : action));
|
|
6061
|
+
const resourceCovers = evidence.resource.kind === "exact" ? envelope.resource.kind === "exact" && evidence.resource.path === envelope.resource.path : envelope.resource.path === evidence.resource.path || envelope.resource.path.startsWith(evidence.resource.path.endsWith("/") ? evidence.resource.path : `${evidence.resource.path}/`);
|
|
6062
|
+
return envelope.authorizationTarget.kind === "policy" && evidence.policyCid === 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) && resourceCovers && envelope.actions.every((action) => actions.has(action === "read" ? "tinycloud.kv/get" : action === "list" ? "tinycloud.kv/list" : "tinycloud.kv/put")) && 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);
|
|
6063
|
+
}
|
|
6064
|
+
async function verifyLegacyV2(envelope, options) {
|
|
6065
|
+
try {
|
|
6066
|
+
if (!await verifyEnvelopeV2(envelope, { expectedSignerDid: envelope.signature.signerDid })) throw new Error("signature");
|
|
6067
|
+
} catch {
|
|
6068
|
+
throw new ShareReceiveError("signature-invalid", "legacy envelope signature is invalid");
|
|
6069
|
+
}
|
|
6070
|
+
const expiry = Date.parse(envelope.expiry);
|
|
6071
|
+
if (!Number.isFinite(expiry)) throw new ShareReceiveError("envelope-invalid", "legacy share expiry is invalid");
|
|
6072
|
+
if (expiry <= (options.now?.() ?? Date.now())) throw new ShareReceiveError("expired", "legacy share has expired", { expiresAt: envelope.expiry });
|
|
6073
|
+
if (envelope.authorizationTarget.kind === "policy") {
|
|
6074
|
+
if (options.trustedPolicyAuthority === void 0) throw new ShareReceiveError("envelope-invalid", "a legacy policy authority is required");
|
|
6075
|
+
let evidence;
|
|
6076
|
+
try {
|
|
6077
|
+
evidence = await options.trustedPolicyAuthority.resolve({ policyCid: envelope.authorizationTarget.policyCid, envelope });
|
|
6078
|
+
} catch {
|
|
6079
|
+
}
|
|
6080
|
+
if (evidence === void 0 || !evidenceMatchesV2(envelope, evidence)) throw new ShareReceiveError("capability-invalid", "legacy policy evidence does not match");
|
|
6081
|
+
if (options.trustedSignerDid !== void 0 && options.trustedSignerDid !== evidence.signerDid) throw new ShareReceiveError("signature-invalid", "legacy signer is not trusted");
|
|
6082
|
+
} else if (options.trustedSignerDid === void 0 || options.trustedSignerDid !== envelope.signature.signerDid || envelope.authorizationTarget.kind === "recipientDid" && envelope.recipientMatcher.kind === "recipientDid" && envelope.authorizationTarget.did !== envelope.recipientMatcher.value) {
|
|
6083
|
+
throw new ShareReceiveError("capability-invalid", "legacy recipient authority does not match");
|
|
6084
|
+
}
|
|
6085
|
+
}
|
|
5854
6086
|
async function inspectShare(link, options = {}) {
|
|
6087
|
+
if (isLegacyV2Link(link)) {
|
|
6088
|
+
const legacy = await resolveLegacyV2(link, options);
|
|
6089
|
+
await verifyLegacyV2(legacy.envelope, options);
|
|
6090
|
+
options.onResolvedAddressedEnvelope?.(legacy.envelope, legacy.cid);
|
|
6091
|
+
return { metadata: metadataFor(legacy.envelope, legacy.origin), link: { origin: legacy.origin, cid: legacy.cid, kind: "compact" } };
|
|
6092
|
+
}
|
|
5855
6093
|
const resolved = await resolvePolicyShare(link, options);
|
|
5856
6094
|
return { metadata: metadataFor(resolved.envelope, resolved.origin), link: { origin: resolved.origin, cid: resolved.cid, kind: "policy" } };
|
|
5857
6095
|
}
|
|
6096
|
+
function isLegacyV2Link(link) {
|
|
6097
|
+
try {
|
|
6098
|
+
const url = new URL(link);
|
|
6099
|
+
return /^\/s\/bafkrei[a-z2-7]{52}$/.test(url.pathname) && url.hash.startsWith("#k=");
|
|
6100
|
+
} catch {
|
|
6101
|
+
return false;
|
|
6102
|
+
}
|
|
6103
|
+
}
|
|
5858
6104
|
|
|
5859
6105
|
// src/recipient.ts
|
|
5860
6106
|
var import_ed255194 = require("@noble/curves/ed25519");
|
|
@@ -5881,41 +6127,18 @@ function object2(value, label) {
|
|
|
5881
6127
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${label} is invalid`);
|
|
5882
6128
|
return value;
|
|
5883
6129
|
}
|
|
5884
|
-
var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
5885
6130
|
function toBase64(bytes) {
|
|
5886
|
-
|
|
5887
|
-
for (let i = 0; i < bytes.length; i += 3) {
|
|
5888
|
-
const b0 = bytes[i];
|
|
5889
|
-
const b1 = i + 1 < bytes.length ? bytes[i + 1] : 0;
|
|
5890
|
-
const b2 = i + 2 < bytes.length ? bytes[i + 2] : 0;
|
|
5891
|
-
out += BASE64_ALPHABET[b0 >> 2 & 63];
|
|
5892
|
-
out += BASE64_ALPHABET[(b0 << 4 | b1 >> 4) & 63];
|
|
5893
|
-
out += i + 1 < bytes.length ? BASE64_ALPHABET[(b1 << 2 | b2 >> 6) & 63] : "=";
|
|
5894
|
-
out += i + 2 < bytes.length ? BASE64_ALPHABET[b2 & 63] : "=";
|
|
5895
|
-
}
|
|
5896
|
-
return out;
|
|
6131
|
+
return toBase64Url(bytes).replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(bytes.length / 3) * 4, "=");
|
|
5897
6132
|
}
|
|
5898
6133
|
function fromBase64(value, label) {
|
|
5899
6134
|
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
|
|
5900
6135
|
throw new Error(`${label} is invalid`);
|
|
5901
6136
|
}
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
const v1 = BASE64_ALPHABET.indexOf(value[i + 1]);
|
|
5908
|
-
const v2 = value[i + 2] === "=" ? 0 : BASE64_ALPHABET.indexOf(value[i + 2]);
|
|
5909
|
-
const v3 = value[i + 3] === "=" ? 0 : BASE64_ALPHABET.indexOf(value[i + 3]);
|
|
5910
|
-
const b0 = v0 << 2 | v1 >> 4;
|
|
5911
|
-
const b1 = (v1 & 15) << 4 | v2 >> 2;
|
|
5912
|
-
const b2 = (v2 & 3) << 6 | v3;
|
|
5913
|
-
if (outIdx < out.length) out[outIdx++] = b0;
|
|
5914
|
-
if (outIdx < out.length) out[outIdx++] = b1;
|
|
5915
|
-
if (outIdx < out.length) out[outIdx++] = b2;
|
|
5916
|
-
}
|
|
5917
|
-
if (toBase64(out) !== value) throw new Error(`${label} is invalid`);
|
|
5918
|
-
return out;
|
|
6137
|
+
try {
|
|
6138
|
+
return fromBase64Url(value.replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_"));
|
|
6139
|
+
} catch {
|
|
6140
|
+
throw new Error(`${label} is invalid`);
|
|
6141
|
+
}
|
|
5919
6142
|
}
|
|
5920
6143
|
async function digestBytes(value) {
|
|
5921
6144
|
return toBase64Url(new Uint8Array(await crypto.subtle.digest("SHA-256", value)));
|
|
@@ -6026,16 +6249,16 @@ function matchesInitialCiphertextDigest(bytes, expected) {
|
|
|
6026
6249
|
function canonicalHashHex(value) {
|
|
6027
6250
|
return hex2((0, import_sha2562.sha256)(new TextEncoder().encode(canonicalize(value))));
|
|
6028
6251
|
}
|
|
6029
|
-
async function aesGcmDecrypt(key, blob) {
|
|
6252
|
+
async function aesGcmDecrypt(key, blob, aad) {
|
|
6030
6253
|
if (key.length !== 32 || blob.length < 28) throw new Error("encrypted content is malformed");
|
|
6031
6254
|
const cryptoKey = await crypto.subtle.importKey("raw", key, "AES-GCM", false, ["decrypt"]);
|
|
6032
|
-
return new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv: blob.slice(0, 12) }, cryptoKey, blob.slice(12)));
|
|
6255
|
+
return new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv: blob.slice(0, 12), ...aad && { additionalData: aad } }, cryptoKey, blob.slice(12)));
|
|
6033
6256
|
}
|
|
6034
|
-
async function aesGcmEncrypt(key, plaintext) {
|
|
6257
|
+
async function aesGcmEncrypt(key, plaintext, aad) {
|
|
6035
6258
|
if (key.length !== 32) throw new Error("encrypted content key is malformed");
|
|
6036
6259
|
const nonce = crypto.getRandomValues(new Uint8Array(12));
|
|
6037
6260
|
const cryptoKey = await crypto.subtle.importKey("raw", key, "AES-GCM", false, ["encrypt"]);
|
|
6038
|
-
const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: nonce }, cryptoKey, plaintext));
|
|
6261
|
+
const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: nonce, ...aad && { additionalData: aad } }, cryptoKey, plaintext));
|
|
6039
6262
|
const output = new Uint8Array(nonce.length + ciphertext.length);
|
|
6040
6263
|
output.set(nonce);
|
|
6041
6264
|
output.set(ciphertext, nonce.length);
|
|
@@ -6050,8 +6273,9 @@ function parseV3InlineEncryptedEnvelope(bytes, expected) {
|
|
|
6050
6273
|
}
|
|
6051
6274
|
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("encrypted content envelope is malformed");
|
|
6052
6275
|
const record = value;
|
|
6053
|
-
const allowed = ["v", "networkId", "alg", "keyVersion", "encryptedSymmetricKey", "encryptedSymmetricKeyHash", "ciphertext", "metadata"];
|
|
6054
|
-
|
|
6276
|
+
const allowed = ["v", "networkId", "keyPath", "alg", "keyVersion", "encryptedSymmetricKey", "encryptedSymmetricKeyHash", "ciphertext", "aad", "metadata"];
|
|
6277
|
+
const source = expected.contentSource;
|
|
6278
|
+
if (Object.keys(record).some((key) => !allowed.includes(key)) || record.v !== (source.encryptionKeyPath === void 0 ? 1 : 2) || record.networkId !== source.encryptionNetwork || record.networkId !== expected.encryptionNetwork || record.keyPath !== source.encryptionKeyPath || record.keyPath !== void 0 && record.keyPath !== `share/${expected.shareId}` || record.alg !== "x25519-aes256gcm/v1" || record.keyVersion !== source.keyVersion || typeof record.encryptedSymmetricKey !== "string" || typeof record.encryptedSymmetricKeyHash !== "string" || hex2((0, import_sha2562.sha256)(fromBase64(record.encryptedSymmetricKey, "v3 encrypted symmetric key"))) !== source.encryptedSymmetricKeyDigestHex || record.encryptedSymmetricKeyHash !== canonicalHashHex(record.encryptedSymmetricKey) || typeof record.ciphertext !== "string" || record.aad !== void 0 && typeof record.aad !== "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"))) {
|
|
6055
6279
|
throw new Error("encrypted content envelope binding is invalid");
|
|
6056
6280
|
}
|
|
6057
6281
|
return record;
|
|
@@ -6172,7 +6396,7 @@ var ShareRecipientClient = class {
|
|
|
6172
6396
|
this.session = { sessionId: delegation.sessionCid, expiresAt: new Date(compact.payload.exp * 1e3).toISOString(), actions: envelope.actions, resource: envelope.resource };
|
|
6173
6397
|
return this.session;
|
|
6174
6398
|
}
|
|
6175
|
-
/** Decrypt
|
|
6399
|
+
/** Decrypt unscoped v1 or path-bound v2 content, independently checking canonical and registered raw-key digests. */
|
|
6176
6400
|
async decryptV3Content(bytes) {
|
|
6177
6401
|
const envelope = this.options.envelope;
|
|
6178
6402
|
const signer = this.nativeSigner ?? this.signer;
|
|
@@ -6180,18 +6404,22 @@ var ShareRecipientClient = class {
|
|
|
6180
6404
|
assertArtifactBinding(artifactBindingView(envelope));
|
|
6181
6405
|
if (!matchesInitialCiphertextDigest(bytes, envelope.contentSource.initialCiphertextDigestHex)) throw new Error("v3 owner KV ciphertext digest mismatch");
|
|
6182
6406
|
const encrypted = parseV3InlineEncryptedEnvelope(bytes, envelope);
|
|
6407
|
+
const ciphertext = fromBase64(encrypted.ciphertext, "v3 encrypted content");
|
|
6408
|
+
if (ciphertext.length < 28) throw new Error("encrypted content is malformed");
|
|
6409
|
+
const aad = encrypted.aad === void 0 ? void 0 : fromBase64(encrypted.aad, "v3 encrypted content AAD");
|
|
6183
6410
|
const receiverPrivateKey = crypto.getRandomValues(new Uint8Array(32));
|
|
6184
6411
|
const receiverPublicKey = toBase64(import_ed255194.x25519.getPublicKey(receiverPrivateKey));
|
|
6185
6412
|
const receiverPublicKeyHash = canonicalHashHex(receiverPublicKey);
|
|
6186
|
-
const body = { type: "tinycloud.encryption.decrypt/v1", targetNode: this.v3NodeAudience, networkId: encrypted.networkId, alg: encrypted.alg, keyVersion: encrypted.keyVersion, encryptedSymmetricKey: encrypted.encryptedSymmetricKey, encryptedSymmetricKeyHash: encrypted.encryptedSymmetricKeyHash, receiverPublicKey, receiverPublicKeyHash };
|
|
6413
|
+
const body = { type: "tinycloud.encryption.decrypt/v1", targetNode: this.v3NodeAudience, networkId: encrypted.networkId, ...encrypted.keyPath === void 0 ? {} : { keyPath: encrypted.keyPath }, alg: encrypted.alg, keyVersion: encrypted.keyVersion, encryptedSymmetricKey: encrypted.encryptedSymmetricKey, encryptedSymmetricKeyHash: encrypted.encryptedSymmetricKeyHash, receiverPublicKey, receiverPublicKeyHash };
|
|
6187
6414
|
const bodyHash = hex2((0, import_sha2562.sha256)(new TextEncoder().encode(canonicalize(body))));
|
|
6188
6415
|
const session = verifyCompactUcanAuthorization(this.v3Authorization, this.session.sessionId);
|
|
6189
6416
|
const now = Math.floor(Date.now() / 1e3);
|
|
6417
|
+
const decryptResource = encrypted.keyPath === void 0 ? encrypted.networkId : `${encrypted.networkId}/${encrypted.keyPath}`;
|
|
6190
6418
|
const invocation = await signCompactUcanAuthorization({
|
|
6191
6419
|
issuerDid: this.options.holderDid,
|
|
6192
6420
|
audienceDid: this.v3NodeAudience,
|
|
6193
|
-
attenuation: { [
|
|
6194
|
-
facts: [{ type: body.type, targetNode: body.targetNode, networkId: body.networkId, bodyHash, encryptedSymmetricKeyHash: body.encryptedSymmetricKeyHash, receiverPublicKeyHash, alg: body.alg, keyVersion: body.keyVersion }],
|
|
6421
|
+
attenuation: { [decryptResource]: { "tinycloud.encryption/decrypt": [{}] } },
|
|
6422
|
+
facts: [{ type: body.type, targetNode: body.targetNode, networkId: body.networkId, ...encrypted.keyPath === void 0 ? {} : { keyPath: encrypted.keyPath }, bodyHash, encryptedSymmetricKeyHash: body.encryptedSymmetricKeyHash, receiverPublicKeyHash, alg: body.alg, keyVersion: body.keyVersion }],
|
|
6195
6423
|
proofs: [session.cid],
|
|
6196
6424
|
notBefore: now,
|
|
6197
6425
|
expiresAt: Math.min(now + 60, session.payload.exp),
|
|
@@ -6215,7 +6443,7 @@ var ShareRecipientClient = class {
|
|
|
6215
6443
|
const symmetricKey = await aesGcmDecrypt(shared, wrapped.slice(33));
|
|
6216
6444
|
shared.fill(0);
|
|
6217
6445
|
if (symmetricKey.length !== 32) throw new Error("v3 content key is malformed");
|
|
6218
|
-
const plaintext = await aesGcmDecrypt(symmetricKey,
|
|
6446
|
+
const plaintext = await aesGcmDecrypt(symmetricKey, ciphertext, aad);
|
|
6219
6447
|
this.v3ContentKey?.fill(0);
|
|
6220
6448
|
this.v3ContentKey = symmetricKey;
|
|
6221
6449
|
this.v3ContentEnvelope = encrypted;
|
|
@@ -6227,7 +6455,9 @@ var ShareRecipientClient = class {
|
|
|
6227
6455
|
/** Re-encrypt edited v3 content with the admitted content key. */
|
|
6228
6456
|
async encryptV3Content(bytes, mediaType) {
|
|
6229
6457
|
if (this.options.envelope.version !== 3 || this.v3ContentKey === void 0 || this.v3ContentEnvelope === void 0) throw new Error("v3 content must be decrypted before it can be saved");
|
|
6230
|
-
|
|
6458
|
+
const envelope = this.v3ContentEnvelope;
|
|
6459
|
+
const aad = envelope.aad === void 0 ? void 0 : fromBase64(envelope.aad, "v3 encrypted content AAD");
|
|
6460
|
+
return new TextEncoder().encode(canonicalize({ ...envelope, ciphertext: toBase64(await aesGcmEncrypt(this.v3ContentKey, bytes, aad)), metadata: { ...envelope.metadata ?? {}, contentType: mediaType } }));
|
|
6231
6461
|
}
|
|
6232
6462
|
async resumeWithProof(envelope, resumeToken, proof) {
|
|
6233
6463
|
void envelope;
|