Skip to content

feat(sdk-wasm): multi-resource create_delegation#48

Merged
skgbafa merged 2 commits into
mainfrom
feat/create-delegation-multi-resource
Apr 10, 2026
Merged

feat(sdk-wasm): multi-resource create_delegation#48
skgbafa merged 2 commits into
mainfrom
feat/create-delegation-multi-resource

Conversation

@skgbafa
Copy link
Copy Markdown
Contributor

@skgbafa skgbafa commented Apr 10, 2026

Summary

Extend Session::create_delegation and the createDelegation WASM export to accept the same multi-resource abilities shape as prepareSession:
HashMap<Service, HashMap<Path, Vec<Ability>>>. A single UCAN now encodes every (service, path, actions) entry in one signed blob, scoped to a single target space.

What was broken

  1. Service was hardcoded to "kv" in the Rust delegation path — delegations for sql, duckdb, hooks, or capabilities services were silently wrong (the resource URI was built with kv in the service segment).
  2. Single-entry only — callers that needed to delegate multiple (service, path) pairs had to issue N separate UCANs. This is wrong semantically (the user model is one delegation per recipient) and, in the legacy wallet-signed fallback, forces N wallet prompts. It also broke the manifest use case in the js-sdk, where an app's manifest can declare multiple permission entries in a single delegations[].permissions block.

What's fixed

  • create_delegation now takes abilities: HashMap<Service, HashMap<Path, Vec<Ability>>> and walks every (service, path, actions) tuple, accumulating them into one Capabilities object via with_actions. One signed UCAN, many resource URIs in its attenuation.
  • The createDelegation WASM export takes abilities: JsValue in the same { service: { path: [action] } } shape the JS side already uses for prepareSession.
  • DelegationResult now returns resources: Vec<DelegatedResource> — a sorted list of {service, space, path, actions} describing every capability the UCAN grants. Sort order is lexicographic on (service, path) so tests and JS consumers get deterministic output regardless of HashMap iteration.
  • Validation errors surface as typed variants: EmptyAbilities, EmptyPathsForService, EmptyActionsForPath. Previously an empty actions list under a path would silently sign a useless UCAN.

Tests (11 total, all green)

  • create_delegation_single_resource_backward_compat — one service/path round-trips cleanly through the new shape
  • create_delegation_multi_service_same_path — KV + SQL on the same app path produce one UCAN with two attenuation entries
  • create_delegation_multi_service_multi_path — three different services on three different paths in one UCAN, each getting its own entry
  • create_delegation_rejects_empty_abilities
  • create_delegation_rejects_empty_actions_under_path
  • create_delegation_subset_of_parse_recap — end-to-end invariant that every delegated resource is derivable from the session's own parsed recap (the check the JS delegateTo path relies on)

All existing sdk-wasm tests (parse_recap_*, create_session_and_invoke, session_with_additional_spaces) still pass.

Breaking change

The createDelegation WASM export signature changed from

createDelegation(session, delegateDID, spaceId, path, actions, expirationSecs, notBeforeSecs)

to

createDelegation(session, delegateDID, spaceId, abilities, expirationSecs, notBeforeSecs)

The only JS caller is TinyCloudNode.createDelegationWrapper, which is being updated in the matching js-sdk PR (fix/manifest-signin-multi-delegate) that bumps the WASM rev to this commit.

Test plan

  • cargo test -p tinycloud-sdk-wasm --lib — 11/11 pass locally
  • cargo check --workspace — clean
  • CI green
  • js-sdk PR bumps the packages/sdk-rs/Cargo.toml rev to this merge SHA and updates createDelegationWrapper to pass the abilities map

skgbafa added 2 commits April 10, 2026 14:52
Extend Session::create_delegation and the createDelegation WASM export to
accept the same multi-resource abilities shape as prepareSession:
HashMap<Service, HashMap<Path, Vec<Ability>>>. A single UCAN now encodes
every (service, path, actions) entry in one signed blob, scoped to a
single target space.

This fixes two concrete gaps in the previous single-resource API:

1. Service was hardcoded to "kv" in the Rust path — delegations for
   sql, duckdb, hooks, or capabilities services were silently wrong
   (the resource URI was built with the wrong service segment).

2. Callers that needed to delegate multiple (service, path) pairs had
   to issue N separate UCANs, which is both wrong semantically (the
   user model is one delegation per recipient) and forces N separate
   wallet prompts in the legacy wallet-signed fallback path.

The DelegationResult now returns a sorted `resources` vec of
{service, space, path, actions} entries describing every capability
the UCAN grants. Sort order is (service, path) lexicographic so tests
and JS consumers get deterministic output regardless of HashMap
iteration order.

Validation errors (empty abilities map, empty path map for a service,
empty action list under a path) surface as typed DelegationError
variants rather than encoding a useless UCAN.

Tests (11 total in session::test, all green):
- create_delegation_single_resource_backward_compat: one service/path
  still round-trips cleanly
- create_delegation_multi_service_same_path: KV + SQL on the same app
  path produce one UCAN with two attenuation entries
- create_delegation_multi_service_multi_path: three different services
  on three different paths in one UCAN, each getting its own entry
- create_delegation_rejects_empty_abilities
- create_delegation_rejects_empty_actions_under_path
- create_delegation_subset_of_parse_recap: end-to-end invariant that
  every delegated resource is derivable from the session's own parsed
  recap — the check the JS delegateTo path relies on.

Breaking change: the createDelegation WASM export signature changed
from (session, delegateDID, spaceId, path, actions, expirationSecs,
notBeforeSecs) to (session, delegateDID, spaceId, abilities,
expirationSecs, notBeforeSecs). The only JS caller is
TinyCloudNode.createDelegationWrapper, which is being updated in the
matching js-sdk PR (fix/manifest-signin-multi-delegate) that bumps the
WASM rev to this commit.
@skgbafa skgbafa merged commit 6f6fc86 into main Apr 10, 2026
14 checks passed
@skgbafa skgbafa deleted the feat/create-delegation-multi-resource branch April 10, 2026 13:18
skgbafa added a commit to TinyCloudLabs/js-sdk that referenced this pull request Apr 10, 2026
…ation

Bumps the WASM source dependency to the merge commit of
TinyCloudLabs/tinycloud-node#48 (feat(sdk-wasm): multi-resource
create_delegation, 6f6fc86).

The new WASM exports `createDelegation` with the signature
  (session, delegateDID, spaceId, abilities, expirationSecs, notBeforeSecs)
where `abilities` is the same multi-resource map shape that
prepareSession already accepts:
  Record<short service, Record<path, full-URN actions[]>>

A single call now produces ONE signed UCAN whose attenuation carries
every (service, path, actions) entry. This unlocks manifest-driven
single-prompt sign-in flows where one delegation can cover multiple
services on multiple paths to a single backend.

Cargo.lock updated via `cargo update -p tinycloud-sdk-rs -p tinycloud-sdk-wasm`.

The wrapper packages @tinycloud/node-sdk-wasm and @tinycloud/web-sdk-wasm
will pick up the new exports automatically on the next bun run build.
skgbafa added a commit to TinyCloudLabs/js-sdk that referenced this pull request Apr 10, 2026
…ta.1 gaps) (#187)

* chore(sdk-rs): bump tinycloud-node rev to multi-resource create_delegation

Bumps the WASM source dependency to the merge commit of
TinyCloudLabs/tinycloud-node#48 (feat(sdk-wasm): multi-resource
create_delegation, 6f6fc86).

The new WASM exports `createDelegation` with the signature
  (session, delegateDID, spaceId, abilities, expirationSecs, notBeforeSecs)
where `abilities` is the same multi-resource map shape that
prepareSession already accepts:
  Record<short service, Record<path, full-URN actions[]>>

A single call now produces ONE signed UCAN whose attenuation carries
every (service, path, actions) entry. This unlocks manifest-driven
single-prompt sign-in flows where one delegation can cover multiple
services on multiple paths to a single backend.

Cargo.lock updated via `cargo update -p tinycloud-sdk-rs -p tinycloud-sdk-wasm`.

The wrapper packages @tinycloud/node-sdk-wasm and @tinycloud/web-sdk-wasm
will pick up the new exports automatically on the next bun run build.

* feat(sdk-core): multi-resource WASM types + manifest abilities helpers

Update the createDelegation WASM types to match the new multi-resource
Rust API:

- CreateDelegationWasmParams.abilities replaces flat path/actions —
  Record<short service, Record<path, action URNs[]>>
- CreateDelegationWasmResult.resources replaces flat path/actions —
  Vec<DelegatedResource> sorted by (service, path) by the Rust side
- New DelegatedResource type carries (service, space, path, actions)

Add manifest → abilities map bridge helpers in manifest.ts:

- AbilitiesMap type alias for the shape both prepareSession and the
  new createDelegation accept
- resourceCapabilitiesToAbilitiesMap converts a flat list of
  ResourceCapability entries (manifest long-form services, full URN
  actions) into the AbilitiesMap shape, merging duplicates and
  rejecting unknown services with a clear error
- manifestAbilitiesUnion takes a ResolvedCapabilities (from
  resolveManifest) and unions the app's own resources with every
  manifest-declared delegation's permissions, producing the abilities
  map a session should be signed with so the session key acquires
  recap coverage for both runtime calls AND downstream sub-delegations

These helpers are the bridge that lets the node-sdk's signIn drive
its WASM prepareSession call from a manifest, and they also keep
sdk-core as the single owner of the manifest semantics — the node-sdk
layer just calls them.

Update SharingService.ts to use the new abilities-based createDelegation
shape (single-entry call, infer short service from action URN
namespace), and surface validation errors for empty actions or
mixed-service action lists (the WASM call requires the service
segment explicitly).

Test updates: types.schema.test.ts and wasm-validation.test.ts now
exercise both single-resource and multi-resource result shapes.

Exports added from sdk-core/src/index.ts:
  AbilitiesMap, manifestAbilitiesUnion,
  resourceCapabilitiesToAbilitiesMap, DelegatedResource

532/532 sdk-core tests pass.

* feat(node-sdk): manifest-driven signIn + multi-entry delegateTo

Two SDK gaps in 2.1.0-beta.1, both fixed here:

GAP 1 — signIn ignored config.manifest

NodeUserAuthorization.signIn passed `defaultActions` to
prepareSession unconditionally, even when a manifest was installed.
The result was that manifest-declared permissions and pre-delegations
never made it into the session's SIWE recap, defeating the entire
manifest flow.

Fix:

- NodeUserAuthorizationConfig gains an optional `manifest` field
- Manifest is stored on the auth handler and exposed via `manifest`
  getter + `setManifest` setter
- New private `resolveSignInAbilities()` returns:
    * the union (via `manifestAbilitiesUnion` from sdk-core) of the
      app's own resources + every additionalDelegate's permissions,
      converted to the WASM abilities map shape
    * OR `defaultActions` when no manifest is installed (legacy fallback)
- Both signIn() and prepareSessionForSigning() now call this helper
  instead of hardcoding `defaultActions`
- TinyCloudNodeConfig also gains `manifest`; setupAuth() forwards it
- TinyCloudNode exposes `setManifest()` and `manifest` getter
  passthroughs so higher layers (TinyCloudWeb) can keep the manifest
  in sync after construction

Result: signIn now produces a SIWE whose recap covers both the app's
runtime permissions AND the permissions of every manifest-declared
delegation target. This is the precondition that makes the next gap
fix work without a wallet prompt.

GAP 2 — delegateTo hard-failed on multi-entry input

The previous implementation threw "delegateTo currently supports one
permission entry per call" — forcing apps that needed multi-resource
delegations to either loop (N delegations, N CIDs, N POSTs) or fall
back to the legacy wallet path. Both options broke listen-style
single-prompt flows.

Fix:

- delegateTo now accepts any number of PermissionEntry items
- Derivability is checked against the union of ALL entries — partial
  failures throw PermissionNotInManifestError carrying the missing
  entries (no partial issuance)
- createDelegationViaWasmPath converts the entries to one
  multi-resource abilities map and calls the WASM createDelegation
  ONCE — producing one signed UCAN whose attenuation covers every
  (service, path, actions) entry
- Returns the existing { delegation, prompted } shape (single
  PortableDelegation, not an array). The new optional
  PortableDelegation.resources field carries the full multi-resource
  breakdown for consumers that need it; the flat path/actions fields
  mirror the first sorted resource for back-compat
- All entries must target the same space (one UCAN = one space);
  cross-space delegation throws with a clear error
- forceWalletSign + multi-entry combination is rejected with a clear
  error since the legacy wallet path is single-entry only

Legacy createDelegation public API:

- The legacy compatibility shim no longer gates on entries.length === 1.
  All N-entry legacy calls now route through delegateTo first, falling
  back to the wallet path on PermissionNotInManifestError as before.
- createDelegationWrapper updated to send the new abilities map shape
  to WASM and handle the new resources[] return shape.

Tests:

- TinyCloudNode.delegateTo.test.ts: replaced the "multi-entry throws"
  test with two new tests:
  * multi-entry → ONE UCAN with merged abilities map (asserts the
    abilities map sent to WASM has both services correctly grouped)
  * multi-entry with one missing cap → PermissionNotInManifestError
    surfaces ALL missing (no partial issuance)
  Existing tests updated to return the new resources-array WASM shape
- TinyCloudNode.signInManifest.test.ts (new file): four tests
  covering the manifest-driven signIn path:
  * no manifest → falls back to defaultActions (legacy)
  * manifest with own permissions → recap reflects manifest tier
  * manifest with delegations → recap unions app + delegate caps
    (the headline listen test)
  * setManifest() post-construction takes effect on next signIn

28/28 node-sdk tests pass.

PortableDelegation gains an optional `resources?: DelegatedResource[]`
field. Single-resource delegations still populate the flat path +
actions; multi-resource delegations populate `resources` AND mirror
the first entry into path + actions. Consumers reading `resources`
get the full picture; legacy consumers reading flat fields keep
working for single-resource cases.

* feat(web-sdk): forward manifest into TinyCloudNode + align WASM ABI

Wire the TinyCloudWeb manifest plumbing through to the underlying
TinyCloudNode so signIn() can drive its SIWE recap from the
manifest:

- _init() forwards `this._manifest` into nodeConfig.manifest, which
  the node-sdk constructor passes through to NodeUserAuthorization
- setManifest() now also calls `this._node?.setManifest()` so
  post-construction manifest updates reach the node before the next
  signIn (previously only the local _manifest field was updated, so
  the node still had the construction-time manifest)
- requestPermissions's writeManifest callback similarly mirrors the
  expanded manifest onto the node so the post-escalation signIn picks
  up the new capability set automatically
- Drop the "Phase 4" stale-doc notes from the manifest field, the
  _manifest private field, and the requestPermissions JSDoc — the
  Phase 5 refactor is now in place

Update BrowserWasmBindings.createDelegation to match the new WASM
ABI: it now takes
  (session, delegateDID, spaceId, abilities, expirationSecs,
   notBeforeSecs)
where `abilities` is the multi-resource map shape
`Record<short service, Record<path, action URNs[]>>` — same shape
prepareSession already accepts. The previous 7-arg flat-path
signature is gone.

Full monorepo build is clean with these changes.

* chore: changeset for manifest-driven signIn + multi-entry delegateTo

Bumps:
- @tinycloud/sdk-core minor (new types + helpers, breaking schema change)
- @tinycloud/node-sdk minor (manifest config + delegateTo multi-entry)
- @tinycloud/web-sdk minor (manifest forwarding to node)
- @tinycloud/node-sdk-wasm patch (rev bump)
- @tinycloud/web-sdk-wasm patch (rev bump)

In pre-mode this rolls forward to the next 2.1.0-beta.N version.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant