@tinycloud/sdk-core 2.11.0-beta.0 → 2.11.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.cjs CHANGED
@@ -6702,15 +6702,28 @@ function normalizeSpace(space) {
6702
6702
  function isCapabilitySubset(requested, granted) {
6703
6703
  const missing = [];
6704
6704
  for (const req of requested) {
6705
- const match = granted.find((g) => canonicalizeEntryMatches(req, g));
6706
- if (match === void 0) {
6705
+ const matchingScope = granted.filter(
6706
+ (entry) => canonicalizeEntryScopeMatches(req, entry)
6707
+ );
6708
+ const requestedActions = expandActionShortNames(
6709
+ req.service,
6710
+ req.actions
6711
+ );
6712
+ const grantedActions = matchingScope.flatMap(
6713
+ (entry) => expandActionShortNames(entry.service, entry.actions)
6714
+ );
6715
+ const covered = requestedActions.every(
6716
+ (requestedAction) => grantedActions.some(
6717
+ (grantedAction) => actionContains(grantedAction, requestedAction)
6718
+ )
6719
+ );
6720
+ if (!covered) {
6707
6721
  missing.push(cloneEntry(req));
6708
- continue;
6709
6722
  }
6710
6723
  }
6711
6724
  return { subset: missing.length === 0, missing };
6712
6725
  }
6713
- function canonicalizeEntryMatches(requested, granted) {
6726
+ function canonicalizeEntryScopeMatches(requested, granted) {
6714
6727
  if (requested.service !== granted.service) {
6715
6728
  return false;
6716
6729
  }
@@ -6720,15 +6733,6 @@ function canonicalizeEntryMatches(requested, granted) {
6720
6733
  if (!pathContains2(granted.path, requested.path)) {
6721
6734
  return false;
6722
6735
  }
6723
- const reqActions = new Set(
6724
- expandActionShortNames(requested.service, requested.actions)
6725
- );
6726
- const grantedActions = expandActionShortNames(granted.service, granted.actions);
6727
- for (const a of reqActions) {
6728
- if (!grantedActions.some((grantedAction) => actionContains(grantedAction, a))) {
6729
- return false;
6730
- }
6731
- }
6732
6736
  if (!recapCaveatsEqual(requested.caveats, granted.caveats)) {
6733
6737
  return false;
6734
6738
  }
@@ -10126,6 +10130,10 @@ async function createPolicyEnforcementDelegation(input) {
10126
10130
  spaceId: input.spaceId,
10127
10131
  path: input.path,
10128
10132
  actions: [...input.actions],
10133
+ ...input.decryption === void 0 ? {} : {
10134
+ decryptionNetworkId: input.decryption.networkId,
10135
+ decryptionAction: input.decryption.action
10136
+ },
10129
10137
  contentSourceDigest: input.contentSourceDigest,
10130
10138
  expiresAt: input.expiresAt
10131
10139
  };
@@ -10145,7 +10153,9 @@ function computeOwnerShareRegistrationCid(registration) {
10145
10153
  function validateOwnerSharePolicyRegistration(value, expected) {
10146
10154
  if (cid(expected.policy.bytes) !== expected.policy.cid) throw new Error("submitted owner-share policy bytes do not match its CID");
10147
10155
  const root = assertObject(value, ["registration", "proof"], "owner-share registration response");
10148
- const registration = assertObject(root.registration, ["registrationCid", "policyCid", "ownerDelegationCid", "enforcementDelegationCid", "ownerDid", "shareKeyDid", "enforcerDid", "shareId", "recipientMatcher", "target", "resource", "actions", "contentSource", "contentSourceDigest", "registeredAt", "expiresAt"], "owner-share registration");
10156
+ const rawRegistration = root.registration;
10157
+ const hasDecryption = typeof rawRegistration === "object" && rawRegistration !== null && Object.prototype.hasOwnProperty.call(rawRegistration, "decryption");
10158
+ const registration = assertObject(root.registration, ["registrationCid", "policyCid", "ownerDelegationCid", "enforcementDelegationCid", "ownerDid", "shareKeyDid", "enforcerDid", "shareId", "recipientMatcher", "target", "resource", "actions", ...hasDecryption ? ["decryption"] : [], "contentSource", "contentSourceDigest", "registeredAt", "expiresAt"], "owner-share registration");
10149
10159
  const proof = assertObject(root.proof, ["alg", "kid", "signature"], "owner-share registration proof");
10150
10160
  if (registration.policyCid !== expected.policy.cid || registration.ownerDelegationCid !== expected.ownerDelegation.delegationCid || registration.enforcementDelegationCid !== expected.enforcementDelegation.cid || registration.contentSourceDigest !== expected.contentSourceDigest) throw new Error("owner-share registration is not bound to the submitted chain");
10151
10161
  let policyValue;
@@ -10161,7 +10171,9 @@ function validateOwnerSharePolicyRegistration(value, expected) {
10161
10171
  const registrationTarget = registration.target;
10162
10172
  const registrationResource = registration.resource;
10163
10173
  const enforcementFacts = expected.enforcementDelegation.facts;
10164
- if (typeof target?.origin !== "string" || typeof target.nodeAudience !== "string" || typeof target.enforcerDid !== "string" || typeof target.spaceId !== "string" || typeof resource?.kind !== "string" || !["exact", "prefix"].includes(resource.kind) || typeof resource?.path !== "string" || !Array.isArray(policy.actions) || policy.ownerDid !== registration.ownerDid || policy.shareKeyDid !== registration.shareKeyDid || policy.shareId !== registration.shareId || canonicalizeSignedObjectUnsigned(policy.recipientMatcher) !== canonicalizeSignedObjectUnsigned(registration.recipientMatcher) || target.origin !== registrationTarget.origin || target.nodeAudience !== registrationTarget.nodeAudience || target.enforcerDid !== registration.enforcerDid || target.spaceId !== registrationTarget.spaceId || resource.kind !== registrationResource.kind || resource.path !== registrationResource.path || canonicalizeSignedObjectUnsigned(policy.actions) !== canonicalizeSignedObjectUnsigned(registration.actions) || canonicalizeSignedObjectUnsigned(policy.contentSource) !== canonicalizeSignedObjectUnsigned(registration.contentSource) || policy.expiresAt !== registration.expiresAt || policy.contentSourceDigest !== registration.contentSourceDigest || enforcementFacts.ownerDelegationCid !== registration.ownerDelegationCid || enforcementFacts.policyCid !== registration.policyCid || enforcementFacts.shareKeyDid !== registration.shareKeyDid || enforcementFacts.enforcerDid !== registration.enforcerDid || enforcementFacts.nodeAudience !== registrationTarget.nodeAudience || enforcementFacts.spaceId !== registrationTarget.spaceId || enforcementFacts.path !== registrationResource.path || canonicalizeSignedObjectUnsigned(enforcementFacts.actions) !== canonicalizeSignedObjectUnsigned(registration.actions) || enforcementFacts.expiresAt !== registration.expiresAt) throw new Error("owner-share registration is not bound to the canonical policy");
10174
+ const decryption = policy.decryption;
10175
+ const decryptionMatches = decryption === void 0 ? registration.decryption === void 0 : registration.decryption !== void 0 && canonicalizeSignedObjectUnsigned(decryption) === canonicalizeSignedObjectUnsigned(registration.decryption);
10176
+ if (typeof target?.origin !== "string" || typeof target.nodeAudience !== "string" || typeof target.enforcerDid !== "string" || typeof target.spaceId !== "string" || typeof resource?.kind !== "string" || !["exact", "prefix"].includes(resource.kind) || typeof resource?.path !== "string" || !Array.isArray(policy.actions) || policy.ownerDid !== registration.ownerDid || policy.shareKeyDid !== registration.shareKeyDid || policy.shareId !== registration.shareId || canonicalizeSignedObjectUnsigned(policy.recipientMatcher) !== canonicalizeSignedObjectUnsigned(registration.recipientMatcher) || target.origin !== registrationTarget.origin || target.nodeAudience !== registrationTarget.nodeAudience || target.enforcerDid !== registration.enforcerDid || target.spaceId !== registrationTarget.spaceId || resource.kind !== registrationResource.kind || resource.path !== registrationResource.path || canonicalizeSignedObjectUnsigned(policy.actions) !== canonicalizeSignedObjectUnsigned(registration.actions) || !decryptionMatches || canonicalizeSignedObjectUnsigned(policy.contentSource) !== canonicalizeSignedObjectUnsigned(registration.contentSource) || policy.expiresAt !== registration.expiresAt || policy.contentSourceDigest !== registration.contentSourceDigest || enforcementFacts.ownerDelegationCid !== registration.ownerDelegationCid || enforcementFacts.policyCid !== registration.policyCid || enforcementFacts.shareKeyDid !== registration.shareKeyDid || enforcementFacts.enforcerDid !== registration.enforcerDid || enforcementFacts.nodeAudience !== registrationTarget.nodeAudience || enforcementFacts.spaceId !== registrationTarget.spaceId || enforcementFacts.path !== registrationResource.path || canonicalizeSignedObjectUnsigned(enforcementFacts.actions) !== canonicalizeSignedObjectUnsigned(registration.actions) || enforcementFacts.decryptionNetworkId !== decryption?.networkId || enforcementFacts.decryptionAction !== decryption?.action || enforcementFacts.expiresAt !== registration.expiresAt) throw new Error("owner-share registration is not bound to the canonical policy");
10165
10177
  if (typeof registration.registrationCid !== "string" || typeof registration.expiresAt !== "string" || typeof registration.registeredAt !== "string") throw new Error("owner-share registration timestamps are invalid");
10166
10178
  if (new Date(registration.expiresAt).toISOString() !== registration.expiresAt || Date.parse(registration.expiresAt) <= Date.now()) throw new Error("owner-share registration is expired or non-canonical");
10167
10179
  const { registrationCid: _registrationCid, ...registrationCore } = registration;
@@ -12112,6 +12124,22 @@ var ACCOUNT_SPACES_PATH = "spaces/";
12112
12124
  var AccountService = class {
12113
12125
  constructor(config) {
12114
12126
  this.config = config;
12127
+ /**
12128
+ * Per-account-space memo for {@link ensureAccountIndex}.
12129
+ *
12130
+ * Applying the index migration is a no-op once the schema exists, but the
12131
+ * SQL service still spends two round trips discovering that (create the
12132
+ * migrations table, then read the applied-migration rows). Account bootstrap
12133
+ * touches the index eight times, so without a memo that is ~14 wasted
12134
+ * requests on the cold sign-in path. The in-flight dedupe inside SQLService
12135
+ * never helps here: the node SDK builds a fresh SQLService per
12136
+ * `getAccountDb()` call, so its lock map is always empty.
12137
+ *
12138
+ * Keyed by account space id, so a different account never reuses this entry.
12139
+ * Failures are evicted so a later call retries instead of inheriting a
12140
+ * permanently poisoned result.
12141
+ */
12142
+ this.accountIndexReady = /* @__PURE__ */ new Map();
12115
12143
  this.applications = {
12116
12144
  list: async (options2 = {}) => {
12117
12145
  if (options2.preferIndex) {
@@ -12145,7 +12173,7 @@ var AccountService = class {
12145
12173
  if (!loaded.ok) return accountErr(loaded.error);
12146
12174
  return (0, import_sdk_services6.ok)(applicationFromRecord(key, loaded.data.data));
12147
12175
  },
12148
- register: async (manifest) => {
12176
+ register: async (manifest, options2 = {}) => {
12149
12177
  const manifests = Array.isArray(manifest) ? manifest : [manifest];
12150
12178
  const request = composeManifestRequest(manifests);
12151
12179
  if (request.registryRecords.length === 0) {
@@ -12163,7 +12191,7 @@ var AccountService = class {
12163
12191
  let registered;
12164
12192
  for (const record of request.registryRecords) {
12165
12193
  const manifestHash = hashJson(record.manifests);
12166
- if (await this.indexHasApplicationHash(record.app_id, manifestHash)) {
12194
+ if (!options2.assumeUnregistered && await this.indexHasApplicationHash(record.app_id, manifestHash)) {
12167
12195
  registered = {
12168
12196
  appId: record.app_id,
12169
12197
  manifests: record.manifests,
@@ -12344,6 +12372,7 @@ var AccountService = class {
12344
12372
  const delegations = await this.delegations.list();
12345
12373
  if (!delegations.ok) return delegations;
12346
12374
  const syncedAt = (/* @__PURE__ */ new Date()).toISOString();
12375
+ this.invalidateAccountIndexMemo();
12347
12376
  const schema = await this.ensureAccountIndex(dbResult.data);
12348
12377
  if (!schema.ok) return schema;
12349
12378
  const statements = [
@@ -12743,7 +12772,41 @@ var AccountService = class {
12743
12772
  ]);
12744
12773
  });
12745
12774
  }
12775
+ /**
12776
+ * Apply the account index schema at most once per account space.
12777
+ *
12778
+ * The index is a cache over canonical KV state, and every caller already
12779
+ * tolerates it being absent (`ignoreIndexFailure` / `isMissingIndexError`),
12780
+ * so a memo that is wrong in the "already applied" direction degrades to a
12781
+ * `no such table` read that the caller handles. A memo that is wrong in the
12782
+ * other direction would be a real bug, so failures are never cached.
12783
+ */
12746
12784
  async ensureAccountIndex(db) {
12785
+ const key = this.config.getAccountSpaceId() ?? "";
12786
+ const cached = this.accountIndexReady.get(key);
12787
+ if (cached) return cached;
12788
+ const pending = this.applyAccountIndex(db);
12789
+ this.accountIndexReady.set(key, pending);
12790
+ const evict = () => {
12791
+ if (this.accountIndexReady.get(key) === pending) {
12792
+ this.accountIndexReady.delete(key);
12793
+ }
12794
+ };
12795
+ let settled;
12796
+ try {
12797
+ settled = await pending;
12798
+ } catch (error) {
12799
+ evict();
12800
+ throw error;
12801
+ }
12802
+ if (!settled.ok) evict();
12803
+ return settled;
12804
+ }
12805
+ /** Drop the memo so the next {@link ensureAccountIndex} re-applies the schema. */
12806
+ invalidateAccountIndexMemo() {
12807
+ this.accountIndexReady.delete(this.config.getAccountSpaceId() ?? "");
12808
+ }
12809
+ async applyAccountIndex(db) {
12747
12810
  const migrated = await db.migrations.apply({
12748
12811
  namespace: ACCOUNT_INDEX_NAMESPACE,
12749
12812
  migrations: [