@tinycloud/node-sdk 2.1.0-beta.1 → 2.1.0-beta.2

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
@@ -17172,7 +17172,8 @@ import {
17172
17172
  SessionExpiredError,
17173
17173
  expandActionShortNames,
17174
17174
  isCapabilitySubset,
17175
- parseRecapCapabilities
17175
+ parseRecapCapabilities,
17176
+ SERVICE_LONG_TO_SHORT
17176
17177
  } from "@tinycloud/sdk-core";
17177
17178
 
17178
17179
  // src/authorization/NodeUserAuthorization.ts
@@ -17181,7 +17182,9 @@ import {
17181
17182
  submitHostDelegation,
17182
17183
  activateSessionWithHost,
17183
17184
  checkNodeInfo,
17184
- AutoApproveSpaceCreationHandler
17185
+ AutoApproveSpaceCreationHandler,
17186
+ manifestAbilitiesUnion,
17187
+ resolveManifest
17185
17188
  } from "@tinycloud/sdk-core";
17186
17189
 
17187
17190
  // src/authorization/strategies.ts
@@ -17321,8 +17324,25 @@ var NodeUserAuthorization = class {
17321
17324
  this.enablePublicSpace = config.enablePublicSpace ?? true;
17322
17325
  this.nonce = config.nonce;
17323
17326
  this.siweConfig = config.siweConfig;
17327
+ this._manifest = config.manifest;
17324
17328
  this.sessionManager = this.wasm.createSessionManager();
17325
17329
  }
17330
+ /**
17331
+ * Return the manifest currently driving sign-in behavior, or
17332
+ * `undefined` if none is set. Used by TinyCloudWeb/TinyCloudNode
17333
+ * internals to surface the manifest for requestPermissions flows
17334
+ * without forcing the caller to track it separately.
17335
+ */
17336
+ get manifest() {
17337
+ return this._manifest;
17338
+ }
17339
+ /**
17340
+ * Install or replace the stored manifest. Takes effect on the next
17341
+ * `signIn()` call — the current session (if any) is not touched.
17342
+ */
17343
+ setManifest(manifest) {
17344
+ this._manifest = manifest;
17345
+ }
17326
17346
  /**
17327
17347
  * The current active session (web-core compatible).
17328
17348
  */
@@ -17339,6 +17359,39 @@ var NodeUserAuthorization = class {
17339
17359
  get nodeFeatures() {
17340
17360
  return this._nodeFeatures;
17341
17361
  }
17362
+ /**
17363
+ * Compute the `abilities` map the WASM `prepareSession` call should
17364
+ * see at sign-in time.
17365
+ *
17366
+ * When a manifest is installed, we resolve it and union together:
17367
+ * - the app's own `resources` (what it needs at runtime)
17368
+ * - every `additionalDelegates[*].permissions` list (what it will
17369
+ * re-delegate to other DIDs post sign-in)
17370
+ *
17371
+ * into the short-service / path / full-URN-actions shape the WASM
17372
+ * layer expects. This is the key invariant that lets
17373
+ * {@link TinyCloudNode.delegateTo} issue manifest-declared
17374
+ * delegations via the session key (no wallet prompt): the session's
17375
+ * own recap already covers every action those delegations need.
17376
+ *
17377
+ * When no manifest is installed, we fall back to the
17378
+ * {@link defaultActions} table so existing callers see no change.
17379
+ *
17380
+ * This is a pure function of `this._manifest` + `this.defaultActions`
17381
+ * — the manifest resolution performs no I/O and throws a
17382
+ * {@link ManifestValidationError} on structural problems (missing
17383
+ * id/name, unparseable expiry, etc), which will surface at sign-in
17384
+ * rather than being silently swallowed.
17385
+ *
17386
+ * @internal
17387
+ */
17388
+ resolveSignInAbilities() {
17389
+ if (this._manifest === void 0) {
17390
+ return this.defaultActions;
17391
+ }
17392
+ const resolved = resolveManifest(this._manifest);
17393
+ return manifestAbilitiesUnion(resolved);
17394
+ }
17342
17395
  /**
17343
17396
  * Build SIWE overrides from the top-level nonce and siweConfig.
17344
17397
  * - Top-level `nonce` is seeded first so `siweConfig.nonce` wins if both are set.
@@ -17542,7 +17595,7 @@ var NodeUserAuthorization = class {
17542
17595
  const now = /* @__PURE__ */ new Date();
17543
17596
  const expirationTime = new Date(now.getTime() + this.sessionExpirationMs);
17544
17597
  const prepared = this.wasm.prepareSession({
17545
- abilities: this.defaultActions,
17598
+ abilities: this.resolveSignInAbilities(),
17546
17599
  address,
17547
17600
  chainId,
17548
17601
  domain: this.domain,
@@ -17685,7 +17738,7 @@ var NodeUserAuthorization = class {
17685
17738
  const now = /* @__PURE__ */ new Date();
17686
17739
  const expirationTime = new Date(now.getTime() + this.sessionExpirationMs);
17687
17740
  const prepared = this.wasm.prepareSession({
17688
- abilities: this.defaultActions,
17741
+ abilities: this.resolveSignInAbilities(),
17689
17742
  address,
17690
17743
  chainId,
17691
17744
  domain: this.domain,
@@ -18200,12 +18253,35 @@ var _TinyCloudNode = class _TinyCloudNode {
18200
18253
  enablePublicSpace: config.enablePublicSpace ?? true,
18201
18254
  spaceCreationHandler: config.spaceCreationHandler,
18202
18255
  nonce: config.nonce,
18203
- siweConfig: config.siweConfig
18256
+ siweConfig: config.siweConfig,
18257
+ manifest: config.manifest
18204
18258
  });
18205
18259
  this.tc = new TinyCloud(this.auth, {
18206
18260
  invokeAny: this.wasmBindings.invokeAny
18207
18261
  });
18208
18262
  }
18263
+ /**
18264
+ * Install or replace the manifest that drives the SIWE recap at
18265
+ * sign-in. Takes effect on the next `signIn()` call — the current
18266
+ * session (if any) is not touched. Wire this up from a higher
18267
+ * layer (e.g. TinyCloudWeb.setManifest) so the manifest is kept
18268
+ * in sync across the stack.
18269
+ */
18270
+ setManifest(manifest) {
18271
+ if (!this.auth) {
18272
+ throw new Error(
18273
+ "setManifest requires wallet mode. Provide a signer or privateKey in the TinyCloudNode config."
18274
+ );
18275
+ }
18276
+ this.auth.setManifest(manifest);
18277
+ }
18278
+ /**
18279
+ * Return the manifest currently installed on the auth handler,
18280
+ * or `undefined` if none is set.
18281
+ */
18282
+ get manifest() {
18283
+ return this.auth?.manifest;
18284
+ }
18209
18285
  /**
18210
18286
  * Get the primary identity DID for this user.
18211
18287
  * - If wallet connected and signed in: returns PKH DID (did:pkh:eip155:{chainId}:{address})
@@ -18712,7 +18788,19 @@ var _TinyCloudNode = class _TinyCloudNode {
18712
18788
  }
18713
18789
  /**
18714
18790
  * Wrapper for the WASM createDelegation function.
18715
- * Adapts the WASM interface to what SharingService expects.
18791
+ *
18792
+ * The WASM call now takes a multi-resource `abilities` map
18793
+ * (matching `prepareSession`'s shape) and emits ONE UCAN that
18794
+ * covers every `(service, path, actions)` entry. We mirror the raw
18795
+ * result back through `CreateDelegationWasmResult`, converting the
18796
+ * seconds-since-epoch `expiry` to a Date and normalizing the
18797
+ * `delegateDid` → `delegateDID` case.
18798
+ *
18799
+ * Both SharingService (single-entry) and
18800
+ * {@link TinyCloudNode.delegateTo} (multi-entry) drive this through
18801
+ * the same code path so there's exactly one place that touches the
18802
+ * WASM boundary.
18803
+ *
18716
18804
  * @internal
18717
18805
  */
18718
18806
  createDelegationWrapper(params) {
@@ -18727,18 +18815,19 @@ var _TinyCloudNode = class _TinyCloudNode {
18727
18815
  wasmSession,
18728
18816
  params.delegateDID,
18729
18817
  params.spaceId,
18730
- params.path,
18731
- params.actions,
18818
+ params.abilities,
18732
18819
  params.expirationSecs,
18733
18820
  params.notBeforeSecs
18734
18821
  );
18735
18822
  return {
18736
18823
  delegation: result.delegation,
18737
18824
  cid: result.cid,
18738
- delegateDID: result.delegateDid,
18739
- path: result.path,
18740
- actions: result.actions,
18741
- expiry: new Date(result.expiry * 1e3)
18825
+ // Rust serde `rename_all = "camelCase"` emits `delegateDid`
18826
+ // (lowercase d); the TypeScript interface uses `delegateDID`
18827
+ // (historical, matches Delegation.delegateDID). Normalize here.
18828
+ delegateDID: result.delegateDid ?? result.delegateDID,
18829
+ expiry: new Date(result.expiry * 1e3),
18830
+ resources: result.resources
18742
18831
  };
18743
18832
  }
18744
18833
  /**
@@ -19213,24 +19302,38 @@ var _TinyCloudNode = class _TinyCloudNode {
19213
19302
  /**
19214
19303
  * Issue a delegation using the capability-chain flow.
19215
19304
  *
19216
- * When the requested permissions are a subset of the current session's
19217
- * recap, the delegation is signed by the session key via WASM — no wallet
19218
- * prompt. When they are not, a {@link PermissionNotInManifestError} is
19219
- * raised so the caller can trigger an escalation flow (e.g.
19220
- * `TinyCloudWeb.requestPermissions`). Passing `forceWalletSign: true`
19221
- * bypasses the derivability check and always uses the wallet-signed SIWE
19222
- * path used by the legacy `createDelegation` fallback and by callers
19223
- * that want explicit wallet confirmation.
19305
+ * When every requested permission is a subset of the current
19306
+ * session's recap, the delegation is signed by the session key via
19307
+ * WASM — no wallet prompt. When at least one is NOT derivable, a
19308
+ * {@link PermissionNotInManifestError} is raised (carrying the
19309
+ * missing entries) so the caller can trigger an escalation flow
19310
+ * (e.g. `TinyCloudWeb.requestPermissions`). Passing
19311
+ * `forceWalletSign: true` bypasses the derivability check and
19312
+ * always uses the wallet-signed SIWE path — used by the legacy
19313
+ * `createDelegation` fallback and by callers that want explicit
19314
+ * wallet confirmation.
19224
19315
  *
19225
- * Current limitation: exactly one {@link PermissionEntry} per call. For
19226
- * multi-resource delegation, call `delegateTo` multiple times. This keeps
19227
- * each delegation a single `(spaceId, path)` grant, which matches the
19228
- * underlying `PortableDelegation` shape.
19316
+ * Multi-entry delegations are now emitted as **one** signed UCAN:
19317
+ * the underlying WASM `createDelegation` takes a full
19318
+ * `HashMap<Service, HashMap<Path, Vec<Ability>>>` abilities map
19319
+ * and produces a single attenuation carrying every
19320
+ * `(service, path, actions)` entry. The returned
19321
+ * {@link DelegateToResult.delegation} is that single blob, and
19322
+ * apps can POST it to their backend exactly like a single-entry
19323
+ * delegation (the server verifies all granted resources from one
19324
+ * UCAN).
19229
19325
  *
19230
- * @throws {@link SessionExpiredError} when there is no session or the
19231
- * current session has expired (or will within the 60s safety margin).
19232
- * @throws {@link PermissionNotInManifestError} when the requested entries
19233
- * are not a subset of the granted session capabilities and
19326
+ * For single-entry requests the `PortableDelegation.path` and
19327
+ * `.actions` fields mirror the one granted entry. For
19328
+ * multi-entry requests they mirror the **first** entry (stable
19329
+ * lexicographic order from the Rust side); consumers that need
19330
+ * the full picture read `PortableDelegation.resources`.
19331
+ *
19332
+ * @throws {@link SessionExpiredError} when there is no session or
19333
+ * the current session has expired (or will within the 60s
19334
+ * safety margin).
19335
+ * @throws {@link PermissionNotInManifestError} when any requested
19336
+ * entry is not a subset of the granted session capabilities and
19234
19337
  * `forceWalletSign` is not set.
19235
19338
  */
19236
19339
  async delegateTo(did, permissions, options) {
@@ -19251,16 +19354,10 @@ var _TinyCloudNode = class _TinyCloudNode {
19251
19354
  "delegateTo requires a non-empty permissions array"
19252
19355
  );
19253
19356
  }
19254
- if (permissions.length > 1) {
19255
- throw new Error(
19256
- "delegateTo currently supports one permission entry per call. Call delegateTo multiple times for multi-resource delegation."
19257
- );
19258
- }
19259
- const entry = permissions[0];
19260
- const expandedEntry = {
19357
+ const expandedEntries = permissions.map((entry) => ({
19261
19358
  ...entry,
19262
19359
  actions: expandActionShortNames(entry.service, entry.actions)
19263
- };
19360
+ }));
19264
19361
  const now = /* @__PURE__ */ new Date();
19265
19362
  const expiryMs = resolveExpiryMs(options?.expiry);
19266
19363
  const expirationTime = new Date(now.getTime() + expiryMs);
@@ -19269,9 +19366,14 @@ var _TinyCloudNode = class _TinyCloudNode {
19269
19366
  effectiveExpiration = sessionExpiry;
19270
19367
  }
19271
19368
  if (options?.forceWalletSign) {
19369
+ if (expandedEntries.length > 1) {
19370
+ throw new Error(
19371
+ "delegateTo with forceWalletSign=true supports at most one PermissionEntry. Multi-entry requests must go through the session-key UCAN path (drop forceWalletSign) or the legacy createDelegation method."
19372
+ );
19373
+ }
19272
19374
  const delegation2 = await this.createDelegationLegacyWalletPath(
19273
19375
  did,
19274
- expandedEntry,
19376
+ expandedEntries[0],
19275
19377
  effectiveExpiration
19276
19378
  );
19277
19379
  return { delegation: delegation2, prompted: true };
@@ -19280,14 +19382,13 @@ var _TinyCloudNode = class _TinyCloudNode {
19280
19382
  (siwe) => this.wasmBindings.parseRecapFromSiwe(siwe),
19281
19383
  session.siwe
19282
19384
  );
19283
- const requested = [expandedEntry];
19284
- const { subset, missing } = isCapabilitySubset(requested, granted);
19385
+ const { subset, missing } = isCapabilitySubset(expandedEntries, granted);
19285
19386
  if (!subset) {
19286
19387
  throw new PermissionNotInManifestError(missing, granted);
19287
19388
  }
19288
19389
  const delegation = await this.createDelegationViaWasmPath(
19289
19390
  did,
19290
- expandedEntry,
19391
+ expandedEntries,
19291
19392
  effectiveExpiration,
19292
19393
  session
19293
19394
  );
@@ -19296,14 +19397,60 @@ var _TinyCloudNode = class _TinyCloudNode {
19296
19397
  /**
19297
19398
  * Issue a delegation via the session-key UCAN WASM path.
19298
19399
  *
19299
- * The caller has already verified the request is derivable from the
19300
- * current session; we just need to shape the inputs for
19301
- * {@link createDelegationWrapper}.
19400
+ * The caller has already verified every entry is derivable from
19401
+ * the current session; we build one multi-resource abilities map
19402
+ * and emit one signed UCAN covering them all.
19403
+ *
19404
+ * All entries must share the same target space (the UCAN is
19405
+ * scoped to a single space). If they don't, this throws — mixing
19406
+ * spaces in a single delegation is not supported by the underlying
19407
+ * Rust create_delegation call and the resulting UCAN would be
19408
+ * under-specified.
19302
19409
  *
19303
19410
  * @internal
19304
19411
  */
19305
- async createDelegationViaWasmPath(did, entry, expirationTime, session) {
19306
- const spaceId = entry.space === "default" ? session.spaceId : entry.space;
19412
+ async createDelegationViaWasmPath(did, entries, expirationTime, session) {
19413
+ if (entries.length === 0) {
19414
+ throw new Error(
19415
+ "createDelegationViaWasmPath requires a non-empty entries array"
19416
+ );
19417
+ }
19418
+ const resolvedSpaces = /* @__PURE__ */ new Set();
19419
+ for (const entry of entries) {
19420
+ const spaceId2 = entry.space === "default" ? session.spaceId : entry.space;
19421
+ resolvedSpaces.add(spaceId2);
19422
+ }
19423
+ if (resolvedSpaces.size !== 1) {
19424
+ throw new Error(
19425
+ `delegateTo: all permission entries must target the same space, got ${resolvedSpaces.size}: ${JSON.stringify([...resolvedSpaces])}`
19426
+ );
19427
+ }
19428
+ const spaceId = [...resolvedSpaces][0];
19429
+ const abilities = {};
19430
+ for (const entry of entries) {
19431
+ const shortService = SERVICE_LONG_TO_SHORT[entry.service];
19432
+ if (shortService === void 0) {
19433
+ throw new Error(
19434
+ `delegateTo: unknown service '${entry.service}' \u2014 no short-form mapping`
19435
+ );
19436
+ }
19437
+ if (abilities[shortService] === void 0) {
19438
+ abilities[shortService] = {};
19439
+ }
19440
+ const pathsMap = abilities[shortService];
19441
+ const existing = pathsMap[entry.path];
19442
+ if (existing === void 0) {
19443
+ pathsMap[entry.path] = [...entry.actions];
19444
+ } else {
19445
+ const seen = new Set(existing);
19446
+ for (const action of entry.actions) {
19447
+ if (!seen.has(action)) {
19448
+ existing.push(action);
19449
+ seen.add(action);
19450
+ }
19451
+ }
19452
+ }
19453
+ }
19307
19454
  const serviceSession = {
19308
19455
  delegationHeader: session.delegationHeader,
19309
19456
  delegationCid: session.delegationCid,
@@ -19316,16 +19463,17 @@ var _TinyCloudNode = class _TinyCloudNode {
19316
19463
  session: serviceSession,
19317
19464
  delegateDID: did,
19318
19465
  spaceId,
19319
- path: entry.path,
19320
- actions: entry.actions,
19466
+ abilities,
19321
19467
  expirationSecs
19322
19468
  });
19469
+ const primary = result.resources[0];
19323
19470
  return {
19324
19471
  cid: result.cid,
19325
19472
  delegationHeader: { Authorization: `Bearer ${result.delegation}` },
19326
19473
  spaceId,
19327
- path: entry.path,
19328
- actions: entry.actions,
19474
+ path: primary.path,
19475
+ actions: primary.actions,
19476
+ resources: result.resources,
19329
19477
  disableSubDelegation: false,
19330
19478
  expiry: result.expiry,
19331
19479
  delegateDID: did,
@@ -19381,19 +19529,17 @@ var _TinyCloudNode = class _TinyCloudNode {
19381
19529
  params.path,
19382
19530
  params.spaceIdOverride
19383
19531
  );
19384
- if (entries.length === 1) {
19385
- try {
19386
- const result = await this.delegateTo(
19387
- resolvedDelegateDID,
19388
- [entries[0]],
19389
- params.expiryMs !== void 0 ? { expiry: params.expiryMs } : void 0
19390
- );
19391
- return result.delegation;
19392
- } catch (err) {
19393
- if (err instanceof PermissionNotInManifestError) {
19394
- } else {
19395
- throw err;
19396
- }
19532
+ try {
19533
+ const result = await this.delegateTo(
19534
+ resolvedDelegateDID,
19535
+ entries,
19536
+ params.expiryMs !== void 0 ? { expiry: params.expiryMs } : void 0
19537
+ );
19538
+ return result.delegation;
19539
+ } catch (err) {
19540
+ if (err instanceof PermissionNotInManifestError) {
19541
+ } else {
19542
+ throw err;
19397
19543
  }
19398
19544
  }
19399
19545
  return this.createDelegationWalletPath({
@@ -19855,7 +20001,7 @@ import {
19855
20001
  PermissionNotInManifestError as PermissionNotInManifestError2,
19856
20002
  SessionExpiredError as SessionExpiredError2,
19857
20003
  ManifestValidationError,
19858
- resolveManifest,
20004
+ resolveManifest as resolveManifest2,
19859
20005
  validateManifest,
19860
20006
  loadManifest,
19861
20007
  isCapabilitySubset as isCapabilitySubset2,
@@ -19976,7 +20122,7 @@ export {
19976
20122
  makePublicSpaceId2 as makePublicSpaceId,
19977
20123
  parseExpiry2 as parseExpiry,
19978
20124
  parseSpaceUri,
19979
- resolveManifest,
20125
+ resolveManifest2 as resolveManifest,
19980
20126
  serializeDelegation,
19981
20127
  validateManifest
19982
20128
  };