@calimero-network/agent-skills 0.1.1 → 0.3.0
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/SKILL.md +7 -2
- package/package.json +2 -2
- package/skills/calimero-client-js/SKILL.md +58 -5
- package/skills/calimero-client-js/references/auth.md +141 -36
- package/skills/calimero-client-js/references/rpc-calls.md +121 -30
- package/skills/calimero-client-js/references/sso.md +17 -12
- package/skills/calimero-client-js/references/websocket-events.md +129 -36
- package/skills/calimero-client-js/rules/token-refresh.md +59 -21
- package/skills/calimero-client-py/SKILL.md +4 -0
- package/skills/calimero-desktop/SKILL.md +7 -1
- package/skills/calimero-desktop/references/sso-integration.md +49 -22
- package/skills/calimero-node/SKILL.md +55 -11
- package/skills/calimero-node/references/meroctl-commands.md +139 -36
- package/skills/calimero-rust-sdk/SKILL.md +54 -16
- package/skills/calimero-rust-sdk/rules/state-derives.md +46 -0
- package/skills/calimero-sdk-js/SKILL.md +137 -0
- package/skills/calimero-sdk-js/references/build-pipeline.md +98 -0
- package/skills/calimero-sdk-js/references/collections.md +132 -0
- package/skills/calimero-sdk-js/references/events.md +59 -0
- package/skills/calimero-sdk-js/rules/crdt-only-state.md +47 -0
- package/skills/calimero-sdk-js/rules/no-console-log.md +38 -0
- package/skills/calimero-sdk-js/rules/view-decorator.md +46 -0
package/SKILL.md
CHANGED
|
@@ -6,8 +6,9 @@ This package provides AI agent skills for building on the Calimero Network stack
|
|
|
6
6
|
|
|
7
7
|
| Skill | Install command | When to use |
|
|
8
8
|
| --- | --- | --- |
|
|
9
|
-
| `calimero-rust-sdk` | `npx @calimero-network/agent-skills calimero-rust-sdk` | Building Rust WASM applications |
|
|
10
|
-
| `calimero-
|
|
9
|
+
| `calimero-rust-sdk` | `npx @calimero-network/agent-skills calimero-rust-sdk` | Building Rust WASM applications that run on a node |
|
|
10
|
+
| `calimero-sdk-js` | `npx @calimero-network/agent-skills calimero-sdk-js` | Building TypeScript/JS WASM applications that run on a node |
|
|
11
|
+
| `calimero-client-js` | `npx @calimero-network/agent-skills calimero-client-js` | Browser/Node.js frontends connecting to a node (not building apps) |
|
|
11
12
|
| `calimero-registry` | `npx @calimero-network/agent-skills calimero-registry` | Signing and publishing apps to the registry |
|
|
12
13
|
| `calimero-desktop` | `npx @calimero-network/agent-skills calimero-desktop` | Integrating apps with Calimero Desktop SSO |
|
|
13
14
|
| `calimero-node` | `npx @calimero-network/agent-skills calimero-node` | Node operators and meroctl scripting |
|
|
@@ -22,11 +23,15 @@ Skills should be loaded when the following are detected in the project:
|
|
|
22
23
|
| Signal | Load skill |
|
|
23
24
|
| --- | --- |
|
|
24
25
|
| `calimero-sdk` in `Cargo.toml` | `calimero-rust-sdk` |
|
|
26
|
+
| `@calimero-network/calimero-sdk-js` in `package.json` | `calimero-sdk-js` |
|
|
27
|
+
| `@State` / `@Logic` decorators in TypeScript source | `calimero-sdk-js` |
|
|
28
|
+
| `calimero-sdk build` in any script | `calimero-sdk-js` |
|
|
25
29
|
| `@calimero-network/calimero-client` in `package.json` | `calimero-client-js` |
|
|
26
30
|
| `@calimero-network/mero-js` in `package.json` | `calimero-client-js` |
|
|
27
31
|
| `mero-sign` in any script or Makefile | `calimero-registry` |
|
|
28
32
|
| `calimero-registry` CLI usage | `calimero-registry` |
|
|
29
33
|
| `access_token` read from `window.location.hash` | `calimero-desktop` |
|
|
34
|
+
| `readDesktopSSO` / `hash.get('access_token')` pattern in frontend code | `calimero-desktop` |
|
|
30
35
|
| `merobox` in `package.json` or `requirements.txt` | `calimero-merobox` |
|
|
31
36
|
| `calimero-client-py` in `requirements.txt` or `pyproject.toml` | `calimero-client-py` |
|
|
32
37
|
| `calimero-abi-codegen` or `abi.json` in project | `calimero-abi-codegen` |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@calimero-network/agent-skills",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "AI agent skills for Calimero Network development — Rust SDK, JS client, registry publishing, desktop SSO, and more.",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "AI agent skills for Calimero Network development — Rust SDK, JS SDK, JS client, registry publishing, desktop SSO, and more.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"calimero",
|
|
7
7
|
"agent-skills",
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# calimero-client-js — Agent Instructions
|
|
2
2
|
|
|
3
|
-
You are helping a developer connect a **browser or Node.js frontend** to a Calimero node
|
|
3
|
+
You are helping a developer connect a **browser or Node.js frontend** to a Calimero node
|
|
4
|
+
using `@calimero-network/calimero-client` or `@calimero-network/mero-js`.
|
|
5
|
+
|
|
6
|
+
> **NOT this skill** if the developer is building the application logic that *runs on the
|
|
7
|
+
> node* in TypeScript — that is `calimero-sdk-js` (`@calimero-network/calimero-sdk-js`).
|
|
8
|
+
> This skill is for the *client* side: auth, RPC calls, and WebSocket subscriptions from
|
|
9
|
+
> a browser or backend service.
|
|
4
10
|
|
|
5
11
|
## Package versions
|
|
6
12
|
|
|
@@ -9,6 +15,10 @@ You are helping a developer connect a **browser or Node.js frontend** to a Calim
|
|
|
9
15
|
| `@calimero-network/calimero-client` | latest | Stable client for browser/Node — auth, RPC, WebSocket |
|
|
10
16
|
| `@calimero-network/mero-js` | `>=2.0.0-beta.1` | v2 API — all request fields are **camelCase** |
|
|
11
17
|
|
|
18
|
+
**Which to use:** new projects should prefer `@calimero-network/mero-js` v2. If you are
|
|
19
|
+
maintaining an existing codebase that uses `calimero-client`, check for snake_case field
|
|
20
|
+
names before migrating — do not mix both packages in the same project.
|
|
21
|
+
|
|
12
22
|
## Critical: mero-js v2 uses camelCase
|
|
13
23
|
|
|
14
24
|
v2 changed all request field names from `snake_case` to `camelCase`.
|
|
@@ -33,10 +43,53 @@ pnpm add @calimero-network/mero-js
|
|
|
33
43
|
|
|
34
44
|
## Core workflow
|
|
35
45
|
|
|
36
|
-
1.
|
|
37
|
-
2.
|
|
38
|
-
3. Call app methods via
|
|
39
|
-
4. Subscribe to events via
|
|
46
|
+
1. On startup: read SSO tokens from URL hash (if opened by Desktop), otherwise check `localStorage` for existing session, otherwise show login
|
|
47
|
+
2. Store tokens using the provided storage helpers (`setAppEndpointKey`, `setAccessToken`, etc.)
|
|
48
|
+
3. Call app methods via the `rpcClient` singleton using `rpcClient.execute()`
|
|
49
|
+
4. Subscribe to events via `WsSubscriptionsClient`
|
|
50
|
+
|
|
51
|
+
## Minimal working example
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import {
|
|
55
|
+
rpcClient,
|
|
56
|
+
getContextId,
|
|
57
|
+
getExecutorPublicKey,
|
|
58
|
+
getAppEndpointKey,
|
|
59
|
+
setAppEndpointKey,
|
|
60
|
+
setAccessToken,
|
|
61
|
+
setRefreshToken,
|
|
62
|
+
setContextAndIdentityFromJWT,
|
|
63
|
+
WsSubscriptionsClient,
|
|
64
|
+
} from '@calimero-network/calimero-client';
|
|
65
|
+
|
|
66
|
+
// 1. Store auth tokens (after SSO or login)
|
|
67
|
+
setAppEndpointKey('http://localhost:2428');
|
|
68
|
+
setAccessToken(accessToken);
|
|
69
|
+
setRefreshToken(refreshToken);
|
|
70
|
+
setContextAndIdentityFromJWT(accessToken); // extracts contextId + executorPublicKey
|
|
71
|
+
|
|
72
|
+
// 2. Call an app method
|
|
73
|
+
const response = await rpcClient.execute<{ key: string }, string | null>({
|
|
74
|
+
contextId: getContextId()!,
|
|
75
|
+
method: 'get',
|
|
76
|
+
argsJson: { key: 'hello' },
|
|
77
|
+
executorPublicKey: getExecutorPublicKey()!,
|
|
78
|
+
});
|
|
79
|
+
console.log(response.result?.output);
|
|
80
|
+
|
|
81
|
+
// 3. Subscribe to real-time events
|
|
82
|
+
const ws = new WsSubscriptionsClient(getAppEndpointKey()!, '/ws');
|
|
83
|
+
await ws.connect();
|
|
84
|
+
ws.subscribe([getContextId()!]);
|
|
85
|
+
ws.addCallback((event) => {
|
|
86
|
+
if (event.type === 'ExecutionEvent') {
|
|
87
|
+
for (const e of event.data.events) {
|
|
88
|
+
console.log(e.kind, e.data);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
```
|
|
40
93
|
|
|
41
94
|
## References
|
|
42
95
|
|
|
@@ -1,58 +1,163 @@
|
|
|
1
1
|
# Authentication
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Storage helpers
|
|
4
|
+
|
|
5
|
+
`@calimero-network/calimero-client` provides these storage functions (backed by `localStorage`):
|
|
4
6
|
|
|
5
7
|
```typescript
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
8
|
+
import {
|
|
9
|
+
// Node URL
|
|
10
|
+
setAppEndpointKey,
|
|
11
|
+
getAppEndpointKey,
|
|
12
|
+
// JWT tokens
|
|
13
|
+
setAccessToken,
|
|
14
|
+
getAccessToken,
|
|
15
|
+
setRefreshToken,
|
|
16
|
+
getRefreshToken,
|
|
17
|
+
// Context and identity (extracted from JWT)
|
|
18
|
+
setContextId,
|
|
19
|
+
getContextId,
|
|
20
|
+
setExecutorPublicKey,
|
|
21
|
+
getExecutorPublicKey,
|
|
22
|
+
setContextAndIdentityFromJWT, // extracts + stores contextId + executorPublicKey from JWT
|
|
23
|
+
// App ID
|
|
24
|
+
setApplicationId,
|
|
25
|
+
getApplicationId,
|
|
26
|
+
// Auth endpoint (separate from node URL)
|
|
27
|
+
setAuthEndpointURL,
|
|
28
|
+
getAuthEndpointURL,
|
|
29
|
+
// Decoded JWT payload
|
|
30
|
+
getJWTObject, // returns { context_id, context_identity, exp, permissions, ... }
|
|
31
|
+
// Full auth config (throws if required fields are missing)
|
|
32
|
+
getAuthConfig, // returns { appEndpointKey, contextId, executorPublicKey, jwtToken }
|
|
33
|
+
// Logout (clears all tokens + reloads)
|
|
34
|
+
clientLogout,
|
|
35
|
+
} from '@calimero-network/calimero-client';
|
|
26
36
|
```
|
|
27
37
|
|
|
28
|
-
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## SSO login (from Calimero Desktop — recommended path)
|
|
29
41
|
|
|
30
|
-
|
|
31
|
-
them automatically if you use the provided storage helpers:
|
|
42
|
+
When the app is opened by Desktop, tokens arrive in the URL hash. Store them:
|
|
32
43
|
|
|
33
44
|
```typescript
|
|
34
|
-
|
|
45
|
+
function initFromDesktopSSO(): boolean {
|
|
46
|
+
const hash = new URLSearchParams(window.location.hash.slice(1));
|
|
47
|
+
const accessToken = hash.get('access_token');
|
|
48
|
+
const refreshToken = hash.get('refresh_token');
|
|
49
|
+
const nodeUrl = hash.get('node_url');
|
|
50
|
+
const appId = hash.get('application_id');
|
|
35
51
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
//
|
|
52
|
+
if (!accessToken || !nodeUrl) return false;
|
|
53
|
+
|
|
54
|
+
// Store everything
|
|
55
|
+
setAppEndpointKey(nodeUrl);
|
|
56
|
+
setAccessToken(accessToken);
|
|
57
|
+
if (refreshToken) setRefreshToken(refreshToken);
|
|
58
|
+
if (appId) setApplicationId(appId);
|
|
59
|
+
|
|
60
|
+
// Extract contextId and executorPublicKey from JWT claims
|
|
61
|
+
setContextAndIdentityFromJWT(accessToken);
|
|
62
|
+
|
|
63
|
+
// Remove tokens from URL bar (don't let them sit in browser history)
|
|
64
|
+
history.replaceState(null, '', window.location.pathname + window.location.search);
|
|
65
|
+
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
39
68
|
```
|
|
40
69
|
|
|
41
|
-
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Manual login (no Desktop)
|
|
42
73
|
|
|
43
74
|
```typescript
|
|
44
|
-
import {
|
|
75
|
+
import { setAppEndpointKey, setAccessToken, setRefreshToken, setContextAndIdentityFromJWT } from '@calimero-network/calimero-client';
|
|
45
76
|
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
|
|
77
|
+
async function login(nodeUrl: string, accessToken: string, refreshToken: string) {
|
|
78
|
+
setAppEndpointKey(nodeUrl);
|
|
79
|
+
setAccessToken(accessToken);
|
|
80
|
+
setRefreshToken(refreshToken);
|
|
81
|
+
setContextAndIdentityFromJWT(accessToken);
|
|
49
82
|
}
|
|
50
83
|
```
|
|
51
84
|
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Checking if authenticated
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { getAuthConfig } from '@calimero-network/calimero-client';
|
|
91
|
+
|
|
92
|
+
function isAuthenticated(): boolean {
|
|
93
|
+
const config = getAuthConfig();
|
|
94
|
+
return config.error === null;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Reading the JWT payload
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { getJWTObject } from '@calimero-network/calimero-client';
|
|
104
|
+
|
|
105
|
+
const jwt = getJWTObject();
|
|
106
|
+
// jwt.context_id — the context this token is scoped to
|
|
107
|
+
// jwt.context_identity — the identity (executor public key)
|
|
108
|
+
// jwt.exp — expiry timestamp (seconds)
|
|
109
|
+
// jwt.permissions — array of permission strings
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
52
114
|
## Logout
|
|
53
115
|
|
|
54
116
|
```typescript
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
//
|
|
117
|
+
import { clientLogout } from '@calimero-network/calimero-client';
|
|
118
|
+
|
|
119
|
+
// Clears all tokens from localStorage and reloads the page
|
|
120
|
+
clientLogout();
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## App startup pattern (SSO + fallback)
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import {
|
|
129
|
+
setAppEndpointKey, setAccessToken, setRefreshToken,
|
|
130
|
+
setApplicationId, setContextAndIdentityFromJWT,
|
|
131
|
+
getAuthConfig,
|
|
132
|
+
} from '@calimero-network/calimero-client';
|
|
133
|
+
|
|
134
|
+
async function bootstrap() {
|
|
135
|
+
// Try SSO from Desktop hash
|
|
136
|
+
const hash = new URLSearchParams(window.location.hash.slice(1));
|
|
137
|
+
const accessToken = hash.get('access_token');
|
|
138
|
+
const nodeUrl = hash.get('node_url');
|
|
139
|
+
|
|
140
|
+
if (accessToken && nodeUrl) {
|
|
141
|
+
setAppEndpointKey(nodeUrl);
|
|
142
|
+
setAccessToken(accessToken);
|
|
143
|
+
const rt = hash.get('refresh_token');
|
|
144
|
+
if (rt) setRefreshToken(rt);
|
|
145
|
+
const appId = hash.get('application_id');
|
|
146
|
+
if (appId) setApplicationId(appId);
|
|
147
|
+
setContextAndIdentityFromJWT(accessToken);
|
|
148
|
+
history.replaceState(null, '', window.location.pathname);
|
|
149
|
+
renderApp();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check if already authenticated from a previous session
|
|
154
|
+
const config = getAuthConfig();
|
|
155
|
+
if (config.error === null) {
|
|
156
|
+
renderApp();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Not authenticated — show manual login
|
|
161
|
+
renderLogin();
|
|
162
|
+
}
|
|
58
163
|
```
|
|
@@ -2,74 +2,165 @@
|
|
|
2
2
|
|
|
3
3
|
Calling application methods on a Calimero node.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## The `rpcClient` singleton
|
|
6
|
+
|
|
7
|
+
`@calimero-network/calimero-client` exports a pre-configured `rpcClient` singleton.
|
|
8
|
+
Import it directly — do not construct `JsonRpcClient` manually.
|
|
6
9
|
|
|
7
10
|
```typescript
|
|
8
11
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
rpcClient,
|
|
13
|
+
getContextId,
|
|
14
|
+
getExecutorPublicKey,
|
|
12
15
|
} from '@calimero-network/calimero-client';
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Execute a method (mutation or view — same call)
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
const
|
|
22
|
+
```typescript
|
|
23
|
+
const response = await rpcClient.execute<ArgsType, OutputType>({
|
|
24
|
+
contextId: getContextId()!,
|
|
25
|
+
method: 'methodName',
|
|
26
|
+
argsJson: { /* your args */ },
|
|
27
|
+
executorPublicKey: getExecutorPublicKey()!,
|
|
28
|
+
});
|
|
16
29
|
|
|
17
|
-
|
|
30
|
+
if (response.error) {
|
|
31
|
+
console.error(response.error.error.cause.info?.message);
|
|
32
|
+
} else {
|
|
33
|
+
console.log(response.result?.output);
|
|
34
|
+
}
|
|
18
35
|
```
|
|
19
36
|
|
|
37
|
+
---
|
|
38
|
+
|
|
20
39
|
## Calling a mutation (changes state)
|
|
21
40
|
|
|
22
41
|
```typescript
|
|
23
|
-
const response = await
|
|
24
|
-
contextId:
|
|
42
|
+
const response = await rpcClient.execute<{ key: string; value: string }, void>({
|
|
43
|
+
contextId: getContextId()!,
|
|
25
44
|
method: 'set',
|
|
26
45
|
argsJson: { key: 'hello', value: 'world' },
|
|
27
|
-
executorPublicKey:
|
|
46
|
+
executorPublicKey: getExecutorPublicKey()!,
|
|
28
47
|
});
|
|
29
48
|
|
|
30
49
|
if (response.error) {
|
|
31
|
-
console.error(response.error);
|
|
50
|
+
console.error('set failed:', response.error.error.cause.info?.message);
|
|
32
51
|
}
|
|
33
52
|
```
|
|
34
53
|
|
|
35
54
|
## Calling a view (read-only)
|
|
36
55
|
|
|
37
56
|
```typescript
|
|
38
|
-
const response = await
|
|
39
|
-
contextId:
|
|
57
|
+
const response = await rpcClient.execute<{ key: string }, string | null>({
|
|
58
|
+
contextId: getContextId()!,
|
|
40
59
|
method: 'get',
|
|
41
60
|
argsJson: { key: 'hello' },
|
|
42
|
-
executorPublicKey:
|
|
61
|
+
executorPublicKey: getExecutorPublicKey()!,
|
|
43
62
|
});
|
|
44
63
|
|
|
45
|
-
|
|
64
|
+
if (!response.error) {
|
|
65
|
+
console.log(response.result?.output); // "world"
|
|
66
|
+
}
|
|
46
67
|
```
|
|
47
68
|
|
|
69
|
+
---
|
|
70
|
+
|
|
48
71
|
## Response shape
|
|
49
72
|
|
|
50
73
|
```typescript
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
74
|
+
// Success:
|
|
75
|
+
{ result: { output: T } }
|
|
76
|
+
|
|
77
|
+
// Error:
|
|
78
|
+
{
|
|
79
|
+
error: {
|
|
80
|
+
id: number;
|
|
81
|
+
jsonrpc: string;
|
|
82
|
+
code: number; // HTTP-like code (400, 401, 500)
|
|
83
|
+
error: {
|
|
84
|
+
name: string; // e.g. "FunctionCallError"
|
|
85
|
+
cause: {
|
|
86
|
+
name: string;
|
|
87
|
+
info?: { message: string };
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
headers?: Record<string, string>;
|
|
91
|
+
}
|
|
59
92
|
}
|
|
60
93
|
```
|
|
61
94
|
|
|
62
|
-
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Error names
|
|
98
|
+
|
|
99
|
+
| name | meaning |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `FunctionCallError` | App method returned an error |
|
|
102
|
+
| `RpcExecutionError` | Node couldn't execute the method |
|
|
103
|
+
| `InvalidRequestError` | Malformed request (wrong context-id, missing args) |
|
|
104
|
+
| `AuthenticationError` | JWT expired, revoked, or missing |
|
|
105
|
+
| `UnknownServerError` | Unexpected server error |
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Error handling with 401
|
|
110
|
+
|
|
111
|
+
The HTTP client inside `rpcClient` automatically refreshes tokens on `401 token_expired`.
|
|
112
|
+
For `token_revoked` or `invalid_token` you need to re-authenticate:
|
|
63
113
|
|
|
64
114
|
```typescript
|
|
65
|
-
const response = await
|
|
115
|
+
const response = await rpcClient.execute({ ... });
|
|
66
116
|
|
|
67
117
|
if (response.error) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
118
|
+
const { code, headers } = response.error;
|
|
119
|
+
const authError = headers?.['x-auth-error'];
|
|
120
|
+
|
|
121
|
+
if (code === 401 && (authError === 'token_revoked' || authError === 'invalid_token')) {
|
|
122
|
+
// Token cannot be refreshed — send user to login
|
|
123
|
+
clientLogout();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
throw new Error(response.error.error.cause.info?.message ?? 'Unknown error');
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Complete example: typed wrapper
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
import {
|
|
137
|
+
rpcClient,
|
|
138
|
+
getContextId,
|
|
139
|
+
getExecutorPublicKey,
|
|
140
|
+
} from '@calimero-network/calimero-client';
|
|
141
|
+
|
|
142
|
+
async function setItem(key: string, value: string): Promise<void> {
|
|
143
|
+
const response = await rpcClient.execute<{ key: string; value: string }, void>({
|
|
144
|
+
contextId: getContextId()!,
|
|
145
|
+
method: 'set',
|
|
146
|
+
argsJson: { key, value },
|
|
147
|
+
executorPublicKey: getExecutorPublicKey()!,
|
|
148
|
+
});
|
|
149
|
+
if (response.error) {
|
|
150
|
+
throw new Error(response.error.error.cause.info?.message);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function getItem(key: string): Promise<string | null> {
|
|
155
|
+
const response = await rpcClient.execute<{ key: string }, string | null>({
|
|
156
|
+
contextId: getContextId()!,
|
|
157
|
+
method: 'get',
|
|
158
|
+
argsJson: { key },
|
|
159
|
+
executorPublicKey: getExecutorPublicKey()!,
|
|
160
|
+
});
|
|
161
|
+
if (response.error) {
|
|
162
|
+
throw new Error(response.error.error.cause.info?.message);
|
|
72
163
|
}
|
|
73
|
-
|
|
164
|
+
return response.result?.output ?? null;
|
|
74
165
|
}
|
|
75
166
|
```
|
|
@@ -34,18 +34,29 @@ function readSSOParams(): {
|
|
|
34
34
|
|
|
35
35
|
## Using SSO tokens on app startup
|
|
36
36
|
|
|
37
|
+
Use the storage helpers from `@calimero-network/calimero-client`:
|
|
38
|
+
|
|
37
39
|
```typescript
|
|
40
|
+
import {
|
|
41
|
+
setAppEndpointKey,
|
|
42
|
+
setAccessToken,
|
|
43
|
+
setRefreshToken,
|
|
44
|
+
setApplicationId,
|
|
45
|
+
setContextAndIdentityFromJWT,
|
|
46
|
+
} from '@calimero-network/calimero-client';
|
|
47
|
+
|
|
38
48
|
async function initApp() {
|
|
39
49
|
const sso = readSSOParams();
|
|
40
50
|
|
|
41
51
|
if (sso.accessToken && sso.nodeUrl) {
|
|
42
52
|
// Opened from Desktop — store tokens and skip login
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
setAppEndpointKey(sso.nodeUrl);
|
|
54
|
+
setAccessToken(sso.accessToken);
|
|
55
|
+
if (sso.refreshToken) setRefreshToken(sso.refreshToken);
|
|
56
|
+
if (sso.applicationId) setApplicationId(sso.applicationId);
|
|
57
|
+
// Extract contextId + executorPublicKey from the JWT claims
|
|
58
|
+
setContextAndIdentityFromJWT(sso.accessToken);
|
|
59
|
+
// Clear hash so tokens aren't in browser history
|
|
49
60
|
history.replaceState(null, '', window.location.pathname);
|
|
50
61
|
renderApp();
|
|
51
62
|
} else {
|
|
@@ -55,12 +66,6 @@ async function initApp() {
|
|
|
55
66
|
}
|
|
56
67
|
```
|
|
57
68
|
|
|
58
|
-
## calimero-client reads hash automatically
|
|
59
|
-
|
|
60
|
-
If you use `@calimero-network/calimero-client`'s built-in auth helpers, the library
|
|
61
|
-
reads `window.location.hash` automatically and stores the tokens. You don't need the
|
|
62
|
-
manual parsing above unless you're building a custom auth flow.
|
|
63
|
-
|
|
64
69
|
## Important
|
|
65
70
|
|
|
66
71
|
Always fall back to manual login if the hash params are absent — the app must work
|