@tinycloud/share-sdk 1.0.0-beta.6 → 1.0.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +116 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -3
- package/dist/index.d.ts +34 -3
- package/dist/index.js +112 -41
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -41,9 +41,11 @@ __export(index_exports, {
|
|
|
41
41
|
SharePublishError: () => SharePublishError,
|
|
42
42
|
ShareReceiveError: () => ShareReceiveError,
|
|
43
43
|
ShareRecipientClient: () => ShareRecipientClient,
|
|
44
|
+
addressedCredentialRequirement: () => addressedCredentialRequirement,
|
|
44
45
|
authorizationMethodForTarget: () => authorizationMethodForTarget,
|
|
45
46
|
authorizeShare: () => authorizeShare,
|
|
46
47
|
canonicalOwnerSharePolicy: () => canonicalOwnerSharePolicy,
|
|
48
|
+
canonicalShareFilename: () => canonicalShareFilename,
|
|
47
49
|
canonicalize: () => canonicalize,
|
|
48
50
|
checkBearerDelegation: () => checkBearerDelegation,
|
|
49
51
|
claimShare: () => claimShare,
|
|
@@ -55,6 +57,7 @@ __export(index_exports, {
|
|
|
55
57
|
createShareV2HolderBindingArtifact: () => createShareV2HolderBindingArtifact,
|
|
56
58
|
deliverCredentialInvitation: () => deliverCredentialInvitation,
|
|
57
59
|
fromBase64Url: () => fromBase64Url,
|
|
60
|
+
hasUnsafeFilenameCodePoint: () => hasUnsafeFilenameCodePoint,
|
|
58
61
|
historyRecordForPublishedShare: () => historyRecordForPublishedShare,
|
|
59
62
|
inspectShare: () => inspectShare,
|
|
60
63
|
listShares: () => listShares,
|
|
@@ -63,6 +66,7 @@ __export(index_exports, {
|
|
|
63
66
|
openNativeShare: () => openNativeShare,
|
|
64
67
|
parseAddressedEnvelope: () => parseAddressedEnvelope,
|
|
65
68
|
parseNativeShareUrl: () => parseNativeShareUrl,
|
|
69
|
+
prepareAddressedShare: () => prepareAddressedShare,
|
|
66
70
|
publishAddressedShare: () => publishAddressedShare,
|
|
67
71
|
publishPolicyShare: () => publishPolicyShare,
|
|
68
72
|
publishTargetShare: () => publishTargetShare,
|
|
@@ -83,6 +87,19 @@ __export(index_exports, {
|
|
|
83
87
|
});
|
|
84
88
|
module.exports = __toCommonJS(index_exports);
|
|
85
89
|
|
|
90
|
+
// src/filename-policy.ts
|
|
91
|
+
var UNSAFE_FILENAME_CODE_POINT = /[\p{Cc}\p{Cf}\p{Cs}\u2028\u2029]/u;
|
|
92
|
+
function hasUnsafeFilenameCodePoint(value) {
|
|
93
|
+
return UNSAFE_FILENAME_CODE_POINT.test(value);
|
|
94
|
+
}
|
|
95
|
+
function canonicalShareFilename(value) {
|
|
96
|
+
const canonical = value.normalize("NFC");
|
|
97
|
+
if (canonical.length === 0 || canonical === "." || canonical === ".." || canonical.includes("/") || canonical.includes("\\") || hasUnsafeFilenameCodePoint(canonical)) {
|
|
98
|
+
throw new TypeError("share filename is unsafe");
|
|
99
|
+
}
|
|
100
|
+
return canonical;
|
|
101
|
+
}
|
|
102
|
+
|
|
86
103
|
// ../share-envelope/dist/index.js
|
|
87
104
|
var import_sha2 = require("@noble/hashes/sha2");
|
|
88
105
|
var import_ed25519 = require("@noble/curves/ed25519");
|
|
@@ -5764,6 +5781,21 @@ function serialize(value) {
|
|
|
5764
5781
|
function canonicalize(value) {
|
|
5765
5782
|
return serialize(value);
|
|
5766
5783
|
}
|
|
5784
|
+
var LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
5785
|
+
function isCanonicalEmailDomain(value) {
|
|
5786
|
+
if (value.length === 0 || value.length > 253) return false;
|
|
5787
|
+
const labels = value.split(".");
|
|
5788
|
+
return labels.length >= 2 && labels.every((label) => LABEL.test(label)) && !/^[0-9]+$/.test(labels.at(-1));
|
|
5789
|
+
}
|
|
5790
|
+
function canonicalMailbox(value) {
|
|
5791
|
+
const email = value.trim().toLowerCase();
|
|
5792
|
+
const at = email.indexOf("@");
|
|
5793
|
+
if (at <= 0 || at !== email.lastIndexOf("@")) return void 0;
|
|
5794
|
+
const local = email.slice(0, at);
|
|
5795
|
+
const domain = email.slice(at + 1);
|
|
5796
|
+
if (local.length > 64 || !/^[a-z0-9#$&'*+/=?^_`{|}~-]+(?:\.[a-z0-9#$&'*+/=?^_`{|}~-]+)*$/.test(local) || !isCanonicalEmailDomain(domain)) return void 0;
|
|
5797
|
+
return Object.freeze({ email, domain });
|
|
5798
|
+
}
|
|
5767
5799
|
async function signCompactUcanAuthorization(input) {
|
|
5768
5800
|
if (input.notBefore >= input.expiresAt || input.expiresAt - input.notBefore > 60) {
|
|
5769
5801
|
throw new TypeError("compact invocation lifetime must be between one and 60 seconds");
|
|
@@ -6509,12 +6541,6 @@ var base58flickr2 = baseX2({
|
|
|
6509
6541
|
});
|
|
6510
6542
|
|
|
6511
6543
|
// src/targets.ts
|
|
6512
|
-
function validEmail(value) {
|
|
6513
|
-
return /^[^@\s]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i.test(value);
|
|
6514
|
-
}
|
|
6515
|
-
function validDomain(value) {
|
|
6516
|
-
return /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?))+$/i.test(value);
|
|
6517
|
-
}
|
|
6518
6544
|
function validRecipientDid(value) {
|
|
6519
6545
|
if (value.length === 0 || value.length > 2048 || /[\u0000-\u0020\u007f]/.test(value)) return false;
|
|
6520
6546
|
const parts = value.split(":");
|
|
@@ -6544,18 +6570,28 @@ function normalizeShareTarget(target) {
|
|
|
6544
6570
|
return { kind: target.kind, did: target.did };
|
|
6545
6571
|
}
|
|
6546
6572
|
if (target.kind === "email") {
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
return { kind: target.kind, address:
|
|
6573
|
+
const mailbox = canonicalMailbox(target.address);
|
|
6574
|
+
if (mailbox === void 0) throw new TypeError("recipient email is invalid");
|
|
6575
|
+
return { kind: target.kind, address: mailbox.email };
|
|
6550
6576
|
}
|
|
6551
|
-
|
|
6552
|
-
|
|
6577
|
+
const domain = target.domain.toLowerCase();
|
|
6578
|
+
if (!isCanonicalEmailDomain(domain)) throw new TypeError("recipient email domain is invalid");
|
|
6579
|
+
return { kind: target.kind, domain };
|
|
6553
6580
|
}
|
|
6554
6581
|
function targetAuthorizationMethod(target) {
|
|
6555
6582
|
return authorizationMethodForTarget(target);
|
|
6556
6583
|
}
|
|
6584
|
+
function publishFilename(value) {
|
|
6585
|
+
try {
|
|
6586
|
+
return canonicalShareFilename(value);
|
|
6587
|
+
} catch {
|
|
6588
|
+
throw new SharePublishError("invalid-argument", hasUnsafeFilenameCodePoint(value) ? "filename contains control or invisible characters" : "filename must be one safe path segment");
|
|
6589
|
+
}
|
|
6590
|
+
}
|
|
6557
6591
|
async function publishTargetShare(input) {
|
|
6558
6592
|
const target = normalizeShareTarget(input.target);
|
|
6593
|
+
const filename = publishFilename(input.filename);
|
|
6594
|
+
const files = input.files?.map((file) => ({ ...file, filename: publishFilename(file.filename) }));
|
|
6559
6595
|
if (input.targetAdapter === void 0) {
|
|
6560
6596
|
if (target.kind === "bearer") throw new SharePublishError("authority-required", "native bearer publication requires an authenticated TinyCloud node");
|
|
6561
6597
|
return {
|
|
@@ -6581,9 +6617,6 @@ async function publishTargetShare(input) {
|
|
|
6581
6617
|
return bytes3;
|
|
6582
6618
|
})();
|
|
6583
6619
|
if (source.byteLength === 0) throw new SharePublishError("invalid-argument", "share content is empty");
|
|
6584
|
-
if (!input.filename || input.filename === "." || input.filename === ".." || /[/\\\u0000-\u001f\u007f]/.test(input.filename)) {
|
|
6585
|
-
throw new SharePublishError("invalid-argument", "filename must be one safe path segment");
|
|
6586
|
-
}
|
|
6587
6620
|
if (target.kind === "bearer" && input.allowBinary !== true) {
|
|
6588
6621
|
try {
|
|
6589
6622
|
new TextDecoder("utf-8", { fatal: true }).decode(source);
|
|
@@ -6591,10 +6624,10 @@ async function publishTargetShare(input) {
|
|
|
6591
6624
|
throw new SharePublishError("invalid-argument", "Markdown input must be valid UTF-8");
|
|
6592
6625
|
}
|
|
6593
6626
|
}
|
|
6594
|
-
const
|
|
6627
|
+
const publishFiles = files === void 0 || files.length === 0 ? [{ bytes: source }] : files;
|
|
6595
6628
|
const limit = input.maxBytes ?? SHARE_CONTENT_LIMIT;
|
|
6596
6629
|
let totalBytes = 0;
|
|
6597
|
-
for (const file of
|
|
6630
|
+
for (const file of publishFiles) {
|
|
6598
6631
|
totalBytes += file.bytes.byteLength;
|
|
6599
6632
|
if (!Number.isSafeInteger(totalBytes) || totalBytes > limit) {
|
|
6600
6633
|
throw new SharePublishError("max-bytes-exceeded", "share publication exceeds the combined byte limit");
|
|
@@ -6605,8 +6638,8 @@ async function publishTargetShare(input) {
|
|
|
6605
6638
|
if (!Number.isFinite(expiresAt.getTime()) || expiresAt.getTime() <= nowMs) throw new SharePublishError("invalid-argument", "expiresAt must be a valid future time");
|
|
6606
6639
|
return input.targetAdapter.publish({
|
|
6607
6640
|
source,
|
|
6608
|
-
filename
|
|
6609
|
-
...
|
|
6641
|
+
filename,
|
|
6642
|
+
...files === void 0 ? {} : { files },
|
|
6610
6643
|
...input.mediaType === void 0 ? {} : { mediaType: input.mediaType },
|
|
6611
6644
|
...input.resourceKind === void 0 ? {} : { resourceKind: input.resourceKind },
|
|
6612
6645
|
...input.actions === void 0 ? {} : { actions: input.actions },
|
|
@@ -6636,9 +6669,7 @@ function targetKind(target) {
|
|
|
6636
6669
|
}
|
|
6637
6670
|
function assertSafeInput(input) {
|
|
6638
6671
|
if (!/^[A-Za-z0-9_-]{16,128}$/.test(input.shareId)) throw new TypeError("addressed share id is invalid");
|
|
6639
|
-
if (input.filename.length === 0 || input.filename === "." || input.filename === ".." || /[/\\\u0000-\u001f\u007f]/.test(input.filename)) throw new TypeError("addressed filename is invalid");
|
|
6640
6672
|
if (!Number.isSafeInteger(input.byteLength) || input.byteLength < 0 || input.byteLength > SHARE_CONTENT_LIMIT) throw new TypeError("addressed content length is invalid");
|
|
6641
|
-
if (input.actions.length === 0 || input.policyActions.length === 0) throw new TypeError("addressed share actions are empty");
|
|
6642
6673
|
if (!Number.isFinite(input.expiresAt.getTime()) || input.expiresAt.getTime() <= Date.now()) throw new TypeError("addressed share expiry must be in the future");
|
|
6643
6674
|
if (input.artifact === "html" && (input.resource.kind !== "prefix" || !input.actions.includes("read") || !input.actions.includes("list"))) throw new TypeError("html artifacts require a readable prefix");
|
|
6644
6675
|
if (input.contentSource.shareId !== input.shareId || input.contentSource.selector !== input.resource.kind) throw new TypeError("addressed content source is not bound to the share");
|
|
@@ -6731,23 +6762,60 @@ function publicationResult(input) {
|
|
|
6731
6762
|
Object.defineProperty(result, "deliveryMaterial", { enumerable: false, value: input.deliveryMaterial });
|
|
6732
6763
|
return result;
|
|
6733
6764
|
}
|
|
6765
|
+
var EMAIL_PROFILE = { id: "tinycloud.email-proof/v1", version: 1 };
|
|
6734
6766
|
var EMAIL_DOMAIN_PROFILE = { id: "tinycloud.email-domain-proof/v1", version: 1 };
|
|
6735
6767
|
var EMAIL_CREDENTIAL_TYPE = { id: "opencredentials.email/v1", version: 1 };
|
|
6768
|
+
var EMAIL_DESCRIPTOR_DIGEST = "1tg-qphmKBVtNwzVg9xyz-xxqt_xtMXAsQyXw46m8S0";
|
|
6736
6769
|
var EMAIL_DOMAIN_DESCRIPTOR_DIGEST = "33X5mAkZZgApdD3xh_T-3KS5moop0J2Nloi2nqWsdWY";
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
const
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6770
|
+
var EMAIL_CREDENTIAL_ISSUER_DID = "did:web:issuer.credentials.org";
|
|
6771
|
+
var EMAIL_CREDENTIAL_ISSUER_KID = `${EMAIL_CREDENTIAL_ISSUER_DID}#controller`;
|
|
6772
|
+
function mailboxCredentialCommitment(target) {
|
|
6773
|
+
const profile = target.kind === "email" ? EMAIL_PROFILE : EMAIL_DOMAIN_PROFILE;
|
|
6774
|
+
const requirement = target.kind === "email" ? { type: "TinyCloudCredentialRequirement", version: 1, profile, credentialType: EMAIL_CREDENTIAL_TYPE, claims: { email: target.address }, maxAgeSeconds: 3600 } : { type: "TinyCloudCredentialRequirement", version: 1, profile, credentialType: EMAIL_CREDENTIAL_TYPE, claims: { emailDomain: target.domain }, maxAgeSeconds: 300 };
|
|
6775
|
+
return {
|
|
6776
|
+
type: "TinyCloudPolicyCredentialRequirement",
|
|
6777
|
+
version: 1,
|
|
6778
|
+
requirementDigest: toBase64Url((0, import_sha2562.sha256)(textEncoder.encode(canonicalize(requirement)))),
|
|
6779
|
+
descriptorDigest: target.kind === "email" ? EMAIL_DESCRIPTOR_DIGEST : EMAIL_DOMAIN_DESCRIPTOR_DIGEST,
|
|
6780
|
+
issuerDid: EMAIL_CREDENTIAL_ISSUER_DID,
|
|
6781
|
+
issuerKid: EMAIL_CREDENTIAL_ISSUER_KID,
|
|
6782
|
+
profile,
|
|
6783
|
+
credentialType: EMAIL_CREDENTIAL_TYPE
|
|
6784
|
+
};
|
|
6785
|
+
}
|
|
6786
|
+
function addressedCredentialRequirement(target) {
|
|
6787
|
+
const normalized = normalizeShareTarget(target);
|
|
6788
|
+
if (normalized.kind !== "email" && normalized.kind !== "emailDomain") throw new TypeError("a mailbox target is required");
|
|
6789
|
+
return mailboxCredentialCommitment(normalized);
|
|
6790
|
+
}
|
|
6791
|
+
function prepareAddressedShare(request) {
|
|
6792
|
+
try {
|
|
6793
|
+
canonicalShareFilename(request.filename);
|
|
6794
|
+
} catch {
|
|
6795
|
+
throw new TypeError("addressed filename is invalid");
|
|
6796
|
+
}
|
|
6797
|
+
if (request.actions.length === 0 || request.policyActions.length === 0) throw new TypeError("addressed share actions are empty");
|
|
6798
|
+
const target = normalizeShareTarget(request.target);
|
|
6749
6799
|
if (target.kind === "bearer") throw new TypeError("addressed target is required");
|
|
6750
|
-
if (target.kind === "
|
|
6800
|
+
if (target.kind === "recipientDid") return { target };
|
|
6801
|
+
if (target.kind === "emailDomain") {
|
|
6802
|
+
if (!request.actions.every((action) => action === "read" || action === "list") || !request.policyActions.every((action) => action === "tinycloud.kv/get" || action === "tinycloud.kv/list" || action === "tinycloud.kv/metadata")) throw new TypeError("email-domain shares are view-only");
|
|
6803
|
+
if (request.deliveryEmail !== void 0) throw new TypeError("email-domain shares are not emailed");
|
|
6804
|
+
}
|
|
6805
|
+
return { target, credentialRequirement: mailboxCredentialCommitment(target) };
|
|
6806
|
+
}
|
|
6807
|
+
function assertMailboxCommitment(commitment, target) {
|
|
6808
|
+
const label = target.kind === "email" ? "email" : "email-domain";
|
|
6809
|
+
if (commitment === void 0) throw new TypeError(`${label} shares require a credential requirement`);
|
|
6810
|
+
const expected = mailboxCredentialCommitment(target);
|
|
6811
|
+
if (canonicalize(commitment.profile) !== canonicalize(expected.profile) || canonicalize(commitment.credentialType) !== canonicalize(expected.credentialType) || commitment.descriptorDigest !== expected.descriptorDigest || commitment.issuerDid !== expected.issuerDid || commitment.issuerKid !== expected.issuerKid) throw new TypeError(`${label} shares require the ${label} credential profile`);
|
|
6812
|
+
if (commitment.requirementDigest !== expected.requirementDigest) throw new TypeError(`credential requirement is not bound to the email ${target.kind === "email" ? "address" : "domain"}`);
|
|
6813
|
+
}
|
|
6814
|
+
async function publishAddressedShare(input) {
|
|
6815
|
+
assertSafeInput(input);
|
|
6816
|
+
const { target } = prepareAddressedShare(input);
|
|
6817
|
+
const options = { ...input, filename: canonicalShareFilename(input.filename) };
|
|
6818
|
+
if (target.kind !== "recipientDid") assertMailboxCommitment(options.credentialRequirement, target);
|
|
6751
6819
|
const expiry = rfc3339Seconds(options.expiresAt);
|
|
6752
6820
|
const matcher = targetMatcher(target);
|
|
6753
6821
|
const capabilities = sortCanonical([
|
|
@@ -7858,17 +7926,20 @@ var ShareNotifyError = class extends Error {
|
|
|
7858
7926
|
};
|
|
7859
7927
|
function recipientMatchesShareRecord(record, recipient) {
|
|
7860
7928
|
const matcher = record.recipientMatcher;
|
|
7861
|
-
|
|
7862
|
-
if (
|
|
7863
|
-
|
|
7864
|
-
|
|
7929
|
+
const mailbox = canonicalMailbox(recipient);
|
|
7930
|
+
if (mailbox === void 0) return false;
|
|
7931
|
+
if (matcher.kind === "exactEmail") return canonicalMailbox(matcher.value)?.email === mailbox.email;
|
|
7932
|
+
if (matcher.kind === "emailDomain") return mailbox.domain === matcher.value;
|
|
7933
|
+
return false;
|
|
7865
7934
|
}
|
|
7866
7935
|
async function notifyShare(input) {
|
|
7867
|
-
|
|
7868
|
-
if (input.
|
|
7936
|
+
const mailbox = canonicalMailbox(input.recipient);
|
|
7937
|
+
if (!input.shareId || mailbox === void 0) throw new ShareNotifyError("recipient is invalid");
|
|
7938
|
+
const recipient = mailbox.email;
|
|
7939
|
+
if (input.record !== void 0 && !recipientMatchesShareRecord(input.record, recipient)) {
|
|
7869
7940
|
throw new ShareNotifyError("recipient does not match the stored share target");
|
|
7870
7941
|
}
|
|
7871
|
-
const idempotencyKey = input.idempotencyKey ?? await defaultIdempotencyKey(input.shareId,
|
|
7942
|
+
const idempotencyKey = input.idempotencyKey ?? await defaultIdempotencyKey(input.shareId, recipient);
|
|
7872
7943
|
const attemptsLimit = input.maxAttempts ?? 3;
|
|
7873
7944
|
if (!Number.isSafeInteger(attemptsLimit) || attemptsLimit < 1 || attemptsLimit > 8) throw new ShareNotifyError("maxAttempts is invalid");
|
|
7874
7945
|
let attempts = 0;
|
|
@@ -7877,7 +7948,7 @@ async function notifyShare(input) {
|
|
|
7877
7948
|
if (input.signal?.aborted) throw new ShareNotifyError("share delivery was cancelled");
|
|
7878
7949
|
attempts += 1;
|
|
7879
7950
|
try {
|
|
7880
|
-
const state = await input.adapter.deliver({ shareId: input.shareId, recipient
|
|
7951
|
+
const state = await input.adapter.deliver({ shareId: input.shareId, recipient, idempotencyKey, ...input.record === void 0 ? {} : { record: input.record }, ...input.signal === void 0 ? {} : { signal: input.signal } });
|
|
7881
7952
|
return { protocol: "tinycloud-share", version: 1, shareId: input.shareId, state, idempotencyKey, attempts };
|
|
7882
7953
|
} catch (error) {
|
|
7883
7954
|
lastError = error;
|
|
@@ -8606,9 +8677,11 @@ async function openNativeShare(sharing, link) {
|
|
|
8606
8677
|
SharePublishError,
|
|
8607
8678
|
ShareReceiveError,
|
|
8608
8679
|
ShareRecipientClient,
|
|
8680
|
+
addressedCredentialRequirement,
|
|
8609
8681
|
authorizationMethodForTarget,
|
|
8610
8682
|
authorizeShare,
|
|
8611
8683
|
canonicalOwnerSharePolicy,
|
|
8684
|
+
canonicalShareFilename,
|
|
8612
8685
|
canonicalize,
|
|
8613
8686
|
checkBearerDelegation,
|
|
8614
8687
|
claimShare,
|
|
@@ -8620,6 +8693,7 @@ async function openNativeShare(sharing, link) {
|
|
|
8620
8693
|
createShareV2HolderBindingArtifact,
|
|
8621
8694
|
deliverCredentialInvitation,
|
|
8622
8695
|
fromBase64Url,
|
|
8696
|
+
hasUnsafeFilenameCodePoint,
|
|
8623
8697
|
historyRecordForPublishedShare,
|
|
8624
8698
|
inspectShare,
|
|
8625
8699
|
listShares,
|
|
@@ -8628,6 +8702,7 @@ async function openNativeShare(sharing, link) {
|
|
|
8628
8702
|
openNativeShare,
|
|
8629
8703
|
parseAddressedEnvelope,
|
|
8630
8704
|
parseNativeShareUrl,
|
|
8705
|
+
prepareAddressedShare,
|
|
8631
8706
|
publishAddressedShare,
|
|
8632
8707
|
publishPolicyShare,
|
|
8633
8708
|
publishTargetShare,
|