Skip to content

Commit fbdc96a

Browse files
authored
feat(agent): getCanisterEnv and safeGetCanisterEnv (experimental) (#1156)
Introduces the `getCanisterEnv` and `safeGetCanisterEnv` functions to load the canister environment from the `ic_env` cookie. This is an _experimental_ feature. Additionally, adds the `@types/jsdom` package to the dev dependencies to avoid typescript errors. Related: dfinity/sdk#4387.
1 parent f273c08 commit fbdc96a

File tree

18 files changed

+819
-12
lines changed

18 files changed

+819
-12
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## [Unreleased]
44

5+
- feat(agent): introduce the `getCanisterEnv` and `safeGetCanisterEnv` functions to load the canister environment from the `ic_env` cookie. (experimental)
6+
57
## [4.1.1] - 2025-10-21
68

79
- fix(agent): remove exported `CanisterInstallMode` type

docs/scripts/generate-docs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ async function main() {
3030
typedocOptions: {
3131
entryPoints: ['../packages/*'],
3232
packageOptions: {
33-
entryPoints: ['src/index.ts'],
33+
entryPoints: ['src/index.ts', 'src/canister-env/index.ts'],
3434
tsconfig: './tsconfig.json',
3535
readme: 'README.md',
3636
alwaysCreateEntryPointModule: true, // puts everything into <package>/api folder

docs/src/content/docs/_sidebar.json

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,20 @@
44
"items": [
55
{ "label": "Overview", "link": "/" },
66
{ "label": "Installation", "link": "/installation" },
7-
{ "label": "Quick Start", "link": "/quick-start" },
7+
{ "label": "Quick Start", "link": "/quick-start" }
8+
]
9+
},
10+
{
11+
"label": "Guides",
12+
"items": [
13+
{
14+
"label": "Canister Environment",
15+
"link": "/canister-environment",
16+
"badge": {
17+
"text": "Experimental",
18+
"variant": "caution"
19+
}
20+
},
821
{ "label": "Typescript", "link": "/typescript" },
922
{ "label": "Library Development", "link": "/library-development" }
1023
]
@@ -27,6 +40,18 @@
2740
"collapsed": true,
2841
"directory": "libs/agent/api"
2942
}
43+
},
44+
{
45+
"label": "Canister Environment API",
46+
"badge": {
47+
"text": "Experimental",
48+
"variant": "caution"
49+
},
50+
"collapsed": true,
51+
"autogenerate": {
52+
"collapsed": true,
53+
"directory": "libs/agent/canister-env/api"
54+
}
3055
}
3156
]
3257
},
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
---
2+
title: Canister Environment
3+
description: Get the canister environment in a JavaScript client application.
4+
prev: false
5+
next:
6+
label: Canister Environment API
7+
link: libs/agent/canister-env/api
8+
---
9+
10+
import { Aside } from '@astrojs/starlight/components';
11+
12+
This guide explains how a JavaScript client application can load the configuration served by the [asset canister](https://internetcomputer.org/docs/building-apps/frontends/using-an-asset-canister) using the `@icp-sdk/core` package.
13+
14+
<Aside>This guide assumes you are running your application in a browser environment, where the [`document.cookie`](https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie) API is available.</Aside>
15+
16+
## Canister Environment
17+
18+
Before looking at the [usage](#usage), let's understand what canister environment variables are and how they are used by the asset canister.
19+
20+
Since [Proposal 138597](https://dashboard.internetcomputer.org/proposal/138597) was accepted, the Internet Computer network allows developers to set the environment variables for their canisters. The main goal of this feature is to allow developers to configure their canisters at runtime, without having to rebuild and redeploy their canisters.
21+
22+
Since [dfinity/sdk#4387](https://github.com/dfinity/sdk/pull/4387), the asset canister is leveraging this feature to serve some environment variables to the frontend application it hosts. The goal is the same: allow the frontend application to be built once and deployed once, and then configured at runtime. This unlocks use cases such as:
23+
24+
- Shipping your entire full-stack application as a single bundle and allowing anyone to install it on any subnet on the Internet Computer network
25+
- Configuring your frontend application with just one canister call that does not require re-deployments
26+
- Proposing frontends bundled within a single canister in a DAO framework
27+
28+
### Flow
29+
30+
Here's an overview of the environment variables flow
31+
32+
```mermaid
33+
sequenceDiagram
34+
participant frontend as Browser (End User)
35+
participant developer as Developer
36+
participant asset_canister as Asset Canister
37+
participant backend_canister as Backend Canister
38+
39+
Note over developer, asset_canister: Deployment of the asset canister
40+
developer->>asset_canister: Set the `PUBLIC_CANISTER_ID:backend` environment variable via management canister
41+
frontend->>asset_canister: Request HTML assets
42+
asset_canister->>frontend: Serve HTML assets with `ic_env` cookie (contains `ic_root_key` and `PUBLIC_CANISTER_ID:backend`)
43+
Note over frontend: Configure the actor with root key and PUBLIC_CANISTER_ID:backend variables
44+
frontend->>backend_canister: Call canister method
45+
```
46+
47+
The flow is the following:
48+
49+
1. The developer deploys the backend canister.
50+
2. The developer sets the `PUBLIC_CANISTER_ID:backend` environment variable to the backend canister ID via the management canister.
51+
3. The browser of the end user requests the HTML assets from the asset canister.
52+
4. The asset canister serves the HTML assets with the `ic_env` cookie. In this example, it serves the `ic_root_key` (by default, see [below](#the-ic_env-cookie)) and `PUBLIC_CANISTER_ID:backend` variables.
53+
5. The browser of the end user decodes the `ic_env` cookie and configures the actor with the root key and `PUBLIC_CANISTER_ID:backend` variables.
54+
6. The browser of the end user calls the canister method using the configured actor.
55+
56+
### The `ic_env` cookie
57+
58+
In order to serve the environment variables to the frontend application _synchronously_, the asset canister serves all the HTML assets with the `ic_env` cookie. The value of this cookie is an [URI-encoded](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) string of the environment variables. The value can be decoded using the [`decodeURIComponent`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent) function and mapped to a JavaScript object that can be used by the frontend application.
59+
60+
The `ic_env` cookie value contains the following properties:
61+
62+
- `ic_root_key`: the [root key](https://internetcomputer.org/docs/references/ic-interface-spec/#root-of-trust) of the Internet Computer network where the asset canister is deployed. It is always present in the cookie.
63+
- any canister environment variable prefixed with `PUBLIC_`. A common case for the asset canister is to serve the `PUBLIC_CANISTER_ID:<canister-name>` environment variable, which allows the frontend application to know the canister ID to instantiate the [actor](https://js.icp.build/core/latest/libs/agent/api/classes/actor/) for.
64+
65+
## Usage
66+
67+
<Aside type="caution">The `@icp-sdk/core/agent/canister-env` module is experimental and may change in the future.</Aside>
68+
69+
In a frontend application, you can load the canister environment from the `ic_env` cookie using the `@icp-sdk/core` package. Specifically, you can use the [`getCanisterEnv`](./libs/agent/canister-env/api/functions/getCanisterEnv.md) function from the [`@icp-sdk/core/canister-env`](./libs/agent/canister-env/api/index.md) module to get the canister environment:
70+
71+
```typescript
72+
import { getCanisterEnv } from "@icp-sdk/core/agent/canister-env";
73+
74+
const canisterEnv = getCanisterEnv();
75+
76+
console.log(getCanisterEnv.IC_ROOT_KEY); // served by default by the asset canister
77+
```
78+
79+
You can also use the [`safeGetCanisterEnv`](./libs/agent/canister-env/api/functions/safeGetCanisterEnv.md) function to get the canister environment without throwing an error if the cookie is not present.
80+
81+
In order to preserve the type-safety at build time, you can pass a generic type parameter to the `getCanisterEnv` function to extend the `CanisterEnv` interface with your own environment variables:
82+
83+
```typescript
84+
type MyCanisterEnv = {
85+
['PUBLIC_CANISTER_ID:backend']: string;
86+
}
87+
88+
const canisterEnv = getCanisterEnv<MyCanisterEnv>();
89+
90+
console.log(canisterEnv.IC_ROOT_KEY); // served by default by the asset canister
91+
console.log(canisterEnv['PUBLIC_CANISTER_ID:backend']); // type-safe access to the environment variable
92+
console.log(canisterEnv['PUBLIC_MY_PROPERTY']); // will fail to compile
93+
```
94+
95+
You must make sure that the property names that you specify in the generic type parameter are set as canister environment variables on the asset canister (which will make them available in the `ic_env` cookie).
96+
97+
For more options on how to type the canister environment, see the [`CanisterEnv`](./libs/agent/canister-env/api/interfaces/CanisterEnv.md) interface documentation.
98+
99+
### Usage with an Actor
100+
101+
The canister environment is a convenient way to configure the [actor](https://js.icp.build/core/latest/libs/agent/api/classes/actor/) for the backend canister.
102+
103+
Assuming you have configured the asset canister with the `PUBLIC_CANISTER_ID:backend` environment variable, you can instantiate the actor as follows:
104+
105+
```typescript
106+
import { getCanisterEnv } from "@icp-sdk/core/agent/canister-env";
107+
import { createActor } from "./backend/api/hello_world"; // generated by the https://js.icp.build/bindgen tool
108+
109+
interface CanisterEnv {
110+
readonly "PUBLIC_CANISTER_ID:backend": string;
111+
}
112+
113+
const canisterEnv = getCanisterEnv<CanisterEnv>();
114+
const canisterId = canisterEnv["PUBLIC_CANISTER_ID:backend"];
115+
116+
const helloWorldActor = createActor(canisterId, {
117+
agentOptions: {
118+
rootKey: !import.meta.env.DEV ? canisterEnv.IC_ROOT_KEY : undefined,
119+
shouldFetchRootKey: import.meta.env.DEV,
120+
},
121+
});
122+
123+
// Now you can call the backend canister methods
124+
console.log(helloWorldActor.greet("World"));
125+
```
126+
127+
This avoids having to pass environment variables to the actor at build time or fetching them before instantiating it.

docs/src/content/docs/library-development.mdx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
---
22
title: Library Development
33
description: Guide for building libraries that depend on @icp-sdk/core
4-
sidebar:
5-
order: 4
4+
prev: false
5+
next:
6+
label: Agent Module
67
---
78

89
If you're building a library that depends on `@icp-sdk/core`, there are some specific considerations to ensure your library works correctly with bundlers and provides the best experience for your users.

docs/src/content/docs/quick-start.mdx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ This is the most common way to use the `@icp-sdk/core` package to interact with
2121
import { Principal } from "@icp-sdk/core/principal";
2222
import { createActor } from "./api/hello-world";
2323

24+
// For a convenient way to get the canister ID,
25+
// see the https://js.icp.build/core/latest/canister-environment/ guide.
2426
const canisterId = Principal.fromText('uqqxf-5h777-77774-qaaaa-cai');
2527
const identity = Ed25519KeyIdentity.generate();
2628

@@ -81,6 +83,8 @@ import { IDL } from '@icp-sdk/core/candid';
8183
import { Principal } from '@icp-sdk/core/principal';
8284

8385
const identity = Ed25519KeyIdentity.generate();
86+
// For a convenient way to get the canister ID,
87+
// see the https://js.icp.build/core/latest/canister-environment/ guide.
8488
const canisterId = Principal.fromText('uqqxf-5h777-77774-qaaaa-cai');
8589

8690
const agent = await HttpAgent.create({
@@ -101,5 +105,6 @@ If you are using TypeScript, have a look at the [TypeScript guide](./typescript.
101105

102106
## Next Steps
103107

104-
- Have a look at the [Examples repo](https://github.com/dfinity/examples) to see how to use the `@icp-sdk/core` package in a real-world application.
108+
- Have a look at the [Canister Environment guide](./canister-environment.mdx) for how to load the canister environment from the asset canister.
105109
- Have a look at the [@icp-sdk/bindgen](https://js.icp.build/bindgen/) package for how to generate the bindings for your canister.
110+
- Have a look at the [Examples repo](https://github.com/dfinity/examples) to see how to use the `@icp-sdk/core` package in a real-world application.

docs/src/content/docs/typescript.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
title: Typescript
33
description: Typescript guide for the @icp-sdk/core package.
4+
prev: false
45
next:
56
label: Agent Module
67
---

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"@size-limit/preset-small-lib": "^11.1.6",
1414
"@tsconfig/node18": "^18.2.4",
1515
"@types/jest": "^29.5.14",
16+
"@types/jsdom": "^27.0.0",
1617
"@types/node": "^20.8.6",
1718
"@types/text-encoding": "^0.0.40",
1819
"@typescript-eslint/eslint-plugin": "^8.32.1",

packages/agent/package.json

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,17 @@
3737
"default": "./lib/esm/index.js",
3838
"types": "./lib/esm/index.d.ts",
3939
"exports": {
40-
"types": "./lib/esm/index.d.ts",
41-
"import": "./lib/esm/index.js",
42-
"node": "./lib/cjs/index.js",
43-
"require": "./lib/cjs/index.js",
44-
"default": "./lib/esm/index.js"
40+
".": {
41+
"types": "./lib/esm/index.d.ts",
42+
"import": "./lib/esm/index.js",
43+
"node": "./lib/cjs/index.js",
44+
"require": "./lib/cjs/index.js",
45+
"default": "./lib/esm/index.js"
46+
},
47+
"./canister-env": {
48+
"types": "./lib/esm/canister-env/index.d.ts",
49+
"import": "./lib/esm/canister-env/index.js"
50+
}
4551
},
4652
"scripts": {
4753
"build": "tsc -b && tsc -p tsconfig.cjs.json && cp src/package.json lib/esm/package.json",

packages/agent/src/actor.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,13 +240,16 @@ export class Actor {
240240
* @param interfaceFactory - the interface factory for the actor, typically generated by the [`@icp-sdk/bindgen`](https://js.icp.build/bindgen/) package
241241
* @param configuration - the configuration for the actor
242242
* @returns an actor with the given interface factory and configuration
243+
* @see The {@link https://js.icp.build/core/latest/canister-environment/ | Canister Environment Guide} for more details on how to configure an actor using the canister environment.
243244
* @example
244245
* Using the interface factory generated by the [`@icp-sdk/bindgen`](https://js.icp.build/bindgen/) package:
245246
* ```ts
246247
* import { Actor, HttpAgent } from '@icp-sdk/core/agent';
247248
* import { Principal } from '@icp-sdk/core/principal';
248249
* import { idlFactory } from './api/declarations/hello-world.did';
249250
*
251+
* // For a convenient way to get the canister ID,
252+
* // see the https://js.icp.build/core/latest/canister-environment/ guide.
250253
* const canisterId = Principal.fromText('rrkah-fqaaa-aaaaa-aaaaq-cai');
251254
*
252255
* const agent = await HttpAgent.create({
@@ -268,6 +271,8 @@ export class Actor {
268271
* import { Principal } from '@icp-sdk/core/principal';
269272
* import { createActor } from './api/hello-world';
270273
*
274+
* // For a convenient way to get the canister ID,
275+
* // see the https://js.icp.build/core/latest/canister-environment/ guide.
271276
* const canisterId = Principal.fromText('rrkah-fqaaa-aaaaa-aaaaq-cai');
272277
*
273278
* const agent = await HttpAgent.create({

0 commit comments

Comments
 (0)