@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/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.search !== "" || url.username !== "" || url.password !== "") throw new ShareEnvelopeError("invalid-link", "share link is not canonical");
9336
- if (url.pathname === "/s/inline" && url.hash.startsWith("#v=2&p=")) {
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(url.hash.slice("#v=2&p=".length));
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 value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes2));
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
- if (payload.v !== 2 || typeof payload.c !== "string" || typeof payload.cid !== "string" || payload.k !== void 0 && typeof payload.k !== "string" || Object.keys(payload).some((key3) => !["v", "c", "cid", "k"].includes(key3))) throw new ShareEnvelopeError("invalid-link", "inline payload fields are invalid");
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 === "/s/inline" && url.hash.startsWith("#v=2&p=")) {
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
- return new URL(link).pathname.startsWith("/s/");
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 import_ed2551910 = require("@noble/curves/ed25519");
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 fields = ["policyId", "recipient", "resource", "credentialType", "returnLink", "envelopeRef", "audience", "issuedAt", "expiresAt", "nonce"];
12367
- 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.audience !== expected.credentialsAudience || 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" || signature.signerDid !== target.nodeAudience) throw new Error("v3 share delivery authorization is not bound to the submitted request");
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 || !import_ed2551910.ed25519.verify(admissionSignature, digest4, didKeyBytes2(String(signature.signerDid)))) throw new Error("v3 share delivery admission signature is invalid");
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 import_ed2551911 = require("@noble/curves/ed25519");
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 body = await response.json();
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 import_ed2551911.ed25519.verify(
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 import_ed2551911.ed25519.verify(signature, payload, publicKey);
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);