@tinycloud/node-sdk 2.1.0-beta.1 → 2.1.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
@@ -17327,8 +17327,25 @@ var NodeUserAuthorization = class {
17327
17327
  this.enablePublicSpace = config.enablePublicSpace ?? true;
17328
17328
  this.nonce = config.nonce;
17329
17329
  this.siweConfig = config.siweConfig;
17330
+ this._manifest = config.manifest;
17330
17331
  this.sessionManager = this.wasm.createSessionManager();
17331
17332
  }
17333
+ /**
17334
+ * Return the manifest currently driving sign-in behavior, or
17335
+ * `undefined` if none is set. Used by TinyCloudWeb/TinyCloudNode
17336
+ * internals to surface the manifest for requestPermissions flows
17337
+ * without forcing the caller to track it separately.
17338
+ */
17339
+ get manifest() {
17340
+ return this._manifest;
17341
+ }
17342
+ /**
17343
+ * Install or replace the stored manifest. Takes effect on the next
17344
+ * `signIn()` call — the current session (if any) is not touched.
17345
+ */
17346
+ setManifest(manifest) {
17347
+ this._manifest = manifest;
17348
+ }
17332
17349
  /**
17333
17350
  * The current active session (web-core compatible).
17334
17351
  */
@@ -17345,6 +17362,39 @@ var NodeUserAuthorization = class {
17345
17362
  get nodeFeatures() {
17346
17363
  return this._nodeFeatures;
17347
17364
  }
17365
+ /**
17366
+ * Compute the `abilities` map the WASM `prepareSession` call should
17367
+ * see at sign-in time.
17368
+ *
17369
+ * When a manifest is installed, we resolve it and union together:
17370
+ * - the app's own `resources` (what it needs at runtime)
17371
+ * - every `additionalDelegates[*].permissions` list (what it will
17372
+ * re-delegate to other DIDs post sign-in)
17373
+ *
17374
+ * into the short-service / path / full-URN-actions shape the WASM
17375
+ * layer expects. This is the key invariant that lets
17376
+ * {@link TinyCloudNode.delegateTo} issue manifest-declared
17377
+ * delegations via the session key (no wallet prompt): the session's
17378
+ * own recap already covers every action those delegations need.
17379
+ *
17380
+ * When no manifest is installed, we fall back to the
17381
+ * {@link defaultActions} table so existing callers see no change.
17382
+ *
17383
+ * This is a pure function of `this._manifest` + `this.defaultActions`
17384
+ * — the manifest resolution performs no I/O and throws a
17385
+ * {@link ManifestValidationError} on structural problems (missing
17386
+ * id/name, unparseable expiry, etc), which will surface at sign-in
17387
+ * rather than being silently swallowed.
17388
+ *
17389
+ * @internal
17390
+ */
17391
+ resolveSignInAbilities() {
17392
+ if (this._manifest === void 0) {
17393
+ return this.defaultActions;
17394
+ }
17395
+ const resolved = (0, import_sdk_core.resolveManifest)(this._manifest);
17396
+ return (0, import_sdk_core.manifestAbilitiesUnion)(resolved);
17397
+ }
17348
17398
  /**
17349
17399
  * Build SIWE overrides from the top-level nonce and siweConfig.
17350
17400
  * - Top-level `nonce` is seeded first so `siweConfig.nonce` wins if both are set.
@@ -17548,7 +17598,7 @@ var NodeUserAuthorization = class {
17548
17598
  const now = /* @__PURE__ */ new Date();
17549
17599
  const expirationTime = new Date(now.getTime() + this.sessionExpirationMs);
17550
17600
  const prepared = this.wasm.prepareSession({
17551
- abilities: this.defaultActions,
17601
+ abilities: this.resolveSignInAbilities(),
17552
17602
  address,
17553
17603
  chainId,
17554
17604
  domain: this.domain,
@@ -17691,7 +17741,7 @@ var NodeUserAuthorization = class {
17691
17741
  const now = /* @__PURE__ */ new Date();
17692
17742
  const expirationTime = new Date(now.getTime() + this.sessionExpirationMs);
17693
17743
  const prepared = this.wasm.prepareSession({
17694
- abilities: this.defaultActions,
17744
+ abilities: this.resolveSignInAbilities(),
17695
17745
  address,
17696
17746
  chainId,
17697
17747
  domain: this.domain,
@@ -18197,12 +18247,35 @@ var _TinyCloudNode = class _TinyCloudNode {
18197
18247
  enablePublicSpace: config.enablePublicSpace ?? true,
18198
18248
  spaceCreationHandler: config.spaceCreationHandler,
18199
18249
  nonce: config.nonce,
18200
- siweConfig: config.siweConfig
18250
+ siweConfig: config.siweConfig,
18251
+ manifest: config.manifest
18201
18252
  });
18202
18253
  this.tc = new import_sdk_core4.TinyCloud(this.auth, {
18203
18254
  invokeAny: this.wasmBindings.invokeAny
18204
18255
  });
18205
18256
  }
18257
+ /**
18258
+ * Install or replace the manifest that drives the SIWE recap at
18259
+ * sign-in. Takes effect on the next `signIn()` call — the current
18260
+ * session (if any) is not touched. Wire this up from a higher
18261
+ * layer (e.g. TinyCloudWeb.setManifest) so the manifest is kept
18262
+ * in sync across the stack.
18263
+ */
18264
+ setManifest(manifest) {
18265
+ if (!this.auth) {
18266
+ throw new Error(
18267
+ "setManifest requires wallet mode. Provide a signer or privateKey in the TinyCloudNode config."
18268
+ );
18269
+ }
18270
+ this.auth.setManifest(manifest);
18271
+ }
18272
+ /**
18273
+ * Return the manifest currently installed on the auth handler,
18274
+ * or `undefined` if none is set.
18275
+ */
18276
+ get manifest() {
18277
+ return this.auth?.manifest;
18278
+ }
18206
18279
  /**
18207
18280
  * Get the primary identity DID for this user.
18208
18281
  * - If wallet connected and signed in: returns PKH DID (did:pkh:eip155:{chainId}:{address})
@@ -18709,7 +18782,19 @@ var _TinyCloudNode = class _TinyCloudNode {
18709
18782
  }
18710
18783
  /**
18711
18784
  * Wrapper for the WASM createDelegation function.
18712
- * Adapts the WASM interface to what SharingService expects.
18785
+ *
18786
+ * The WASM call now takes a multi-resource `abilities` map
18787
+ * (matching `prepareSession`'s shape) and emits ONE UCAN that
18788
+ * covers every `(service, path, actions)` entry. We mirror the raw
18789
+ * result back through `CreateDelegationWasmResult`, converting the
18790
+ * seconds-since-epoch `expiry` to a Date and normalizing the
18791
+ * `delegateDid` → `delegateDID` case.
18792
+ *
18793
+ * Both SharingService (single-entry) and
18794
+ * {@link TinyCloudNode.delegateTo} (multi-entry) drive this through
18795
+ * the same code path so there's exactly one place that touches the
18796
+ * WASM boundary.
18797
+ *
18713
18798
  * @internal
18714
18799
  */
18715
18800
  createDelegationWrapper(params) {
@@ -18724,18 +18809,19 @@ var _TinyCloudNode = class _TinyCloudNode {
18724
18809
  wasmSession,
18725
18810
  params.delegateDID,
18726
18811
  params.spaceId,
18727
- params.path,
18728
- params.actions,
18812
+ params.abilities,
18729
18813
  params.expirationSecs,
18730
18814
  params.notBeforeSecs
18731
18815
  );
18732
18816
  return {
18733
18817
  delegation: result.delegation,
18734
18818
  cid: result.cid,
18735
- delegateDID: result.delegateDid,
18736
- path: result.path,
18737
- actions: result.actions,
18738
- expiry: new Date(result.expiry * 1e3)
18819
+ // Rust serde `rename_all = "camelCase"` emits `delegateDid`
18820
+ // (lowercase d); the TypeScript interface uses `delegateDID`
18821
+ // (historical, matches Delegation.delegateDID). Normalize here.
18822
+ delegateDID: result.delegateDid ?? result.delegateDID,
18823
+ expiry: new Date(result.expiry * 1e3),
18824
+ resources: result.resources
18739
18825
  };
18740
18826
  }
18741
18827
  /**
@@ -19210,24 +19296,38 @@ var _TinyCloudNode = class _TinyCloudNode {
19210
19296
  /**
19211
19297
  * Issue a delegation using the capability-chain flow.
19212
19298
  *
19213
- * When the requested permissions are a subset of the current session's
19214
- * recap, the delegation is signed by the session key via WASM — no wallet
19215
- * prompt. When they are not, a {@link PermissionNotInManifestError} is
19216
- * raised so the caller can trigger an escalation flow (e.g.
19217
- * `TinyCloudWeb.requestPermissions`). Passing `forceWalletSign: true`
19218
- * bypasses the derivability check and always uses the wallet-signed SIWE
19219
- * path used by the legacy `createDelegation` fallback and by callers
19220
- * that want explicit wallet confirmation.
19299
+ * When every requested permission is a subset of the current
19300
+ * session's recap, the delegation is signed by the session key via
19301
+ * WASM — no wallet prompt. When at least one is NOT derivable, a
19302
+ * {@link PermissionNotInManifestError} is raised (carrying the
19303
+ * missing entries) so the caller can trigger an escalation flow
19304
+ * (e.g. `TinyCloudWeb.requestPermissions`). Passing
19305
+ * `forceWalletSign: true` bypasses the derivability check and
19306
+ * always uses the wallet-signed SIWE path — used by the legacy
19307
+ * `createDelegation` fallback and by callers that want explicit
19308
+ * wallet confirmation.
19221
19309
  *
19222
- * Current limitation: exactly one {@link PermissionEntry} per call. For
19223
- * multi-resource delegation, call `delegateTo` multiple times. This keeps
19224
- * each delegation a single `(spaceId, path)` grant, which matches the
19225
- * underlying `PortableDelegation` shape.
19310
+ * Multi-entry delegations are now emitted as **one** signed UCAN:
19311
+ * the underlying WASM `createDelegation` takes a full
19312
+ * `HashMap<Service, HashMap<Path, Vec<Ability>>>` abilities map
19313
+ * and produces a single attenuation carrying every
19314
+ * `(service, path, actions)` entry. The returned
19315
+ * {@link DelegateToResult.delegation} is that single blob, and
19316
+ * apps can POST it to their backend exactly like a single-entry
19317
+ * delegation (the server verifies all granted resources from one
19318
+ * UCAN).
19226
19319
  *
19227
- * @throws {@link SessionExpiredError} when there is no session or the
19228
- * current session has expired (or will within the 60s safety margin).
19229
- * @throws {@link PermissionNotInManifestError} when the requested entries
19230
- * are not a subset of the granted session capabilities and
19320
+ * For single-entry requests the `PortableDelegation.path` and
19321
+ * `.actions` fields mirror the one granted entry. For
19322
+ * multi-entry requests they mirror the **first** entry (stable
19323
+ * lexicographic order from the Rust side); consumers that need
19324
+ * the full picture read `PortableDelegation.resources`.
19325
+ *
19326
+ * @throws {@link SessionExpiredError} when there is no session or
19327
+ * the current session has expired (or will within the 60s
19328
+ * safety margin).
19329
+ * @throws {@link PermissionNotInManifestError} when any requested
19330
+ * entry is not a subset of the granted session capabilities and
19231
19331
  * `forceWalletSign` is not set.
19232
19332
  */
19233
19333
  async delegateTo(did, permissions, options) {
@@ -19248,16 +19348,10 @@ var _TinyCloudNode = class _TinyCloudNode {
19248
19348
  "delegateTo requires a non-empty permissions array"
19249
19349
  );
19250
19350
  }
19251
- if (permissions.length > 1) {
19252
- throw new Error(
19253
- "delegateTo currently supports one permission entry per call. Call delegateTo multiple times for multi-resource delegation."
19254
- );
19255
- }
19256
- const entry = permissions[0];
19257
- const expandedEntry = {
19351
+ const expandedEntries = permissions.map((entry) => ({
19258
19352
  ...entry,
19259
19353
  actions: (0, import_sdk_core4.expandActionShortNames)(entry.service, entry.actions)
19260
- };
19354
+ }));
19261
19355
  const now = /* @__PURE__ */ new Date();
19262
19356
  const expiryMs = resolveExpiryMs(options?.expiry);
19263
19357
  const expirationTime = new Date(now.getTime() + expiryMs);
@@ -19266,9 +19360,14 @@ var _TinyCloudNode = class _TinyCloudNode {
19266
19360
  effectiveExpiration = sessionExpiry;
19267
19361
  }
19268
19362
  if (options?.forceWalletSign) {
19363
+ if (expandedEntries.length > 1) {
19364
+ throw new Error(
19365
+ "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."
19366
+ );
19367
+ }
19269
19368
  const delegation2 = await this.createDelegationLegacyWalletPath(
19270
19369
  did,
19271
- expandedEntry,
19370
+ expandedEntries[0],
19272
19371
  effectiveExpiration
19273
19372
  );
19274
19373
  return { delegation: delegation2, prompted: true };
@@ -19277,14 +19376,13 @@ var _TinyCloudNode = class _TinyCloudNode {
19277
19376
  (siwe) => this.wasmBindings.parseRecapFromSiwe(siwe),
19278
19377
  session.siwe
19279
19378
  );
19280
- const requested = [expandedEntry];
19281
- const { subset, missing } = (0, import_sdk_core4.isCapabilitySubset)(requested, granted);
19379
+ const { subset, missing } = (0, import_sdk_core4.isCapabilitySubset)(expandedEntries, granted);
19282
19380
  if (!subset) {
19283
19381
  throw new import_sdk_core4.PermissionNotInManifestError(missing, granted);
19284
19382
  }
19285
19383
  const delegation = await this.createDelegationViaWasmPath(
19286
19384
  did,
19287
- expandedEntry,
19385
+ expandedEntries,
19288
19386
  effectiveExpiration,
19289
19387
  session
19290
19388
  );
@@ -19293,14 +19391,60 @@ var _TinyCloudNode = class _TinyCloudNode {
19293
19391
  /**
19294
19392
  * Issue a delegation via the session-key UCAN WASM path.
19295
19393
  *
19296
- * The caller has already verified the request is derivable from the
19297
- * current session; we just need to shape the inputs for
19298
- * {@link createDelegationWrapper}.
19394
+ * The caller has already verified every entry is derivable from
19395
+ * the current session; we build one multi-resource abilities map
19396
+ * and emit one signed UCAN covering them all.
19397
+ *
19398
+ * All entries must share the same target space (the UCAN is
19399
+ * scoped to a single space). If they don't, this throws — mixing
19400
+ * spaces in a single delegation is not supported by the underlying
19401
+ * Rust create_delegation call and the resulting UCAN would be
19402
+ * under-specified.
19299
19403
  *
19300
19404
  * @internal
19301
19405
  */
19302
- async createDelegationViaWasmPath(did, entry, expirationTime, session) {
19303
- const spaceId = entry.space === "default" ? session.spaceId : entry.space;
19406
+ async createDelegationViaWasmPath(did, entries, expirationTime, session) {
19407
+ if (entries.length === 0) {
19408
+ throw new Error(
19409
+ "createDelegationViaWasmPath requires a non-empty entries array"
19410
+ );
19411
+ }
19412
+ const resolvedSpaces = /* @__PURE__ */ new Set();
19413
+ for (const entry of entries) {
19414
+ const spaceId2 = entry.space === "default" ? session.spaceId : entry.space;
19415
+ resolvedSpaces.add(spaceId2);
19416
+ }
19417
+ if (resolvedSpaces.size !== 1) {
19418
+ throw new Error(
19419
+ `delegateTo: all permission entries must target the same space, got ${resolvedSpaces.size}: ${JSON.stringify([...resolvedSpaces])}`
19420
+ );
19421
+ }
19422
+ const spaceId = [...resolvedSpaces][0];
19423
+ const abilities = {};
19424
+ for (const entry of entries) {
19425
+ const shortService = import_sdk_core4.SERVICE_LONG_TO_SHORT[entry.service];
19426
+ if (shortService === void 0) {
19427
+ throw new Error(
19428
+ `delegateTo: unknown service '${entry.service}' \u2014 no short-form mapping`
19429
+ );
19430
+ }
19431
+ if (abilities[shortService] === void 0) {
19432
+ abilities[shortService] = {};
19433
+ }
19434
+ const pathsMap = abilities[shortService];
19435
+ const existing = pathsMap[entry.path];
19436
+ if (existing === void 0) {
19437
+ pathsMap[entry.path] = [...entry.actions];
19438
+ } else {
19439
+ const seen = new Set(existing);
19440
+ for (const action of entry.actions) {
19441
+ if (!seen.has(action)) {
19442
+ existing.push(action);
19443
+ seen.add(action);
19444
+ }
19445
+ }
19446
+ }
19447
+ }
19304
19448
  const serviceSession = {
19305
19449
  delegationHeader: session.delegationHeader,
19306
19450
  delegationCid: session.delegationCid,
@@ -19313,16 +19457,17 @@ var _TinyCloudNode = class _TinyCloudNode {
19313
19457
  session: serviceSession,
19314
19458
  delegateDID: did,
19315
19459
  spaceId,
19316
- path: entry.path,
19317
- actions: entry.actions,
19460
+ abilities,
19318
19461
  expirationSecs
19319
19462
  });
19463
+ const primary = result.resources[0];
19320
19464
  return {
19321
19465
  cid: result.cid,
19322
19466
  delegationHeader: { Authorization: `Bearer ${result.delegation}` },
19323
19467
  spaceId,
19324
- path: entry.path,
19325
- actions: entry.actions,
19468
+ path: primary.path,
19469
+ actions: primary.actions,
19470
+ resources: result.resources,
19326
19471
  disableSubDelegation: false,
19327
19472
  expiry: result.expiry,
19328
19473
  delegateDID: did,
@@ -19378,19 +19523,17 @@ var _TinyCloudNode = class _TinyCloudNode {
19378
19523
  params.path,
19379
19524
  params.spaceIdOverride
19380
19525
  );
19381
- if (entries.length === 1) {
19382
- try {
19383
- const result = await this.delegateTo(
19384
- resolvedDelegateDID,
19385
- [entries[0]],
19386
- params.expiryMs !== void 0 ? { expiry: params.expiryMs } : void 0
19387
- );
19388
- return result.delegation;
19389
- } catch (err) {
19390
- if (err instanceof import_sdk_core4.PermissionNotInManifestError) {
19391
- } else {
19392
- throw err;
19393
- }
19526
+ try {
19527
+ const result = await this.delegateTo(
19528
+ resolvedDelegateDID,
19529
+ entries,
19530
+ params.expiryMs !== void 0 ? { expiry: params.expiryMs } : void 0
19531
+ );
19532
+ return result.delegation;
19533
+ } catch (err) {
19534
+ if (err instanceof import_sdk_core4.PermissionNotInManifestError) {
19535
+ } else {
19536
+ throw err;
19394
19537
  }
19395
19538
  }
19396
19539
  return this.createDelegationWalletPath({