@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/core.js CHANGED
@@ -203,7 +203,9 @@ import {
203
203
  submitHostDelegation,
204
204
  activateSessionWithHost,
205
205
  checkNodeInfo,
206
- AutoApproveSpaceCreationHandler
206
+ AutoApproveSpaceCreationHandler,
207
+ manifestAbilitiesUnion,
208
+ resolveManifest
207
209
  } from "@tinycloud/sdk-core";
208
210
 
209
211
  // src/authorization/strategies.ts
@@ -272,8 +274,25 @@ var NodeUserAuthorization = class {
272
274
  this.enablePublicSpace = config.enablePublicSpace ?? true;
273
275
  this.nonce = config.nonce;
274
276
  this.siweConfig = config.siweConfig;
277
+ this._manifest = config.manifest;
275
278
  this.sessionManager = this.wasm.createSessionManager();
276
279
  }
280
+ /**
281
+ * Return the manifest currently driving sign-in behavior, or
282
+ * `undefined` if none is set. Used by TinyCloudWeb/TinyCloudNode
283
+ * internals to surface the manifest for requestPermissions flows
284
+ * without forcing the caller to track it separately.
285
+ */
286
+ get manifest() {
287
+ return this._manifest;
288
+ }
289
+ /**
290
+ * Install or replace the stored manifest. Takes effect on the next
291
+ * `signIn()` call — the current session (if any) is not touched.
292
+ */
293
+ setManifest(manifest) {
294
+ this._manifest = manifest;
295
+ }
277
296
  /**
278
297
  * The current active session (web-core compatible).
279
298
  */
@@ -290,6 +309,39 @@ var NodeUserAuthorization = class {
290
309
  get nodeFeatures() {
291
310
  return this._nodeFeatures;
292
311
  }
312
+ /**
313
+ * Compute the `abilities` map the WASM `prepareSession` call should
314
+ * see at sign-in time.
315
+ *
316
+ * When a manifest is installed, we resolve it and union together:
317
+ * - the app's own `resources` (what it needs at runtime)
318
+ * - every `additionalDelegates[*].permissions` list (what it will
319
+ * re-delegate to other DIDs post sign-in)
320
+ *
321
+ * into the short-service / path / full-URN-actions shape the WASM
322
+ * layer expects. This is the key invariant that lets
323
+ * {@link TinyCloudNode.delegateTo} issue manifest-declared
324
+ * delegations via the session key (no wallet prompt): the session's
325
+ * own recap already covers every action those delegations need.
326
+ *
327
+ * When no manifest is installed, we fall back to the
328
+ * {@link defaultActions} table so existing callers see no change.
329
+ *
330
+ * This is a pure function of `this._manifest` + `this.defaultActions`
331
+ * — the manifest resolution performs no I/O and throws a
332
+ * {@link ManifestValidationError} on structural problems (missing
333
+ * id/name, unparseable expiry, etc), which will surface at sign-in
334
+ * rather than being silently swallowed.
335
+ *
336
+ * @internal
337
+ */
338
+ resolveSignInAbilities() {
339
+ if (this._manifest === void 0) {
340
+ return this.defaultActions;
341
+ }
342
+ const resolved = resolveManifest(this._manifest);
343
+ return manifestAbilitiesUnion(resolved);
344
+ }
293
345
  /**
294
346
  * Build SIWE overrides from the top-level nonce and siweConfig.
295
347
  * - Top-level `nonce` is seeded first so `siweConfig.nonce` wins if both are set.
@@ -493,7 +545,7 @@ var NodeUserAuthorization = class {
493
545
  const now = /* @__PURE__ */ new Date();
494
546
  const expirationTime = new Date(now.getTime() + this.sessionExpirationMs);
495
547
  const prepared = this.wasm.prepareSession({
496
- abilities: this.defaultActions,
548
+ abilities: this.resolveSignInAbilities(),
497
549
  address,
498
550
  chainId,
499
551
  domain: this.domain,
@@ -636,7 +688,7 @@ var NodeUserAuthorization = class {
636
688
  const now = /* @__PURE__ */ new Date();
637
689
  const expirationTime = new Date(now.getTime() + this.sessionExpirationMs);
638
690
  const prepared = this.wasm.prepareSession({
639
- abilities: this.defaultActions,
691
+ abilities: this.resolveSignInAbilities(),
640
692
  address,
641
693
  chainId,
642
694
  domain: this.domain,
@@ -825,7 +877,8 @@ import {
825
877
  SessionExpiredError,
826
878
  expandActionShortNames,
827
879
  isCapabilitySubset,
828
- parseRecapCapabilities
880
+ parseRecapCapabilities,
881
+ SERVICE_LONG_TO_SHORT
829
882
  } from "@tinycloud/sdk-core";
830
883
 
831
884
  // src/DelegatedAccess.ts
@@ -1176,12 +1229,35 @@ var _TinyCloudNode = class _TinyCloudNode {
1176
1229
  enablePublicSpace: config.enablePublicSpace ?? true,
1177
1230
  spaceCreationHandler: config.spaceCreationHandler,
1178
1231
  nonce: config.nonce,
1179
- siweConfig: config.siweConfig
1232
+ siweConfig: config.siweConfig,
1233
+ manifest: config.manifest
1180
1234
  });
1181
1235
  this.tc = new TinyCloud(this.auth, {
1182
1236
  invokeAny: this.wasmBindings.invokeAny
1183
1237
  });
1184
1238
  }
1239
+ /**
1240
+ * Install or replace the manifest that drives the SIWE recap at
1241
+ * sign-in. Takes effect on the next `signIn()` call — the current
1242
+ * session (if any) is not touched. Wire this up from a higher
1243
+ * layer (e.g. TinyCloudWeb.setManifest) so the manifest is kept
1244
+ * in sync across the stack.
1245
+ */
1246
+ setManifest(manifest) {
1247
+ if (!this.auth) {
1248
+ throw new Error(
1249
+ "setManifest requires wallet mode. Provide a signer or privateKey in the TinyCloudNode config."
1250
+ );
1251
+ }
1252
+ this.auth.setManifest(manifest);
1253
+ }
1254
+ /**
1255
+ * Return the manifest currently installed on the auth handler,
1256
+ * or `undefined` if none is set.
1257
+ */
1258
+ get manifest() {
1259
+ return this.auth?.manifest;
1260
+ }
1185
1261
  /**
1186
1262
  * Get the primary identity DID for this user.
1187
1263
  * - If wallet connected and signed in: returns PKH DID (did:pkh:eip155:{chainId}:{address})
@@ -1688,7 +1764,19 @@ var _TinyCloudNode = class _TinyCloudNode {
1688
1764
  }
1689
1765
  /**
1690
1766
  * Wrapper for the WASM createDelegation function.
1691
- * Adapts the WASM interface to what SharingService expects.
1767
+ *
1768
+ * The WASM call now takes a multi-resource `abilities` map
1769
+ * (matching `prepareSession`'s shape) and emits ONE UCAN that
1770
+ * covers every `(service, path, actions)` entry. We mirror the raw
1771
+ * result back through `CreateDelegationWasmResult`, converting the
1772
+ * seconds-since-epoch `expiry` to a Date and normalizing the
1773
+ * `delegateDid` → `delegateDID` case.
1774
+ *
1775
+ * Both SharingService (single-entry) and
1776
+ * {@link TinyCloudNode.delegateTo} (multi-entry) drive this through
1777
+ * the same code path so there's exactly one place that touches the
1778
+ * WASM boundary.
1779
+ *
1692
1780
  * @internal
1693
1781
  */
1694
1782
  createDelegationWrapper(params) {
@@ -1703,18 +1791,19 @@ var _TinyCloudNode = class _TinyCloudNode {
1703
1791
  wasmSession,
1704
1792
  params.delegateDID,
1705
1793
  params.spaceId,
1706
- params.path,
1707
- params.actions,
1794
+ params.abilities,
1708
1795
  params.expirationSecs,
1709
1796
  params.notBeforeSecs
1710
1797
  );
1711
1798
  return {
1712
1799
  delegation: result.delegation,
1713
1800
  cid: result.cid,
1714
- delegateDID: result.delegateDid,
1715
- path: result.path,
1716
- actions: result.actions,
1717
- expiry: new Date(result.expiry * 1e3)
1801
+ // Rust serde `rename_all = "camelCase"` emits `delegateDid`
1802
+ // (lowercase d); the TypeScript interface uses `delegateDID`
1803
+ // (historical, matches Delegation.delegateDID). Normalize here.
1804
+ delegateDID: result.delegateDid ?? result.delegateDID,
1805
+ expiry: new Date(result.expiry * 1e3),
1806
+ resources: result.resources
1718
1807
  };
1719
1808
  }
1720
1809
  /**
@@ -2189,24 +2278,38 @@ var _TinyCloudNode = class _TinyCloudNode {
2189
2278
  /**
2190
2279
  * Issue a delegation using the capability-chain flow.
2191
2280
  *
2192
- * When the requested permissions are a subset of the current session's
2193
- * recap, the delegation is signed by the session key via WASM — no wallet
2194
- * prompt. When they are not, a {@link PermissionNotInManifestError} is
2195
- * raised so the caller can trigger an escalation flow (e.g.
2196
- * `TinyCloudWeb.requestPermissions`). Passing `forceWalletSign: true`
2197
- * bypasses the derivability check and always uses the wallet-signed SIWE
2198
- * path used by the legacy `createDelegation` fallback and by callers
2199
- * that want explicit wallet confirmation.
2200
- *
2201
- * Current limitation: exactly one {@link PermissionEntry} per call. For
2202
- * multi-resource delegation, call `delegateTo` multiple times. This keeps
2203
- * each delegation a single `(spaceId, path)` grant, which matches the
2204
- * underlying `PortableDelegation` shape.
2205
- *
2206
- * @throws {@link SessionExpiredError} when there is no session or the
2207
- * current session has expired (or will within the 60s safety margin).
2208
- * @throws {@link PermissionNotInManifestError} when the requested entries
2209
- * are not a subset of the granted session capabilities and
2281
+ * When every requested permission is a subset of the current
2282
+ * session's recap, the delegation is signed by the session key via
2283
+ * WASM — no wallet prompt. When at least one is NOT derivable, a
2284
+ * {@link PermissionNotInManifestError} is raised (carrying the
2285
+ * missing entries) so the caller can trigger an escalation flow
2286
+ * (e.g. `TinyCloudWeb.requestPermissions`). Passing
2287
+ * `forceWalletSign: true` bypasses the derivability check and
2288
+ * always uses the wallet-signed SIWE path — used by the legacy
2289
+ * `createDelegation` fallback and by callers that want explicit
2290
+ * wallet confirmation.
2291
+ *
2292
+ * Multi-entry delegations are now emitted as **one** signed UCAN:
2293
+ * the underlying WASM `createDelegation` takes a full
2294
+ * `HashMap<Service, HashMap<Path, Vec<Ability>>>` abilities map
2295
+ * and produces a single attenuation carrying every
2296
+ * `(service, path, actions)` entry. The returned
2297
+ * {@link DelegateToResult.delegation} is that single blob, and
2298
+ * apps can POST it to their backend exactly like a single-entry
2299
+ * delegation (the server verifies all granted resources from one
2300
+ * UCAN).
2301
+ *
2302
+ * For single-entry requests the `PortableDelegation.path` and
2303
+ * `.actions` fields mirror the one granted entry. For
2304
+ * multi-entry requests they mirror the **first** entry (stable
2305
+ * lexicographic order from the Rust side); consumers that need
2306
+ * the full picture read `PortableDelegation.resources`.
2307
+ *
2308
+ * @throws {@link SessionExpiredError} when there is no session or
2309
+ * the current session has expired (or will within the 60s
2310
+ * safety margin).
2311
+ * @throws {@link PermissionNotInManifestError} when any requested
2312
+ * entry is not a subset of the granted session capabilities and
2210
2313
  * `forceWalletSign` is not set.
2211
2314
  */
2212
2315
  async delegateTo(did, permissions, options) {
@@ -2227,16 +2330,10 @@ var _TinyCloudNode = class _TinyCloudNode {
2227
2330
  "delegateTo requires a non-empty permissions array"
2228
2331
  );
2229
2332
  }
2230
- if (permissions.length > 1) {
2231
- throw new Error(
2232
- "delegateTo currently supports one permission entry per call. Call delegateTo multiple times for multi-resource delegation."
2233
- );
2234
- }
2235
- const entry = permissions[0];
2236
- const expandedEntry = {
2333
+ const expandedEntries = permissions.map((entry) => ({
2237
2334
  ...entry,
2238
2335
  actions: expandActionShortNames(entry.service, entry.actions)
2239
- };
2336
+ }));
2240
2337
  const now = /* @__PURE__ */ new Date();
2241
2338
  const expiryMs = resolveExpiryMs(options?.expiry);
2242
2339
  const expirationTime = new Date(now.getTime() + expiryMs);
@@ -2245,9 +2342,14 @@ var _TinyCloudNode = class _TinyCloudNode {
2245
2342
  effectiveExpiration = sessionExpiry;
2246
2343
  }
2247
2344
  if (options?.forceWalletSign) {
2345
+ if (expandedEntries.length > 1) {
2346
+ throw new Error(
2347
+ "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."
2348
+ );
2349
+ }
2248
2350
  const delegation2 = await this.createDelegationLegacyWalletPath(
2249
2351
  did,
2250
- expandedEntry,
2352
+ expandedEntries[0],
2251
2353
  effectiveExpiration
2252
2354
  );
2253
2355
  return { delegation: delegation2, prompted: true };
@@ -2256,14 +2358,13 @@ var _TinyCloudNode = class _TinyCloudNode {
2256
2358
  (siwe) => this.wasmBindings.parseRecapFromSiwe(siwe),
2257
2359
  session.siwe
2258
2360
  );
2259
- const requested = [expandedEntry];
2260
- const { subset, missing } = isCapabilitySubset(requested, granted);
2361
+ const { subset, missing } = isCapabilitySubset(expandedEntries, granted);
2261
2362
  if (!subset) {
2262
2363
  throw new PermissionNotInManifestError(missing, granted);
2263
2364
  }
2264
2365
  const delegation = await this.createDelegationViaWasmPath(
2265
2366
  did,
2266
- expandedEntry,
2367
+ expandedEntries,
2267
2368
  effectiveExpiration,
2268
2369
  session
2269
2370
  );
@@ -2272,14 +2373,60 @@ var _TinyCloudNode = class _TinyCloudNode {
2272
2373
  /**
2273
2374
  * Issue a delegation via the session-key UCAN WASM path.
2274
2375
  *
2275
- * The caller has already verified the request is derivable from the
2276
- * current session; we just need to shape the inputs for
2277
- * {@link createDelegationWrapper}.
2376
+ * The caller has already verified every entry is derivable from
2377
+ * the current session; we build one multi-resource abilities map
2378
+ * and emit one signed UCAN covering them all.
2379
+ *
2380
+ * All entries must share the same target space (the UCAN is
2381
+ * scoped to a single space). If they don't, this throws — mixing
2382
+ * spaces in a single delegation is not supported by the underlying
2383
+ * Rust create_delegation call and the resulting UCAN would be
2384
+ * under-specified.
2278
2385
  *
2279
2386
  * @internal
2280
2387
  */
2281
- async createDelegationViaWasmPath(did, entry, expirationTime, session) {
2282
- const spaceId = entry.space === "default" ? session.spaceId : entry.space;
2388
+ async createDelegationViaWasmPath(did, entries, expirationTime, session) {
2389
+ if (entries.length === 0) {
2390
+ throw new Error(
2391
+ "createDelegationViaWasmPath requires a non-empty entries array"
2392
+ );
2393
+ }
2394
+ const resolvedSpaces = /* @__PURE__ */ new Set();
2395
+ for (const entry of entries) {
2396
+ const spaceId2 = entry.space === "default" ? session.spaceId : entry.space;
2397
+ resolvedSpaces.add(spaceId2);
2398
+ }
2399
+ if (resolvedSpaces.size !== 1) {
2400
+ throw new Error(
2401
+ `delegateTo: all permission entries must target the same space, got ${resolvedSpaces.size}: ${JSON.stringify([...resolvedSpaces])}`
2402
+ );
2403
+ }
2404
+ const spaceId = [...resolvedSpaces][0];
2405
+ const abilities = {};
2406
+ for (const entry of entries) {
2407
+ const shortService = SERVICE_LONG_TO_SHORT[entry.service];
2408
+ if (shortService === void 0) {
2409
+ throw new Error(
2410
+ `delegateTo: unknown service '${entry.service}' \u2014 no short-form mapping`
2411
+ );
2412
+ }
2413
+ if (abilities[shortService] === void 0) {
2414
+ abilities[shortService] = {};
2415
+ }
2416
+ const pathsMap = abilities[shortService];
2417
+ const existing = pathsMap[entry.path];
2418
+ if (existing === void 0) {
2419
+ pathsMap[entry.path] = [...entry.actions];
2420
+ } else {
2421
+ const seen = new Set(existing);
2422
+ for (const action of entry.actions) {
2423
+ if (!seen.has(action)) {
2424
+ existing.push(action);
2425
+ seen.add(action);
2426
+ }
2427
+ }
2428
+ }
2429
+ }
2283
2430
  const serviceSession = {
2284
2431
  delegationHeader: session.delegationHeader,
2285
2432
  delegationCid: session.delegationCid,
@@ -2292,16 +2439,17 @@ var _TinyCloudNode = class _TinyCloudNode {
2292
2439
  session: serviceSession,
2293
2440
  delegateDID: did,
2294
2441
  spaceId,
2295
- path: entry.path,
2296
- actions: entry.actions,
2442
+ abilities,
2297
2443
  expirationSecs
2298
2444
  });
2445
+ const primary = result.resources[0];
2299
2446
  return {
2300
2447
  cid: result.cid,
2301
2448
  delegationHeader: { Authorization: `Bearer ${result.delegation}` },
2302
2449
  spaceId,
2303
- path: entry.path,
2304
- actions: entry.actions,
2450
+ path: primary.path,
2451
+ actions: primary.actions,
2452
+ resources: result.resources,
2305
2453
  disableSubDelegation: false,
2306
2454
  expiry: result.expiry,
2307
2455
  delegateDID: did,
@@ -2357,19 +2505,17 @@ var _TinyCloudNode = class _TinyCloudNode {
2357
2505
  params.path,
2358
2506
  params.spaceIdOverride
2359
2507
  );
2360
- if (entries.length === 1) {
2361
- try {
2362
- const result = await this.delegateTo(
2363
- resolvedDelegateDID,
2364
- [entries[0]],
2365
- params.expiryMs !== void 0 ? { expiry: params.expiryMs } : void 0
2366
- );
2367
- return result.delegation;
2368
- } catch (err) {
2369
- if (err instanceof PermissionNotInManifestError) {
2370
- } else {
2371
- throw err;
2372
- }
2508
+ try {
2509
+ const result = await this.delegateTo(
2510
+ resolvedDelegateDID,
2511
+ entries,
2512
+ params.expiryMs !== void 0 ? { expiry: params.expiryMs } : void 0
2513
+ );
2514
+ return result.delegation;
2515
+ } catch (err) {
2516
+ if (err instanceof PermissionNotInManifestError) {
2517
+ } else {
2518
+ throw err;
2373
2519
  }
2374
2520
  }
2375
2521
  return this.createDelegationWalletPath({
@@ -2697,7 +2843,7 @@ import {
2697
2843
  PermissionNotInManifestError as PermissionNotInManifestError2,
2698
2844
  SessionExpiredError as SessionExpiredError2,
2699
2845
  ManifestValidationError,
2700
- resolveManifest,
2846
+ resolveManifest as resolveManifest2,
2701
2847
  validateManifest,
2702
2848
  loadManifest,
2703
2849
  isCapabilitySubset as isCapabilitySubset2,
@@ -2805,7 +2951,7 @@ export {
2805
2951
  makePublicSpaceId2 as makePublicSpaceId,
2806
2952
  parseExpiry2 as parseExpiry,
2807
2953
  parseSpaceUri,
2808
- resolveManifest,
2954
+ resolveManifest2 as resolveManifest,
2809
2955
  serializeDelegation,
2810
2956
  validateManifest
2811
2957
  };