Skip to content

Checking access...

Oracle-Bridge Canister Call Patterns

Reference for developers adding new oracle-bridge → IC canister interactions. Source: AI-R90 from BL-019 Retrospective (anonymous identity blocker).


Query vs Update Methods

IC canister methods come in two types:

TypeCandid annotationIdentity requiredExample
Query['query']No (anonymous OK)validate_session, get_user_roles
Update[] (no annotation)Yes (controller principal)assign_role, revoke_role, logout_all

Query methods are read-only, fast (~200ms), and can be called with anonymous identity (2vxsx-fae). They do not check caller() against the canister's controller list.

Update methods modify canister state, go through consensus (~2s), and typically check caller() against an internal controller or admin list. Using anonymous identity returns "Unauthorized".


Identity Patterns

Anonymous (for queries)

When only calling query methods, no identity configuration is needed:

ts
const agent = HttpAgent.createSync({ host });
const actor = Actor.createActor(idlFactory, { agent, canisterId });
// actor.validate_session(token, null)  // works — query method

Authenticated (for updates)

When calling controller-only update methods, use Ed25519KeyIdentity:

ts
import { Ed25519KeyIdentity } from '@dfinity/identity';

// PRIV_KEY_B64 is a base64-encoded key. It may be:
//   - 32 bytes: raw Ed25519 seed
//   - 64 bytes: NaCl extended key (seed + public key concatenated)
// Ed25519KeyIdentity.fromSecretKey() requires exactly the 32-byte seed.
const rawKey = Buffer.from(cfg.privKeyB64, 'base64');
const seed = rawKey.length === 64 ? rawKey.subarray(0, 32) : rawKey;
const identity = Ed25519KeyIdentity.fromSecretKey(new Uint8Array(seed));

const agent = HttpAgent.createSync({ host, identity });
const actor = Actor.createActor(idlFactory, { agent, canisterId });
// actor.assign_role(userId, { Admin: null })  // works — signed by controller

Current Implementation

createAuthServiceActor() in oracle-bridge/src/ic/auth-service-idl.ts handles both patterns automatically:

  • If PRIV_KEY_B64 is set → creates authenticated identity (required for staging/production)
  • If not set → falls back to anonymous with a warning (local dev only)

Adding a New Canister Call

When adding a new oracle-bridge route that calls a canister method:

  1. Check the Candid annotation in the .did file or IDL factory:

    • ['query'] → anonymous OK
    • [] (empty) → needs authenticated identity
  2. Use createAuthServiceActor() (or equivalent helper) which already handles identity setup. Do not create a new HttpAgent manually.

  3. Verify on staging — unit tests mock the agent entirely, so identity mismatches are invisible until staging. Always test update calls against the real canister.

  4. If the canister has an internal controller list (like auth-service's s.controllers), ensure the oracle-bridge principal is registered. For auth-service, this happens via set_user_service() which syncs the controller list.


Canister Method Reference (auth-service)

MethodTypeIdentityPurpose
validate_sessionqueryanonymousSession validation
get_user_rolesqueryanonymousRead user roles
login_email_passwordupdateanonymous*Login (no controller check)
refresh_tokensupdateanonymous*Token refresh (no controller check)
logoutupdateanonymous*Single session logout
assign_roleupdatecontrollerRole assignment (admin only)
revoke_roleupdatecontrollerRole revocation (admin only)
logout_allupdatecontrollerInvalidate all sessions
update_session_principalupdatecontrollerLink II principal to session

*These update methods authenticate via the session token in arguments, not the caller principal.


Common Gotchas

  1. 64-byte NaCl key: PRIV_KEY_B64 on the VPS is a 64-byte concatenated keypair (private seed + public key). Ed25519KeyIdentity.fromSecretKey() wants only the first 32 bytes. Always use rawKey.subarray(0, 32).

  2. Controller bootstrap: Even with the correct identity, the oracle-bridge principal must be in the canister's internal controller list. For auth-service, call set_user_service(user_service_canister_id) from the oracle-bridge identity to trigger the controller sync.

  3. Unit tests hide identity issues: Tests mock @dfinity/agent entirely, so anonymous vs authenticated identity is never tested. Only staging smoke tests catch this.



UUID vs IC Principal in Session-Based Canister Calls

Source: AI-R101 from BL-021 Retrospective

validate_access_token returns a user_id in UUID format (e.g., 904599ed-12b6-4314-b0ba-677066afd2ce). This is not an IC Principal.

The membership canister uses IC Principals as member identifiers. Calling Principal::from_text(uuid) will fail — UUIDs are not valid IC Principal text encoding.

Pattern for mutation methods that need IC principal identity

When an oracle-bridge route needs to call a canister update method on behalf of a specific member:

  1. Use req.session.ic_principal — populated by the requireSessionAuth middleware from auth-service session data. This field was added in BL-027 (update_session_principal flow).
  2. Call controller-only admin methods on the target canister (e.g., update_profile_admin(ic_principal, request)) using oracle-bridge's authenticated Ed25519 identity.
  3. Do not use _with_session canister methods for mutations that require IC principal identification — the UUID/Principal mismatch makes them non-functional.

Pattern for query methods

For query methods (get_directory, get_member_profile), anonymous identity is fine — no caller identity is checked on read-only operations.

Summary table

Operation typeIdentity neededUse
Query (read)NoneAnonymous HttpAgent
Update on behalf of userIC Principalreq.session.ic_principal + controller Ed25519 identity
Admin update (no user context)Controller onlyEd25519 identity from PRIV_KEY_B64

Reference: BL-021 retrospective, BL-022.4 user-service admin method pattern.


Route File Architecture (AI-R102)

Source: AI-R102 from BL-020 Retrospective

As of 2026-02-18, oracle-bridge has 8 route files: auth, blog-analytics, blog-canister, blog-webhook, discussions, image-upload, members, notifications, roles.

Decision: Continue with flat route file structure. The current count is manageable. Re-evaluate when route count exceeds 12 or when shared business logic between routes creates duplication.

Each route file is self-contained with its own validation, canister calls, and response formatting. No shared service layer is needed at current scale. If duplication emerges (e.g., multiple routes calling the same canister method with the same error handling), extract a shared helper module at that point rather than speculatively.



Controller Audit Before Canister Deploy (AI-R112)

Source: AI-R112 from BL-022 Retrospective

Before deploying any canister WASM (build or upgrade), verify that the github-ci identity is registered as a controller. Attempting to deploy without controller access fails at the deploy step — after the WASM has already been built — wasting build time and causing confusing errors.

Audit command:

bash
DFX_WARNING=-mainnet_plaintext_identity \
  dfx canister --network ic status <canister-id> --identity github-ci

Look for github-ci's principal in the Controllers: list in the output.

If not a controller: Initiate a controller transfer from an identity that IS a controller (typically the deploying developer's local identity) BEFORE running any build or deploy step:

bash
# Add github-ci as controller (run with an identity that is already a controller)
DFX_WARNING=-mainnet_plaintext_identity \
  dfx canister --network ic update-settings <canister-id> \
  --add-controller <github-ci-principal>

The github-ci principal can be retrieved with:

bash
dfx identity --identity github-ci get-principal

Rule: Controller verification is a pre-flight check, not a post-failure recovery step. Add it to your deploy runbook before WASM build commands.


getUserServiceActor() vs getControllerUserServiceActor() (AI-R216)

Source: AI-R213/AI-R216 from Bugfix Sprint 1 Retrospective

oracle-bridge has TWO actor factory functions for calling the user-service canister:

FunctionIdentityUse for
getUserServiceActor()AnonymousQuery methods only: get_display_name, get_user_by_ii_principal, is_ready_for_payment, get_payment_history
getControllerUserServiceActor()Ed25519 from PRIV_KEY_B64Controller-gated update methods: create_user_ii, update_user_ii_profile, approve_parental_consent, send_parental_consent_email, get_password_hash_by_email_hash, confirm_payment

How to tell which to use

Check the Candid IDL in src/ic/user-service-idl.ts:

  • Method has ['query'] annotation → getUserServiceActor() is safe
  • Method has [] (empty array) annotation → must use getControllerUserServiceActor()

Bug class: Bugfix Sprint 1

Three controller-gated methods (create_user_ii, update_user_ii_profile, approve_parental_consent) were using getUserServiceActor() (anonymous identity). This worked in local dev (PocketIC has no controller enforcement by default) but silently fails in staging/production — the canister rejects the call with "Not a controller" error, but the oracle-bridge error handling made the failure ambiguous.

Canister Method Reference (user-service)

MethodTypeIdentityPurpose
get_display_namequeryanonymousDisplay name lookup
get_user_by_ii_principalqueryanonymousII principal → user_id lookup
is_ready_for_paymentqueryanonymousPayment readiness check
get_payment_historyqueryanonymousPayment history
create_user_iiupdatecontrollerCreate new II-first user
update_user_ii_profileupdatecontrollerSet email/DOB/display name for II user
approve_parental_consentupdatecontrollerApprove COPPA parental consent
send_parental_consent_emailupdatecontrollerTrigger consent email via HTTP outcall
get_password_hash_by_email_hashupdatecontrollerRetrieve password hash for off-chain verification
confirm_paymentupdatecontrollerRecord confirmed payment

TypeScript Actor Interface

Both query and controller methods are on the UserServicePasswordActor interface in src/ic/user-service-idl.ts. The interface name is misleading (historical — it was originally only for password hash retrieval) but now covers all controller-gated user-service methods.


Created 2026-02-17 — AI-R90 from BL-019 Epic RetrospectiveUpdated 2026-02-18 — AI-R101 (UUID/Principal), AI-R102 (route architecture) from BL-020/BL-021 RetrospectivesUpdated 2026-02-18 — AI-R112 (controller audit pre-deploy) from BL-022 RetrospectiveUpdated 2026-03-08 — AI-R216 (getUserServiceActor vs getControllerUserServiceActor) from Bugfix Sprint 1 Retrospective

Hello World Co-Op DAO