@tinycloud/sdk-core 3.3.0-beta.17 → 3.3.0-beta.20

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.js CHANGED
@@ -18382,11 +18382,16 @@ async function resolveTinyCloudHosts(subject, options2 = {}) {
18382
18382
  fetch: fetchFn,
18383
18383
  verifyRecords: options2.verifyRecords
18384
18384
  });
18385
- return {
18386
- hosts: location.multiaddrs.map((addr) => multiaddrToHttpUrl(addr)),
18387
- location
18388
- };
18385
+ const hosts = location.multiaddrs.map((addr) => multiaddrToHttpUrl(addr));
18386
+ if (location.source === "fallback" && !fallbackDeprecationWarned) {
18387
+ fallbackDeprecationWarned = true;
18388
+ console.warn(
18389
+ `[tinycloud:location] Deprecated: no registry host for this account, using fallback ${hosts[0]}. The silent fallback will be removed in a future major; opt in to registryPolicy "fail-closed" now.`
18390
+ );
18391
+ }
18392
+ return { hosts, location };
18389
18393
  }
18394
+ var fallbackDeprecationWarned = false;
18390
18395
  function memoizeFetch(fetchFn) {
18391
18396
  const cache2 = /* @__PURE__ */ new Map();
18392
18397
  return (async (input, init) => {
@@ -18599,11 +18604,18 @@ function publishedHttpOrigins(record2) {
18599
18604
  return origins;
18600
18605
  }
18601
18606
  async function boundedResponseText(response, label, maxBytes) {
18607
+ const bytes2 = await boundedResponseBytes(response, maxBytes);
18608
+ if (bytes2 === null) {
18609
+ throw new LocationRecordValidationError(`${label} response is too large`);
18610
+ }
18611
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes2);
18612
+ }
18613
+ async function boundedResponseBytes(response, maxBytes) {
18602
18614
  const declaredLength = response.headers.get("content-length");
18603
18615
  if (declaredLength !== null && (!/^\d+$/.test(declaredLength) || Number(declaredLength) > maxBytes)) {
18604
- throw new LocationRecordValidationError(`${label} response is too large`);
18616
+ return null;
18605
18617
  }
18606
- if (response.body === null) return "";
18618
+ if (response.body === null) return new Uint8Array();
18607
18619
  const reader = response.body.getReader();
18608
18620
  const chunks = [];
18609
18621
  let total = 0;
@@ -18615,7 +18627,7 @@ async function boundedResponseText(response, label, maxBytes) {
18615
18627
  total += value.byteLength;
18616
18628
  if (total > maxBytes) {
18617
18629
  await reader.cancel().catch(() => void 0);
18618
- throw new LocationRecordValidationError(`${label} response is too large`);
18630
+ return null;
18619
18631
  }
18620
18632
  chunks.push(value);
18621
18633
  }
@@ -18628,7 +18640,7 @@ async function boundedResponseText(response, label, maxBytes) {
18628
18640
  bytes2.set(chunk, offset);
18629
18641
  offset += chunk.byteLength;
18630
18642
  }
18631
- return new TextDecoder("utf-8", { fatal: true }).decode(bytes2);
18643
+ return bytes2;
18632
18644
  }
18633
18645
  function boundedSignal(parent, timeoutMs) {
18634
18646
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
@@ -18926,6 +18938,699 @@ function decodeBase64Url4(value) {
18926
18938
  }
18927
18939
  return Uint8Array.from(bytes2);
18928
18940
  }
18941
+
18942
+ // src/registry-verdict.ts
18943
+ import { ed25519 as ed2551912 } from "@noble/curves/ed25519";
18944
+ import { verifyMessage as verifyMessage3 } from "viem";
18945
+ var RegistryLookupInputError = class extends TypeError {
18946
+ constructor(code3, message) {
18947
+ super(message);
18948
+ this.code = code3;
18949
+ this.name = "RegistryLookupInputError";
18950
+ }
18951
+ };
18952
+ var REGISTRY_LOOKUP_MAX_BODY_BYTES = 64 * 1024;
18953
+ var REGISTRY_NOT_FOUND_BODY = '{"error":"location record not found"}';
18954
+ var MAX_JSON_DEPTH = 32;
18955
+ var JSON_SIMPLE_ESCAPES = {
18956
+ '"': '"',
18957
+ "\\": "\\",
18958
+ "/": "/",
18959
+ b: "\b",
18960
+ f: "\f",
18961
+ n: "\n",
18962
+ r: "\r",
18963
+ t: " "
18964
+ };
18965
+ var SECP256K1_N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
18966
+ var BASE64URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
18967
+ var IPV4_OCTET = "(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])";
18968
+ var IPV4 = new RegExp(`^${IPV4_OCTET}(\\.${IPV4_OCTET}){3}$`);
18969
+ var DNS_LABEL = "[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?";
18970
+ var DNS_NAME = new RegExp(`^${DNS_LABEL}(\\.${DNS_LABEL})*$`);
18971
+ var HOST_MULTIADDR = /^\/(ip4|ip6|dns|dns4|dns6)\/([^/]+)\/tcp\/([1-9][0-9]{0,4})\/(https|http|tls\/http)$/;
18972
+ var RFC3339_DATE_TIME = /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]+)?(Z|[+-]([0-9]{2}):([0-9]{2}))$/;
18973
+ async function classifyRegistryLookup(subject, response, options2 = {}) {
18974
+ if (!isCanonicalSubject(subject)) {
18975
+ throw new RegistryLookupInputError(
18976
+ "invalid-subject",
18977
+ "subject must be did:pkh:eip155:<chainId>:<EIP-55 address> or an Ed25519 did:key"
18978
+ );
18979
+ }
18980
+ const remembered = options2.rememberedSequence ?? null;
18981
+ if (remembered !== null && (!Number.isSafeInteger(remembered) || remembered < 0)) {
18982
+ throw new RegistryLookupInputError(
18983
+ "invalid-remembered-sequence",
18984
+ "rememberedSequence must be a non-negative safe integer"
18985
+ );
18986
+ }
18987
+ const unavailable = (reason, message) => ({
18988
+ status: "unavailable",
18989
+ subject,
18990
+ source: "offchain",
18991
+ reason,
18992
+ message,
18993
+ hosts: []
18994
+ });
18995
+ if (response.kind === "network-error") {
18996
+ return unavailable("network-error", "location registry could not be reached");
18997
+ }
18998
+ if (response.kind === "timeout") {
18999
+ return unavailable("timeout", "location registry lookup timed out");
19000
+ }
19001
+ if (response.status !== 200 && response.status !== 404) {
19002
+ return unavailable("http-status", `location registry returned HTTP ${response.status}`);
19003
+ }
19004
+ if (response.body.length > REGISTRY_LOOKUP_MAX_BODY_BYTES || new TextEncoder().encode(response.body).byteLength > REGISTRY_LOOKUP_MAX_BODY_BYTES) {
19005
+ return unavailable("oversize-response", "location registry response is too large");
19006
+ }
19007
+ if (response.status === 404) {
19008
+ const body = response.body;
19009
+ let start = 0;
19010
+ let end = body.length;
19011
+ while (start < end && " \n\r".includes(body[start])) start++;
19012
+ while (end > start && " \n\r".includes(body[end - 1])) end--;
19013
+ if (body.slice(start, end) !== REGISTRY_NOT_FOUND_BODY) {
19014
+ return unavailable(
19015
+ "malformed-response",
19016
+ "location registry 404 is not the registry's not-found response"
19017
+ );
19018
+ }
19019
+ if (remembered !== null) {
19020
+ return unavailable(
19021
+ "not-found-after-registered",
19022
+ `location registry has no record, but sequence ${remembered} was seen before`
19023
+ );
19024
+ }
19025
+ return { status: "unregistered", subject, source: "offchain", hosts: [] };
19026
+ }
19027
+ let record2;
19028
+ try {
19029
+ record2 = parseRecordResponse(response.body);
19030
+ } catch (error) {
19031
+ return unavailable(
19032
+ "malformed-response",
19033
+ error instanceof Error ? error.message : "location registry response is invalid"
19034
+ );
19035
+ }
19036
+ if (record2.subject !== subject) {
19037
+ return unavailable(
19038
+ "subject-mismatch",
19039
+ "location record subject does not match requested subject"
19040
+ );
19041
+ }
19042
+ if (!await verifyRecordSignature(record2)) {
19043
+ return unavailable("invalid-signature", "location record signature is invalid");
19044
+ }
19045
+ if (remembered !== null && record2.sequence < remembered) {
19046
+ return unavailable(
19047
+ "rollback",
19048
+ `location record sequence ${record2.sequence} is below remembered sequence ${remembered}`
19049
+ );
19050
+ }
19051
+ const hosts = [];
19052
+ for (const address of record2.multiaddrs) {
19053
+ const origin2 = multiaddrOrigin(address);
19054
+ if (origin2 !== null) hosts.push({ urls: [origin2], source: "offchain" });
19055
+ }
19056
+ return {
19057
+ status: "registered",
19058
+ subject,
19059
+ source: "offchain",
19060
+ sequence: record2.sequence,
19061
+ hosts
19062
+ };
19063
+ }
19064
+ function isCanonicalSubject(subject) {
19065
+ if (typeof subject !== "string") return false;
19066
+ const pkh = /^did:pkh:eip155:([1-9][0-9]*):(0x[0-9a-fA-F]{40})$/.exec(subject);
19067
+ if (pkh !== null) {
19068
+ try {
19069
+ return pkhDid(pkh[2], Number(pkh[1])) === subject;
19070
+ } catch {
19071
+ return false;
19072
+ }
19073
+ }
19074
+ return ed25519KeyFromDidKey(subject) !== null;
19075
+ }
19076
+ function ed25519KeyFromDidKey(did) {
19077
+ if (!did.startsWith("did:key:z")) return null;
19078
+ const identifier = did.slice("did:key:".length);
19079
+ let bytes2;
19080
+ try {
19081
+ bytes2 = bases.base58btc.decode(identifier);
19082
+ } catch {
19083
+ return null;
19084
+ }
19085
+ if (bytes2.length !== 34 || bytes2[0] !== 237 || bytes2[1] !== 1) {
19086
+ return null;
19087
+ }
19088
+ return bytes2.slice(2);
19089
+ }
19090
+ var JsonInteger = class {
19091
+ constructor(lexeme) {
19092
+ this.lexeme = lexeme;
19093
+ }
19094
+ };
19095
+ function parseStrictJson(text3) {
19096
+ let i = 0;
19097
+ const fail2 = (what) => {
19098
+ throw new SyntaxError(`location registry response is not strict JSON: ${what} at ${i}`);
19099
+ };
19100
+ const skipWhitespace2 = () => {
19101
+ while (i < text3.length && " \n\r".includes(text3[i])) i++;
19102
+ };
19103
+ const readHex4 = () => {
19104
+ const hex3 = text3.slice(i, i + 4);
19105
+ if (!/^[0-9a-fA-F]{4}$/.test(hex3)) fail2("bad \\u escape");
19106
+ i += 4;
19107
+ return parseInt(hex3, 16);
19108
+ };
19109
+ const parseString = () => {
19110
+ i++;
19111
+ let out = "";
19112
+ for (; ; ) {
19113
+ if (i >= text3.length) fail2("unterminated string");
19114
+ const code3 = text3.charCodeAt(i);
19115
+ if (code3 === 34) {
19116
+ i++;
19117
+ return out;
19118
+ }
19119
+ if (code3 < 32) fail2("control character in string");
19120
+ if (code3 === 92) {
19121
+ const escape2 = text3[i + 1];
19122
+ i += 2;
19123
+ if (escape2 !== void 0 && Object.prototype.hasOwnProperty.call(JSON_SIMPLE_ESCAPES, escape2)) {
19124
+ out += JSON_SIMPLE_ESCAPES[escape2];
19125
+ continue;
19126
+ }
19127
+ if (escape2 !== "u") fail2("bad escape");
19128
+ const unit = readHex4();
19129
+ if (unit >= 55296 && unit <= 56319) {
19130
+ if (text3[i] !== "\\" || text3[i + 1] !== "u") fail2("unpaired surrogate");
19131
+ i += 2;
19132
+ const low = readHex4();
19133
+ if (low < 56320 || low > 57343) fail2("unpaired surrogate");
19134
+ out += String.fromCharCode(unit, low);
19135
+ } else if (unit >= 56320 && unit <= 57343) {
19136
+ fail2("unpaired surrogate");
19137
+ } else {
19138
+ out += String.fromCharCode(unit);
19139
+ }
19140
+ continue;
19141
+ }
19142
+ out += text3[i];
19143
+ i++;
19144
+ }
19145
+ };
19146
+ const parseValue = (depth) => {
19147
+ skipWhitespace2();
19148
+ const c = text3[i];
19149
+ if (c === "{" || c === "[") {
19150
+ if (depth >= MAX_JSON_DEPTH) fail2("nesting too deep");
19151
+ i++;
19152
+ skipWhitespace2();
19153
+ const close = c === "{" ? "}" : "]";
19154
+ const object4 = /* @__PURE__ */ new Map();
19155
+ const array = [];
19156
+ if (text3[i] === close) {
19157
+ i++;
19158
+ return c === "{" ? object4 : array;
19159
+ }
19160
+ for (; ; ) {
19161
+ if (c === "{") {
19162
+ skipWhitespace2();
19163
+ if (text3[i] !== '"') fail2("expected key");
19164
+ const key = parseString();
19165
+ if (object4.has(key)) fail2("duplicate key");
19166
+ skipWhitespace2();
19167
+ if (text3[i] !== ":") fail2("expected ':'");
19168
+ i++;
19169
+ object4.set(key, parseValue(depth + 1));
19170
+ } else {
19171
+ array.push(parseValue(depth + 1));
19172
+ }
19173
+ skipWhitespace2();
19174
+ if (text3[i] === ",") {
19175
+ i++;
19176
+ continue;
19177
+ }
19178
+ if (text3[i] !== close) fail2(`expected ',' or '${close}'`);
19179
+ i++;
19180
+ return c === "{" ? object4 : array;
19181
+ }
19182
+ }
19183
+ if (c === '"') return parseString();
19184
+ for (const [literal, value2] of [["true", true], ["false", false], ["null", null]]) {
19185
+ if (text3.startsWith(literal, i)) {
19186
+ i += literal.length;
19187
+ return value2;
19188
+ }
19189
+ }
19190
+ const number = /-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?/y;
19191
+ number.lastIndex = i;
19192
+ const match = number.exec(text3);
19193
+ if (match === null) return fail2("unexpected token");
19194
+ if (match[2] !== void 0 || match[3] !== void 0) fail2("non-integer number");
19195
+ i += match[0].length;
19196
+ return new JsonInteger(match[0]);
19197
+ };
19198
+ const value = parseValue(0);
19199
+ skipWhitespace2();
19200
+ if (i !== text3.length) fail2("trailing data");
19201
+ return value;
19202
+ }
19203
+ function parseRecordResponse(body) {
19204
+ const root = parseStrictJson(body);
19205
+ const record2 = root instanceof Map ? root.get("record") : void 0;
19206
+ if (!(record2 instanceof Map)) {
19207
+ throw new SyntaxError("location registry response has no record object");
19208
+ }
19209
+ const version = record2.get("version");
19210
+ const subject = record2.get("subject");
19211
+ const multiaddrs = record2.get("multiaddrs");
19212
+ const updatedAt = record2.get("updated_at");
19213
+ const sequence = record2.get("sequence");
19214
+ const signature = record2.get("signature");
19215
+ if (!(version instanceof JsonInteger) || version.lexeme !== "1") {
19216
+ throw new SyntaxError("record.version must be the integer 1");
19217
+ }
19218
+ if (typeof subject !== "string") {
19219
+ throw new SyntaxError("record.subject must be a string");
19220
+ }
19221
+ if (!Array.isArray(multiaddrs) || !multiaddrs.every((entry) => typeof entry === "string" && entry.length > 0)) {
19222
+ throw new SyntaxError("record.multiaddrs must be an array of non-empty strings");
19223
+ }
19224
+ if (typeof updatedAt !== "string" || !isRfc3339DateTime(updatedAt)) {
19225
+ throw new SyntaxError("record.updated_at must be an RFC 3339 date-time");
19226
+ }
19227
+ if (!(sequence instanceof JsonInteger) || !/^(0|[1-9][0-9]*)$/.test(sequence.lexeme) || !Number.isSafeInteger(Number(sequence.lexeme))) {
19228
+ throw new SyntaxError("record.sequence must be a non-negative safe integer");
19229
+ }
19230
+ if (typeof signature !== "string" || signature.length === 0) {
19231
+ throw new SyntaxError("record.signature must be a non-empty string");
19232
+ }
19233
+ return {
19234
+ subject,
19235
+ multiaddrs,
19236
+ updated_at: updatedAt,
19237
+ sequence: Number(sequence.lexeme),
19238
+ signature
19239
+ };
19240
+ }
19241
+ function isRfc3339DateTime(value) {
19242
+ const match = RFC3339_DATE_TIME.exec(value);
19243
+ if (match === null) return false;
19244
+ const [year, month, day, hour, minute, second] = match.slice(1, 7).map(Number);
19245
+ const leap = year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
19246
+ const daysInMonth = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
19247
+ return daysInMonth !== void 0 && day >= 1 && day <= daysInMonth && hour <= 23 && minute <= 59 && second <= 59 && (match[8] === "Z" || Number(match[9]) <= 23 && Number(match[10]) <= 59);
19248
+ }
19249
+ async function verifyRecordSignature(record2) {
19250
+ const payload = canonicalLocationPayload({
19251
+ version: 1,
19252
+ subject: record2.subject,
19253
+ multiaddrs: record2.multiaddrs,
19254
+ updated_at: record2.updated_at,
19255
+ sequence: record2.sequence
19256
+ });
19257
+ const { signature } = record2;
19258
+ if (record2.subject.startsWith("did:pkh:")) {
19259
+ if (!/^0x[0-9a-fA-F]{130}$/.test(signature)) return false;
19260
+ const r = BigInt(`0x${signature.slice(2, 66)}`);
19261
+ const s = BigInt(`0x${signature.slice(66, 130)}`);
19262
+ const v = parseInt(signature.slice(130), 16);
19263
+ if (r === 0n || r >= SECP256K1_N || s === 0n || s > SECP256K1_N / 2n) return false;
19264
+ if (v !== 0 && v !== 1 && v !== 27 && v !== 28) return false;
19265
+ const address = record2.subject.slice(record2.subject.lastIndexOf(":") + 1);
19266
+ try {
19267
+ return await verifyMessage3({
19268
+ address,
19269
+ message: payload,
19270
+ signature
19271
+ });
19272
+ } catch {
19273
+ return false;
19274
+ }
19275
+ }
19276
+ const publicKey = ed25519KeyFromDidKey(record2.subject);
19277
+ if (publicKey === null || !/^[A-Za-z0-9_-]{86}$/.test(signature)) return false;
19278
+ const bytes2 = new Uint8Array(64);
19279
+ let buffer = 0;
19280
+ let bits = 0;
19281
+ let length2 = 0;
19282
+ for (const char of signature) {
19283
+ buffer = (buffer << 6 | BASE64URL.indexOf(char)) & 65535;
19284
+ bits += 6;
19285
+ if (bits >= 8) {
19286
+ bits -= 8;
19287
+ bytes2[length2++] = buffer >> bits & 255;
19288
+ }
19289
+ }
19290
+ try {
19291
+ return ed2551912.verify(bytes2, new TextEncoder().encode(payload), publicKey, { zip215: false });
19292
+ } catch {
19293
+ return false;
19294
+ }
19295
+ }
19296
+ function multiaddrOrigin(address) {
19297
+ const match = HOST_MULTIADDR.exec(address);
19298
+ if (match === null) return null;
19299
+ const [, protocol, host, portText, transport] = match;
19300
+ const port = Number(portText);
19301
+ if (port > 65535) return null;
19302
+ let authority;
19303
+ if (protocol === "ip4") {
19304
+ if (!IPV4.test(host)) return null;
19305
+ authority = host;
19306
+ } else if (protocol === "ip6") {
19307
+ if (!/^[0-9A-Fa-f:.]+$/.test(host) || !host.includes(":")) return null;
19308
+ authority = `[${host}]`;
19309
+ } else {
19310
+ if (host.length > 253 || !DNS_NAME.test(host) || /^(0[xX][0-9a-fA-F]*|[0-9]+)$/.test(host.slice(host.lastIndexOf(".") + 1))) {
19311
+ return null;
19312
+ }
19313
+ authority = host;
19314
+ }
19315
+ const scheme = transport === "http" ? "http" : "https";
19316
+ try {
19317
+ return new URL(`${scheme}://${authority}:${port}`).origin;
19318
+ } catch {
19319
+ return null;
19320
+ }
19321
+ }
19322
+
19323
+ // src/registry-policy.ts
19324
+ var DEFAULT_REGISTRY_OWNER_CHAIN_ID = 1;
19325
+ var DEFAULT_NEW_USER_HOST = DEFAULT_TINYCLOUD_FALLBACK_HOST;
19326
+ var DEFAULT_REGISTRY_LOOKUP_TIMEOUT_MS = 3e3;
19327
+ var DEFAULT_REGISTRY_SEQUENCE_STORAGE_KEY = "tinycloud.registrySequences.v1";
19328
+ var MAX_SUBJECT_LENGTH = 74;
19329
+ function registryOwnerSubjectV1(address, registryChainId = DEFAULT_REGISTRY_OWNER_CHAIN_ID) {
19330
+ return pkhDid(address, registryChainId);
19331
+ }
19332
+ function createInMemoryRegistrySequenceStore() {
19333
+ const sequences = /* @__PURE__ */ new Map();
19334
+ return {
19335
+ get: (subject) => sequences.get(subject),
19336
+ set: (subject, sequence) => {
19337
+ if ((sequences.get(subject) ?? -1) < sequence) sequences.set(subject, sequence);
19338
+ }
19339
+ };
19340
+ }
19341
+ function createLocalStorageRegistrySequenceStore(storage, storageKey = DEFAULT_REGISTRY_SEQUENCE_STORAGE_KEY) {
19342
+ const backing = storage ?? defaultWebStorage();
19343
+ const memory = createInMemoryRegistrySequenceStore();
19344
+ if (!backing) {
19345
+ return memory;
19346
+ }
19347
+ const read2 = () => {
19348
+ const sequences = /* @__PURE__ */ new Map();
19349
+ const raw = backing.getItem(storageKey);
19350
+ if (raw === null) return sequences;
19351
+ let parsed;
19352
+ try {
19353
+ parsed = JSON.parse(raw);
19354
+ } catch {
19355
+ parsed = void 0;
19356
+ }
19357
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
19358
+ for (const [subject, sequence] of Object.entries(parsed)) {
19359
+ if (!isSequence(sequence)) break;
19360
+ sequences.set(subject, sequence);
19361
+ }
19362
+ if (sequences.size === Object.keys(parsed).length) return sequences;
19363
+ }
19364
+ throw new Error(`Corrupt registry sequence store "${storageKey}"`);
19365
+ };
19366
+ return {
19367
+ get: async (subject) => {
19368
+ const sequence = Math.max(read2().get(subject) ?? -1, await memory.get(subject) ?? -1);
19369
+ return sequence < 0 ? void 0 : sequence;
19370
+ },
19371
+ set: async (subject, sequence) => {
19372
+ await memory.set(subject, sequence);
19373
+ const sequences = read2();
19374
+ if ((sequences.get(subject) ?? -1) >= sequence) return;
19375
+ sequences.set(subject, sequence);
19376
+ backing.setItem(storageKey, JSON.stringify(Object.fromEntries(sequences)));
19377
+ }
19378
+ };
19379
+ }
19380
+ function isSequence(value) {
19381
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
19382
+ }
19383
+ async function resolveOwnerHosts(subject, options2 = {}) {
19384
+ if (typeof subject !== "string" || // Bounds the work below; covers any did:pkh with a safe-integer chain ID.
19385
+ subject.length > MAX_SUBJECT_LENGTH || !isCanonicalSubject(subject)) {
19386
+ throw new RegistryLookupInputError(
19387
+ "invalid-subject",
19388
+ "subject must be did:pkh:eip155:<chainId>:<EIP-55 address> or an Ed25519 did:key"
19389
+ );
19390
+ }
19391
+ const store = options2.sequenceStore;
19392
+ const remembered = store === void 0 ? void 0 : await store.get(subject);
19393
+ const registryUrl = options2.registryUrl ?? DEFAULT_TINYCLOUD_LOCATION_REGISTRY_URL;
19394
+ const url = `${registryUrl.replace(/\/$/, "")}/v1/locations/${encodeURIComponent(subject)}`;
19395
+ const observed = await observeRegistry(
19396
+ url,
19397
+ options2.fetch ?? globalThis.fetch,
19398
+ options2.timeoutMs ?? DEFAULT_REGISTRY_LOOKUP_TIMEOUT_MS
19399
+ );
19400
+ if (observed.kind === "oversize" || observed.kind === "invalid-utf8") {
19401
+ const oversize = observed.kind === "oversize";
19402
+ return {
19403
+ status: "unavailable",
19404
+ subject,
19405
+ source: "offchain",
19406
+ reason: oversize ? "oversize-response" : "malformed-response",
19407
+ message: oversize ? "location registry response is too large" : "location registry response is not UTF-8",
19408
+ hosts: []
19409
+ };
19410
+ }
19411
+ const verdict = await classifyRegistryLookup(subject, observed, {
19412
+ rememberedSequence: remembered ?? null
19413
+ });
19414
+ if (store !== void 0 && verdict.status === "registered" && (remembered === void 0 || verdict.sequence > remembered)) {
19415
+ try {
19416
+ await store.set(subject, verdict.sequence);
19417
+ } catch (error) {
19418
+ if (!sequenceWriteWarned) {
19419
+ sequenceWriteWarned = true;
19420
+ console.warn("[tinycloud:registry] could not remember the registry sequence:", error);
19421
+ }
19422
+ }
19423
+ }
19424
+ return verdict;
19425
+ }
19426
+ var sequenceWriteWarned = false;
19427
+ async function observeRegistry(url, fetchFn, timeoutMs) {
19428
+ const controller = new AbortController();
19429
+ let cancelTimer = () => void 0;
19430
+ const timedOut = new Promise((resolve) => {
19431
+ const timer = setTimeout(() => {
19432
+ controller.abort();
19433
+ resolve({ kind: "timeout" });
19434
+ }, timeoutMs);
19435
+ cancelTimer = () => clearTimeout(timer);
19436
+ });
19437
+ try {
19438
+ return await Promise.race([readRegistry(url, fetchFn, controller.signal), timedOut]);
19439
+ } finally {
19440
+ cancelTimer();
19441
+ }
19442
+ }
19443
+ async function readRegistry(url, fetchFn, signal) {
19444
+ let response;
19445
+ try {
19446
+ response = await fetchFn(url, {
19447
+ redirect: "error",
19448
+ headers: { accept: "application/json" },
19449
+ signal
19450
+ });
19451
+ } catch {
19452
+ return { kind: "network-error" };
19453
+ }
19454
+ if (response.status !== 200 && response.status !== 404) {
19455
+ await response.body?.cancel().catch(() => void 0);
19456
+ return { kind: "http", status: response.status, body: "" };
19457
+ }
19458
+ let bytes2;
19459
+ try {
19460
+ bytes2 = await boundedResponseBytes(response, REGISTRY_LOOKUP_MAX_BODY_BYTES);
19461
+ } catch {
19462
+ return { kind: "network-error" };
19463
+ }
19464
+ if (bytes2 === null) return { kind: "oversize" };
19465
+ try {
19466
+ return {
19467
+ kind: "http",
19468
+ status: response.status,
19469
+ body: new TextDecoder("utf-8", { fatal: true }).decode(bytes2)
19470
+ };
19471
+ } catch {
19472
+ return { kind: "invalid-utf8" };
19473
+ }
19474
+ }
19475
+ var NodeNotFoundError = class extends Error {
19476
+ constructor(subject, verdict) {
19477
+ super(
19478
+ verdict.status === "registered" ? `No usable TinyCloud host is registered for ${subject}` : `${subject} is not registered and newUserHost is null`
19479
+ );
19480
+ this.subject = subject;
19481
+ this.verdict = verdict;
19482
+ this.code = "node_not_found";
19483
+ this.name = "NodeNotFoundError";
19484
+ }
19485
+ };
19486
+ var RegistryUnavailableError = class extends Error {
19487
+ constructor(subject, verdict, detail = {}) {
19488
+ super(`Location registry unavailable for ${subject}: ${detail.message ?? verdict?.message}`);
19489
+ this.subject = subject;
19490
+ this.verdict = verdict;
19491
+ this.code = "registry_unavailable";
19492
+ this.name = "RegistryUnavailableError";
19493
+ if (detail.cause !== void 0) this.cause = detail.cause;
19494
+ }
19495
+ };
19496
+ var NodeRegistryEntryExistsError = class extends Error {
19497
+ constructor(subject, host, verdict) {
19498
+ super(`The registry entry for ${subject} does not list ${host}`);
19499
+ this.subject = subject;
19500
+ this.host = host;
19501
+ this.verdict = verdict;
19502
+ this.code = "node_registry_entry_exists";
19503
+ this.name = "NodeRegistryEntryExistsError";
19504
+ this.registeredHosts = secureRecordHosts(verdict);
19505
+ }
19506
+ };
19507
+ async function lookupOwnerRegistryVerdict(subject, options2 = {}) {
19508
+ if (options2.registryUrl === null) {
19509
+ throw new RegistryUnavailableError(subject, void 0, {
19510
+ message: "no location registry is configured"
19511
+ });
19512
+ }
19513
+ try {
19514
+ return await resolveOwnerHosts(subject, {
19515
+ registryUrl: options2.registryUrl,
19516
+ sequenceStore: options2.sequenceStore,
19517
+ fetch: options2.fetch,
19518
+ timeoutMs: options2.timeoutMs
19519
+ });
19520
+ } catch (error) {
19521
+ throw new RegistryUnavailableError(subject, void 0, {
19522
+ message: error instanceof Error ? error.message : String(error),
19523
+ cause: error
19524
+ });
19525
+ }
19526
+ }
19527
+ async function resolveFailClosedTinyCloudHosts(address, options2 = {}) {
19528
+ const newUserHost = options2.newUserHost === null ? null : exactHttpOrigin(options2.newUserHost ?? DEFAULT_NEW_USER_HOST, "newUserHost");
19529
+ const subject = registryOwnerSubjectV1(address, options2.registryChainId);
19530
+ const resolve = async () => {
19531
+ let verdict;
19532
+ let unavailable;
19533
+ try {
19534
+ verdict = await lookupOwnerRegistryVerdict(subject, options2);
19535
+ } catch (error) {
19536
+ if (!(error instanceof RegistryUnavailableError)) throw error;
19537
+ unavailable = error;
19538
+ }
19539
+ if (options2.autoDiscoverLocalNode ?? true) {
19540
+ const linkNames = verdict?.status === "registered" ? secureRecordHosts(verdict).flatMap((host) => LOCAL_LINK_ORIGIN.exec(host)?.[1] ?? []) : [];
19541
+ for (const candidate of [
19542
+ { localNodeUrl: options2.localNodeUrl, localLinkName: options2.localLinkName },
19543
+ ...linkNames.map((localLinkName) => ({ localLinkName }))
19544
+ ]) {
19545
+ const local = await discoverLocalTinyCloudNode({
19546
+ ...candidate,
19547
+ expectedNodeDid: options2.expectedNodeDid,
19548
+ identityStore: options2.localNodeIdentityStore,
19549
+ fetch: options2.fetch
19550
+ });
19551
+ if (local) return { hosts: [local.url], ...verdict === void 0 ? {} : { verdict } };
19552
+ }
19553
+ }
19554
+ if (verdict === void 0) throw unavailable;
19555
+ return { hosts: hostsForVerdict(verdict, newUserHost), verdict };
19556
+ };
19557
+ for (let attempt = 1; ; attempt++) {
19558
+ let failure;
19559
+ try {
19560
+ return await resolve();
19561
+ } catch (error) {
19562
+ if (!(error instanceof NodeNotFoundError) && !(error instanceof RegistryUnavailableError)) {
19563
+ throw error;
19564
+ }
19565
+ failure = error;
19566
+ }
19567
+ if (!options2.onNodeNotFound) throw failure;
19568
+ const decision = await options2.onNodeNotFound({ error: failure, attempt });
19569
+ switch (decision?.action) {
19570
+ case "retry":
19571
+ continue;
19572
+ case "use-host":
19573
+ return {
19574
+ hosts: [exactHttpOrigin(decision.host, "onNodeNotFound host")],
19575
+ ...failure.verdict === void 0 ? {} : { verdict: failure.verdict }
19576
+ };
19577
+ case "abort":
19578
+ throw failure;
19579
+ default:
19580
+ throw new TypeError("onNodeNotFound returned an unknown action");
19581
+ }
19582
+ }
19583
+ }
19584
+ var LOCAL_LINK_ORIGIN = /^https:\/\/([a-z0-9-]+)\.local\.tinycloud\.link$/;
19585
+ function secureRecordHosts(verdict) {
19586
+ const hosts = /* @__PURE__ */ new Set();
19587
+ for (const host of verdict.hosts) {
19588
+ for (const url of host.urls) {
19589
+ try {
19590
+ hosts.add(exactHttpOrigin(url, "registry host"));
19591
+ } catch {
19592
+ }
19593
+ }
19594
+ }
19595
+ return [...hosts];
19596
+ }
19597
+ function hostsForVerdict(verdict, newUserHost) {
19598
+ switch (verdict.status) {
19599
+ case "registered": {
19600
+ const hosts = secureRecordHosts(verdict);
19601
+ if (hosts.length === 0) throw new NodeNotFoundError(verdict.subject, verdict);
19602
+ return hosts;
19603
+ }
19604
+ case "unregistered":
19605
+ if (newUserHost === null) throw new NodeNotFoundError(verdict.subject, verdict);
19606
+ return [newUserHost];
19607
+ case "unavailable":
19608
+ throw new RegistryUnavailableError(verdict.subject, verdict);
19609
+ }
19610
+ }
19611
+ function assertSpaceCreationAllowed(verdict, host) {
19612
+ switch (verdict.status) {
19613
+ case "unregistered":
19614
+ return;
19615
+ case "unavailable":
19616
+ throw new RegistryUnavailableError(verdict.subject, verdict);
19617
+ case "registered": {
19618
+ const origin2 = httpOrigin(host);
19619
+ if (origin2 !== void 0 && secureRecordHosts(verdict).includes(origin2)) {
19620
+ return;
19621
+ }
19622
+ throw new NodeRegistryEntryExistsError(verdict.subject, host, verdict);
19623
+ }
19624
+ }
19625
+ }
19626
+ function httpOrigin(host) {
19627
+ try {
19628
+ const url = new URL(host);
19629
+ return url.protocol === "https:" || url.protocol === "http:" ? url.origin : void 0;
19630
+ } catch {
19631
+ return void 0;
19632
+ }
19633
+ }
18929
19634
  export {
18930
19635
  ACCOUNT_INDEX_SCHEMA2 as ACCOUNT_INDEX_SCHEMA,
18931
19636
  ACCOUNT_MANIFEST_PERMISSIONS2 as ACCOUNT_MANIFEST_PERMISSIONS,
@@ -18974,6 +19679,10 @@ export {
18974
19679
  DEFAULT_LOCAL_NODE_URL,
18975
19680
  DEFAULT_MANIFEST_SPACE,
18976
19681
  DEFAULT_MANIFEST_VERSION,
19682
+ DEFAULT_NEW_USER_HOST,
19683
+ DEFAULT_REGISTRY_LOOKUP_TIMEOUT_MS,
19684
+ DEFAULT_REGISTRY_OWNER_CHAIN_ID,
19685
+ DEFAULT_REGISTRY_SEQUENCE_STORAGE_KEY,
18977
19686
  DEFAULT_SIGNED_READ_URL_EXPIRY_MS,
18978
19687
  DEFAULT_TINYCLOUD_FALLBACK_HOST,
18979
19688
  DEFAULT_TINYCLOUD_LOCATION_REGISTRY_URL,
@@ -19030,6 +19739,8 @@ export {
19030
19739
  NATIVE_PROJECTION_V1_DOMAIN,
19031
19740
  NETWORK_NAME_PATTERN,
19032
19741
  NetworkIdError,
19742
+ NodeNotFoundError,
19743
+ NodeRegistryEntryExistsError,
19033
19744
  OPENKEY_ACTION_ID_SEPARATOR,
19034
19745
  OWNER_NODE_BINDING_TIMEOUT_MS,
19035
19746
  OpenKeyManageKeyError,
@@ -19058,6 +19769,8 @@ export {
19058
19769
  PermissionNotInManifestError,
19059
19770
  PrefixedKVService,
19060
19771
  ProtocolMismatchError,
19772
+ REGISTRY_LOOKUP_MAX_BODY_BYTES,
19773
+ REGISTRY_NOT_FOUND_BODY,
19061
19774
  REQUESTER_ENGINE_RETRY_ATTEMPTS,
19062
19775
  REQUESTER_ENGINE_RETRY_MAX_DELAY_MS,
19063
19776
  REQUESTER_NEAR_EXPIRY_SECONDS,
@@ -19066,6 +19779,8 @@ export {
19066
19779
  ROOT_STATUS_CHECKPOINT_V1_SCHEMA,
19067
19780
  ROOT_STATUS_RENEWAL_V1_DOMAIN,
19068
19781
  ROOT_STATUS_V1_DOMAIN,
19782
+ RegistryLookupInputError,
19783
+ RegistryUnavailableError,
19069
19784
  SECRETS_SPACE,
19070
19785
  SECRET_NAME_RE2 as SECRET_NAME_RE,
19071
19786
  SECRET_RECORDS_SCHEMA,
@@ -19135,6 +19850,7 @@ export {
19135
19850
  admitPolicyCredentialV4,
19136
19851
  appSecretScope,
19137
19852
  applyPrefix,
19853
+ assertSpaceCreationAllowed,
19138
19854
  authorizationVerdictOf2 as authorizationVerdictOf,
19139
19855
  bootstrapEncryptionNetworkId,
19140
19856
  bootstrapSpaceId,
@@ -19165,6 +19881,7 @@ export {
19165
19881
  canonicalizeUnifiedPolicyCapability,
19166
19882
  checkDecryptInvocationInput,
19167
19883
  checkNodeInfo,
19884
+ classifyRegistryLookup,
19168
19885
  clearTinyCloudDebugLogs,
19169
19886
  compactAttenuationContains,
19170
19887
  composeBootstrapSpaceManifest,
@@ -19182,7 +19899,9 @@ export {
19182
19899
  createEmailDomainCredentialRequirement,
19183
19900
  createHolderBinding,
19184
19901
  createInMemoryLocalNodeIdentityStore,
19902
+ createInMemoryRegistrySequenceStore,
19185
19903
  createLocalStorageLocalNodeIdentityStore,
19904
+ createLocalStorageRegistrySequenceStore,
19186
19905
  createOpenKeyCallbackSigningStrategy,
19187
19906
  createOpenKeyManageKeySigningStrategy,
19188
19907
  createPolicyEnforcementDelegation,
@@ -19266,6 +19985,7 @@ export {
19266
19985
  jcsCanonicalize,
19267
19986
  loadManifest,
19268
19987
  locationPayloadForRecord,
19988
+ lookupOwnerRegistryVerdict,
19269
19989
  mailboxBelongsToDomain,
19270
19990
  makePkhSpaceId,
19271
19991
  makePublicSpaceId,
@@ -19314,11 +20034,14 @@ export {
19314
20034
  publishLocationRecord,
19315
20035
  recapCaveatsEqual,
19316
20036
  registerPolicyV3,
20037
+ registryOwnerSubjectV1,
19317
20038
  renewPolicyRootStatusV3,
19318
20039
  requestTinyCloudManageKeyScope,
19319
20040
  resolveCloudLocation,
20041
+ resolveFailClosedTinyCloudHosts,
19320
20042
  resolveManifest,
19321
20043
  resolveManifestKnowledgeRoot,
20044
+ resolveOwnerHosts,
19322
20045
  resolveSecretListPrefix,
19323
20046
  resolveSecretPath2 as resolveSecretPath,
19324
20047
  resolveTinyCloudHosts,