@tinycloud/sdk-core 3.8.0-beta.2 → 3.8.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
@@ -3437,6 +3437,7 @@ import { sha256 as sha2564 } from "@noble/hashes/sha256";
3437
3437
  import { blake3 } from "@noble/hashes/blake3";
3438
3438
  import { ed25519 as ed255193 } from "@noble/curves/ed25519";
3439
3439
  import { z as z2 } from "zod";
3440
+ import { parseEncryptionResource as parseNamedEncryptionResource } from "@tinycloud/sdk-services";
3440
3441
 
3441
3442
  // src/http-error.ts
3442
3443
  import { ErrorCodes, serviceError } from "@tinycloud/sdk-services";
@@ -3705,12 +3706,9 @@ function parseKvResource(resource) {
3705
3706
  return { space, path };
3706
3707
  }
3707
3708
  function parseEncryptionResource(resource) {
3708
- if (!resource.startsWith("urn:tinycloud:encryption:")) throw new Error("encryption capability is invalid");
3709
- const rest = resource.slice("urn:tinycloud:encryption:".length);
3710
- const separator = rest.lastIndexOf(":");
3711
- const ownerDid = separator < 0 ? "" : rest.slice(0, separator);
3712
- const network = separator < 0 ? "" : rest.slice(separator + 1);
3713
- if (!ownerDid.startsWith("did:") || network.length === 0 || network.includes(":") || network.includes("/") || network.includes("%") || network.includes("?") || network.includes("#")) {
3709
+ try {
3710
+ if (parseNamedEncryptionResource(resource).keyPath?.endsWith("/")) throw new Error("prefix");
3711
+ } catch {
3714
3712
  throw new Error("encryption capability is invalid");
3715
3713
  }
3716
3714
  }
@@ -6177,7 +6175,11 @@ var DelegationErrorCodes = {
6177
6175
  KV_SERVICE_UNAVAILABLE: "KV_SERVICE_UNAVAILABLE",
6178
6176
  DATA_FETCH_FAILED: "DATA_FETCH_FAILED",
6179
6177
  VALIDATION_ERROR: "VALIDATION_ERROR",
6180
- UNSUPPORTED_OPERATION: "UNSUPPORTED_OPERATION"
6178
+ UNSUPPORTED_OPERATION: "UNSUPPORTED_OPERATION",
6179
+ /** The node answered 503: the delegation chain is too deep to evaluate right now. Retry later. */
6180
+ CHAIN_TOO_DEEP: "CHAIN_TOO_DEEP",
6181
+ /** The node answered 400 "invalid delegation query cursor": malformed, foreign or from another filter. Start the listing again without it. */
6182
+ INVALID_CURSOR: "INVALID_CURSOR"
6181
6183
  };
6182
6184
  var DelegationSchema = z4.object({
6183
6185
  /** Content identifier (CID) of the delegation */
@@ -6254,7 +6256,7 @@ var AccountDelegationPageSchema = z4.object({
6254
6256
  nextCursor: z4.string().optional()
6255
6257
  }).strict();
6256
6258
  var AccountDelegationQueryOptionsSchema = z4.object({
6257
- direction: z4.enum(["granted", "received", "all"]).optional(),
6259
+ direction: z4.enum(["granted", "received", "all", "issued"]).optional(),
6258
6260
  status: z4.enum(["active", "pending", "expired", "revoked", "ancestor_revoked"]).optional(),
6259
6261
  space: z4.string().trim().min(1).optional(),
6260
6262
  limit: z4.number().int().min(1).max(100).optional(),
@@ -6440,6 +6442,8 @@ var DelegationManagerConfigSchema = z4.object({
6440
6442
  ).optional(),
6441
6443
  /** Optional owner-signed authorization for direct CID revocation */
6442
6444
  signOwnerRevocation: z4.custom().optional(),
6445
+ /** Optional signer for proofless invocations by the session's own did:key (issued-delegation list and revoke) */
6446
+ sessionInvocationSigner: z4.custom().optional(),
6443
6447
  /** Optional custom fetch implementation */
6444
6448
  fetch: z4.unknown().refine(
6445
6449
  (val) => val === void 0 || typeof val === "function",
@@ -6535,7 +6539,7 @@ var CreateDelegationWasmResultSchema = z4.object({
6535
6539
  resources: z4.array(DelegatedResourceSchema)
6536
6540
  });
6537
6541
 
6538
- // src/delegations/DelegationManager.ts
6542
+ // src/delegations/shared.ts
6539
6543
  var DelegationAction = {
6540
6544
  REVOKE: "tinycloud.delegation/revoke",
6541
6545
  STATUS: "tinycloud.delegation/status",
@@ -6552,6 +6556,8 @@ function createError(code3, message, cause, meta) {
6552
6556
  meta
6553
6557
  };
6554
6558
  }
6559
+
6560
+ // src/delegations/DelegationManager.ts
6555
6561
  async function readInvokedJson(response, action) {
6556
6562
  const text3 = await response.text();
6557
6563
  if (text3.trim() === "") {
@@ -6574,11 +6580,13 @@ var DelegationManager = class {
6574
6580
  * @param config - Configuration including hosts, session, and invoke function
6575
6581
  */
6576
6582
  constructor(config) {
6583
+ this.issuedState = { issuedQuerySupported: false };
6577
6584
  this.hosts = config.hosts;
6578
6585
  this.session = config.session;
6579
6586
  this.invoke = config.invoke;
6580
6587
  this.invokeAny = config.invokeAny;
6581
6588
  this.signOwnerRevocation = config.signOwnerRevocation;
6589
+ this.sessionInvocationSigner = config.sessionInvocationSigner;
6582
6590
  this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
6583
6591
  this.accountSpaceId = config.accountSpaceId;
6584
6592
  }
@@ -6801,8 +6809,14 @@ var DelegationManager = class {
6801
6809
  };
6802
6810
  }
6803
6811
  }
6804
- /** Query lifecycle-complete delegation history for the authenticated account. */
6805
- async query(options2 = {}) {
6812
+ /**
6813
+ * Query lifecycle-complete delegation history for the authenticated account.
6814
+ *
6815
+ * `scope.spaces` lists the spaces to sign `tinycloud.delegation/list` for, in order. Omitted, the
6816
+ * account space is tried, then the session's own space. A caller that knows which of them the session
6817
+ * holds passes only those; an empty list sends nothing and answers PERMISSION_DENIED.
6818
+ */
6819
+ async query(options2 = {}, scope = {}) {
6806
6820
  const validated = AccountDelegationQueryOptionsSchema.safeParse(options2);
6807
6821
  if (!validated.success) {
6808
6822
  return {
@@ -6815,6 +6829,7 @@ var DelegationManager = class {
6815
6829
  )
6816
6830
  };
6817
6831
  }
6832
+ if (validated.data.direction === "issued") return this.queryIssued(validated.data);
6818
6833
  if (!this.invokeAny) {
6819
6834
  return {
6820
6835
  ok: false,
@@ -6836,7 +6851,7 @@ var DelegationManager = class {
6836
6851
  }
6837
6852
  try {
6838
6853
  let response;
6839
- for (const spaceId of /* @__PURE__ */ new Set([accountSpaceId, this.session.spaceId])) {
6854
+ for (const spaceId of new Set(scope.spaces ?? [accountSpaceId, this.session.spaceId])) {
6840
6855
  const headers = Object.fromEntries(new Headers(this.invokeAny(this.session, [{
6841
6856
  spaceId,
6842
6857
  service: "delegation",
@@ -6896,6 +6911,45 @@ var DelegationManager = class {
6896
6911
  };
6897
6912
  }
6898
6913
  }
6914
+ /** The state the lazily loaded issued-delegation module reads and updates. */
6915
+ issuedContext() {
6916
+ return {
6917
+ host: this.host,
6918
+ fetchFn: this.fetchFn,
6919
+ sessionInvocationSigner: this.sessionInvocationSigner,
6920
+ state: this.issuedState
6921
+ };
6922
+ }
6923
+ /**
6924
+ * Lists the delegations this session's own key issued (`direction: "issued"`).
6925
+ * The implementation loads on first use.
6926
+ */
6927
+ async queryIssued(options2) {
6928
+ const issued = await import("@tinycloud/sdk-core/delegations/issued");
6929
+ return issued.queryIssued(this.issuedContext(), options2);
6930
+ }
6931
+ /**
6932
+ * Every delegation this session's key issued, following `nextCursor` until
6933
+ * it is absent. `maxItems` stops early once that many are collected;
6934
+ * `untilCid` stops after the page that contains that delegation. Items come
6935
+ * in CID order (each carries `issuedAt`). The implementation loads on first use.
6936
+ */
6937
+ async queryAllIssued(options2 = {}) {
6938
+ const issued = await import("@tinycloud/sdk-core/delegations/issued");
6939
+ return issued.queryAllIssued((page) => this.query(page), options2);
6940
+ }
6941
+ /**
6942
+ * Revokes a delegation this session's key issued, with no proof: a UCAN
6943
+ * signed by the session's own did:key carrying `urn:cid:<cid>` and
6944
+ * `tinycloud.delegation/revoke`. Revoking a delegation that is already revoked
6945
+ * succeeds. A refusal (the key is not the delegator) carries the node's
6946
+ * message; 503 means the chain is too deep right now, retry later.
6947
+ * The implementation loads on first use.
6948
+ */
6949
+ async revokeIssued(cid2) {
6950
+ const issued = await import("@tinycloud/sdk-core/delegations/issued");
6951
+ return issued.revokeIssued(this.issuedContext(), cid2);
6952
+ }
6899
6953
  /**
6900
6954
  * Gets the full delegation chain for a given delegation.
6901
6955
  *
@@ -7202,7 +7256,8 @@ function validateEncodedShareData(data) {
7202
7256
  // src/manifest.ts
7203
7257
  import ms from "ms";
7204
7258
  import { ACCOUNT_MANIFEST_PERMISSIONS } from "@tinycloud/bootstrap";
7205
- import { resolveSecretPath, SECRET_NAME_RE } from "@tinycloud/sdk-services";
7259
+ import { resolveSecretPath, SECRET_NAME_RE, parseNetworkId } from "@tinycloud/sdk-services";
7260
+ import { parseEncryptionResource as parseEncryptionResource2 } from "@tinycloud/sdk-services/encryption/resource";
7206
7261
  var DEFAULT_KNOWLEDGE_ROOT = "knowledge/index.md";
7207
7262
  var ManifestValidationError = class extends Error {
7208
7263
  constructor(message) {
@@ -7325,6 +7380,16 @@ function expandEncryptionPermissionEntry(entry) {
7325
7380
  `tinycloud.encryption entries require path to be a networkId URN (got ${JSON.stringify(entry.path)})`
7326
7381
  );
7327
7382
  }
7383
+ const sharePrefix = entry.path.endsWith("/share/");
7384
+ if (entry.path.includes("/") && !sharePrefix) {
7385
+ throw new ManifestValidationError("named keys go in manifest.keys");
7386
+ }
7387
+ try {
7388
+ const network = parseNetworkId(sharePrefix ? entry.path.slice(0, -"/share/".length) : entry.path);
7389
+ if (sharePrefix && network.name !== "default") throw new Error("share prefix requires default network");
7390
+ } catch {
7391
+ throw new ManifestValidationError("tinycloud.encryption entries require a valid networkId URN");
7392
+ }
7328
7393
  const normalizedActions = [];
7329
7394
  for (const action of entry.actions) {
7330
7395
  if (action === "decrypt" || action === "tinycloud.encryption/decrypt") {
@@ -7356,6 +7421,9 @@ function expandEncryptionPermissionEntry(entry) {
7356
7421
  seen.add(a);
7357
7422
  }
7358
7423
  }
7424
+ if (sharePrefix && dedupedActions.some((action) => action !== "tinycloud.encryption/decrypt")) {
7425
+ throw new ManifestValidationError("share prefix only supports decrypt");
7426
+ }
7359
7427
  return [
7360
7428
  {
7361
7429
  service: ENCRYPTION_PERMISSION_SERVICE,
@@ -7371,6 +7439,29 @@ function expandEncryptionPermissionEntry(entry) {
7371
7439
  function expandPermissionEntries(entries) {
7372
7440
  return entries.flatMap(expandPermissionEntry);
7373
7441
  }
7442
+ function expandDelegationPermissionEntries(entries) {
7443
+ return entries.flatMap((entry) => {
7444
+ if (entry.service !== ENCRYPTION_PERMISSION_SERVICE || typeof entry.path !== "string" || !entry.path.includes("/")) {
7445
+ return expandPermissionEntry(entry);
7446
+ }
7447
+ let resource;
7448
+ try {
7449
+ resource = parseEncryptionResource2(entry.path);
7450
+ } catch {
7451
+ throw new ManifestValidationError("delegation requires a canonical exact named-key resource");
7452
+ }
7453
+ if (resource.kind !== "key" || !Array.isArray(entry.actions) || entry.actions.length === 0 || entry.actions.some((action) => action !== "decrypt" && action !== "tinycloud.encryption/decrypt")) {
7454
+ throw new ManifestValidationError("named-key delegation requires an exact key and decrypt only");
7455
+ }
7456
+ return [{
7457
+ ...entry,
7458
+ space: ENCRYPTION_MANIFEST_SPACE,
7459
+ path: `${resource.networkId}/${resource.path}`,
7460
+ actions: ["tinycloud.encryption/decrypt"],
7461
+ skipPrefix: true
7462
+ }];
7463
+ });
7464
+ }
7374
7465
  function applyPrefix(prefix, path, skipPrefix) {
7375
7466
  if (skipPrefix) {
7376
7467
  return path;
@@ -7449,8 +7540,56 @@ function validateManifest(input) {
7449
7540
  if (m.secrets !== void 0) {
7450
7541
  validateManifestSecrets(m.secrets);
7451
7542
  }
7543
+ if (Object.prototype.hasOwnProperty.call(m, "keys_app_id") && (typeof m.keys_app_id !== "string" || !KEY_APP_ID.test(m.keys_app_id))) {
7544
+ throw new ManifestValidationError("manifest.keys_app_id is invalid");
7545
+ }
7546
+ validateManifestKeys(m.keys_app_id ?? m.app_id, m.keys);
7452
7547
  return m;
7453
7548
  }
7549
+ var KEY_SEGMENT = "[a-z0-9][a-z0-9._-]{0,63}";
7550
+ var KEY_APP_ID = new RegExp(`^${KEY_SEGMENT}$`);
7551
+ var KEY_PATH = new RegExp(`^${KEY_SEGMENT}(?:/${KEY_SEGMENT}){2,7}$`);
7552
+ var KEY_ABILITIES = ["key.create", "decrypt", "key.block", "key.unblock"];
7553
+ function validateManifestKeys(appId, keys) {
7554
+ if (keys === void 0) return;
7555
+ if (!Array.isArray(keys) || keys.length > 32) {
7556
+ throw new ManifestValidationError("manifest.keys must contain at most 32 entries");
7557
+ }
7558
+ if (keys.length === 0) return;
7559
+ if (!KEY_APP_ID.test(appId)) {
7560
+ throw new ManifestValidationError("manifest.keys requires a valid app_id");
7561
+ }
7562
+ const paths = /* @__PURE__ */ new Set();
7563
+ let prefixSeen = false;
7564
+ for (const [index, value] of keys.entries()) {
7565
+ const label = `manifest.keys[${index}]`;
7566
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
7567
+ throw new ManifestValidationError(`${label} must be an object`);
7568
+ }
7569
+ const entry = value;
7570
+ const fields = Object.keys(entry);
7571
+ const hasPath = Object.prototype.hasOwnProperty.call(entry, "path");
7572
+ const hasPrefix = Object.prototype.hasOwnProperty.call(entry, "prefix");
7573
+ if (hasPath === hasPrefix || fields.some((field) => !["path", "prefix", "abilities"].includes(field))) {
7574
+ throw new ManifestValidationError(`${label} must have one path or prefix and abilities`);
7575
+ }
7576
+ if (!Array.isArray(entry.abilities) || entry.abilities.length === 0 || new Set(entry.abilities).size !== entry.abilities.length || entry.abilities.some((ability) => typeof ability !== "string" || !KEY_ABILITIES.includes(ability))) {
7577
+ throw new ManifestValidationError(`${label}.abilities is invalid`);
7578
+ }
7579
+ if (hasPath) {
7580
+ const path = entry.path;
7581
+ if (typeof path !== "string" || path.length > 255 || !KEY_PATH.test(path) || !path.startsWith(`apps/${appId}/`) || paths.has(path)) {
7582
+ throw new ManifestValidationError(`${label}.path is invalid`);
7583
+ }
7584
+ paths.add(path);
7585
+ } else {
7586
+ if (entry.prefix !== `apps/${appId}/` || prefixSeen) {
7587
+ throw new ManifestValidationError(`${label}.prefix is invalid`);
7588
+ }
7589
+ prefixSeen = true;
7590
+ }
7591
+ }
7592
+ }
7454
7593
  function resolveManifestKnowledgeRoot(knowledge) {
7455
7594
  if (knowledge === void 0) {
7456
7595
  return void 0;
@@ -7968,6 +8107,7 @@ function manifestAbilitiesUnion(resolved) {
7968
8107
  }
7969
8108
 
7970
8109
  // src/capabilities.ts
8110
+ import { encryptionResourceContains } from "@tinycloud/sdk-services/encryption/resource";
7971
8111
  var PermissionNotInManifestError = class extends Error {
7972
8112
  constructor(missing, granted) {
7973
8113
  super(
@@ -8019,10 +8159,9 @@ function isCapabilitySubset(requested, granted) {
8019
8159
  const grantedActions = matchingScope.flatMap(
8020
8160
  (entry) => expandActionShortNames(entry.service, entry.actions)
8021
8161
  );
8162
+ const namedKeyRequest = req.service === "tinycloud.encryption" && req.path.startsWith("urn:tinycloud:encryption:") && req.path.includes("/");
8022
8163
  const covered = requestedActions.every(
8023
- (requestedAction) => grantedActions.some(
8024
- (grantedAction) => actionContains(grantedAction, requestedAction)
8025
- )
8164
+ (requestedAction) => grantedActions.some((grantedAction) => namedKeyRequest ? grantedAction === requestedAction : actionContains(grantedAction, requestedAction))
8026
8165
  );
8027
8166
  if (!covered) {
8028
8167
  missing.push(cloneEntry(req));
@@ -8037,7 +8176,9 @@ function canonicalizeEntryScopeMatches(requested, granted) {
8037
8176
  if (normalizeSpace(requested.space ?? DEFAULT_MANIFEST_SPACE) !== normalizeSpace(granted.space ?? DEFAULT_MANIFEST_SPACE)) {
8038
8177
  return false;
8039
8178
  }
8040
- if (!pathContains2(granted.path, requested.path)) {
8179
+ const namedKeyRequest = requested.service === "tinycloud.encryption" && requested.path.startsWith("urn:tinycloud:encryption:") && requested.path.includes("/");
8180
+ const coveredPath = namedKeyRequest ? encryptionResourceContains(granted.path, requested.path) : pathContains2(granted.path, requested.path);
8181
+ if (!coveredPath) {
8041
8182
  return false;
8042
8183
  }
8043
8184
  if (!recapCaveatsEqual(requested.caveats, granted.caveats)) {
@@ -12242,15 +12383,15 @@ function markDelegationTerminal(token, key) {
12242
12383
  // src/networkId.ts
12243
12384
  import {
12244
12385
  buildNetworkId,
12245
- parseNetworkId
12386
+ parseNetworkId as parseNetworkId2
12246
12387
  } from "@tinycloud/sdk-services";
12247
12388
  function canonicalizeNetworkId(networkId) {
12248
- const parsed = parseNetworkId(networkId);
12389
+ const parsed = parseNetworkId2(networkId);
12249
12390
  return buildNetworkId(canonicalizeDid(parsed.ownerDid), parsed.name);
12250
12391
  }
12251
12392
  function parseCanonicalNetworkId(networkId) {
12252
12393
  const canonical = canonicalizeNetworkId(networkId);
12253
- return parseNetworkId(canonical);
12394
+ return parseNetworkId2(canonical);
12254
12395
  }
12255
12396
 
12256
12397
  // src/storage.schema.ts
@@ -15063,7 +15204,7 @@ import {
15063
15204
  resolveSecretPath as resolveSecretPath2,
15064
15205
  EncryptionService,
15065
15206
  DecryptTransportResponseError,
15066
- parseNetworkId as parseNetworkId2,
15207
+ parseNetworkId as parseNetworkId3,
15067
15208
  buildNetworkId as buildNetworkId2,
15068
15209
  isNetworkId,
15069
15210
  networkDiscoveryKey,
@@ -16117,8 +16258,14 @@ function diffImmutableSiweFields(original, signed, options2) {
16117
16258
  function extractRecapAttenuations(siwe) {
16118
16259
  const merged = {};
16119
16260
  const lines = siwe.split(/\r?\n/);
16261
+ let resources = false;
16120
16262
  for (const line of lines) {
16121
- const match = line.match(/urn:recap:([A-Za-z0-9_-]+=*)/);
16263
+ if (line === "Resources:") {
16264
+ resources = true;
16265
+ continue;
16266
+ }
16267
+ if (!resources) continue;
16268
+ const match = line.match(/^- urn:recap:([A-Za-z0-9_-]+=*)$/);
16122
16269
  if (!match) continue;
16123
16270
  const encoded = match[1];
16124
16271
  let decodedJson;
@@ -19833,6 +19980,7 @@ export {
19833
19980
  ensureNetworkUsableForDecrypt,
19834
19981
  err5 as err,
19835
19982
  expandActionShortNames,
19983
+ expandDelegationPermissionEntries,
19836
19984
  expandPermissionEntries,
19837
19985
  expandPermissionEntry,
19838
19986
  extractImmutableSiweFields,
@@ -19888,7 +20036,7 @@ export {
19888
20036
  parseCapabilityResource,
19889
20037
  parseCompactUcanAuthorization,
19890
20038
  parseExpiry,
19891
- parseNetworkId2 as parseNetworkId,
20039
+ parseNetworkId3 as parseNetworkId,
19892
20040
  parseNodeNativePortableDelegation,
19893
20041
  parsePkhDid,
19894
20042
  parsePolicySessionUcan,