@tinycloud/share-sdk 1.4.0 → 1.4.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{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 +283 -13
- 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 +283 -13
- 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-DVyTQBl0.d.ts → web-recipient-entry-CPntAP0H.d.ts} +46 -5
- package/dist/{web-recipient-entry-CiUzdSQs.d.cts → web-recipient-entry-Cy1fXl_5.d.cts} +46 -5
- package/dist/web-recipient.cjs +258 -11
- 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 +258 -11
- package/dist/web-recipient.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -5363,6 +5363,23 @@ var unifiedEncryptionNetworkSchema = external_exports.string().refine((value) =>
|
|
|
5363
5363
|
const network = separator < 0 ? "" : rest.slice(separator + 1);
|
|
5364
5364
|
return owner.startsWith("did:") && owner.length > 4 && network.length > 0 && !/[:/%?#\s]/.test(network);
|
|
5365
5365
|
}, { message: "expected canonical TinyCloud encryption network" });
|
|
5366
|
+
var unifiedEncryptionResourceSchema = external_exports.string().refine((value) => {
|
|
5367
|
+
const prefix = "urn:tinycloud:encryption:";
|
|
5368
|
+
if (!value.startsWith(prefix)) return false;
|
|
5369
|
+
const match = /^(did:.+):([^/:?#\s]+)(?:\/(.+))?$/.exec(value.slice(prefix.length));
|
|
5370
|
+
if (match === null) return false;
|
|
5371
|
+
const path = match[3];
|
|
5372
|
+
return path === void 0 || path.split("/").every(
|
|
5373
|
+
(segment) => segment.length > 0 && segment !== "." && segment !== ".." && !/[\\:%?#\s]/.test(segment)
|
|
5374
|
+
);
|
|
5375
|
+
}, { message: "expected a canonical TinyCloud encryption resource" });
|
|
5376
|
+
var unifiedEncryptionKeyPathSchema = external_exports.string().regex(/^share\/[^/]+$/).refine(
|
|
5377
|
+
(value) => {
|
|
5378
|
+
const segment = value.slice("share/".length);
|
|
5379
|
+
return segment !== "." && segment !== ".." && !/[\\:%?#\s]/.test(segment);
|
|
5380
|
+
},
|
|
5381
|
+
{ message: "expected an exact share key path" }
|
|
5382
|
+
);
|
|
5366
5383
|
var unifiedKvCapabilitySchema = external_exports.object({
|
|
5367
5384
|
kind: external_exports.literal("kv"),
|
|
5368
5385
|
resource: unifiedResourceSchema,
|
|
@@ -5376,7 +5393,7 @@ var unifiedKvCapabilitySchema = external_exports.object({
|
|
|
5376
5393
|
}).strict();
|
|
5377
5394
|
var unifiedEncryptionCapabilitySchema = external_exports.object({
|
|
5378
5395
|
kind: external_exports.literal("encryption"),
|
|
5379
|
-
resource:
|
|
5396
|
+
resource: unifiedEncryptionResourceSchema,
|
|
5380
5397
|
action: external_exports.literal("tinycloud.encryption/decrypt")
|
|
5381
5398
|
}).strict();
|
|
5382
5399
|
var unifiedCapabilitySchema = external_exports.union([unifiedKvCapabilitySchema, unifiedEncryptionCapabilitySchema]);
|
|
@@ -5385,6 +5402,7 @@ var unifiedContentSourceSchema = external_exports.object({
|
|
|
5385
5402
|
kvResource: unifiedResourceSchema,
|
|
5386
5403
|
selector: external_exports.union([external_exports.literal("exact"), external_exports.literal("prefix")]),
|
|
5387
5404
|
encryptionNetwork: unifiedEncryptionNetworkSchema,
|
|
5405
|
+
encryptionKeyPath: unifiedEncryptionKeyPathSchema.optional(),
|
|
5388
5406
|
encryptedSymmetricKeyDigestHex: external_exports.string().regex(/^[0-9a-f]{64}$/),
|
|
5389
5407
|
keyVersion: external_exports.number().int().positive(),
|
|
5390
5408
|
mode: external_exports.union([external_exports.literal("mutable"), external_exports.literal("immutable")]),
|
|
@@ -5502,16 +5520,21 @@ function validateV3Invariants(value, ctx) {
|
|
|
5502
5520
|
if (value.attestedEnforcerBinding.nodeAudience !== value.target.nodeAudience || value.attestedEnforcerBinding.signature.signerDid !== value.attestedEnforcerBinding.nodeAudience || Date.parse(value.attestedEnforcerBinding.expiresAt) < Date.parse(value.expiry)) {
|
|
5503
5521
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["attestedEnforcerBinding"], message: "enforcer binding does not cover the target and share lifetime" });
|
|
5504
5522
|
}
|
|
5505
|
-
|
|
5523
|
+
const encryptionKeyPath = value.contentSource.encryptionKeyPath;
|
|
5524
|
+
if (encryptionKeyPath !== void 0 && encryptionKeyPath !== `share/${value.contentSource.shareId}`) {
|
|
5525
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["contentSource", "encryptionKeyPath"], message: "share key path must name this share" });
|
|
5526
|
+
}
|
|
5527
|
+
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) {
|
|
5506
5528
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["contentSource"], message: "content source is not bound to policy" });
|
|
5507
5529
|
}
|
|
5508
5530
|
if (value.policy.signature.suite !== "Ed25519" || value.policy.signature.signerDid !== value.policy.ownerDid) {
|
|
5509
5531
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["policy", "signature"], message: "policy must be signed by its owner" });
|
|
5510
5532
|
}
|
|
5511
5533
|
const kvCapabilities = value.policy.capabilityCeiling.filter((capability) => capability.kind === "kv");
|
|
5512
|
-
const
|
|
5534
|
+
const expectedEncryptionResource = encryptionKeyPath === void 0 ? value.encryptionNetwork : `${value.encryptionNetwork}/${encryptionKeyPath}`;
|
|
5535
|
+
const decryptCapabilities = value.policy.capabilityCeiling.filter((capability) => capability.kind === "encryption" && capability.resource === expectedEncryptionResource && capability.action === "tinycloud.encryption/decrypt");
|
|
5513
5536
|
if (value.policy.capabilityCeiling.length !== 2 || kvCapabilities.length !== 1 || decryptCapabilities.length !== 1 || kvCapabilities[0]?.resource !== value.contentSource.kvResource || kvCapabilities[0]?.selector !== value.contentSource.selector) {
|
|
5514
|
-
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["policy", "capabilityCeiling"], message: "policy ceiling must contain exact decrypt
|
|
5537
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["policy", "capabilityCeiling"], message: "policy ceiling must contain the exact decrypt resource" });
|
|
5515
5538
|
}
|
|
5516
5539
|
}
|
|
5517
5540
|
var unsignedShareEnvelopeV3Schema = unsignedShareEnvelopeV3BaseSchema.superRefine(validateV3Invariants);
|
|
@@ -5699,6 +5722,7 @@ var SEALED_BLOB_VERSION = 1;
|
|
|
5699
5722
|
var AAD = utf8Bytes(ENVELOPE_AAD_LABEL);
|
|
5700
5723
|
var KEY_LENGTH = 32;
|
|
5701
5724
|
var NONCE_LENGTH = 12;
|
|
5725
|
+
var TAG_LENGTH = 16;
|
|
5702
5726
|
var HEADER_LENGTH = 1;
|
|
5703
5727
|
function assertKey(key32) {
|
|
5704
5728
|
if (key32.length !== KEY_LENGTH) {
|
|
@@ -5726,6 +5750,17 @@ async function seal(plaintextBytes, key32) {
|
|
|
5726
5750
|
blob.set(ciphertext, HEADER_LENGTH + nonce.length);
|
|
5727
5751
|
return { blob, cid: await computeCid(blob) };
|
|
5728
5752
|
}
|
|
5753
|
+
async function open(blob, key32) {
|
|
5754
|
+
if (blob.length < HEADER_LENGTH + NONCE_LENGTH + TAG_LENGTH) {
|
|
5755
|
+
throw new TypeError(`sealed blob too short: ${blob.length} bytes`);
|
|
5756
|
+
}
|
|
5757
|
+
if (blob[0] !== SEALED_BLOB_VERSION) {
|
|
5758
|
+
throw new TypeError(`unknown sealed blob version: ${blob[0]}`);
|
|
5759
|
+
}
|
|
5760
|
+
const nonce = blob.subarray(HEADER_LENGTH, HEADER_LENGTH + NONCE_LENGTH);
|
|
5761
|
+
const ciphertext = blob.subarray(HEADER_LENGTH + NONCE_LENGTH);
|
|
5762
|
+
return decryptEnvelope(nonce, ciphertext, key32);
|
|
5763
|
+
}
|
|
5729
5764
|
async function encryptEnvelope(plaintextBytes, key32) {
|
|
5730
5765
|
const key = await importAesKey(key32, "encrypt");
|
|
5731
5766
|
const nonce = globalThis.crypto.getRandomValues(new Uint8Array(NONCE_LENGTH));
|
|
@@ -5738,6 +5773,19 @@ async function encryptEnvelope(plaintextBytes, key32) {
|
|
|
5738
5773
|
);
|
|
5739
5774
|
return { nonce, ciphertext };
|
|
5740
5775
|
}
|
|
5776
|
+
async function decryptEnvelope(nonce, ciphertext, key32) {
|
|
5777
|
+
if (nonce.length !== NONCE_LENGTH) {
|
|
5778
|
+
throw new TypeError(`nonce must be ${NONCE_LENGTH} bytes, got ${nonce.length}`);
|
|
5779
|
+
}
|
|
5780
|
+
const key = await importAesKey(key32, "decrypt");
|
|
5781
|
+
return new Uint8Array(
|
|
5782
|
+
await globalThis.crypto.subtle.decrypt(
|
|
5783
|
+
{ name: "AES-GCM", iv: nonce, additionalData: AAD },
|
|
5784
|
+
key,
|
|
5785
|
+
ciphertext
|
|
5786
|
+
)
|
|
5787
|
+
);
|
|
5788
|
+
}
|
|
5741
5789
|
function isPlainRecord(value) {
|
|
5742
5790
|
const proto = Object.getPrototypeOf(value);
|
|
5743
5791
|
return proto === Object.prototype || proto === null;
|
|
@@ -5883,6 +5931,7 @@ function equal(left, right) {
|
|
|
5883
5931
|
return left.length === right.length && left.every((byte, index) => byte === right[index]);
|
|
5884
5932
|
}
|
|
5885
5933
|
var ED25519_VERIFY_OPTS2 = { zip215: false };
|
|
5934
|
+
var ENVELOPE_V2_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v2\0";
|
|
5886
5935
|
var ENVELOPE_V3_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v3\0";
|
|
5887
5936
|
var POLICY_V1_SIGNATURE_DOMAIN = "xyz.tinycloud.policy/policy/v1\0";
|
|
5888
5937
|
var POLICY_V2_SIGNATURE_DOMAIN = "xyz.tinycloud.policy/policy/v2\0";
|
|
@@ -5890,6 +5939,14 @@ var CONTENT_SOURCE_V1_DOMAIN = "xyz.tinycloud.policy/ContentSource/v1\0";
|
|
|
5890
5939
|
var POLICY_CAPABILITY_V1_DOMAIN = "xyz.tinycloud.policy/PolicyCapability/v1\0";
|
|
5891
5940
|
var NATIVE_PROJECTION_V1_DOMAIN = "xyz.tinycloud.policy/NativeProjection/v1\0";
|
|
5892
5941
|
var ATTESTED_ENFORCER_V2_DOMAIN = "xyz.tinycloud.policy/AttestedEnforcerBinding/v2\0";
|
|
5942
|
+
function signingBytesV2(unsigned) {
|
|
5943
|
+
const domain = utf8Bytes(ENVELOPE_V2_SIGNATURE_DOMAIN);
|
|
5944
|
+
const body = utf8Bytes(canonicalize(unsigned));
|
|
5945
|
+
const bytes3 = new Uint8Array(domain.length + body.length);
|
|
5946
|
+
bytes3.set(domain);
|
|
5947
|
+
bytes3.set(body, domain.length);
|
|
5948
|
+
return bytes3;
|
|
5949
|
+
}
|
|
5893
5950
|
function signingBytesV3(unsigned) {
|
|
5894
5951
|
const domain = utf8Bytes(ENVELOPE_V3_SIGNATURE_DOMAIN);
|
|
5895
5952
|
const body = utf8Bytes(canonicalize(unsigned));
|
|
@@ -5898,6 +5955,17 @@ function signingBytesV3(unsigned) {
|
|
|
5898
5955
|
bytes3.set(body, domain.length);
|
|
5899
5956
|
return bytes3;
|
|
5900
5957
|
}
|
|
5958
|
+
function verifyEnvelopeV2SignatureOnly(envelope) {
|
|
5959
|
+
const parsed = shareEnvelopeV2Schema.parse(envelope);
|
|
5960
|
+
const { signature, ...unsigned } = parsed;
|
|
5961
|
+
return import_ed255193.ed25519.verify(fromBase64Url(signature.value), signingBytesV2(unsigned), ed25519PublicKeyFromDidKey(signature.signerDid), ED25519_VERIFY_OPTS2);
|
|
5962
|
+
}
|
|
5963
|
+
async function verifyEnvelopeV2(envelope, options) {
|
|
5964
|
+
const parsed = shareEnvelopeV2Schema.parse(envelope);
|
|
5965
|
+
if (parsed.signature.signerDid !== options.expectedSignerDid || !verifyEnvelopeV2SignatureOnly(parsed)) return false;
|
|
5966
|
+
if (parsed.authorizationTarget.kind === "policy") return await computeCid(fromBase64Url(parsed.authorizationTarget.policyBytes)) === parsed.authorizationTarget.policyCid;
|
|
5967
|
+
return true;
|
|
5968
|
+
}
|
|
5901
5969
|
function verifyEnvelopeV3SignatureOnly(envelope) {
|
|
5902
5970
|
const parsed = shareEnvelopeV3Schema.parse(envelope);
|
|
5903
5971
|
const { signature, ...unsigned } = parsed;
|
|
@@ -5984,6 +6052,52 @@ function base32Lower(bytes3) {
|
|
|
5984
6052
|
if (bits > 0) output += alphabet[buffer << 5 - bits & 31];
|
|
5985
6053
|
return output;
|
|
5986
6054
|
}
|
|
6055
|
+
var KEY_LENGTH2 = 32;
|
|
6056
|
+
function assertCanonicalCid(cidString) {
|
|
6057
|
+
const cid2 = CID.parse(cidString);
|
|
6058
|
+
if (cid2.version !== 1 || cid2.code !== code || cid2.multihash.code !== 18 || // sha2-256 (0x12) only, at the link layer too
|
|
6059
|
+
cid2.toString() !== cidString) {
|
|
6060
|
+
throw new TypeError(`not a canonical CIDv1 raw sha2-256 base32 CID: ${cidString}`);
|
|
6061
|
+
}
|
|
6062
|
+
}
|
|
6063
|
+
function parseShareUrl(url, options = {}) {
|
|
6064
|
+
const parsed = new URL(url);
|
|
6065
|
+
if (parsed.protocol !== "https:") {
|
|
6066
|
+
throw new TypeError(`share URL must be https, got ${parsed.protocol}`);
|
|
6067
|
+
}
|
|
6068
|
+
if (parsed.username !== "" || parsed.password !== "") {
|
|
6069
|
+
throw new TypeError("share URL must not carry userinfo");
|
|
6070
|
+
}
|
|
6071
|
+
if (parsed.search !== "") {
|
|
6072
|
+
throw new TypeError("share URL must not have a query string");
|
|
6073
|
+
}
|
|
6074
|
+
if (options.expectedOrigin !== void 0) {
|
|
6075
|
+
if (!isCanonicalHttpsOrigin(options.expectedOrigin)) {
|
|
6076
|
+
throw new TypeError(
|
|
6077
|
+
`expectedOrigin must be a canonical https origin, got ${options.expectedOrigin}`
|
|
6078
|
+
);
|
|
6079
|
+
}
|
|
6080
|
+
if (parsed.origin !== options.expectedOrigin) {
|
|
6081
|
+
throw new TypeError(
|
|
6082
|
+
`share URL origin ${parsed.origin} does not match expected ${options.expectedOrigin}`
|
|
6083
|
+
);
|
|
6084
|
+
}
|
|
6085
|
+
}
|
|
6086
|
+
const match = /^\/s\/([a-z2-7]+)$/.exec(parsed.pathname);
|
|
6087
|
+
if (!match || match[1] === void 0) {
|
|
6088
|
+
throw new TypeError(`not a share URL path: ${parsed.pathname}`);
|
|
6089
|
+
}
|
|
6090
|
+
const ciphertextCid = match[1];
|
|
6091
|
+
assertCanonicalCid(ciphertextCid);
|
|
6092
|
+
if (!parsed.hash.startsWith("#k=")) {
|
|
6093
|
+
throw new TypeError("share URL is missing the #k= key fragment");
|
|
6094
|
+
}
|
|
6095
|
+
const key32 = fromBase64Url(parsed.hash.slice("#k=".length));
|
|
6096
|
+
if (key32.length !== KEY_LENGTH2) {
|
|
6097
|
+
throw new TypeError(`fragment key must be ${KEY_LENGTH2} bytes, got ${key32.length}`);
|
|
6098
|
+
}
|
|
6099
|
+
return { ciphertextCid, key32 };
|
|
6100
|
+
}
|
|
5987
6101
|
|
|
5988
6102
|
// src/artifact-core.ts
|
|
5989
6103
|
var ARTIFACT_ROOT = "artifacts";
|
|
@@ -6085,7 +6199,7 @@ function toShareErrorInfo(error) {
|
|
|
6085
6199
|
return { protocol: "tinycloud-share", version: SHARE_RESULT_VERSION, error: { code: "fetch-failed" } };
|
|
6086
6200
|
}
|
|
6087
6201
|
function metadataFor(envelope, origin) {
|
|
6088
|
-
const kind = envelope.recipientMatcher.kind === "recipientDid" ? "recipientDid" : envelope.recipientMatcher.kind === "exactEmail" ? "email" : "emailDomain";
|
|
6202
|
+
const kind = envelope.recipientMatcher.kind === "recipientDid" ? "recipientDid" : envelope.recipientMatcher.kind === "exactEmail" ? "email" : envelope.recipientMatcher.kind === "emailDomain" ? "emailDomain" : "bearer";
|
|
6089
6203
|
return {
|
|
6090
6204
|
protocol: "tinycloud-share",
|
|
6091
6205
|
version: 1,
|
|
@@ -6100,7 +6214,7 @@ function metadataFor(envelope, origin) {
|
|
|
6100
6214
|
...envelope.display.filename === void 0 ? {} : { filename: envelope.display.filename },
|
|
6101
6215
|
...envelope.display.mode === void 0 ? {} : { mode: envelope.display.mode }
|
|
6102
6216
|
},
|
|
6103
|
-
...envelope.
|
|
6217
|
+
...envelope.version === 3 && envelope.artifactPin !== void 0 ? { artifact: { id: envelope.artifactPin.artifactId, version: envelope.artifactPin.version } } : {}
|
|
6104
6218
|
};
|
|
6105
6219
|
}
|
|
6106
6220
|
function receiveErrorFor(error) {
|
|
@@ -6198,14 +6312,165 @@ async function resolvePolicyShare(link, options) {
|
|
|
6198
6312
|
options.onResolvedAddressedEnvelope?.(envelope, cid2);
|
|
6199
6313
|
return { envelope, origin: url.origin, cid: cid2 };
|
|
6200
6314
|
}
|
|
6315
|
+
async function resolveLegacyV2(link, options) {
|
|
6316
|
+
let parsed;
|
|
6317
|
+
try {
|
|
6318
|
+
parsed = parseShareUrl(link, { ...options.expectedOrigin === void 0 ? {} : { expectedOrigin: options.expectedOrigin } });
|
|
6319
|
+
} catch {
|
|
6320
|
+
throw new ShareReceiveError("invalid-link", "legacy share link format is invalid");
|
|
6321
|
+
}
|
|
6322
|
+
try {
|
|
6323
|
+
options.signal?.throwIfAborted();
|
|
6324
|
+
const origin = new URL(link).origin;
|
|
6325
|
+
const limit = options.maxSealedBlobBytes ?? DEFAULT_MAX_SEALED_BLOB_BYTES;
|
|
6326
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope limit is invalid");
|
|
6327
|
+
let sealed;
|
|
6328
|
+
if (options.fetchBlob !== void 0) {
|
|
6329
|
+
try {
|
|
6330
|
+
sealed = await options.fetchBlob({ origin, cid: parsed.ciphertextCid, ...options.signal === void 0 ? {} : { signal: options.signal } });
|
|
6331
|
+
} catch {
|
|
6332
|
+
throw new ShareReceiveError("fetch-failed", "legacy registry unavailable");
|
|
6333
|
+
}
|
|
6334
|
+
} else {
|
|
6335
|
+
if (options.registryBaseUrl === void 0) throw new ShareReceiveError("fetch-failed", "legacy registry is required");
|
|
6336
|
+
const response = await (options.fetch ?? globalThis.fetch)(`${options.registryBaseUrl.replace(/\/+$/, "")}/ipfs/${parsed.ciphertextCid}?format=raw`, {
|
|
6337
|
+
headers: { accept: "application/vnd.ipld.raw" },
|
|
6338
|
+
redirect: "error",
|
|
6339
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
6340
|
+
});
|
|
6341
|
+
if (!response.ok) throw new ShareReceiveError("fetch-failed", "legacy registry unavailable");
|
|
6342
|
+
if (Number(response.headers.get("content-length")) > limit) throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope exceeds the size limit");
|
|
6343
|
+
const reader = response.body?.getReader();
|
|
6344
|
+
if (reader === void 0) throw new ShareReceiveError("fetch-failed", "legacy registry returned no body");
|
|
6345
|
+
const chunks = [];
|
|
6346
|
+
let length3 = 0;
|
|
6347
|
+
for (; ; ) {
|
|
6348
|
+
const part = await reader.read();
|
|
6349
|
+
if (part.done) break;
|
|
6350
|
+
length3 += part.value.length;
|
|
6351
|
+
if (length3 > limit) {
|
|
6352
|
+
await reader.cancel();
|
|
6353
|
+
throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope exceeds the size limit");
|
|
6354
|
+
}
|
|
6355
|
+
chunks.push(part.value);
|
|
6356
|
+
}
|
|
6357
|
+
sealed = new Uint8Array(length3);
|
|
6358
|
+
let offset = 0;
|
|
6359
|
+
for (const chunk of chunks) {
|
|
6360
|
+
sealed.set(chunk, offset);
|
|
6361
|
+
offset += chunk.length;
|
|
6362
|
+
}
|
|
6363
|
+
}
|
|
6364
|
+
options.signal?.throwIfAborted();
|
|
6365
|
+
if (!(sealed instanceof Uint8Array) || sealed.length > limit) throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope exceeds the size limit");
|
|
6366
|
+
if (await computeCid(sealed) !== parsed.ciphertextCid) throw new ShareReceiveError("cid-mismatch", "legacy envelope CID does not match");
|
|
6367
|
+
let plaintext;
|
|
6368
|
+
try {
|
|
6369
|
+
plaintext = await open(sealed, parsed.key32);
|
|
6370
|
+
} catch {
|
|
6371
|
+
throw new ShareReceiveError("decrypt-failed", "legacy envelope could not be opened");
|
|
6372
|
+
}
|
|
6373
|
+
let envelope;
|
|
6374
|
+
try {
|
|
6375
|
+
const encoded = new TextDecoder("utf-8", { fatal: true }).decode(plaintext);
|
|
6376
|
+
const value = JSON.parse(encoded);
|
|
6377
|
+
if (canonicalize(value) !== encoded) throw new Error("non-canonical envelope");
|
|
6378
|
+
envelope = shareEnvelopeV2Schema.parse(value);
|
|
6379
|
+
} catch {
|
|
6380
|
+
throw new ShareReceiveError("envelope-invalid", "legacy envelope is invalid");
|
|
6381
|
+
}
|
|
6382
|
+
return { envelope, origin, cid: parsed.ciphertextCid };
|
|
6383
|
+
} finally {
|
|
6384
|
+
parsed.key32.fill(0);
|
|
6385
|
+
}
|
|
6386
|
+
}
|
|
6387
|
+
function evidenceMatchesV2(envelope, evidence) {
|
|
6388
|
+
const actions = new Set(evidence.actions.map((action) => action === "read" ? "tinycloud.kv/get" : action === "list" ? "tinycloud.kv/list" : action === "edit" ? "tinycloud.kv/put" : action));
|
|
6389
|
+
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}/`);
|
|
6390
|
+
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);
|
|
6391
|
+
}
|
|
6392
|
+
async function verifyLegacyV2(envelope, options) {
|
|
6393
|
+
try {
|
|
6394
|
+
if (!await verifyEnvelopeV2(envelope, { expectedSignerDid: envelope.signature.signerDid })) throw new Error("signature");
|
|
6395
|
+
} catch {
|
|
6396
|
+
throw new ShareReceiveError("signature-invalid", "legacy envelope signature is invalid");
|
|
6397
|
+
}
|
|
6398
|
+
const expiry = Date.parse(envelope.expiry);
|
|
6399
|
+
if (!Number.isFinite(expiry)) throw new ShareReceiveError("envelope-invalid", "legacy share expiry is invalid");
|
|
6400
|
+
if (expiry <= (options.now?.() ?? Date.now())) throw new ShareReceiveError("expired", "legacy share has expired", { expiresAt: envelope.expiry });
|
|
6401
|
+
if (envelope.authorizationTarget.kind === "policy") {
|
|
6402
|
+
if (options.trustedPolicyAuthority === void 0) throw new ShareReceiveError("envelope-invalid", "a legacy policy authority is required");
|
|
6403
|
+
let evidence;
|
|
6404
|
+
try {
|
|
6405
|
+
evidence = await options.trustedPolicyAuthority.resolve({ policyCid: envelope.authorizationTarget.policyCid, envelope });
|
|
6406
|
+
} catch {
|
|
6407
|
+
}
|
|
6408
|
+
if (evidence === void 0 || !evidenceMatchesV2(envelope, evidence)) throw new ShareReceiveError("capability-invalid", "legacy policy evidence does not match");
|
|
6409
|
+
if (options.trustedSignerDid !== void 0 && options.trustedSignerDid !== evidence.signerDid) throw new ShareReceiveError("signature-invalid", "legacy signer is not trusted");
|
|
6410
|
+
} 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) {
|
|
6411
|
+
throw new ShareReceiveError("capability-invalid", "legacy recipient authority does not match");
|
|
6412
|
+
}
|
|
6413
|
+
}
|
|
6201
6414
|
async function inspectShare(link, options = {}) {
|
|
6415
|
+
if (isLegacyV2Link(link)) {
|
|
6416
|
+
const legacy = await resolveLegacyV2(link, options);
|
|
6417
|
+
await verifyLegacyV2(legacy.envelope, options);
|
|
6418
|
+
options.onResolvedAddressedEnvelope?.(legacy.envelope, legacy.cid);
|
|
6419
|
+
return { metadata: metadataFor(legacy.envelope, legacy.origin), link: { origin: legacy.origin, cid: legacy.cid, kind: "compact" } };
|
|
6420
|
+
}
|
|
6202
6421
|
const resolved = await resolvePolicyShare(link, options);
|
|
6203
6422
|
return { metadata: metadataFor(resolved.envelope, resolved.origin), link: { origin: resolved.origin, cid: resolved.cid, kind: "policy" } };
|
|
6204
6423
|
}
|
|
6205
6424
|
async function receiveShare(link, options = {}) {
|
|
6425
|
+
if (isLegacyV2Link(link)) return receiveLegacyV2(link, options);
|
|
6206
6426
|
const resolved = await resolvePolicyShare(link, options);
|
|
6207
6427
|
return { state: "authorization-required", method: resolved.envelope.recipientMatcher.kind === "recipientDid" ? "openkey-device" : "email-claim" };
|
|
6208
6428
|
}
|
|
6429
|
+
function isLegacyV2Link(link) {
|
|
6430
|
+
try {
|
|
6431
|
+
const url = new URL(link);
|
|
6432
|
+
return /^\/s\/bafkrei[a-z2-7]{52}$/.test(url.pathname) && url.hash.startsWith("#k=");
|
|
6433
|
+
} catch {
|
|
6434
|
+
return false;
|
|
6435
|
+
}
|
|
6436
|
+
}
|
|
6437
|
+
async function receiveLegacyV2(link, options) {
|
|
6438
|
+
const resolved = await resolveLegacyV2(link, options);
|
|
6439
|
+
const { envelope } = resolved;
|
|
6440
|
+
await verifyLegacyV2(envelope, options);
|
|
6441
|
+
options.onResolvedAddressedEnvelope?.(envelope, resolved.cid);
|
|
6442
|
+
const method = envelope.recipientMatcher.kind === "recipientDid" ? "openkey-device" : "email-claim";
|
|
6443
|
+
if (options.authorization === void 0) return { state: "authorization-required", method };
|
|
6444
|
+
const result = options.authorizationResumeToken === void 0 ? await options.authorization.begin({ envelope, method }) : await options.authorization.resume({
|
|
6445
|
+
envelope,
|
|
6446
|
+
method,
|
|
6447
|
+
resumeToken: options.authorizationResumeToken,
|
|
6448
|
+
...options.authorizationProof === void 0 ? {} : { proof: options.authorizationProof }
|
|
6449
|
+
});
|
|
6450
|
+
if (result.state === "authorization-required") return result;
|
|
6451
|
+
if (result.state === "denied") throw new ShareReceiveError("authorization-denied", "legacy share authorization was denied");
|
|
6452
|
+
if (result.value === null || typeof result.value !== "object" || !(result.value.bytes instanceof Uint8Array) || result.value.proof === void 0 || options.authorization.verifyResult === void 0) {
|
|
6453
|
+
throw new ShareReceiveError("content-integrity-failed", "legacy content lacks a verifiable proof");
|
|
6454
|
+
}
|
|
6455
|
+
const authorized = result.value;
|
|
6456
|
+
try {
|
|
6457
|
+
if (!await options.authorization.verifyResult({ envelope, value: authorized, proof: authorized.proof })) throw new Error("proof");
|
|
6458
|
+
} catch {
|
|
6459
|
+
throw new ShareReceiveError("content-integrity-failed", "legacy content proof is invalid");
|
|
6460
|
+
}
|
|
6461
|
+
const bytes3 = authorized.bytes.slice();
|
|
6462
|
+
if (bytes3.length > (options.maxContentBlobBytes ?? DEFAULT_MAX_CONTENT_BLOB_BYTES)) throw new ShareReceiveError("max-bytes-exceeded", "legacy content exceeds the size limit");
|
|
6463
|
+
const bodyDigest = toBase64Url(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes3)));
|
|
6464
|
+
if (authorized.bodyDigest !== bodyDigest || authorized.contentSourceDigest !== envelope.contentSourceDigest || authorized.binding.shareId !== envelope.shareId || authorized.binding.delegationCid !== envelope.delegationCid || authorized.binding.authorityMaterialHandle !== envelope.authorityMaterialHandle || authorized.binding.authorityMaterialDigest !== envelope.authorityMaterialDigest || authorized.binding.resource.kind !== envelope.resource.kind || authorized.binding.resource.path !== envelope.resource.path || authorized.binding.action !== void 0 && !envelope.actions.some((action) => action === authorized.binding.action || `tinycloud.kv/${action}` === authorized.binding.action) || envelope.metadata.byteLength !== void 0 && envelope.metadata.byteLength !== bytes3.length) {
|
|
6465
|
+
throw new ShareReceiveError("content-integrity-failed", "legacy content binding does not match");
|
|
6466
|
+
}
|
|
6467
|
+
let text;
|
|
6468
|
+
try {
|
|
6469
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes3);
|
|
6470
|
+
} catch {
|
|
6471
|
+
}
|
|
6472
|
+
return { metadata: metadataFor(envelope, resolved.origin), link: { origin: resolved.origin, cid: resolved.cid, kind: "compact" }, bytes: bytes3, ...text === void 0 ? {} : { text } };
|
|
6473
|
+
}
|
|
6209
6474
|
|
|
6210
6475
|
// src/publish.ts
|
|
6211
6476
|
var SHARE_CONTENT_LIMIT = 100 * 1024 * 1024;
|
|
@@ -6740,7 +7005,10 @@ async function publishTargetShare(input) {
|
|
|
6740
7005
|
expiresAt,
|
|
6741
7006
|
expiryWasExplicit: input.expiryWasExplicit ?? input.expiresAt !== void 0,
|
|
6742
7007
|
origin: input.origin,
|
|
6743
|
-
...input.notify === void 0 ? {} : { notify: input.notify }
|
|
7008
|
+
...input.notify === void 0 ? {} : { notify: input.notify },
|
|
7009
|
+
...input.credentialDescriptor === void 0 ? {} : { credentialDescriptor: input.credentialDescriptor },
|
|
7010
|
+
...input.issuerDid === void 0 ? {} : { issuerDid: input.issuerDid },
|
|
7011
|
+
...input.issuerKid === void 0 ? {} : { issuerKid: input.issuerKid }
|
|
6744
7012
|
});
|
|
6745
7013
|
}
|
|
6746
7014
|
|
|
@@ -6828,7 +7096,7 @@ async function createPolicy(options, capabilities) {
|
|
|
6828
7096
|
function addressedPlan(options) {
|
|
6829
7097
|
const capabilities = sortCanonical([
|
|
6830
7098
|
{ kind: "kv", resource: options.contentSource.kvResource, selector: options.resource.kind, actions: [...options.policyActions] },
|
|
6831
|
-
{ kind: "encryption", resource: options.contentSource.encryptionNetwork
|
|
7099
|
+
{ kind: "encryption", resource: options.contentSource.encryptionKeyPath === void 0 ? options.contentSource.encryptionNetwork : `${options.contentSource.encryptionNetwork}/${options.contentSource.encryptionKeyPath}`, action: "tinycloud.encryption/decrypt" }
|
|
6832
7100
|
]);
|
|
6833
7101
|
return {
|
|
6834
7102
|
expiry: rfc3339Seconds(options.expiresAt),
|
|
@@ -7158,6 +7426,7 @@ async function addressedShareBodyUpperBound(input) {
|
|
|
7158
7426
|
kvResource: `${input.spaceId}/kv/${path}`,
|
|
7159
7427
|
selector: input.resource.kind,
|
|
7160
7428
|
encryptionNetwork: input.encryptionNetwork,
|
|
7429
|
+
...input.encryptionKeyPath === void 0 ? {} : { encryptionKeyPath: input.encryptionKeyPath },
|
|
7161
7430
|
encryptedSymmetricKeyDigestHex: "f".repeat(64),
|
|
7162
7431
|
keyVersion: Number.MAX_SAFE_INTEGER,
|
|
7163
7432
|
mode: "immutable",
|
|
@@ -8718,8 +8987,8 @@ function parseV3InlineEncryptedEnvelope(bytes3, expected) {
|
|
|
8718
8987
|
}
|
|
8719
8988
|
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("encrypted content envelope is malformed");
|
|
8720
8989
|
const record = value;
|
|
8721
|
-
const allowed = ["v", "networkId", "alg", "keyVersion", "encryptedSymmetricKey", "encryptedSymmetricKeyHash", "ciphertext", "metadata"];
|
|
8722
|
-
if (Object.keys(record).some((key) => !allowed.includes(key)) || record.v !== 1 || record.networkId !== expected.encryptionNetwork || record.alg !== "x25519-aes256gcm/v1" || record.keyVersion !== expected.contentSource.keyVersion || typeof record.encryptedSymmetricKey !== "string" || typeof record.encryptedSymmetricKeyHash !== "string" || record.encryptedSymmetricKeyHash !== expected.contentSource.encryptedSymmetricKeyDigestHex || record.encryptedSymmetricKeyHash !== canonicalHashHex(record.encryptedSymmetricKey) || typeof record.ciphertext !== "string" || record.metadata !== void 0 && (record.metadata === null || typeof record.metadata !== "object" || Array.isArray(record.metadata) || Object.values(record.metadata).some((entry) => typeof entry !== "string"))) {
|
|
8990
|
+
const allowed = ["v", "networkId", "keyPath", "alg", "keyVersion", "encryptedSymmetricKey", "encryptedSymmetricKeyHash", "ciphertext", "metadata"];
|
|
8991
|
+
if (Object.keys(record).some((key) => !allowed.includes(key)) || record.v !== 1 || record.networkId !== expected.encryptionNetwork || record.keyPath !== expected.contentSource.encryptionKeyPath || record.keyPath !== void 0 && record.keyPath !== `share/${expected.shareId}` || record.alg !== "x25519-aes256gcm/v1" || record.keyVersion !== expected.contentSource.keyVersion || typeof record.encryptedSymmetricKey !== "string" || typeof record.encryptedSymmetricKeyHash !== "string" || record.encryptedSymmetricKeyHash !== expected.contentSource.encryptedSymmetricKeyDigestHex || record.encryptedSymmetricKeyHash !== canonicalHashHex(record.encryptedSymmetricKey) || typeof record.ciphertext !== "string" || record.metadata !== void 0 && (record.metadata === null || typeof record.metadata !== "object" || Array.isArray(record.metadata) || Object.values(record.metadata).some((entry) => typeof entry !== "string"))) {
|
|
8723
8992
|
throw new Error("encrypted content envelope binding is invalid");
|
|
8724
8993
|
}
|
|
8725
8994
|
return record;
|
|
@@ -8851,15 +9120,16 @@ var ShareRecipientClient = class {
|
|
|
8851
9120
|
const receiverPrivateKey = crypto.getRandomValues(new Uint8Array(32));
|
|
8852
9121
|
const receiverPublicKey = toBase64(import_ed255195.x25519.getPublicKey(receiverPrivateKey));
|
|
8853
9122
|
const receiverPublicKeyHash = canonicalHashHex(receiverPublicKey);
|
|
8854
|
-
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 };
|
|
9123
|
+
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 };
|
|
8855
9124
|
const bodyHash = hex3((0, import_sha2565.sha256)(new TextEncoder().encode(canonicalize(body))));
|
|
8856
9125
|
const session = verifyCompactUcanAuthorization(this.v3Authorization, this.session.sessionId);
|
|
8857
9126
|
const now = Math.floor(Date.now() / 1e3);
|
|
9127
|
+
const decryptResource = encrypted.keyPath === void 0 ? encrypted.networkId : `${encrypted.networkId}/${encrypted.keyPath}`;
|
|
8858
9128
|
const invocation = await signCompactUcanAuthorization({
|
|
8859
9129
|
issuerDid: this.options.holderDid,
|
|
8860
9130
|
audienceDid: this.v3NodeAudience,
|
|
8861
|
-
attenuation: { [
|
|
8862
|
-
facts: [{ type: body.type, targetNode: body.targetNode, networkId: body.networkId, bodyHash, encryptedSymmetricKeyHash: body.encryptedSymmetricKeyHash, receiverPublicKeyHash, alg: body.alg, keyVersion: body.keyVersion }],
|
|
9131
|
+
attenuation: { [decryptResource]: { "tinycloud.encryption/decrypt": [{}] } },
|
|
9132
|
+
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 }],
|
|
8863
9133
|
proofs: [session.cid],
|
|
8864
9134
|
notBefore: now,
|
|
8865
9135
|
expiresAt: Math.min(now + 60, session.payload.exp),
|