@tinycloud/sdk-core 2.2.0-beta.0 → 2.2.0-beta.3

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
@@ -2754,9 +2754,7 @@ function parseExpiry(duration) {
2754
2754
  `expiry must be a non-empty duration string (got ${JSON.stringify(duration)})`
2755
2755
  );
2756
2756
  }
2757
- const parsed = ms(
2758
- duration
2759
- );
2757
+ const parsed = ms(duration);
2760
2758
  if (typeof parsed !== "number" || !Number.isFinite(parsed) || parsed <= 0) {
2761
2759
  throw new ManifestValidationError(
2762
2760
  `invalid expiry duration: ${JSON.stringify(duration)}`
@@ -2811,23 +2809,33 @@ function validateManifest(input) {
2811
2809
  );
2812
2810
  }
2813
2811
  if (typeof m.app_id !== "string" || m.app_id.length === 0) {
2814
- throw new ManifestValidationError("manifest.app_id is required and must be a non-empty string");
2812
+ throw new ManifestValidationError(
2813
+ "manifest.app_id is required and must be a non-empty string"
2814
+ );
2815
2815
  }
2816
2816
  if (typeof m.name !== "string" || m.name.length === 0) {
2817
- throw new ManifestValidationError("manifest.name is required and must be a non-empty string");
2817
+ throw new ManifestValidationError(
2818
+ "manifest.name is required and must be a non-empty string"
2819
+ );
2818
2820
  }
2819
2821
  if (m.did !== void 0 && (typeof m.did !== "string" || m.did.length === 0)) {
2820
- throw new ManifestValidationError("manifest.did must be a non-empty DID string");
2822
+ throw new ManifestValidationError(
2823
+ "manifest.did must be a non-empty DID string"
2824
+ );
2821
2825
  }
2822
2826
  if (m.space !== void 0 && (typeof m.space !== "string" || m.space.length === 0)) {
2823
- throw new ManifestValidationError("manifest.space must be a non-empty string");
2827
+ throw new ManifestValidationError(
2828
+ "manifest.space must be a non-empty string"
2829
+ );
2824
2830
  }
2825
2831
  if (m.expiry !== void 0) {
2826
2832
  parseExpiry(m.expiry);
2827
2833
  }
2828
2834
  if (m.permissions !== void 0) {
2829
2835
  if (!Array.isArray(m.permissions)) {
2830
- throw new ManifestValidationError("manifest.permissions must be an array");
2836
+ throw new ManifestValidationError(
2837
+ "manifest.permissions must be an array"
2838
+ );
2831
2839
  }
2832
2840
  m.permissions.forEach(
2833
2841
  (p, i) => validatePermissionEntry(p, `permissions[${i}]`)
@@ -2844,7 +2852,9 @@ function validatePermissionEntry(p, path) {
2844
2852
  throw new ManifestValidationError(`${path}.service is required`);
2845
2853
  }
2846
2854
  if (entry.space !== void 0 && (typeof entry.space !== "string" || entry.space.length === 0)) {
2847
- throw new ManifestValidationError(`${path}.space must be a non-empty string`);
2855
+ throw new ManifestValidationError(
2856
+ `${path}.space must be a non-empty string`
2857
+ );
2848
2858
  }
2849
2859
  if (typeof entry.path !== "string") {
2850
2860
  throw new ManifestValidationError(
@@ -2935,7 +2945,8 @@ function resolveEntry(entry, prefix, _inheritedExpiryMs, inheritedSpace) {
2935
2945
  // Only populate `expiryMs` when the entry had its own expiry override.
2936
2946
  // When absent, callers use the parent (delegation or manifest) expiry
2937
2947
  // which is carried on ResolvedDelegate.expiryMs / ResolvedCapabilities.expiryMs.
2938
- ...entryExpiryMs !== void 0 ? { expiryMs: entryExpiryMs } : {}
2948
+ ...entryExpiryMs !== void 0 ? { expiryMs: entryExpiryMs } : {},
2949
+ ...entry.description !== void 0 ? { description: entry.description } : {}
2939
2950
  };
2940
2951
  }
2941
2952
  function cloneResourceCapability(entry) {
@@ -2944,7 +2955,8 @@ function cloneResourceCapability(entry) {
2944
2955
  space: entry.space,
2945
2956
  path: entry.path,
2946
2957
  actions: [...entry.actions],
2947
- ...entry.expiryMs !== void 0 ? { expiryMs: entry.expiryMs } : {}
2958
+ ...entry.expiryMs !== void 0 ? { expiryMs: entry.expiryMs } : {},
2959
+ ...entry.description !== void 0 ? { description: entry.description } : {}
2948
2960
  };
2949
2961
  }
2950
2962
  function clonePermissionEntry(entry) {
@@ -2954,7 +2966,8 @@ function clonePermissionEntry(entry) {
2954
2966
  path: entry.path,
2955
2967
  actions: [...entry.actions],
2956
2968
  ...entry.skipPrefix !== void 0 ? { skipPrefix: entry.skipPrefix } : {},
2957
- ...entry.expiry !== void 0 ? { expiry: entry.expiry } : {}
2969
+ ...entry.expiry !== void 0 ? { expiry: entry.expiry } : {},
2970
+ ...entry.description !== void 0 ? { description: entry.description } : {}
2958
2971
  };
2959
2972
  }
2960
2973
  function dedupeResources(resources) {
@@ -2973,6 +2986,9 @@ function dedupeResources(resources) {
2973
2986
  seen.add(action);
2974
2987
  }
2975
2988
  }
2989
+ if (existing.description === void 0 && resource.description !== void 0) {
2990
+ existing.description = resource.description;
2991
+ }
2976
2992
  }
2977
2993
  return [...byKey.values()];
2978
2994
  }
@@ -2981,11 +2997,7 @@ function accountRegistryPermission() {
2981
2997
  service: "tinycloud.kv",
2982
2998
  space: ACCOUNT_REGISTRY_SPACE,
2983
2999
  path: ACCOUNT_REGISTRY_PATH,
2984
- actions: [
2985
- "tinycloud.kv/get",
2986
- "tinycloud.kv/put",
2987
- "tinycloud.kv/list"
2988
- ]
3000
+ actions: ["tinycloud.kv/get", "tinycloud.kv/put", "tinycloud.kv/list"]
2989
3001
  };
2990
3002
  }
2991
3003
  function composeManifestRequest(inputs, options = {}) {
@@ -4238,6 +4250,339 @@ async function checkNodeInfo(host, sdkProtocol, fetchFn = globalThis.fetch.bind(
4238
4250
  };
4239
4251
  }
4240
4252
 
4253
+ // src/location.ts
4254
+ import { multiaddr } from "@multiformats/multiaddr";
4255
+ import { multiaddrToUri } from "@multiformats/multiaddr-to-uri";
4256
+ import { uriToMultiaddr } from "@multiformats/uri-to-multiaddr";
4257
+ import { ed25519 } from "@noble/curves/ed25519";
4258
+ import { bases } from "multiformats/basics";
4259
+ import { verifyMessage } from "viem";
4260
+ var LocationRecordValidationError = class extends Error {
4261
+ constructor(message) {
4262
+ super(`Location record validation failed: ${message}`);
4263
+ this.name = "LocationRecordValidationError";
4264
+ }
4265
+ };
4266
+ var CloudLocationResolutionError = class extends Error {
4267
+ constructor(subject, attempts) {
4268
+ super(`Unable to resolve TinyCloud location for ${subject}`);
4269
+ this.name = "CloudLocationResolutionError";
4270
+ this.attempts = attempts;
4271
+ }
4272
+ };
4273
+ function locationPayloadForRecord(record) {
4274
+ return {
4275
+ version: record.version,
4276
+ subject: record.subject,
4277
+ multiaddrs: [...record.multiaddrs],
4278
+ updated_at: record.updated_at,
4279
+ sequence: record.sequence
4280
+ };
4281
+ }
4282
+ function canonicalLocationPayload(payload) {
4283
+ return JSON.stringify({
4284
+ version: payload.version,
4285
+ subject: payload.subject,
4286
+ multiaddrs: payload.multiaddrs,
4287
+ updated_at: payload.updated_at,
4288
+ sequence: payload.sequence
4289
+ });
4290
+ }
4291
+ async function signLocationRecord(payload, signer) {
4292
+ validateLocationRecordPayload(payload);
4293
+ const message = canonicalLocationPayload(payload);
4294
+ const signature = signer.type === "did:pkh" ? await signer.signMessage(message) : base64UrlEncode2(await signer.signBytes(new TextEncoder().encode(message)));
4295
+ return { ...payload, signature };
4296
+ }
4297
+ function validateLocationRecordPayload(input) {
4298
+ if (input === null || typeof input !== "object") {
4299
+ throw new LocationRecordValidationError("payload must be an object");
4300
+ }
4301
+ const payload = input;
4302
+ if (payload.version !== 1) {
4303
+ throw new LocationRecordValidationError("version must be 1");
4304
+ }
4305
+ validateSubject(payload.subject);
4306
+ validateMultiaddrs(payload.multiaddrs);
4307
+ if (typeof payload.updated_at !== "string" || Number.isNaN(Date.parse(payload.updated_at))) {
4308
+ throw new LocationRecordValidationError("updated_at must be an ISO timestamp");
4309
+ }
4310
+ if (typeof payload.sequence !== "number" || !Number.isSafeInteger(payload.sequence) || payload.sequence < 0) {
4311
+ throw new LocationRecordValidationError(
4312
+ "sequence must be a non-negative safe integer"
4313
+ );
4314
+ }
4315
+ return {
4316
+ version: 1,
4317
+ subject: payload.subject,
4318
+ multiaddrs: [...payload.multiaddrs],
4319
+ updated_at: payload.updated_at,
4320
+ sequence: payload.sequence
4321
+ };
4322
+ }
4323
+ function validateLocationRecord(input) {
4324
+ const payload = validateLocationRecordPayload(input);
4325
+ const signature = input.signature;
4326
+ if (typeof signature !== "string" || signature.length === 0) {
4327
+ throw new LocationRecordValidationError("signature must be a non-empty string");
4328
+ }
4329
+ return { ...payload, signature };
4330
+ }
4331
+ async function verifyLocationRecord(input) {
4332
+ const record = validateLocationRecord(input);
4333
+ const payload = canonicalLocationPayload(locationPayloadForRecord(record));
4334
+ if (record.subject.startsWith("did:pkh:")) {
4335
+ return verifyPkhSignature(record.subject, payload, record.signature);
4336
+ }
4337
+ if (record.subject.startsWith("did:key:")) {
4338
+ return verifyDidKeySignature(record.subject, payload, record.signature);
4339
+ }
4340
+ return false;
4341
+ }
4342
+ async function fetchLocationRecord(registryUrl, subject, fetchFn = globalThis.fetch) {
4343
+ const url = `${registryUrl.replace(/\/$/, "")}/v1/locations/${encodeURIComponent(subject)}`;
4344
+ const response = await fetchFn(url);
4345
+ if (response.status === 404) {
4346
+ return null;
4347
+ }
4348
+ if (!response.ok) {
4349
+ throw new Error(`location registry returned HTTP ${response.status}`);
4350
+ }
4351
+ const body = await response.json();
4352
+ if (body.record === void 0) {
4353
+ throw new LocationRecordValidationError("registry response missing record");
4354
+ }
4355
+ return validateLocationRecord(body.record);
4356
+ }
4357
+ async function resolveCloudLocation(subject, options = {}) {
4358
+ validateSubject(subject);
4359
+ const verifyRecords = options.verifyRecords ?? true;
4360
+ const attempts = await Promise.all([
4361
+ resolveExplicit(subject, options.explicitMultiaddrs),
4362
+ resolveBlockchain(subject, options.blockchain, verifyRecords),
4363
+ resolveCentralized(subject, options, verifyRecords),
4364
+ resolveFallback(subject, options.fallbackMultiaddrs)
4365
+ ]);
4366
+ const winner = attempts.find((attempt) => attempt.candidate)?.candidate;
4367
+ if (!winner) {
4368
+ throw new CloudLocationResolutionError(subject, attempts);
4369
+ }
4370
+ return {
4371
+ subject,
4372
+ source: winner.source,
4373
+ multiaddrs: [...winner.multiaddrs],
4374
+ ...winner.record ? { record: winner.record } : {},
4375
+ attempts,
4376
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
4377
+ };
4378
+ }
4379
+ function multiaddrToHttpUrl(input) {
4380
+ const uri = multiaddrToUri(multiaddr(input));
4381
+ if (!uri.startsWith("http://") && !uri.startsWith("https://")) {
4382
+ throw new LocationRecordValidationError(
4383
+ `multiaddr does not resolve to http/https: ${input}`
4384
+ );
4385
+ }
4386
+ return uri;
4387
+ }
4388
+ function httpUrlToMultiaddr(input) {
4389
+ const url = new URL(input);
4390
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
4391
+ throw new LocationRecordValidationError("URL must use http or https");
4392
+ }
4393
+ return uriToMultiaddr(url.toString()).toString();
4394
+ }
4395
+ async function resolveExplicit(subject, multiaddrs) {
4396
+ return resolveAttempt("explicit", async () => {
4397
+ if (multiaddrs === void 0 || multiaddrs.length === 0) {
4398
+ return null;
4399
+ }
4400
+ return toCandidate(subject, "explicit", multiaddrs, false);
4401
+ });
4402
+ }
4403
+ async function resolveBlockchain(subject, resolver, verifyRecords) {
4404
+ return resolveAttempt("blockchain", async () => {
4405
+ if (!resolver) {
4406
+ return null;
4407
+ }
4408
+ return toCandidate(subject, "blockchain", await resolver(subject), verifyRecords);
4409
+ });
4410
+ }
4411
+ async function resolveCentralized(subject, options, verifyRecords) {
4412
+ return resolveAttempt("centralized", async () => {
4413
+ if (!options.centralizedRegistryUrl) {
4414
+ return null;
4415
+ }
4416
+ const record = await fetchLocationRecord(
4417
+ options.centralizedRegistryUrl,
4418
+ subject,
4419
+ options.fetch
4420
+ );
4421
+ return toCandidate(subject, "centralized", record, verifyRecords);
4422
+ });
4423
+ }
4424
+ async function resolveFallback(subject, multiaddrs) {
4425
+ return resolveAttempt("fallback", async () => {
4426
+ if (multiaddrs === void 0 || multiaddrs.length === 0) {
4427
+ return null;
4428
+ }
4429
+ return toCandidate(subject, "fallback", multiaddrs, false);
4430
+ });
4431
+ }
4432
+ async function resolveAttempt(source, resolve) {
4433
+ try {
4434
+ const candidate = await resolve();
4435
+ return candidate ? { source, candidate } : { source };
4436
+ } catch (error) {
4437
+ return {
4438
+ source,
4439
+ error: error instanceof Error ? error : new Error(String(error))
4440
+ };
4441
+ }
4442
+ }
4443
+ async function toCandidate(subject, source, input, verifyRecord) {
4444
+ if (input === null || input === void 0) {
4445
+ return null;
4446
+ }
4447
+ if (Array.isArray(input)) {
4448
+ validateMultiaddrs(input);
4449
+ return { source, multiaddrs: [...input] };
4450
+ }
4451
+ const maybeRecord = input;
4452
+ if (maybeRecord.version === 1 && maybeRecord.signature !== void 0) {
4453
+ const record = validateLocationRecord(input);
4454
+ if (record.subject !== subject) {
4455
+ throw new LocationRecordValidationError(
4456
+ "location record subject does not match requested subject"
4457
+ );
4458
+ }
4459
+ if (verifyRecord && !await verifyLocationRecord(record)) {
4460
+ throw new LocationRecordValidationError("location record signature is invalid");
4461
+ }
4462
+ return { source, multiaddrs: [...record.multiaddrs], record };
4463
+ }
4464
+ const candidateInput = input;
4465
+ if (!Array.isArray(candidateInput.multiaddrs)) {
4466
+ throw new LocationRecordValidationError("candidate multiaddrs must be an array");
4467
+ }
4468
+ validateMultiaddrs(candidateInput.multiaddrs);
4469
+ if (candidateInput.record !== void 0) {
4470
+ const record = validateLocationRecord(candidateInput.record);
4471
+ if (record.subject !== subject) {
4472
+ throw new LocationRecordValidationError(
4473
+ "location record subject does not match requested subject"
4474
+ );
4475
+ }
4476
+ if (verifyRecord && !await verifyLocationRecord(record)) {
4477
+ throw new LocationRecordValidationError("location record signature is invalid");
4478
+ }
4479
+ return { source, multiaddrs: [...candidateInput.multiaddrs], record };
4480
+ }
4481
+ return { source, multiaddrs: [...candidateInput.multiaddrs] };
4482
+ }
4483
+ function validateSubject(subject) {
4484
+ if (typeof subject !== "string" || subject.length === 0) {
4485
+ throw new LocationRecordValidationError("subject must be a non-empty string");
4486
+ }
4487
+ if (!subject.startsWith("did:pkh:") && !subject.startsWith("did:key:")) {
4488
+ throw new LocationRecordValidationError("subject must be did:pkh or did:key");
4489
+ }
4490
+ }
4491
+ function validateMultiaddrs(input) {
4492
+ if (!Array.isArray(input)) {
4493
+ throw new LocationRecordValidationError("multiaddrs must be an array");
4494
+ }
4495
+ for (const addr of input) {
4496
+ if (typeof addr !== "string" || addr.length === 0) {
4497
+ throw new LocationRecordValidationError(
4498
+ "multiaddr entries must be non-empty strings"
4499
+ );
4500
+ }
4501
+ try {
4502
+ multiaddr(addr);
4503
+ } catch {
4504
+ throw new LocationRecordValidationError(`invalid multiaddr: ${addr}`);
4505
+ }
4506
+ }
4507
+ }
4508
+ async function verifyPkhSignature(did, payload, signature) {
4509
+ const address = did.split(":").at(-1);
4510
+ if (!address || !/^0x[a-fA-F0-9]{40}$/.test(address)) {
4511
+ throw new LocationRecordValidationError(
4512
+ "did:pkh subject must end with an EVM address"
4513
+ );
4514
+ }
4515
+ if (!/^0x[0-9a-fA-F]+$/.test(signature)) {
4516
+ throw new LocationRecordValidationError("did:pkh signature must be hex");
4517
+ }
4518
+ return verifyMessage({
4519
+ address,
4520
+ message: payload,
4521
+ signature
4522
+ });
4523
+ }
4524
+ function verifyDidKeySignature(did, payload, signature) {
4525
+ const publicKey = ed25519PublicKeyFromDidKey(did);
4526
+ const signatureBytes = decodeBase64Url(signature);
4527
+ if (signatureBytes.length !== 64) {
4528
+ throw new LocationRecordValidationError(
4529
+ "did:key signature must be a base64url Ed25519 signature"
4530
+ );
4531
+ }
4532
+ return ed25519.verify(signatureBytes, new TextEncoder().encode(payload), publicKey);
4533
+ }
4534
+ function ed25519PublicKeyFromDidKey(did) {
4535
+ const identifier = did.slice("did:key:".length);
4536
+ if (!identifier.startsWith("z")) {
4537
+ throw new LocationRecordValidationError("did:key must use base58btc multibase");
4538
+ }
4539
+ const bytes = bases.base58btc.decode(identifier);
4540
+ if (bytes.length !== 34 || bytes[0] !== 237 || bytes[1] !== 1) {
4541
+ throw new LocationRecordValidationError("did:key must be an Ed25519 public key");
4542
+ }
4543
+ return bytes.slice(2);
4544
+ }
4545
+ function base64UrlEncode2(bytes) {
4546
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
4547
+ let output = "";
4548
+ for (let i = 0; i < bytes.length; i += 3) {
4549
+ const a = bytes[i];
4550
+ const b = bytes[i + 1];
4551
+ const c = bytes[i + 2];
4552
+ const triplet = a << 16 | (b ?? 0) << 8 | (c ?? 0);
4553
+ output += alphabet[triplet >> 18 & 63];
4554
+ output += alphabet[triplet >> 12 & 63];
4555
+ if (i + 1 < bytes.length) {
4556
+ output += alphabet[triplet >> 6 & 63];
4557
+ }
4558
+ if (i + 2 < bytes.length) {
4559
+ output += alphabet[triplet & 63];
4560
+ }
4561
+ }
4562
+ return output;
4563
+ }
4564
+ function decodeBase64Url(value) {
4565
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
4566
+ const bytes = [];
4567
+ let buffer = 0;
4568
+ let bits = 0;
4569
+ for (const char of value) {
4570
+ const index = alphabet.indexOf(char);
4571
+ if (index < 0) {
4572
+ throw new LocationRecordValidationError(
4573
+ "did:key signature must be base64url"
4574
+ );
4575
+ }
4576
+ buffer = buffer << 6 | index;
4577
+ bits += 6;
4578
+ if (bits >= 8) {
4579
+ bits -= 8;
4580
+ bytes.push(buffer >> bits & 255);
4581
+ }
4582
+ }
4583
+ return Uint8Array.from(bytes);
4584
+ }
4585
+
4241
4586
  // src/capabilities.ts
4242
4587
  var PermissionNotInManifestError = class extends Error {
4243
4588
  constructor(missing, granted) {
@@ -4358,6 +4703,7 @@ export {
4358
4703
  CapabilityKeyRegistry,
4359
4704
  CapabilityKeyRegistryErrorCodes,
4360
4705
  ClientSessionSchema,
4706
+ CloudLocationResolutionError,
4361
4707
  DEFAULT_DEFAULTS,
4362
4708
  DEFAULT_EXPIRY,
4363
4709
  DEFAULT_MANIFEST_SPACE,
@@ -4373,6 +4719,7 @@ export {
4373
4719
  ErrorCodes2 as ErrorCodes,
4374
4720
  HooksService2 as HooksService,
4375
4721
  KVService2 as KVService,
4722
+ LocationRecordValidationError,
4376
4723
  ManifestValidationError,
4377
4724
  PermissionNotInManifestError,
4378
4725
  PrefixedKVService,
@@ -4398,6 +4745,7 @@ export {
4398
4745
  activateSessionWithHost,
4399
4746
  applyPrefix,
4400
4747
  buildSpaceUri,
4748
+ canonicalLocationPayload,
4401
4749
  checkNodeInfo,
4402
4750
  composeManifestRequest,
4403
4751
  createCapabilityKeyRegistry,
@@ -4409,23 +4757,32 @@ export {
4409
4757
  defaultSpaceCreationHandler,
4410
4758
  err4 as err,
4411
4759
  expandActionShortNames,
4760
+ fetchLocationRecord,
4412
4761
  fetchPeerId,
4762
+ httpUrlToMultiaddr,
4413
4763
  isCapabilitySubset,
4414
4764
  loadManifest,
4765
+ locationPayloadForRecord,
4415
4766
  makePublicSpaceId,
4416
4767
  manifestAbilitiesUnion,
4768
+ multiaddrToHttpUrl,
4417
4769
  normalizeDefaults,
4418
4770
  ok4 as ok,
4419
4771
  parseExpiry,
4420
4772
  parseRecapCapabilities,
4421
4773
  parseSpaceUri,
4774
+ resolveCloudLocation,
4422
4775
  resolveManifest,
4423
4776
  resourceCapabilitiesToAbilitiesMap,
4424
4777
  resourceCapabilitiesToSpaceAbilitiesMap,
4425
4778
  serviceError4 as serviceError,
4779
+ signLocationRecord,
4426
4780
  submitHostDelegation,
4427
4781
  validateClientSession,
4782
+ validateLocationRecord,
4783
+ validateLocationRecordPayload,
4428
4784
  validateManifest,
4429
- validatePersistedSessionData
4785
+ validatePersistedSessionData,
4786
+ verifyLocationRecord
4430
4787
  };
4431
4788
  //# sourceMappingURL=index.js.map