@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.d.cts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { f as ShareMetadata,
|
|
1
|
+
export { f as ShareMetadata, r as ShareRecipientClient, u as inspectShare } from './web-recipient-entry-Bn3yRB7g.cjs';
|
|
2
2
|
import '@tinycloud/share-envelope';
|
|
3
3
|
import './authorization-CmP1skO8.cjs';
|
package/dist/web-recipient.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { f as ShareMetadata,
|
|
1
|
+
export { f as ShareMetadata, r as ShareRecipientClient, u as inspectShare } from './web-recipient-entry-CTFoIH8l.js';
|
|
2
2
|
import '@tinycloud/share-envelope';
|
|
3
3
|
import './authorization-CmP1skO8.js';
|
package/dist/web-recipient.js
CHANGED
|
@@ -5225,6 +5225,23 @@ var unifiedEncryptionNetworkSchema = external_exports.string().refine((value) =>
|
|
|
5225
5225
|
const network = separator < 0 ? "" : rest.slice(separator + 1);
|
|
5226
5226
|
return owner.startsWith("did:") && owner.length > 4 && network.length > 0 && !/[:/%?#\s]/.test(network);
|
|
5227
5227
|
}, { message: "expected canonical TinyCloud encryption network" });
|
|
5228
|
+
var unifiedEncryptionResourceSchema = external_exports.string().refine((value) => {
|
|
5229
|
+
const prefix = "urn:tinycloud:encryption:";
|
|
5230
|
+
if (!value.startsWith(prefix)) return false;
|
|
5231
|
+
const match = /^(did:.+):([^/:?#\s]+)(?:\/(.+))?$/.exec(value.slice(prefix.length));
|
|
5232
|
+
if (match === null) return false;
|
|
5233
|
+
const path = match[3];
|
|
5234
|
+
return path === void 0 || path.split("/").every(
|
|
5235
|
+
(segment) => segment.length > 0 && segment !== "." && segment !== ".." && !/[\\:%?#\s]/.test(segment)
|
|
5236
|
+
);
|
|
5237
|
+
}, { message: "expected a canonical TinyCloud encryption resource" });
|
|
5238
|
+
var unifiedEncryptionKeyPathSchema = external_exports.string().regex(/^share\/[^/]+$/).refine(
|
|
5239
|
+
(value) => {
|
|
5240
|
+
const segment = value.slice("share/".length);
|
|
5241
|
+
return segment !== "." && segment !== ".." && !/[\\:%?#\s]/.test(segment);
|
|
5242
|
+
},
|
|
5243
|
+
{ message: "expected an exact share key path" }
|
|
5244
|
+
);
|
|
5228
5245
|
var unifiedKvCapabilitySchema = external_exports.object({
|
|
5229
5246
|
kind: external_exports.literal("kv"),
|
|
5230
5247
|
resource: unifiedResourceSchema,
|
|
@@ -5238,7 +5255,7 @@ var unifiedKvCapabilitySchema = external_exports.object({
|
|
|
5238
5255
|
}).strict();
|
|
5239
5256
|
var unifiedEncryptionCapabilitySchema = external_exports.object({
|
|
5240
5257
|
kind: external_exports.literal("encryption"),
|
|
5241
|
-
resource:
|
|
5258
|
+
resource: unifiedEncryptionResourceSchema,
|
|
5242
5259
|
action: external_exports.literal("tinycloud.encryption/decrypt")
|
|
5243
5260
|
}).strict();
|
|
5244
5261
|
var unifiedCapabilitySchema = external_exports.union([unifiedKvCapabilitySchema, unifiedEncryptionCapabilitySchema]);
|
|
@@ -5247,6 +5264,7 @@ var unifiedContentSourceSchema = external_exports.object({
|
|
|
5247
5264
|
kvResource: unifiedResourceSchema,
|
|
5248
5265
|
selector: external_exports.union([external_exports.literal("exact"), external_exports.literal("prefix")]),
|
|
5249
5266
|
encryptionNetwork: unifiedEncryptionNetworkSchema,
|
|
5267
|
+
encryptionKeyPath: unifiedEncryptionKeyPathSchema.optional(),
|
|
5250
5268
|
encryptedSymmetricKeyDigestHex: external_exports.string().regex(/^[0-9a-f]{64}$/),
|
|
5251
5269
|
keyVersion: external_exports.number().int().positive(),
|
|
5252
5270
|
mode: external_exports.union([external_exports.literal("mutable"), external_exports.literal("immutable")]),
|
|
@@ -5364,22 +5382,71 @@ function validateV3Invariants(value, ctx) {
|
|
|
5364
5382
|
if (value.attestedEnforcerBinding.nodeAudience !== value.target.nodeAudience || value.attestedEnforcerBinding.signature.signerDid !== value.attestedEnforcerBinding.nodeAudience || Date.parse(value.attestedEnforcerBinding.expiresAt) < Date.parse(value.expiry)) {
|
|
5365
5383
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["attestedEnforcerBinding"], message: "enforcer binding does not cover the target and share lifetime" });
|
|
5366
5384
|
}
|
|
5367
|
-
|
|
5385
|
+
const encryptionKeyPath = value.contentSource.encryptionKeyPath;
|
|
5386
|
+
if (encryptionKeyPath !== void 0 && encryptionKeyPath !== `share/${value.contentSource.shareId}`) {
|
|
5387
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["contentSource", "encryptionKeyPath"], message: "share key path must name this share" });
|
|
5388
|
+
}
|
|
5389
|
+
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) {
|
|
5368
5390
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["contentSource"], message: "content source is not bound to policy" });
|
|
5369
5391
|
}
|
|
5370
5392
|
if (value.policy.signature.suite !== "Ed25519" || value.policy.signature.signerDid !== value.policy.ownerDid) {
|
|
5371
5393
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["policy", "signature"], message: "policy must be signed by its owner" });
|
|
5372
5394
|
}
|
|
5373
5395
|
const kvCapabilities = value.policy.capabilityCeiling.filter((capability) => capability.kind === "kv");
|
|
5374
|
-
const
|
|
5396
|
+
const expectedEncryptionResource = encryptionKeyPath === void 0 ? value.encryptionNetwork : `${value.encryptionNetwork}/${encryptionKeyPath}`;
|
|
5397
|
+
const decryptCapabilities = value.policy.capabilityCeiling.filter((capability) => capability.kind === "encryption" && capability.resource === expectedEncryptionResource && capability.action === "tinycloud.encryption/decrypt");
|
|
5375
5398
|
if (value.policy.capabilityCeiling.length !== 2 || kvCapabilities.length !== 1 || decryptCapabilities.length !== 1 || kvCapabilities[0]?.resource !== value.contentSource.kvResource || kvCapabilities[0]?.selector !== value.contentSource.selector) {
|
|
5376
|
-
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["policy", "capabilityCeiling"], message: "policy ceiling must contain exact decrypt
|
|
5399
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["policy", "capabilityCeiling"], message: "policy ceiling must contain the exact decrypt resource" });
|
|
5377
5400
|
}
|
|
5378
5401
|
}
|
|
5379
5402
|
var unsignedShareEnvelopeV3Schema = unsignedShareEnvelopeV3BaseSchema.superRefine(validateV3Invariants);
|
|
5380
5403
|
var shareEnvelopeV3Schema = unsignedShareEnvelopeV3BaseSchema.extend({ signature: signatureSchema }).strict().superRefine(validateV3Invariants);
|
|
5381
5404
|
var ENVELOPE_AAD_LABEL = "tinycloud-share-envelope-v1";
|
|
5405
|
+
var SEALED_BLOB_VERSION = 1;
|
|
5382
5406
|
var AAD = utf8Bytes(ENVELOPE_AAD_LABEL);
|
|
5407
|
+
var KEY_LENGTH = 32;
|
|
5408
|
+
var NONCE_LENGTH = 12;
|
|
5409
|
+
var TAG_LENGTH = 16;
|
|
5410
|
+
var HEADER_LENGTH = 1;
|
|
5411
|
+
function assertKey(key32) {
|
|
5412
|
+
if (key32.length !== KEY_LENGTH) {
|
|
5413
|
+
throw new TypeError(`key must be ${KEY_LENGTH} bytes, got ${key32.length}`);
|
|
5414
|
+
}
|
|
5415
|
+
}
|
|
5416
|
+
async function importAesKey(key32, usage) {
|
|
5417
|
+
assertKey(key32);
|
|
5418
|
+
return globalThis.crypto.subtle.importKey(
|
|
5419
|
+
"raw",
|
|
5420
|
+
key32,
|
|
5421
|
+
"AES-GCM",
|
|
5422
|
+
false,
|
|
5423
|
+
[usage]
|
|
5424
|
+
);
|
|
5425
|
+
}
|
|
5426
|
+
async function open(blob, key32) {
|
|
5427
|
+
if (blob.length < HEADER_LENGTH + NONCE_LENGTH + TAG_LENGTH) {
|
|
5428
|
+
throw new TypeError(`sealed blob too short: ${blob.length} bytes`);
|
|
5429
|
+
}
|
|
5430
|
+
if (blob[0] !== SEALED_BLOB_VERSION) {
|
|
5431
|
+
throw new TypeError(`unknown sealed blob version: ${blob[0]}`);
|
|
5432
|
+
}
|
|
5433
|
+
const nonce = blob.subarray(HEADER_LENGTH, HEADER_LENGTH + NONCE_LENGTH);
|
|
5434
|
+
const ciphertext = blob.subarray(HEADER_LENGTH + NONCE_LENGTH);
|
|
5435
|
+
return decryptEnvelope(nonce, ciphertext, key32);
|
|
5436
|
+
}
|
|
5437
|
+
async function decryptEnvelope(nonce, ciphertext, key32) {
|
|
5438
|
+
if (nonce.length !== NONCE_LENGTH) {
|
|
5439
|
+
throw new TypeError(`nonce must be ${NONCE_LENGTH} bytes, got ${nonce.length}`);
|
|
5440
|
+
}
|
|
5441
|
+
const key = await importAesKey(key32, "decrypt");
|
|
5442
|
+
return new Uint8Array(
|
|
5443
|
+
await globalThis.crypto.subtle.decrypt(
|
|
5444
|
+
{ name: "AES-GCM", iv: nonce, additionalData: AAD },
|
|
5445
|
+
key,
|
|
5446
|
+
ciphertext
|
|
5447
|
+
)
|
|
5448
|
+
);
|
|
5449
|
+
}
|
|
5383
5450
|
function isPlainRecord(value) {
|
|
5384
5451
|
const proto = Object.getPrototypeOf(value);
|
|
5385
5452
|
return proto === Object.prototype || proto === null;
|
|
@@ -5510,6 +5577,7 @@ function equal(left, right) {
|
|
|
5510
5577
|
return left.length === right.length && left.every((byte, index) => byte === right[index]);
|
|
5511
5578
|
}
|
|
5512
5579
|
var ED25519_VERIFY_OPTS2 = { zip215: false };
|
|
5580
|
+
var ENVELOPE_V2_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v2\0";
|
|
5513
5581
|
var ENVELOPE_V3_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v3\0";
|
|
5514
5582
|
var POLICY_V1_SIGNATURE_DOMAIN = "xyz.tinycloud.policy/policy/v1\0";
|
|
5515
5583
|
var POLICY_V2_SIGNATURE_DOMAIN = "xyz.tinycloud.policy/policy/v2\0";
|
|
@@ -5517,6 +5585,14 @@ var CONTENT_SOURCE_V1_DOMAIN = "xyz.tinycloud.policy/ContentSource/v1\0";
|
|
|
5517
5585
|
var POLICY_CAPABILITY_V1_DOMAIN = "xyz.tinycloud.policy/PolicyCapability/v1\0";
|
|
5518
5586
|
var NATIVE_PROJECTION_V1_DOMAIN = "xyz.tinycloud.policy/NativeProjection/v1\0";
|
|
5519
5587
|
var ATTESTED_ENFORCER_V2_DOMAIN = "xyz.tinycloud.policy/AttestedEnforcerBinding/v2\0";
|
|
5588
|
+
function signingBytesV2(unsigned) {
|
|
5589
|
+
const domain = utf8Bytes(ENVELOPE_V2_SIGNATURE_DOMAIN);
|
|
5590
|
+
const body = utf8Bytes(canonicalize(unsigned));
|
|
5591
|
+
const bytes = new Uint8Array(domain.length + body.length);
|
|
5592
|
+
bytes.set(domain);
|
|
5593
|
+
bytes.set(body, domain.length);
|
|
5594
|
+
return bytes;
|
|
5595
|
+
}
|
|
5520
5596
|
function signingBytesV3(unsigned) {
|
|
5521
5597
|
const domain = utf8Bytes(ENVELOPE_V3_SIGNATURE_DOMAIN);
|
|
5522
5598
|
const body = utf8Bytes(canonicalize(unsigned));
|
|
@@ -5525,6 +5601,17 @@ function signingBytesV3(unsigned) {
|
|
|
5525
5601
|
bytes.set(body, domain.length);
|
|
5526
5602
|
return bytes;
|
|
5527
5603
|
}
|
|
5604
|
+
function verifyEnvelopeV2SignatureOnly(envelope) {
|
|
5605
|
+
const parsed = shareEnvelopeV2Schema.parse(envelope);
|
|
5606
|
+
const { signature, ...unsigned } = parsed;
|
|
5607
|
+
return ed255193.verify(fromBase64Url(signature.value), signingBytesV2(unsigned), ed25519PublicKeyFromDidKey(signature.signerDid), ED25519_VERIFY_OPTS2);
|
|
5608
|
+
}
|
|
5609
|
+
async function verifyEnvelopeV2(envelope, options) {
|
|
5610
|
+
const parsed = shareEnvelopeV2Schema.parse(envelope);
|
|
5611
|
+
if (parsed.signature.signerDid !== options.expectedSignerDid || !verifyEnvelopeV2SignatureOnly(parsed)) return false;
|
|
5612
|
+
if (parsed.authorizationTarget.kind === "policy") return await computeCid(fromBase64Url(parsed.authorizationTarget.policyBytes)) === parsed.authorizationTarget.policyCid;
|
|
5613
|
+
return true;
|
|
5614
|
+
}
|
|
5528
5615
|
function verifyEnvelopeV3SignatureOnly(envelope) {
|
|
5529
5616
|
const parsed = shareEnvelopeV3Schema.parse(envelope);
|
|
5530
5617
|
const { signature, ...unsigned } = parsed;
|
|
@@ -5611,6 +5698,52 @@ function base32Lower(bytes) {
|
|
|
5611
5698
|
if (bits > 0) output += alphabet[buffer << 5 - bits & 31];
|
|
5612
5699
|
return output;
|
|
5613
5700
|
}
|
|
5701
|
+
var KEY_LENGTH2 = 32;
|
|
5702
|
+
function assertCanonicalCid(cidString) {
|
|
5703
|
+
const cid = CID.parse(cidString);
|
|
5704
|
+
if (cid.version !== 1 || cid.code !== code || cid.multihash.code !== 18 || // sha2-256 (0x12) only, at the link layer too
|
|
5705
|
+
cid.toString() !== cidString) {
|
|
5706
|
+
throw new TypeError(`not a canonical CIDv1 raw sha2-256 base32 CID: ${cidString}`);
|
|
5707
|
+
}
|
|
5708
|
+
}
|
|
5709
|
+
function parseShareUrl(url, options = {}) {
|
|
5710
|
+
const parsed = new URL(url);
|
|
5711
|
+
if (parsed.protocol !== "https:") {
|
|
5712
|
+
throw new TypeError(`share URL must be https, got ${parsed.protocol}`);
|
|
5713
|
+
}
|
|
5714
|
+
if (parsed.username !== "" || parsed.password !== "") {
|
|
5715
|
+
throw new TypeError("share URL must not carry userinfo");
|
|
5716
|
+
}
|
|
5717
|
+
if (parsed.search !== "") {
|
|
5718
|
+
throw new TypeError("share URL must not have a query string");
|
|
5719
|
+
}
|
|
5720
|
+
if (options.expectedOrigin !== void 0) {
|
|
5721
|
+
if (!isCanonicalHttpsOrigin(options.expectedOrigin)) {
|
|
5722
|
+
throw new TypeError(
|
|
5723
|
+
`expectedOrigin must be a canonical https origin, got ${options.expectedOrigin}`
|
|
5724
|
+
);
|
|
5725
|
+
}
|
|
5726
|
+
if (parsed.origin !== options.expectedOrigin) {
|
|
5727
|
+
throw new TypeError(
|
|
5728
|
+
`share URL origin ${parsed.origin} does not match expected ${options.expectedOrigin}`
|
|
5729
|
+
);
|
|
5730
|
+
}
|
|
5731
|
+
}
|
|
5732
|
+
const match = /^\/s\/([a-z2-7]+)$/.exec(parsed.pathname);
|
|
5733
|
+
if (!match || match[1] === void 0) {
|
|
5734
|
+
throw new TypeError(`not a share URL path: ${parsed.pathname}`);
|
|
5735
|
+
}
|
|
5736
|
+
const ciphertextCid = match[1];
|
|
5737
|
+
assertCanonicalCid(ciphertextCid);
|
|
5738
|
+
if (!parsed.hash.startsWith("#k=")) {
|
|
5739
|
+
throw new TypeError("share URL is missing the #k= key fragment");
|
|
5740
|
+
}
|
|
5741
|
+
const key32 = fromBase64Url(parsed.hash.slice("#k=".length));
|
|
5742
|
+
if (key32.length !== KEY_LENGTH2) {
|
|
5743
|
+
throw new TypeError(`fragment key must be ${KEY_LENGTH2} bytes, got ${key32.length}`);
|
|
5744
|
+
}
|
|
5745
|
+
return { ciphertextCid, key32 };
|
|
5746
|
+
}
|
|
5614
5747
|
|
|
5615
5748
|
// src/artifact-core.ts
|
|
5616
5749
|
var ARTIFACT_ROOT = "artifacts";
|
|
@@ -5707,7 +5840,7 @@ var ShareReceiveError = class extends Error {
|
|
|
5707
5840
|
}
|
|
5708
5841
|
};
|
|
5709
5842
|
function metadataFor(envelope, origin) {
|
|
5710
|
-
const kind = envelope.recipientMatcher.kind === "recipientDid" ? "recipientDid" : envelope.recipientMatcher.kind === "exactEmail" ? "email" : "emailDomain";
|
|
5843
|
+
const kind = envelope.recipientMatcher.kind === "recipientDid" ? "recipientDid" : envelope.recipientMatcher.kind === "exactEmail" ? "email" : envelope.recipientMatcher.kind === "emailDomain" ? "emailDomain" : "bearer";
|
|
5711
5844
|
return {
|
|
5712
5845
|
protocol: "tinycloud-share",
|
|
5713
5846
|
version: 1,
|
|
@@ -5722,7 +5855,7 @@ function metadataFor(envelope, origin) {
|
|
|
5722
5855
|
...envelope.display.filename === void 0 ? {} : { filename: envelope.display.filename },
|
|
5723
5856
|
...envelope.display.mode === void 0 ? {} : { mode: envelope.display.mode }
|
|
5724
5857
|
},
|
|
5725
|
-
...envelope.
|
|
5858
|
+
...envelope.version === 3 && envelope.artifactPin !== void 0 ? { artifact: { id: envelope.artifactPin.artifactId, version: envelope.artifactPin.version } } : {}
|
|
5726
5859
|
};
|
|
5727
5860
|
}
|
|
5728
5861
|
function receiveErrorFor(error) {
|
|
@@ -5820,10 +5953,123 @@ async function resolvePolicyShare(link, options) {
|
|
|
5820
5953
|
options.onResolvedAddressedEnvelope?.(envelope, cid);
|
|
5821
5954
|
return { envelope, origin: url.origin, cid };
|
|
5822
5955
|
}
|
|
5956
|
+
async function resolveLegacyV2(link, options) {
|
|
5957
|
+
let parsed;
|
|
5958
|
+
try {
|
|
5959
|
+
parsed = parseShareUrl(link, { ...options.expectedOrigin === void 0 ? {} : { expectedOrigin: options.expectedOrigin } });
|
|
5960
|
+
} catch {
|
|
5961
|
+
throw new ShareReceiveError("invalid-link", "legacy share link format is invalid");
|
|
5962
|
+
}
|
|
5963
|
+
try {
|
|
5964
|
+
options.signal?.throwIfAborted();
|
|
5965
|
+
const origin = new URL(link).origin;
|
|
5966
|
+
const limit = options.maxSealedBlobBytes ?? DEFAULT_MAX_SEALED_BLOB_BYTES;
|
|
5967
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope limit is invalid");
|
|
5968
|
+
let sealed;
|
|
5969
|
+
if (options.fetchBlob !== void 0) {
|
|
5970
|
+
try {
|
|
5971
|
+
sealed = await options.fetchBlob({ origin, cid: parsed.ciphertextCid, ...options.signal === void 0 ? {} : { signal: options.signal } });
|
|
5972
|
+
} catch {
|
|
5973
|
+
throw new ShareReceiveError("fetch-failed", "legacy registry unavailable");
|
|
5974
|
+
}
|
|
5975
|
+
} else {
|
|
5976
|
+
if (options.registryBaseUrl === void 0) throw new ShareReceiveError("fetch-failed", "legacy registry is required");
|
|
5977
|
+
const response = await (options.fetch ?? globalThis.fetch)(`${options.registryBaseUrl.replace(/\/+$/, "")}/ipfs/${parsed.ciphertextCid}?format=raw`, {
|
|
5978
|
+
headers: { accept: "application/vnd.ipld.raw" },
|
|
5979
|
+
redirect: "error",
|
|
5980
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
5981
|
+
});
|
|
5982
|
+
if (!response.ok) throw new ShareReceiveError("fetch-failed", "legacy registry unavailable");
|
|
5983
|
+
if (Number(response.headers.get("content-length")) > limit) throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope exceeds the size limit");
|
|
5984
|
+
const reader = response.body?.getReader();
|
|
5985
|
+
if (reader === void 0) throw new ShareReceiveError("fetch-failed", "legacy registry returned no body");
|
|
5986
|
+
const chunks = [];
|
|
5987
|
+
let length2 = 0;
|
|
5988
|
+
for (; ; ) {
|
|
5989
|
+
const part = await reader.read();
|
|
5990
|
+
if (part.done) break;
|
|
5991
|
+
length2 += part.value.length;
|
|
5992
|
+
if (length2 > limit) {
|
|
5993
|
+
await reader.cancel();
|
|
5994
|
+
throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope exceeds the size limit");
|
|
5995
|
+
}
|
|
5996
|
+
chunks.push(part.value);
|
|
5997
|
+
}
|
|
5998
|
+
sealed = new Uint8Array(length2);
|
|
5999
|
+
let offset = 0;
|
|
6000
|
+
for (const chunk of chunks) {
|
|
6001
|
+
sealed.set(chunk, offset);
|
|
6002
|
+
offset += chunk.length;
|
|
6003
|
+
}
|
|
6004
|
+
}
|
|
6005
|
+
options.signal?.throwIfAborted();
|
|
6006
|
+
if (!(sealed instanceof Uint8Array) || sealed.length > limit) throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope exceeds the size limit");
|
|
6007
|
+
if (await computeCid(sealed) !== parsed.ciphertextCid) throw new ShareReceiveError("cid-mismatch", "legacy envelope CID does not match");
|
|
6008
|
+
let plaintext;
|
|
6009
|
+
try {
|
|
6010
|
+
plaintext = await open(sealed, parsed.key32);
|
|
6011
|
+
} catch {
|
|
6012
|
+
throw new ShareReceiveError("decrypt-failed", "legacy envelope could not be opened");
|
|
6013
|
+
}
|
|
6014
|
+
let envelope;
|
|
6015
|
+
try {
|
|
6016
|
+
const encoded = new TextDecoder("utf-8", { fatal: true }).decode(plaintext);
|
|
6017
|
+
const value = JSON.parse(encoded);
|
|
6018
|
+
if (canonicalize(value) !== encoded) throw new Error("non-canonical envelope");
|
|
6019
|
+
envelope = shareEnvelopeV2Schema.parse(value);
|
|
6020
|
+
} catch {
|
|
6021
|
+
throw new ShareReceiveError("envelope-invalid", "legacy envelope is invalid");
|
|
6022
|
+
}
|
|
6023
|
+
return { envelope, origin, cid: parsed.ciphertextCid };
|
|
6024
|
+
} finally {
|
|
6025
|
+
parsed.key32.fill(0);
|
|
6026
|
+
}
|
|
6027
|
+
}
|
|
6028
|
+
function evidenceMatchesV2(envelope, evidence) {
|
|
6029
|
+
const actions = new Set(evidence.actions.map((action) => action === "read" ? "tinycloud.kv/get" : action === "list" ? "tinycloud.kv/list" : action === "edit" ? "tinycloud.kv/put" : action));
|
|
6030
|
+
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}/`);
|
|
6031
|
+
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);
|
|
6032
|
+
}
|
|
6033
|
+
async function verifyLegacyV2(envelope, options) {
|
|
6034
|
+
try {
|
|
6035
|
+
if (!await verifyEnvelopeV2(envelope, { expectedSignerDid: envelope.signature.signerDid })) throw new Error("signature");
|
|
6036
|
+
} catch {
|
|
6037
|
+
throw new ShareReceiveError("signature-invalid", "legacy envelope signature is invalid");
|
|
6038
|
+
}
|
|
6039
|
+
const expiry = Date.parse(envelope.expiry);
|
|
6040
|
+
if (!Number.isFinite(expiry)) throw new ShareReceiveError("envelope-invalid", "legacy share expiry is invalid");
|
|
6041
|
+
if (expiry <= (options.now?.() ?? Date.now())) throw new ShareReceiveError("expired", "legacy share has expired", { expiresAt: envelope.expiry });
|
|
6042
|
+
if (envelope.authorizationTarget.kind === "policy") {
|
|
6043
|
+
if (options.trustedPolicyAuthority === void 0) throw new ShareReceiveError("envelope-invalid", "a legacy policy authority is required");
|
|
6044
|
+
let evidence;
|
|
6045
|
+
try {
|
|
6046
|
+
evidence = await options.trustedPolicyAuthority.resolve({ policyCid: envelope.authorizationTarget.policyCid, envelope });
|
|
6047
|
+
} catch {
|
|
6048
|
+
}
|
|
6049
|
+
if (evidence === void 0 || !evidenceMatchesV2(envelope, evidence)) throw new ShareReceiveError("capability-invalid", "legacy policy evidence does not match");
|
|
6050
|
+
if (options.trustedSignerDid !== void 0 && options.trustedSignerDid !== evidence.signerDid) throw new ShareReceiveError("signature-invalid", "legacy signer is not trusted");
|
|
6051
|
+
} 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) {
|
|
6052
|
+
throw new ShareReceiveError("capability-invalid", "legacy recipient authority does not match");
|
|
6053
|
+
}
|
|
6054
|
+
}
|
|
5823
6055
|
async function inspectShare(link, options = {}) {
|
|
6056
|
+
if (isLegacyV2Link(link)) {
|
|
6057
|
+
const legacy = await resolveLegacyV2(link, options);
|
|
6058
|
+
await verifyLegacyV2(legacy.envelope, options);
|
|
6059
|
+
options.onResolvedAddressedEnvelope?.(legacy.envelope, legacy.cid);
|
|
6060
|
+
return { metadata: metadataFor(legacy.envelope, legacy.origin), link: { origin: legacy.origin, cid: legacy.cid, kind: "compact" } };
|
|
6061
|
+
}
|
|
5824
6062
|
const resolved = await resolvePolicyShare(link, options);
|
|
5825
6063
|
return { metadata: metadataFor(resolved.envelope, resolved.origin), link: { origin: resolved.origin, cid: resolved.cid, kind: "policy" } };
|
|
5826
6064
|
}
|
|
6065
|
+
function isLegacyV2Link(link) {
|
|
6066
|
+
try {
|
|
6067
|
+
const url = new URL(link);
|
|
6068
|
+
return /^\/s\/bafkrei[a-z2-7]{52}$/.test(url.pathname) && url.hash.startsWith("#k=");
|
|
6069
|
+
} catch {
|
|
6070
|
+
return false;
|
|
6071
|
+
}
|
|
6072
|
+
}
|
|
5827
6073
|
|
|
5828
6074
|
// src/recipient.ts
|
|
5829
6075
|
import { ed25519 as ed255194, x25519 } from "@noble/curves/ed25519";
|
|
@@ -5850,41 +6096,18 @@ function object2(value, label) {
|
|
|
5850
6096
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${label} is invalid`);
|
|
5851
6097
|
return value;
|
|
5852
6098
|
}
|
|
5853
|
-
var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
5854
6099
|
function toBase64(bytes) {
|
|
5855
|
-
|
|
5856
|
-
for (let i = 0; i < bytes.length; i += 3) {
|
|
5857
|
-
const b0 = bytes[i];
|
|
5858
|
-
const b1 = i + 1 < bytes.length ? bytes[i + 1] : 0;
|
|
5859
|
-
const b2 = i + 2 < bytes.length ? bytes[i + 2] : 0;
|
|
5860
|
-
out += BASE64_ALPHABET[b0 >> 2 & 63];
|
|
5861
|
-
out += BASE64_ALPHABET[(b0 << 4 | b1 >> 4) & 63];
|
|
5862
|
-
out += i + 1 < bytes.length ? BASE64_ALPHABET[(b1 << 2 | b2 >> 6) & 63] : "=";
|
|
5863
|
-
out += i + 2 < bytes.length ? BASE64_ALPHABET[b2 & 63] : "=";
|
|
5864
|
-
}
|
|
5865
|
-
return out;
|
|
6100
|
+
return toBase64Url(bytes).replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(bytes.length / 3) * 4, "=");
|
|
5866
6101
|
}
|
|
5867
6102
|
function fromBase64(value, label) {
|
|
5868
6103
|
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
|
|
5869
6104
|
throw new Error(`${label} is invalid`);
|
|
5870
6105
|
}
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
const v1 = BASE64_ALPHABET.indexOf(value[i + 1]);
|
|
5877
|
-
const v2 = value[i + 2] === "=" ? 0 : BASE64_ALPHABET.indexOf(value[i + 2]);
|
|
5878
|
-
const v3 = value[i + 3] === "=" ? 0 : BASE64_ALPHABET.indexOf(value[i + 3]);
|
|
5879
|
-
const b0 = v0 << 2 | v1 >> 4;
|
|
5880
|
-
const b1 = (v1 & 15) << 4 | v2 >> 2;
|
|
5881
|
-
const b2 = (v2 & 3) << 6 | v3;
|
|
5882
|
-
if (outIdx < out.length) out[outIdx++] = b0;
|
|
5883
|
-
if (outIdx < out.length) out[outIdx++] = b1;
|
|
5884
|
-
if (outIdx < out.length) out[outIdx++] = b2;
|
|
5885
|
-
}
|
|
5886
|
-
if (toBase64(out) !== value) throw new Error(`${label} is invalid`);
|
|
5887
|
-
return out;
|
|
6106
|
+
try {
|
|
6107
|
+
return fromBase64Url(value.replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_"));
|
|
6108
|
+
} catch {
|
|
6109
|
+
throw new Error(`${label} is invalid`);
|
|
6110
|
+
}
|
|
5888
6111
|
}
|
|
5889
6112
|
async function digestBytes(value) {
|
|
5890
6113
|
return toBase64Url(new Uint8Array(await crypto.subtle.digest("SHA-256", value)));
|
|
@@ -5995,16 +6218,16 @@ function matchesInitialCiphertextDigest(bytes, expected) {
|
|
|
5995
6218
|
function canonicalHashHex(value) {
|
|
5996
6219
|
return hex2(sha2563(new TextEncoder().encode(canonicalize(value))));
|
|
5997
6220
|
}
|
|
5998
|
-
async function aesGcmDecrypt(key, blob) {
|
|
6221
|
+
async function aesGcmDecrypt(key, blob, aad) {
|
|
5999
6222
|
if (key.length !== 32 || blob.length < 28) throw new Error("encrypted content is malformed");
|
|
6000
6223
|
const cryptoKey = await crypto.subtle.importKey("raw", key, "AES-GCM", false, ["decrypt"]);
|
|
6001
|
-
return new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv: blob.slice(0, 12) }, cryptoKey, blob.slice(12)));
|
|
6224
|
+
return new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv: blob.slice(0, 12), ...aad && { additionalData: aad } }, cryptoKey, blob.slice(12)));
|
|
6002
6225
|
}
|
|
6003
|
-
async function aesGcmEncrypt(key, plaintext) {
|
|
6226
|
+
async function aesGcmEncrypt(key, plaintext, aad) {
|
|
6004
6227
|
if (key.length !== 32) throw new Error("encrypted content key is malformed");
|
|
6005
6228
|
const nonce = crypto.getRandomValues(new Uint8Array(12));
|
|
6006
6229
|
const cryptoKey = await crypto.subtle.importKey("raw", key, "AES-GCM", false, ["encrypt"]);
|
|
6007
|
-
const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: nonce }, cryptoKey, plaintext));
|
|
6230
|
+
const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: nonce, ...aad && { additionalData: aad } }, cryptoKey, plaintext));
|
|
6008
6231
|
const output = new Uint8Array(nonce.length + ciphertext.length);
|
|
6009
6232
|
output.set(nonce);
|
|
6010
6233
|
output.set(ciphertext, nonce.length);
|
|
@@ -6019,8 +6242,9 @@ function parseV3InlineEncryptedEnvelope(bytes, expected) {
|
|
|
6019
6242
|
}
|
|
6020
6243
|
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("encrypted content envelope is malformed");
|
|
6021
6244
|
const record = value;
|
|
6022
|
-
const allowed = ["v", "networkId", "alg", "keyVersion", "encryptedSymmetricKey", "encryptedSymmetricKeyHash", "ciphertext", "metadata"];
|
|
6023
|
-
|
|
6245
|
+
const allowed = ["v", "networkId", "keyPath", "alg", "keyVersion", "encryptedSymmetricKey", "encryptedSymmetricKeyHash", "ciphertext", "aad", "metadata"];
|
|
6246
|
+
const source = expected.contentSource;
|
|
6247
|
+
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(sha2563(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"))) {
|
|
6024
6248
|
throw new Error("encrypted content envelope binding is invalid");
|
|
6025
6249
|
}
|
|
6026
6250
|
return record;
|
|
@@ -6141,7 +6365,7 @@ var ShareRecipientClient = class {
|
|
|
6141
6365
|
this.session = { sessionId: delegation.sessionCid, expiresAt: new Date(compact.payload.exp * 1e3).toISOString(), actions: envelope.actions, resource: envelope.resource };
|
|
6142
6366
|
return this.session;
|
|
6143
6367
|
}
|
|
6144
|
-
/** Decrypt
|
|
6368
|
+
/** Decrypt unscoped v1 or path-bound v2 content, independently checking canonical and registered raw-key digests. */
|
|
6145
6369
|
async decryptV3Content(bytes) {
|
|
6146
6370
|
const envelope = this.options.envelope;
|
|
6147
6371
|
const signer = this.nativeSigner ?? this.signer;
|
|
@@ -6149,18 +6373,22 @@ var ShareRecipientClient = class {
|
|
|
6149
6373
|
assertArtifactBinding(artifactBindingView(envelope));
|
|
6150
6374
|
if (!matchesInitialCiphertextDigest(bytes, envelope.contentSource.initialCiphertextDigestHex)) throw new Error("v3 owner KV ciphertext digest mismatch");
|
|
6151
6375
|
const encrypted = parseV3InlineEncryptedEnvelope(bytes, envelope);
|
|
6376
|
+
const ciphertext = fromBase64(encrypted.ciphertext, "v3 encrypted content");
|
|
6377
|
+
if (ciphertext.length < 28) throw new Error("encrypted content is malformed");
|
|
6378
|
+
const aad = encrypted.aad === void 0 ? void 0 : fromBase64(encrypted.aad, "v3 encrypted content AAD");
|
|
6152
6379
|
const receiverPrivateKey = crypto.getRandomValues(new Uint8Array(32));
|
|
6153
6380
|
const receiverPublicKey = toBase64(x25519.getPublicKey(receiverPrivateKey));
|
|
6154
6381
|
const receiverPublicKeyHash = canonicalHashHex(receiverPublicKey);
|
|
6155
|
-
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 };
|
|
6382
|
+
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 };
|
|
6156
6383
|
const bodyHash = hex2(sha2563(new TextEncoder().encode(canonicalize(body))));
|
|
6157
6384
|
const session = verifyCompactUcanAuthorization(this.v3Authorization, this.session.sessionId);
|
|
6158
6385
|
const now = Math.floor(Date.now() / 1e3);
|
|
6386
|
+
const decryptResource = encrypted.keyPath === void 0 ? encrypted.networkId : `${encrypted.networkId}/${encrypted.keyPath}`;
|
|
6159
6387
|
const invocation = await signCompactUcanAuthorization({
|
|
6160
6388
|
issuerDid: this.options.holderDid,
|
|
6161
6389
|
audienceDid: this.v3NodeAudience,
|
|
6162
|
-
attenuation: { [
|
|
6163
|
-
facts: [{ type: body.type, targetNode: body.targetNode, networkId: body.networkId, bodyHash, encryptedSymmetricKeyHash: body.encryptedSymmetricKeyHash, receiverPublicKeyHash, alg: body.alg, keyVersion: body.keyVersion }],
|
|
6390
|
+
attenuation: { [decryptResource]: { "tinycloud.encryption/decrypt": [{}] } },
|
|
6391
|
+
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 }],
|
|
6164
6392
|
proofs: [session.cid],
|
|
6165
6393
|
notBefore: now,
|
|
6166
6394
|
expiresAt: Math.min(now + 60, session.payload.exp),
|
|
@@ -6184,7 +6412,7 @@ var ShareRecipientClient = class {
|
|
|
6184
6412
|
const symmetricKey = await aesGcmDecrypt(shared, wrapped.slice(33));
|
|
6185
6413
|
shared.fill(0);
|
|
6186
6414
|
if (symmetricKey.length !== 32) throw new Error("v3 content key is malformed");
|
|
6187
|
-
const plaintext = await aesGcmDecrypt(symmetricKey,
|
|
6415
|
+
const plaintext = await aesGcmDecrypt(symmetricKey, ciphertext, aad);
|
|
6188
6416
|
this.v3ContentKey?.fill(0);
|
|
6189
6417
|
this.v3ContentKey = symmetricKey;
|
|
6190
6418
|
this.v3ContentEnvelope = encrypted;
|
|
@@ -6196,7 +6424,9 @@ var ShareRecipientClient = class {
|
|
|
6196
6424
|
/** Re-encrypt edited v3 content with the admitted content key. */
|
|
6197
6425
|
async encryptV3Content(bytes, mediaType) {
|
|
6198
6426
|
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");
|
|
6199
|
-
|
|
6427
|
+
const envelope = this.v3ContentEnvelope;
|
|
6428
|
+
const aad = envelope.aad === void 0 ? void 0 : fromBase64(envelope.aad, "v3 encrypted content AAD");
|
|
6429
|
+
return new TextEncoder().encode(canonicalize({ ...envelope, ciphertext: toBase64(await aesGcmEncrypt(this.v3ContentKey, bytes, aad)), metadata: { ...envelope.metadata ?? {}, contentType: mediaType } }));
|
|
6200
6430
|
}
|
|
6201
6431
|
async resumeWithProof(envelope, resumeToken, proof) {
|
|
6202
6432
|
void envelope;
|