@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.cjs CHANGED
@@ -339,8 +339,25 @@ var NodeUserAuthorization = class {
339
339
  this.enablePublicSpace = config.enablePublicSpace ?? true;
340
340
  this.nonce = config.nonce;
341
341
  this.siweConfig = config.siweConfig;
342
+ this._manifest = config.manifest;
342
343
  this.sessionManager = this.wasm.createSessionManager();
343
344
  }
345
+ /**
346
+ * Return the manifest currently driving sign-in behavior, or
347
+ * `undefined` if none is set. Used by TinyCloudWeb/TinyCloudNode
348
+ * internals to surface the manifest for requestPermissions flows
349
+ * without forcing the caller to track it separately.
350
+ */
351
+ get manifest() {
352
+ return this._manifest;
353
+ }
354
+ /**
355
+ * Install or replace the stored manifest. Takes effect on the next
356
+ * `signIn()` call — the current session (if any) is not touched.
357
+ */
358
+ setManifest(manifest) {
359
+ this._manifest = manifest;
360
+ }
344
361
  /**
345
362
  * The current active session (web-core compatible).
346
363
  */
@@ -357,6 +374,39 @@ var NodeUserAuthorization = class {
357
374
  get nodeFeatures() {
358
375
  return this._nodeFeatures;
359
376
  }
377
+ /**
378
+ * Compute the `abilities` map the WASM `prepareSession` call should
379
+ * see at sign-in time.
380
+ *
381
+ * When a manifest is installed, we resolve it and union together:
382
+ * - the app's own `resources` (what it needs at runtime)
383
+ * - every `additionalDelegates[*].permissions` list (what it will
384
+ * re-delegate to other DIDs post sign-in)
385
+ *
386
+ * into the short-service / path / full-URN-actions shape the WASM
387
+ * layer expects. This is the key invariant that lets
388
+ * {@link TinyCloudNode.delegateTo} issue manifest-declared
389
+ * delegations via the session key (no wallet prompt): the session's
390
+ * own recap already covers every action those delegations need.
391
+ *
392
+ * When no manifest is installed, we fall back to the
393
+ * {@link defaultActions} table so existing callers see no change.
394
+ *
395
+ * This is a pure function of `this._manifest` + `this.defaultActions`
396
+ * — the manifest resolution performs no I/O and throws a
397
+ * {@link ManifestValidationError} on structural problems (missing
398
+ * id/name, unparseable expiry, etc), which will surface at sign-in
399
+ * rather than being silently swallowed.
400
+ *
401
+ * @internal
402
+ */
403
+ resolveSignInAbilities() {
404
+ if (this._manifest === void 0) {
405
+ return this.defaultActions;
406
+ }
407
+ const resolved = (0, import_sdk_core2.resolveManifest)(this._manifest);
408
+ return (0, import_sdk_core2.manifestAbilitiesUnion)(resolved);
409
+ }
360
410
  /**
361
411
  * Build SIWE overrides from the top-level nonce and siweConfig.
362
412
  * - Top-level `nonce` is seeded first so `siweConfig.nonce` wins if both are set.
@@ -560,7 +610,7 @@ var NodeUserAuthorization = class {
560
610
  const now = /* @__PURE__ */ new Date();
561
611
  const expirationTime = new Date(now.getTime() + this.sessionExpirationMs);
562
612
  const prepared = this.wasm.prepareSession({
563
- abilities: this.defaultActions,
613
+ abilities: this.resolveSignInAbilities(),
564
614
  address,
565
615
  chainId,
566
616
  domain: this.domain,
@@ -703,7 +753,7 @@ var NodeUserAuthorization = class {
703
753
  const now = /* @__PURE__ */ new Date();
704
754
  const expirationTime = new Date(now.getTime() + this.sessionExpirationMs);
705
755
  const prepared = this.wasm.prepareSession({
706
- abilities: this.defaultActions,
756
+ abilities: this.resolveSignInAbilities(),
707
757
  address,
708
758
  chainId,
709
759
  domain: this.domain,
@@ -1212,12 +1262,35 @@ var _TinyCloudNode = class _TinyCloudNode {
1212
1262
  enablePublicSpace: config.enablePublicSpace ?? true,
1213
1263
  spaceCreationHandler: config.spaceCreationHandler,
1214
1264
  nonce: config.nonce,
1215
- siweConfig: config.siweConfig
1265
+ siweConfig: config.siweConfig,
1266
+ manifest: config.manifest
1216
1267
  });
1217
1268
  this.tc = new import_sdk_core5.TinyCloud(this.auth, {
1218
1269
  invokeAny: this.wasmBindings.invokeAny
1219
1270
  });
1220
1271
  }
1272
+ /**
1273
+ * Install or replace the manifest that drives the SIWE recap at
1274
+ * sign-in. Takes effect on the next `signIn()` call — the current
1275
+ * session (if any) is not touched. Wire this up from a higher
1276
+ * layer (e.g. TinyCloudWeb.setManifest) so the manifest is kept
1277
+ * in sync across the stack.
1278
+ */
1279
+ setManifest(manifest) {
1280
+ if (!this.auth) {
1281
+ throw new Error(
1282
+ "setManifest requires wallet mode. Provide a signer or privateKey in the TinyCloudNode config."
1283
+ );
1284
+ }
1285
+ this.auth.setManifest(manifest);
1286
+ }
1287
+ /**
1288
+ * Return the manifest currently installed on the auth handler,
1289
+ * or `undefined` if none is set.
1290
+ */
1291
+ get manifest() {
1292
+ return this.auth?.manifest;
1293
+ }
1221
1294
  /**
1222
1295
  * Get the primary identity DID for this user.
1223
1296
  * - If wallet connected and signed in: returns PKH DID (did:pkh:eip155:{chainId}:{address})
@@ -1724,7 +1797,19 @@ var _TinyCloudNode = class _TinyCloudNode {
1724
1797
  }
1725
1798
  /**
1726
1799
  * Wrapper for the WASM createDelegation function.
1727
- * Adapts the WASM interface to what SharingService expects.
1800
+ *
1801
+ * The WASM call now takes a multi-resource `abilities` map
1802
+ * (matching `prepareSession`'s shape) and emits ONE UCAN that
1803
+ * covers every `(service, path, actions)` entry. We mirror the raw
1804
+ * result back through `CreateDelegationWasmResult`, converting the
1805
+ * seconds-since-epoch `expiry` to a Date and normalizing the
1806
+ * `delegateDid` → `delegateDID` case.
1807
+ *
1808
+ * Both SharingService (single-entry) and
1809
+ * {@link TinyCloudNode.delegateTo} (multi-entry) drive this through
1810
+ * the same code path so there's exactly one place that touches the
1811
+ * WASM boundary.
1812
+ *
1728
1813
  * @internal
1729
1814
  */
1730
1815
  createDelegationWrapper(params) {
@@ -1739,18 +1824,19 @@ var _TinyCloudNode = class _TinyCloudNode {
1739
1824
  wasmSession,
1740
1825
  params.delegateDID,
1741
1826
  params.spaceId,
1742
- params.path,
1743
- params.actions,
1827
+ params.abilities,
1744
1828
  params.expirationSecs,
1745
1829
  params.notBeforeSecs
1746
1830
  );
1747
1831
  return {
1748
1832
  delegation: result.delegation,
1749
1833
  cid: result.cid,
1750
- delegateDID: result.delegateDid,
1751
- path: result.path,
1752
- actions: result.actions,
1753
- expiry: new Date(result.expiry * 1e3)
1834
+ // Rust serde `rename_all = "camelCase"` emits `delegateDid`
1835
+ // (lowercase d); the TypeScript interface uses `delegateDID`
1836
+ // (historical, matches Delegation.delegateDID). Normalize here.
1837
+ delegateDID: result.delegateDid ?? result.delegateDID,
1838
+ expiry: new Date(result.expiry * 1e3),
1839
+ resources: result.resources
1754
1840
  };
1755
1841
  }
1756
1842
  /**
@@ -2225,24 +2311,38 @@ var _TinyCloudNode = class _TinyCloudNode {
2225
2311
  /**
2226
2312
  * Issue a delegation using the capability-chain flow.
2227
2313
  *
2228
- * When the requested permissions are a subset of the current session's
2229
- * recap, the delegation is signed by the session key via WASM — no wallet
2230
- * prompt. When they are not, a {@link PermissionNotInManifestError} is
2231
- * raised so the caller can trigger an escalation flow (e.g.
2232
- * `TinyCloudWeb.requestPermissions`). Passing `forceWalletSign: true`
2233
- * bypasses the derivability check and always uses the wallet-signed SIWE
2234
- * path used by the legacy `createDelegation` fallback and by callers
2235
- * that want explicit wallet confirmation.
2236
- *
2237
- * Current limitation: exactly one {@link PermissionEntry} per call. For
2238
- * multi-resource delegation, call `delegateTo` multiple times. This keeps
2239
- * each delegation a single `(spaceId, path)` grant, which matches the
2240
- * underlying `PortableDelegation` shape.
2241
- *
2242
- * @throws {@link SessionExpiredError} when there is no session or the
2243
- * current session has expired (or will within the 60s safety margin).
2244
- * @throws {@link PermissionNotInManifestError} when the requested entries
2245
- * are not a subset of the granted session capabilities and
2314
+ * When every requested permission is a subset of the current
2315
+ * session's recap, the delegation is signed by the session key via
2316
+ * WASM — no wallet prompt. When at least one is NOT derivable, a
2317
+ * {@link PermissionNotInManifestError} is raised (carrying the
2318
+ * missing entries) so the caller can trigger an escalation flow
2319
+ * (e.g. `TinyCloudWeb.requestPermissions`). Passing
2320
+ * `forceWalletSign: true` bypasses the derivability check and
2321
+ * always uses the wallet-signed SIWE path — used by the legacy
2322
+ * `createDelegation` fallback and by callers that want explicit
2323
+ * wallet confirmation.
2324
+ *
2325
+ * Multi-entry delegations are now emitted as **one** signed UCAN:
2326
+ * the underlying WASM `createDelegation` takes a full
2327
+ * `HashMap<Service, HashMap<Path, Vec<Ability>>>` abilities map
2328
+ * and produces a single attenuation carrying every
2329
+ * `(service, path, actions)` entry. The returned
2330
+ * {@link DelegateToResult.delegation} is that single blob, and
2331
+ * apps can POST it to their backend exactly like a single-entry
2332
+ * delegation (the server verifies all granted resources from one
2333
+ * UCAN).
2334
+ *
2335
+ * For single-entry requests the `PortableDelegation.path` and
2336
+ * `.actions` fields mirror the one granted entry. For
2337
+ * multi-entry requests they mirror the **first** entry (stable
2338
+ * lexicographic order from the Rust side); consumers that need
2339
+ * the full picture read `PortableDelegation.resources`.
2340
+ *
2341
+ * @throws {@link SessionExpiredError} when there is no session or
2342
+ * the current session has expired (or will within the 60s
2343
+ * safety margin).
2344
+ * @throws {@link PermissionNotInManifestError} when any requested
2345
+ * entry is not a subset of the granted session capabilities and
2246
2346
  * `forceWalletSign` is not set.
2247
2347
  */
2248
2348
  async delegateTo(did, permissions, options) {
@@ -2263,16 +2363,10 @@ var _TinyCloudNode = class _TinyCloudNode {
2263
2363
  "delegateTo requires a non-empty permissions array"
2264
2364
  );
2265
2365
  }
2266
- if (permissions.length > 1) {
2267
- throw new Error(
2268
- "delegateTo currently supports one permission entry per call. Call delegateTo multiple times for multi-resource delegation."
2269
- );
2270
- }
2271
- const entry = permissions[0];
2272
- const expandedEntry = {
2366
+ const expandedEntries = permissions.map((entry) => ({
2273
2367
  ...entry,
2274
2368
  actions: (0, import_sdk_core5.expandActionShortNames)(entry.service, entry.actions)
2275
- };
2369
+ }));
2276
2370
  const now = /* @__PURE__ */ new Date();
2277
2371
  const expiryMs = resolveExpiryMs(options?.expiry);
2278
2372
  const expirationTime = new Date(now.getTime() + expiryMs);
@@ -2281,9 +2375,14 @@ var _TinyCloudNode = class _TinyCloudNode {
2281
2375
  effectiveExpiration = sessionExpiry;
2282
2376
  }
2283
2377
  if (options?.forceWalletSign) {
2378
+ if (expandedEntries.length > 1) {
2379
+ throw new Error(
2380
+ "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."
2381
+ );
2382
+ }
2284
2383
  const delegation2 = await this.createDelegationLegacyWalletPath(
2285
2384
  did,
2286
- expandedEntry,
2385
+ expandedEntries[0],
2287
2386
  effectiveExpiration
2288
2387
  );
2289
2388
  return { delegation: delegation2, prompted: true };
@@ -2292,14 +2391,13 @@ var _TinyCloudNode = class _TinyCloudNode {
2292
2391
  (siwe) => this.wasmBindings.parseRecapFromSiwe(siwe),
2293
2392
  session.siwe
2294
2393
  );
2295
- const requested = [expandedEntry];
2296
- const { subset, missing } = (0, import_sdk_core5.isCapabilitySubset)(requested, granted);
2394
+ const { subset, missing } = (0, import_sdk_core5.isCapabilitySubset)(expandedEntries, granted);
2297
2395
  if (!subset) {
2298
2396
  throw new import_sdk_core5.PermissionNotInManifestError(missing, granted);
2299
2397
  }
2300
2398
  const delegation = await this.createDelegationViaWasmPath(
2301
2399
  did,
2302
- expandedEntry,
2400
+ expandedEntries,
2303
2401
  effectiveExpiration,
2304
2402
  session
2305
2403
  );
@@ -2308,14 +2406,60 @@ var _TinyCloudNode = class _TinyCloudNode {
2308
2406
  /**
2309
2407
  * Issue a delegation via the session-key UCAN WASM path.
2310
2408
  *
2311
- * The caller has already verified the request is derivable from the
2312
- * current session; we just need to shape the inputs for
2313
- * {@link createDelegationWrapper}.
2409
+ * The caller has already verified every entry is derivable from
2410
+ * the current session; we build one multi-resource abilities map
2411
+ * and emit one signed UCAN covering them all.
2412
+ *
2413
+ * All entries must share the same target space (the UCAN is
2414
+ * scoped to a single space). If they don't, this throws — mixing
2415
+ * spaces in a single delegation is not supported by the underlying
2416
+ * Rust create_delegation call and the resulting UCAN would be
2417
+ * under-specified.
2314
2418
  *
2315
2419
  * @internal
2316
2420
  */
2317
- async createDelegationViaWasmPath(did, entry, expirationTime, session) {
2318
- const spaceId = entry.space === "default" ? session.spaceId : entry.space;
2421
+ async createDelegationViaWasmPath(did, entries, expirationTime, session) {
2422
+ if (entries.length === 0) {
2423
+ throw new Error(
2424
+ "createDelegationViaWasmPath requires a non-empty entries array"
2425
+ );
2426
+ }
2427
+ const resolvedSpaces = /* @__PURE__ */ new Set();
2428
+ for (const entry of entries) {
2429
+ const spaceId2 = entry.space === "default" ? session.spaceId : entry.space;
2430
+ resolvedSpaces.add(spaceId2);
2431
+ }
2432
+ if (resolvedSpaces.size !== 1) {
2433
+ throw new Error(
2434
+ `delegateTo: all permission entries must target the same space, got ${resolvedSpaces.size}: ${JSON.stringify([...resolvedSpaces])}`
2435
+ );
2436
+ }
2437
+ const spaceId = [...resolvedSpaces][0];
2438
+ const abilities = {};
2439
+ for (const entry of entries) {
2440
+ const shortService = import_sdk_core5.SERVICE_LONG_TO_SHORT[entry.service];
2441
+ if (shortService === void 0) {
2442
+ throw new Error(
2443
+ `delegateTo: unknown service '${entry.service}' \u2014 no short-form mapping`
2444
+ );
2445
+ }
2446
+ if (abilities[shortService] === void 0) {
2447
+ abilities[shortService] = {};
2448
+ }
2449
+ const pathsMap = abilities[shortService];
2450
+ const existing = pathsMap[entry.path];
2451
+ if (existing === void 0) {
2452
+ pathsMap[entry.path] = [...entry.actions];
2453
+ } else {
2454
+ const seen = new Set(existing);
2455
+ for (const action of entry.actions) {
2456
+ if (!seen.has(action)) {
2457
+ existing.push(action);
2458
+ seen.add(action);
2459
+ }
2460
+ }
2461
+ }
2462
+ }
2319
2463
  const serviceSession = {
2320
2464
  delegationHeader: session.delegationHeader,
2321
2465
  delegationCid: session.delegationCid,
@@ -2328,16 +2472,17 @@ var _TinyCloudNode = class _TinyCloudNode {
2328
2472
  session: serviceSession,
2329
2473
  delegateDID: did,
2330
2474
  spaceId,
2331
- path: entry.path,
2332
- actions: entry.actions,
2475
+ abilities,
2333
2476
  expirationSecs
2334
2477
  });
2478
+ const primary = result.resources[0];
2335
2479
  return {
2336
2480
  cid: result.cid,
2337
2481
  delegationHeader: { Authorization: `Bearer ${result.delegation}` },
2338
2482
  spaceId,
2339
- path: entry.path,
2340
- actions: entry.actions,
2483
+ path: primary.path,
2484
+ actions: primary.actions,
2485
+ resources: result.resources,
2341
2486
  disableSubDelegation: false,
2342
2487
  expiry: result.expiry,
2343
2488
  delegateDID: did,
@@ -2393,19 +2538,17 @@ var _TinyCloudNode = class _TinyCloudNode {
2393
2538
  params.path,
2394
2539
  params.spaceIdOverride
2395
2540
  );
2396
- if (entries.length === 1) {
2397
- try {
2398
- const result = await this.delegateTo(
2399
- resolvedDelegateDID,
2400
- [entries[0]],
2401
- params.expiryMs !== void 0 ? { expiry: params.expiryMs } : void 0
2402
- );
2403
- return result.delegation;
2404
- } catch (err) {
2405
- if (err instanceof import_sdk_core5.PermissionNotInManifestError) {
2406
- } else {
2407
- throw err;
2408
- }
2541
+ try {
2542
+ const result = await this.delegateTo(
2543
+ resolvedDelegateDID,
2544
+ entries,
2545
+ params.expiryMs !== void 0 ? { expiry: params.expiryMs } : void 0
2546
+ );
2547
+ return result.delegation;
2548
+ } catch (err) {
2549
+ if (err instanceof import_sdk_core5.PermissionNotInManifestError) {
2550
+ } else {
2551
+ throw err;
2409
2552
  }
2410
2553
  }
2411
2554
  return this.createDelegationWalletPath({