@tinycloud/sdk-core 3.0.0-beta.8 → 3.0.0-beta.9
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/delegations/index.cjs +24 -122
- package/dist/delegations/index.cjs.map +1 -1
- package/dist/delegations/index.d.cts +3 -3
- package/dist/delegations/index.d.ts +3 -3
- package/dist/delegations/index.js +25 -127
- package/dist/delegations/index.js.map +1 -1
- package/dist/index.cjs +210 -124
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -4
- package/dist/index.d.ts +57 -4
- package/dist/index.js +211 -129
- package/dist/index.js.map +1 -1
- package/dist/{recipient-types.schema-DjmM_yJu.d.cts → recipient-types.schema-C0igQ1kA.d.cts} +6 -100
- package/dist/{recipient-types.schema-DjmM_yJu.d.ts → recipient-types.schema-C0igQ1kA.d.ts} +6 -100
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -139,6 +139,7 @@ __export(index_exports, {
|
|
|
139
139
|
NETWORK_NAME_PATTERN: () => import_sdk_services8.NETWORK_NAME_PATTERN,
|
|
140
140
|
NetworkIdError: () => import_sdk_services8.NetworkIdError,
|
|
141
141
|
OPENKEY_ACTION_ID_SEPARATOR: () => OPENKEY_ACTION_ID_SEPARATOR,
|
|
142
|
+
OWNER_NODE_BINDING_TIMEOUT_MS: () => OWNER_NODE_BINDING_TIMEOUT_MS,
|
|
142
143
|
OpenKeyManageKeyError: () => OpenKeyManageKeyError,
|
|
143
144
|
POLICY_CAPABILITY_V1_DOMAIN: () => POLICY_CAPABILITY_V1_DOMAIN,
|
|
144
145
|
POLICY_CAPABILITY_V1_SCHEMA: () => POLICY_CAPABILITY_V1_SCHEMA,
|
|
@@ -178,7 +179,6 @@ __export(index_exports, {
|
|
|
178
179
|
SECRET_RECORDS_SCHEMA: () => import_bootstrap4.SECRET_RECORDS_SCHEMA,
|
|
179
180
|
SERVICE_LONG_TO_SHORT: () => SERVICE_LONG_TO_SHORT,
|
|
180
181
|
SERVICE_SHORT_TO_LONG: () => SERVICE_SHORT_TO_LONG,
|
|
181
|
-
SHARE_DELIVERY_AUTHORIZATION_DOMAIN: () => SHARE_DELIVERY_AUTHORIZATION_DOMAIN,
|
|
182
182
|
SPACE: () => import_bootstrap4.SPACE,
|
|
183
183
|
SQL: () => import_bootstrap4.SQL,
|
|
184
184
|
SQLAction: () => import_sdk_services8.SQLAction,
|
|
@@ -394,6 +394,7 @@ __export(index_exports, {
|
|
|
394
394
|
principalDidEquals: () => principalDidEquals,
|
|
395
395
|
projectUnifiedPolicyCapabilities: () => projectUnifiedPolicyCapabilities,
|
|
396
396
|
projectUnifiedPolicyCapability: () => projectUnifiedPolicyCapability,
|
|
397
|
+
publishLocationRecord: () => publishLocationRecord,
|
|
397
398
|
recapCaveatsEqual: () => recapCaveatsEqual,
|
|
398
399
|
registerPolicyV3: () => registerPolicyV3,
|
|
399
400
|
renewPolicyRootStatusV3: () => renewPolicyRootStatusV3,
|
|
@@ -413,7 +414,6 @@ __export(index_exports, {
|
|
|
413
414
|
serviceError: () => import_sdk_services8.serviceError,
|
|
414
415
|
sha256Base64Url: () => sha256Base64Url,
|
|
415
416
|
shareCapabilityAllows: () => shareCapabilityAllows,
|
|
416
|
-
shareDeliveryTrustedKid: () => shareDeliveryTrustedKid,
|
|
417
417
|
signCompactUcanAuthorization: () => signCompactUcanAuthorization,
|
|
418
418
|
signCompactUcanRootAuthorization: () => signCompactUcanRootAuthorization,
|
|
419
419
|
signLocationRecord: () => signLocationRecord,
|
|
@@ -450,7 +450,6 @@ __export(index_exports, {
|
|
|
450
450
|
validatePolicyStatusSignedShape: () => validatePolicyStatusSignedShape,
|
|
451
451
|
validatePolicyStatusUnsigned: () => validatePolicyStatusUnsigned,
|
|
452
452
|
validatePolicyUnsigned: () => validatePolicyUnsigned,
|
|
453
|
-
validateShareDeliveryAuthorizationBytes: () => validateShareDeliveryAuthorizationBytes,
|
|
454
453
|
validateShareDeliveryAuthorizationV3Bytes: () => validateShareDeliveryAuthorizationV3Bytes,
|
|
455
454
|
validateUnifiedPolicyV2: () => validateUnifiedPolicyV2,
|
|
456
455
|
verifyDecryptResponse: () => import_sdk_services8.verifyDecryptResponse,
|
|
@@ -458,6 +457,7 @@ __export(index_exports, {
|
|
|
458
457
|
verifyEip191MessageSignature: () => verifyEip191MessageSignature,
|
|
459
458
|
verifyIssuedCredential: () => verifyIssuedCredential,
|
|
460
459
|
verifyLocationRecord: () => verifyLocationRecord,
|
|
460
|
+
verifyOwnerNodeBinding: () => verifyOwnerNodeBinding,
|
|
461
461
|
verifyPolicy: () => verifyPolicy,
|
|
462
462
|
verifyPolicyEngineRecord: () => verifyPolicyEngineRecord,
|
|
463
463
|
verifyPolicyRootRevocationV3: () => verifyPolicyRootRevocationV3,
|
|
@@ -9332,25 +9332,33 @@ function parsePublishedShareLink(link, options2 = {}) {
|
|
|
9332
9332
|
}
|
|
9333
9333
|
const origin2 = normalizeOrigin(url.origin);
|
|
9334
9334
|
if (options2.trustedOrigins !== void 0 && !options2.trustedOrigins.map(normalizeOrigin).includes(origin2)) throw new ShareEnvelopeError("origin-mismatch", "share link origin is not trusted");
|
|
9335
|
-
if (url.
|
|
9336
|
-
if (url.pathname === "/
|
|
9335
|
+
if (url.username !== "" || url.password !== "") throw new ShareEnvelopeError("invalid-link", "share link is not canonical");
|
|
9336
|
+
if (url.pathname === "/viewer") {
|
|
9337
|
+
const secretPayload = url.search === "" && url.hash.startsWith("#tc2=") ? url.hash.slice("#tc2=".length) : void 0;
|
|
9338
|
+
const publicPayload = url.hash === "" && url.searchParams.size === 1 ? url.searchParams.get("tc2") ?? void 0 : void 0;
|
|
9339
|
+
if (secretPayload === void 0 && publicPayload === void 0) throw new ShareEnvelopeError("invalid-link", "published share link is invalid");
|
|
9340
|
+
if (publicPayload !== void 0 && url.search !== `?tc2=${publicPayload}`) throw new ShareEnvelopeError("invalid-link", "published share link is not canonical");
|
|
9337
9341
|
let payload;
|
|
9338
9342
|
try {
|
|
9339
|
-
const bytes2 = base64UrlToBytes(
|
|
9343
|
+
const bytes2 = base64UrlToBytes(secretPayload ?? publicPayload);
|
|
9340
9344
|
if (bytes2.length > (options2.maxInlineBytes ?? DEFAULT_MAX_INLINE_BYTES) * 2) throw new Error("inline payload too large");
|
|
9341
|
-
const
|
|
9345
|
+
const text2 = new TextDecoder("utf-8", { fatal: true }).decode(bytes2);
|
|
9346
|
+
const value = JSON.parse(text2);
|
|
9342
9347
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("inline payload shape");
|
|
9348
|
+
if (canonicalJson(value) !== text2) throw new Error("inline payload is not canonical JSON");
|
|
9343
9349
|
payload = value;
|
|
9344
9350
|
} catch (error) {
|
|
9345
9351
|
throw new ShareEnvelopeError("invalid-link", error instanceof Error ? error.message : "invalid inline payload");
|
|
9346
9352
|
}
|
|
9347
|
-
|
|
9353
|
+
const payloadKeys = Object.keys(payload);
|
|
9354
|
+
if (payload.v !== 2 || typeof payload.c !== "string" || typeof payload.cid !== "string" || payload.k !== void 0 && typeof payload.k !== "string" || payloadKeys.some((key3) => !["v", "c", "cid", "k"].includes(key3)) || publicPayload !== void 0 && (payloadKeys.length !== 3 || payload.k !== void 0)) throw new ShareEnvelopeError("invalid-link", "inline payload fields are invalid");
|
|
9348
9355
|
const sealed = base64UrlToBytes(payload.c);
|
|
9349
9356
|
if (!verifyPublishedShareCid(payload.cid, sealed)) throw new ShareEnvelopeError("cid-mismatch", "inline payload CID does not match");
|
|
9350
9357
|
const key2 = payload.k === void 0 ? void 0 : base64UrlToBytes(payload.k);
|
|
9351
9358
|
if (key2 !== void 0 && key2.length !== 32) throw new ShareEnvelopeError("invalid-link", "inline payload key is invalid");
|
|
9352
9359
|
return { origin: origin2, cid: payload.cid, ...key2 === void 0 ? {} : { key: key2 }, sealed };
|
|
9353
9360
|
}
|
|
9361
|
+
if (url.search !== "") throw new ShareEnvelopeError("invalid-link", "share link is not canonical");
|
|
9354
9362
|
const match = /^\/s\/([a-z2-7]+)$/.exec(url.pathname);
|
|
9355
9363
|
if (match === null || !url.hash.startsWith("#k=")) throw new ShareEnvelopeError("invalid-link", "published share link is invalid");
|
|
9356
9364
|
const key = base64UrlToBytes(url.hash.slice(3));
|
|
@@ -9370,13 +9378,13 @@ function parseShareUrl(link, options2 = {}) {
|
|
|
9370
9378
|
} catch {
|
|
9371
9379
|
throw new ShareEnvelopeError("invalid-link", "share link must be an absolute URL");
|
|
9372
9380
|
}
|
|
9373
|
-
if (url.search !== "") throw new ShareEnvelopeError("invalid-link", "share links must not contain query parameters");
|
|
9374
9381
|
const origin2 = normalizeOrigin(url.origin);
|
|
9375
9382
|
if (options2.trustedOrigins !== void 0 && !options2.trustedOrigins.map(normalizeOrigin).includes(origin2)) throw new ShareEnvelopeError("origin-mismatch", "share link origin is not trusted");
|
|
9376
|
-
if (url.pathname === "/
|
|
9383
|
+
if (url.pathname === "/viewer" && (url.hash.startsWith("#tc2=") || url.searchParams.has("tc2"))) {
|
|
9377
9384
|
const published = parsePublishedShareLink(link, options2);
|
|
9378
9385
|
return { kind: "inline", origin: origin2, cid: published.cid, key: published.key, url: `${url.origin}${url.pathname}`, protocol: "share-envelope-v2" };
|
|
9379
9386
|
}
|
|
9387
|
+
if (url.search !== "") throw new ShareEnvelopeError("invalid-link", "share links must not contain query parameters");
|
|
9380
9388
|
const match = url.pathname.match(/^\/s\/(i\/)?([^/]+)$/);
|
|
9381
9389
|
if (!match) throw new ShareEnvelopeError("invalid-link", "share link path is invalid");
|
|
9382
9390
|
const fragmentParams = url.hash === "" ? void 0 : new URLSearchParams(url.hash.slice(1));
|
|
@@ -9399,7 +9407,8 @@ function parseShareUrl(link, options2 = {}) {
|
|
|
9399
9407
|
}
|
|
9400
9408
|
function isV2ShareLink(link) {
|
|
9401
9409
|
try {
|
|
9402
|
-
|
|
9410
|
+
const url = new URL(link);
|
|
9411
|
+
return url.pathname.startsWith("/s/") || url.pathname === "/viewer";
|
|
9403
9412
|
} catch {
|
|
9404
9413
|
return link.startsWith(INLINE_PREFIX);
|
|
9405
9414
|
}
|
|
@@ -12212,114 +12221,13 @@ function validateOwnerSharePolicyRegistrationBytes(bytesValue, expected) {
|
|
|
12212
12221
|
return validateOwnerSharePolicyRegistration(value, expected);
|
|
12213
12222
|
}
|
|
12214
12223
|
|
|
12215
|
-
// src/delegations/share-delivery.ts
|
|
12216
|
-
var import_ed255199 = require("@noble/curves/ed25519");
|
|
12217
|
-
var SHARE_DELIVERY_AUTHORIZATION_DOMAIN = "xyz.tinycloud.share/delivery-authorization/v2\0";
|
|
12218
|
-
function isCanonicalHttpsOrigin(value) {
|
|
12219
|
-
let url;
|
|
12220
|
-
try {
|
|
12221
|
-
url = new URL(value);
|
|
12222
|
-
} catch {
|
|
12223
|
-
return false;
|
|
12224
|
-
}
|
|
12225
|
-
return url.protocol === "https:" && url.origin === value && url.pathname === "/" && url.search === "" && url.hash === "" && url.username === "" && url.password === "";
|
|
12226
|
-
}
|
|
12227
|
-
var AUTHORIZATION_KEYS = [
|
|
12228
|
-
"type",
|
|
12229
|
-
"version",
|
|
12230
|
-
"jti",
|
|
12231
|
-
"shareCid",
|
|
12232
|
-
"shareId",
|
|
12233
|
-
"registrationCid",
|
|
12234
|
-
"delegationCid",
|
|
12235
|
-
"enforcementDelegationCid",
|
|
12236
|
-
"envelopeCid",
|
|
12237
|
-
"policyCid",
|
|
12238
|
-
"nodeAudience",
|
|
12239
|
-
"targetOrigin",
|
|
12240
|
-
"openCredentialsAudience",
|
|
12241
|
-
"holder",
|
|
12242
|
-
"recipientMatcher",
|
|
12243
|
-
"deliveryEmail",
|
|
12244
|
-
"shareUrl",
|
|
12245
|
-
"returnOrigin",
|
|
12246
|
-
"documentName",
|
|
12247
|
-
"senderDid",
|
|
12248
|
-
"senderTrust",
|
|
12249
|
-
"contentSource",
|
|
12250
|
-
"contentSourceDigest",
|
|
12251
|
-
"shareExpiresAt",
|
|
12252
|
-
"issuedAt",
|
|
12253
|
-
"reportAbuseToken",
|
|
12254
|
-
"actions",
|
|
12255
|
-
"resource",
|
|
12256
|
-
"authorityMaterialHandle",
|
|
12257
|
-
"authorityMaterialDigest",
|
|
12258
|
-
"requestBodyDigest",
|
|
12259
|
-
"idempotencyKey",
|
|
12260
|
-
"expiresAt",
|
|
12261
|
-
"dataAuthority"
|
|
12262
|
-
];
|
|
12263
|
-
function assertObject2(value, keys, label) {
|
|
12264
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${label} is invalid`);
|
|
12265
|
-
const record2 = value;
|
|
12266
|
-
if (Object.keys(record2).length !== keys.length || keys.some((key) => !Object.prototype.hasOwnProperty.call(record2, key))) throw new Error(`${label} has unknown or missing fields`);
|
|
12267
|
-
return record2;
|
|
12268
|
-
}
|
|
12269
|
-
function fromB642(value) {
|
|
12270
|
-
if (!/^[A-Za-z0-9_-]+$/.test(value)) throw new Error("share delivery value is not canonical base64url");
|
|
12271
|
-
if (typeof atob === "function") {
|
|
12272
|
-
const padded = value.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - value.length % 4) % 4);
|
|
12273
|
-
return Uint8Array.from(atob(padded), (character) => character.charCodeAt(0));
|
|
12274
|
-
}
|
|
12275
|
-
if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(value, "base64url"));
|
|
12276
|
-
throw new Error("base64url decoding is unavailable");
|
|
12277
|
-
}
|
|
12278
|
-
function validateShareDeliveryAuthorizationBytes(bytesValue, expected) {
|
|
12279
|
-
let value;
|
|
12280
|
-
try {
|
|
12281
|
-
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytesValue));
|
|
12282
|
-
} catch {
|
|
12283
|
-
throw new Error("share delivery authorization response is not valid UTF-8 JSON");
|
|
12284
|
-
}
|
|
12285
|
-
const root = assertObject2(value, ["authorization", "proof"], "share delivery authorization response");
|
|
12286
|
-
const authorization = assertObject2(root.authorization, AUTHORIZATION_KEYS, "share delivery authorization");
|
|
12287
|
-
const proof = assertObject2(root.proof, ["alg", "kid", "signature"], "share delivery authorization proof");
|
|
12288
|
-
const request = expected.request;
|
|
12289
|
-
if (authorization.type !== "TinyCloudShareDeliveryAuthorization" || authorization.version !== 2 || authorization.dataAuthority !== false || authorization.jti !== request.jti || authorization.shareCid !== request.shareCid || authorization.shareId !== request.shareId || authorization.registrationCid !== request.registrationCid || authorization.delegationCid !== request.delegationCid || authorization.enforcementDelegationCid !== request.enforcementDelegationCid || authorization.envelopeCid !== request.envelopeCid || authorization.policyCid !== request.policyCid || authorization.deliveryEmail !== request.recipientEmail || authorization.shareUrl !== request.shareUrl || authorization.documentName !== request.documentName || authorization.requestBodyDigest !== request.requestBodyDigest || authorization.expiresAt !== request.expiresAt || authorization.idempotencyKey !== request.idempotencyKey || authorization.reportAbuseToken !== request.jti || authorization.authorityMaterialHandle !== request.registrationCid) {
|
|
12290
|
-
throw new Error("share delivery authorization is not bound to the submitted request");
|
|
12291
|
-
}
|
|
12292
|
-
if (typeof authorization.issuedAt !== "string" || Date.parse(authorization.issuedAt) > Date.now() + 6e4) throw new Error("share delivery authorization issuedAt is invalid");
|
|
12293
|
-
if (new Date(authorization.expiresAt).toISOString() !== authorization.expiresAt) throw new Error("share delivery authorization expiresAt is non-canonical");
|
|
12294
|
-
if (typeof authorization.openCredentialsAudience !== "string" || !isCanonicalHttpsOrigin(authorization.openCredentialsAudience) || authorization.openCredentialsAudience !== expected.credentialsAudience || authorization.openCredentialsAudience === authorization.nodeAudience || authorization.openCredentialsAudience === authorization.returnOrigin) {
|
|
12295
|
-
throw new Error("share delivery authorization openCredentialsAudience is missing or untrusted");
|
|
12296
|
-
}
|
|
12297
|
-
if (proof.alg !== "EdDSA" || typeof proof.kid !== "string" || typeof proof.signature !== "string") throw new Error("share delivery authorization proof is invalid");
|
|
12298
|
-
if (proof.kid !== expected.nodeProof.kid) throw new Error("share delivery authorization proof key is not trusted");
|
|
12299
|
-
const encodedKid = expected.nodeProof.publicKey;
|
|
12300
|
-
const publicKey = encodedKid.length === 34 && encodedKid[0] === 237 && encodedKid[1] === 1 ? encodedKid.slice(2) : encodedKid;
|
|
12301
|
-
if (publicKey.length !== 32) throw new Error("share delivery authorization proof key is invalid");
|
|
12302
|
-
const signatureBytes = fromB642(proof.signature);
|
|
12303
|
-
const signedBytes = new TextEncoder().encode(`${SHARE_DELIVERY_AUTHORIZATION_DOMAIN}${canonicalizeSignedObjectUnsigned(authorization)}`);
|
|
12304
|
-
if (signatureBytes.length !== 64 || !import_ed255199.ed25519.verify(signatureBytes, signedBytes, publicKey)) throw new Error("share delivery authorization proof signature is invalid");
|
|
12305
|
-
return { authorization, proof };
|
|
12306
|
-
}
|
|
12307
|
-
function shareDeliveryTrustedKid(publicKey) {
|
|
12308
|
-
if (publicKey.length !== 32) throw new Error("share delivery node key must be a raw 32-byte Ed25519 public key");
|
|
12309
|
-
const prefixed = new Uint8Array(34);
|
|
12310
|
-
prefixed.set([237, 1]);
|
|
12311
|
-
prefixed.set(publicKey, 2);
|
|
12312
|
-
const did = `did:key:${base58btc.encode(prefixed)}`;
|
|
12313
|
-
return `${did}#${did.slice("did:key:".length)}`;
|
|
12314
|
-
}
|
|
12315
|
-
|
|
12316
12224
|
// src/delegations/share-delivery-v3.ts
|
|
12317
|
-
var
|
|
12225
|
+
var import_ed255199 = require("@noble/curves/ed25519");
|
|
12318
12226
|
var import_sha2567 = require("@noble/hashes/sha256");
|
|
12319
12227
|
var CREDENTIAL_INVITATION_REQUEST_DOMAIN = "xyz.tinycloud.credentials/invitation-request/v1\0";
|
|
12320
12228
|
var DELIVERY_ADMISSION_DOMAIN = "xyz.tinycloud.policy/delivery-admission/v0\0";
|
|
12321
|
-
var REQUEST_KEYS = ["schema", "policyId", "recipient", "resource", "credentialType", "returnLink", "envelopeRef", "audience", "issuedAt", "expiresAt", "nonce"];
|
|
12322
|
-
var ADMISSION_KEYS = ["schema", "policyId", "ownerDid", "recipient", "resource", "actions", "credentialType", "returnLink", "envelopeRef", "senderKeyDid", "audience", "issuedAt", "expiresAt", "nonce", "signature"];
|
|
12229
|
+
var REQUEST_KEYS = ["schema", "policyId", "recipient", "resource", "credentialType", "returnLink", "envelopeRef", "label", "shareExpiresAt", "audience", "issuedAt", "expiresAt", "nonce"];
|
|
12230
|
+
var ADMISSION_KEYS = ["schema", "policyId", "ownerDid", "recipient", "resource", "actions", "credentialType", "returnLink", "envelopeRef", "label", "shareExpiresAt", "senderKeyDid", "audience", "issuedAt", "expiresAt", "nonce", "signature"];
|
|
12323
12231
|
function exactObject2(value, keys, label) {
|
|
12324
12232
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${label} is invalid`);
|
|
12325
12233
|
const record2 = value;
|
|
@@ -12363,15 +12271,16 @@ function validateShareDeliveryAuthorizationV3Bytes(bytes2, expected) {
|
|
|
12363
12271
|
const contentSource = object3(envelope.contentSource, "v3 content source");
|
|
12364
12272
|
const envelopeSignature = object3(envelope.signature, "v3 envelope signature");
|
|
12365
12273
|
const target = object3(envelope.target, "v3 target");
|
|
12366
|
-
const
|
|
12367
|
-
|
|
12274
|
+
const binding = object3(envelope.attestedEnforcerBinding, "v3 attested enforcer binding");
|
|
12275
|
+
const fields = ["policyId", "recipient", "resource", "credentialType", "returnLink", "envelopeRef", "label", "shareExpiresAt", "audience", "issuedAt", "expiresAt", "nonce"];
|
|
12276
|
+
if (request.schema !== "xyz.tinycloud.credentials/invitation-request/v1" || admission.schema !== "xyz.tinycloud.policy/delivery-admission/v0" || fields.some((field) => request[field] !== admission[field]) || request.policyId !== envelope.policyCid || request.recipient !== expected.request.recipientEmail || request.resource !== contentSource.kvResource || request.credentialType !== credentialType.id || request.credentialType !== "opencredentials.email/v1" || request.returnLink !== expected.request.shareUrl || request.envelopeRef !== expected.request.shareCid || request.label !== expected.request.documentName || request.shareExpiresAt !== envelope.expiry || request.audience !== expected.deliveryAudience || request.expiresAt !== expected.request.expiresAt || request.nonce !== expected.request.jti || admission.ownerDid !== envelopeSignature.signerDid || admission.senderKeyDid !== expected.senderKeyDid || canonicalizeSignedObjectUnsigned(admission.actions) !== canonicalizeSignedObjectUnsigned(["tinycloud.kv/get"]) || signature.suite !== "eddsa-ed25519-sha256-jcs-v1" || target.nodeAudience !== binding.nodeAudience || signature.signerDid !== binding.nodeAudience) throw new Error("v3 share delivery authorization is not bound to the submitted request");
|
|
12368
12277
|
const issuedAt = Date.parse(String(request.issuedAt));
|
|
12369
12278
|
const expiresAt = Date.parse(String(request.expiresAt));
|
|
12370
12279
|
if (!Number.isFinite(issuedAt) || !Number.isFinite(expiresAt) || expiresAt <= issuedAt || expiresAt - issuedAt > 15 * 6e4) throw new Error("v3 share delivery authorization time is invalid");
|
|
12371
12280
|
const admissionSignature = decodeBase64Url3(signature.value);
|
|
12372
12281
|
const { signature: _signature, ...unsignedAdmission } = admission;
|
|
12373
12282
|
const digest4 = (0, import_sha2567.sha256)(new TextEncoder().encode(`${DELIVERY_ADMISSION_DOMAIN}${canonicalizeSignedObjectUnsigned(unsignedAdmission)}`));
|
|
12374
|
-
if (admissionSignature.length !== 64 || !
|
|
12283
|
+
if (admissionSignature.length !== 64 || !import_ed255199.ed25519.verify(admissionSignature, digest4, didKeyBytes2(String(signature.signerDid)))) throw new Error("v3 share delivery admission signature is invalid");
|
|
12375
12284
|
return { request, admission };
|
|
12376
12285
|
}
|
|
12377
12286
|
|
|
@@ -17996,7 +17905,7 @@ function portForProtocol(protocol) {
|
|
|
17996
17905
|
}
|
|
17997
17906
|
|
|
17998
17907
|
// src/location.ts
|
|
17999
|
-
var
|
|
17908
|
+
var import_ed2551910 = require("@noble/curves/ed25519");
|
|
18000
17909
|
var import_viem5 = require("viem");
|
|
18001
17910
|
var DEFAULT_TINYCLOUD_LOCATION_REGISTRY_URL = "https://registry.tinycloud.xyz";
|
|
18002
17911
|
var DEFAULT_TINYCLOUD_FALLBACK_HOST = "https://node.tinycloud.xyz";
|
|
@@ -18004,6 +17913,7 @@ var DEFAULT_LOCAL_NODE_URL = "http://127.0.0.1:8000";
|
|
|
18004
17913
|
var LOCAL_LOOPBACK_PROBE_TIMEOUT_MS = 250;
|
|
18005
17914
|
var LOCAL_LINK_PROBE_TIMEOUT_MS = 750;
|
|
18006
17915
|
var LOCAL_LINK_HOST_SUFFIX = ".local.tinycloud.link";
|
|
17916
|
+
var OWNER_NODE_BINDING_TIMEOUT_MS = 5e3;
|
|
18007
17917
|
function createInMemoryLocalNodeIdentityStore() {
|
|
18008
17918
|
const pins = /* @__PURE__ */ new Map();
|
|
18009
17919
|
return {
|
|
@@ -18136,21 +18046,80 @@ async function verifyLocationRecord(input) {
|
|
|
18136
18046
|
}
|
|
18137
18047
|
return false;
|
|
18138
18048
|
}
|
|
18139
|
-
async function fetchLocationRecord(registryUrl, subject, fetchFn = globalThis.fetch) {
|
|
18049
|
+
async function fetchLocationRecord(registryUrl, subject, fetchFn = globalThis.fetch, signal) {
|
|
18140
18050
|
const url = `${registryUrl.replace(/\/$/, "")}/v1/locations/${encodeURIComponent(subject)}`;
|
|
18141
|
-
const response = await fetchFn(url
|
|
18051
|
+
const response = await fetchFn(url, {
|
|
18052
|
+
redirect: "error",
|
|
18053
|
+
headers: { accept: "application/json" },
|
|
18054
|
+
...signal === void 0 ? {} : { signal }
|
|
18055
|
+
});
|
|
18142
18056
|
if (response.status === 404) {
|
|
18143
18057
|
return null;
|
|
18144
18058
|
}
|
|
18145
18059
|
if (!response.ok) {
|
|
18146
18060
|
throw new Error(`location registry returned HTTP ${response.status}`);
|
|
18147
18061
|
}
|
|
18148
|
-
const
|
|
18062
|
+
const text2 = await boundedResponseText(response, "location registry", 64 * 1024);
|
|
18063
|
+
let body;
|
|
18064
|
+
try {
|
|
18065
|
+
body = JSON.parse(text2);
|
|
18066
|
+
} catch {
|
|
18067
|
+
throw new LocationRecordValidationError("registry response is not JSON");
|
|
18068
|
+
}
|
|
18149
18069
|
if (body.record === void 0) {
|
|
18150
18070
|
throw new LocationRecordValidationError("registry response missing record");
|
|
18151
18071
|
}
|
|
18152
18072
|
return validateLocationRecord(body.record);
|
|
18153
18073
|
}
|
|
18074
|
+
async function publishLocationRecord(options2) {
|
|
18075
|
+
validateSubject(options2.subject);
|
|
18076
|
+
const registryUrl = exactHttpOrigin(options2.registryUrl, "registry URL");
|
|
18077
|
+
const nodeOrigin = exactHttpOrigin(options2.nodeOrigin, "node origin");
|
|
18078
|
+
const fetchFn = options2.fetch ?? globalThis.fetch.bind(globalThis);
|
|
18079
|
+
const current = await fetchLocationRecord(registryUrl, options2.subject, fetchFn);
|
|
18080
|
+
if (current !== null) {
|
|
18081
|
+
if (current.subject !== options2.subject || !await verifyLocationRecord(current)) {
|
|
18082
|
+
throw new LocationRecordValidationError("existing location record signature is invalid");
|
|
18083
|
+
}
|
|
18084
|
+
if (publishedHttpOrigins(current).includes(nodeOrigin)) {
|
|
18085
|
+
return current;
|
|
18086
|
+
}
|
|
18087
|
+
}
|
|
18088
|
+
const record2 = await signLocationRecord({
|
|
18089
|
+
version: 1,
|
|
18090
|
+
subject: options2.subject,
|
|
18091
|
+
multiaddrs: [...current?.multiaddrs ?? [], httpUrlToMultiaddr(nodeOrigin)],
|
|
18092
|
+
updated_at: (options2.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
18093
|
+
sequence: (current?.sequence ?? -1) + 1
|
|
18094
|
+
}, options2.signer);
|
|
18095
|
+
const response = await fetchFn(
|
|
18096
|
+
`${registryUrl}/v1/locations/${encodeURIComponent(options2.subject)}`,
|
|
18097
|
+
{
|
|
18098
|
+
method: "PUT",
|
|
18099
|
+
redirect: "error",
|
|
18100
|
+
headers: { accept: "application/json", "content-type": "application/json" },
|
|
18101
|
+
body: JSON.stringify(record2)
|
|
18102
|
+
}
|
|
18103
|
+
);
|
|
18104
|
+
if (!response.ok) {
|
|
18105
|
+
throw new Error(`location registry publish returned HTTP ${response.status}`);
|
|
18106
|
+
}
|
|
18107
|
+
const text2 = await boundedResponseText(response, "location registry", 64 * 1024);
|
|
18108
|
+
let body;
|
|
18109
|
+
try {
|
|
18110
|
+
body = JSON.parse(text2);
|
|
18111
|
+
} catch {
|
|
18112
|
+
throw new LocationRecordValidationError("registry publish response is not JSON");
|
|
18113
|
+
}
|
|
18114
|
+
if (body.record === void 0) {
|
|
18115
|
+
throw new LocationRecordValidationError("registry publish response missing record");
|
|
18116
|
+
}
|
|
18117
|
+
const published = validateLocationRecord(body.record);
|
|
18118
|
+
if (canonicalLocationPayload(locationPayloadForRecord(published)) !== canonicalLocationPayload(locationPayloadForRecord(record2)) || published.signature !== record2.signature || !await verifyLocationRecord(published)) {
|
|
18119
|
+
throw new LocationRecordValidationError("registry publish response does not match the signed record");
|
|
18120
|
+
}
|
|
18121
|
+
return published;
|
|
18122
|
+
}
|
|
18154
18123
|
async function resolveCloudLocation(subject, options2 = {}) {
|
|
18155
18124
|
validateSubject(subject);
|
|
18156
18125
|
const verifyRecords = options2.verifyRecords ?? true;
|
|
@@ -18401,6 +18370,123 @@ function multiaddrToHttpUrl(input) {
|
|
|
18401
18370
|
}
|
|
18402
18371
|
return uri;
|
|
18403
18372
|
}
|
|
18373
|
+
function exactHttpOrigin(value, label) {
|
|
18374
|
+
let url;
|
|
18375
|
+
try {
|
|
18376
|
+
url = new URL(value);
|
|
18377
|
+
} catch {
|
|
18378
|
+
throw new LocationRecordValidationError(`${label} must be an absolute URL origin`);
|
|
18379
|
+
}
|
|
18380
|
+
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
18381
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
|
|
18382
|
+
throw new LocationRecordValidationError(`${label} must use HTTPS (or loopback HTTP)`);
|
|
18383
|
+
}
|
|
18384
|
+
if (url.username !== "" || url.password !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "") {
|
|
18385
|
+
throw new LocationRecordValidationError(`${label} must be an origin without credentials, path, query, or fragment`);
|
|
18386
|
+
}
|
|
18387
|
+
return url.origin;
|
|
18388
|
+
}
|
|
18389
|
+
function publishedHttpOrigins(record2) {
|
|
18390
|
+
const origins = [];
|
|
18391
|
+
for (const address of record2.multiaddrs) {
|
|
18392
|
+
try {
|
|
18393
|
+
origins.push(exactHttpOrigin(multiaddrToHttpUrl(address), "published node URL"));
|
|
18394
|
+
} catch {
|
|
18395
|
+
}
|
|
18396
|
+
}
|
|
18397
|
+
return origins;
|
|
18398
|
+
}
|
|
18399
|
+
async function boundedResponseText(response, label, maxBytes) {
|
|
18400
|
+
const declaredLength = response.headers.get("content-length");
|
|
18401
|
+
if (declaredLength !== null && (!/^\d+$/.test(declaredLength) || Number(declaredLength) > maxBytes)) {
|
|
18402
|
+
throw new LocationRecordValidationError(`${label} response is too large`);
|
|
18403
|
+
}
|
|
18404
|
+
if (response.body === null) return "";
|
|
18405
|
+
const reader = response.body.getReader();
|
|
18406
|
+
const chunks = [];
|
|
18407
|
+
let total = 0;
|
|
18408
|
+
try {
|
|
18409
|
+
while (true) {
|
|
18410
|
+
const { done, value } = await reader.read();
|
|
18411
|
+
if (done) break;
|
|
18412
|
+
if (value === void 0) continue;
|
|
18413
|
+
total += value.byteLength;
|
|
18414
|
+
if (total > maxBytes) {
|
|
18415
|
+
await reader.cancel().catch(() => void 0);
|
|
18416
|
+
throw new LocationRecordValidationError(`${label} response is too large`);
|
|
18417
|
+
}
|
|
18418
|
+
chunks.push(value);
|
|
18419
|
+
}
|
|
18420
|
+
} finally {
|
|
18421
|
+
reader.releaseLock();
|
|
18422
|
+
}
|
|
18423
|
+
const bytes2 = new Uint8Array(total);
|
|
18424
|
+
let offset = 0;
|
|
18425
|
+
for (const chunk of chunks) {
|
|
18426
|
+
bytes2.set(chunk, offset);
|
|
18427
|
+
offset += chunk.byteLength;
|
|
18428
|
+
}
|
|
18429
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes2);
|
|
18430
|
+
}
|
|
18431
|
+
function boundedSignal(parent, timeoutMs) {
|
|
18432
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
18433
|
+
throw new LocationRecordValidationError("owner-node binding timeout must be positive");
|
|
18434
|
+
}
|
|
18435
|
+
const controller = new AbortController();
|
|
18436
|
+
const abortFromParent = () => controller.abort(parent?.reason);
|
|
18437
|
+
if (parent?.aborted) abortFromParent();
|
|
18438
|
+
else parent?.addEventListener("abort", abortFromParent, { once: true });
|
|
18439
|
+
const timer = setTimeout(() => controller.abort(new DOMException("owner-node binding timed out", "TimeoutError")), timeoutMs);
|
|
18440
|
+
return {
|
|
18441
|
+
signal: controller.signal,
|
|
18442
|
+
dispose() {
|
|
18443
|
+
clearTimeout(timer);
|
|
18444
|
+
parent?.removeEventListener("abort", abortFromParent);
|
|
18445
|
+
}
|
|
18446
|
+
};
|
|
18447
|
+
}
|
|
18448
|
+
async function verifyOwnerNodeBinding(options2) {
|
|
18449
|
+
validateSubject(options2.ownerDid);
|
|
18450
|
+
validateSubject(options2.nodeDid);
|
|
18451
|
+
const registryUrl = exactHttpOrigin(options2.registryUrl, "registry URL");
|
|
18452
|
+
const nodeOrigin = exactHttpOrigin(options2.nodeOrigin, "node origin");
|
|
18453
|
+
const fetchFn = options2.fetch ?? globalThis.fetch.bind(globalThis);
|
|
18454
|
+
const lookup = boundedSignal(options2.signal, options2.timeoutMs ?? OWNER_NODE_BINDING_TIMEOUT_MS);
|
|
18455
|
+
try {
|
|
18456
|
+
const record2 = await fetchLocationRecord(registryUrl, options2.ownerDid, fetchFn, lookup.signal);
|
|
18457
|
+
if (record2 === null) {
|
|
18458
|
+
throw new LocationRecordValidationError("owner has no published location record");
|
|
18459
|
+
}
|
|
18460
|
+
if (record2.subject !== options2.ownerDid || !await verifyLocationRecord(record2)) {
|
|
18461
|
+
throw new LocationRecordValidationError("owner location record signature is invalid");
|
|
18462
|
+
}
|
|
18463
|
+
const publishedOrigins = publishedHttpOrigins(record2);
|
|
18464
|
+
if (!publishedOrigins.includes(nodeOrigin)) {
|
|
18465
|
+
throw new LocationRecordValidationError("share target is not in the owner's signed location record");
|
|
18466
|
+
}
|
|
18467
|
+
const response = await fetchFn(new URL("/info", nodeOrigin), {
|
|
18468
|
+
redirect: "error",
|
|
18469
|
+
headers: { accept: "application/json" },
|
|
18470
|
+
signal: lookup.signal
|
|
18471
|
+
});
|
|
18472
|
+
if (!response.ok) {
|
|
18473
|
+
throw new LocationRecordValidationError(`target node /info returned HTTP ${response.status}`);
|
|
18474
|
+
}
|
|
18475
|
+
const text2 = await boundedResponseText(response, "target node /info", 16 * 1024);
|
|
18476
|
+
let body;
|
|
18477
|
+
try {
|
|
18478
|
+
body = JSON.parse(text2);
|
|
18479
|
+
} catch {
|
|
18480
|
+
throw new LocationRecordValidationError("target node /info response is not JSON");
|
|
18481
|
+
}
|
|
18482
|
+
if (body === null || typeof body !== "object" || Array.isArray(body) || body.nodeId !== options2.nodeDid) {
|
|
18483
|
+
throw new LocationRecordValidationError("target node identity does not match the share attestation");
|
|
18484
|
+
}
|
|
18485
|
+
return Object.freeze({ record: record2, nodeOrigin, nodeDid: options2.nodeDid });
|
|
18486
|
+
} finally {
|
|
18487
|
+
lookup.dispose();
|
|
18488
|
+
}
|
|
18489
|
+
}
|
|
18404
18490
|
function httpUrlToMultiaddr(input) {
|
|
18405
18491
|
const url = new URL(input);
|
|
18406
18492
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
@@ -18568,7 +18654,7 @@ function verifyDidKeySignature(did, payload, signature) {
|
|
|
18568
18654
|
"did:key signature must be a base64url Ed25519 signature"
|
|
18569
18655
|
);
|
|
18570
18656
|
}
|
|
18571
|
-
return
|
|
18657
|
+
return import_ed2551910.ed25519.verify(
|
|
18572
18658
|
signatureBytes,
|
|
18573
18659
|
new TextEncoder().encode(payload),
|
|
18574
18660
|
publicKey
|
|
@@ -18576,7 +18662,7 @@ function verifyDidKeySignature(did, payload, signature) {
|
|
|
18576
18662
|
}
|
|
18577
18663
|
function verifyDidKeyEd25519Signature(did, payload, signature) {
|
|
18578
18664
|
const publicKey = ed25519PublicKeyFromDidKey2(did);
|
|
18579
|
-
return
|
|
18665
|
+
return import_ed2551910.ed25519.verify(signature, payload, publicKey);
|
|
18580
18666
|
}
|
|
18581
18667
|
function ed25519PublicKeyFromDidKey2(did) {
|
|
18582
18668
|
const identifier = did.slice("did:key:".length);
|