@calimero-network/agent-skills 0.2.0 → 0.4.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/README.md +137 -17
- package/SKILL.md +31 -23
- package/package.json +2 -2
- package/scripts/install.js +3 -3
- package/scripts/test.js +6 -15
- package/skills/calimero-abi-codegen/SKILL.md +121 -22
- package/skills/calimero-abi-codegen/references/abi-format.md +3 -5
- package/skills/calimero-abi-codegen/references/generated-output.md +12 -4
- package/skills/calimero-abi-codegen/rules/schema-version.md +11 -4
- package/skills/calimero-abi-codegen/rules/unique-names.md +2 -6
- package/skills/calimero-client-js/SKILL.md +127 -22
- package/skills/calimero-client-js/references/auth.md +18 -10
- package/skills/calimero-client-js/references/rpc-calls.md +15 -21
- package/skills/calimero-client-js/references/sso.md +9 -9
- package/skills/calimero-client-js/references/websocket-events.md +73 -59
- package/skills/calimero-client-js/rules/camelcase-api.md +10 -7
- package/skills/calimero-client-js/rules/token-refresh.md +59 -21
- package/skills/calimero-client-py/SKILL.md +26 -10
- package/skills/calimero-client-py/references/api.md +41 -43
- package/skills/calimero-client-py/references/auth.md +7 -7
- package/skills/calimero-client-py/rules/async-usage.md +27 -31
- package/skills/calimero-client-py/rules/stable-node-name.md +7 -7
- package/skills/calimero-core/SKILL.md +135 -0
- package/skills/calimero-core/references/architecture.md +101 -0
- package/skills/calimero-core/references/jsonrpc-protocol.md +192 -0
- package/skills/calimero-core/references/namespaces-groups.md +94 -0
- package/skills/calimero-core/references/storage-types.md +118 -0
- package/skills/calimero-core/references/websocket-events.md +142 -0
- package/skills/calimero-core/rules/context-is-not-app.md +35 -0
- package/skills/calimero-core/rules/crdt-types-only.md +55 -0
- package/skills/calimero-desktop/SKILL.md +25 -14
- package/skills/calimero-desktop/references/sso-integration.md +49 -22
- package/skills/calimero-desktop/rules/sso-fallback.md +3 -2
- package/skills/calimero-merobox/SKILL.md +255 -28
- package/skills/calimero-merobox/references/ci-integration.md +3 -2
- package/skills/calimero-merobox/references/workflow-files.md +7 -5
- package/skills/calimero-merobox/rules/docker-required.md +7 -6
- package/skills/calimero-meroctl/SKILL.md +68 -0
- package/skills/calimero-meroctl/references/commands.md +177 -0
- package/skills/calimero-meroctl/references/scripting.md +80 -0
- package/skills/calimero-meroctl/rules/call-view-flag.md +28 -0
- package/skills/calimero-meroctl/rules/register-node-once.md +34 -0
- package/skills/calimero-merod/SKILL.md +49 -0
- package/skills/calimero-merod/references/health-endpoints.md +90 -0
- package/skills/calimero-merod/references/init-flags.md +84 -0
- package/skills/calimero-merod/rules/init-before-run.md +40 -0
- package/skills/calimero-merod/rules/port-assignments.md +33 -0
- package/skills/calimero-node/SKILL.md +52 -35
- package/skills/calimero-node/references/context-lifecycle.md +34 -17
- package/skills/calimero-node/references/meroctl-commands.md +89 -99
- package/skills/calimero-node/rules/app-vs-context.md +4 -4
- package/skills/calimero-registry/SKILL.md +110 -31
- package/skills/calimero-registry/references/bundle-and-push.md +99 -34
- package/skills/calimero-registry/references/manifest-format.md +56 -35
- package/skills/calimero-registry/references/mero-sign.md +10 -9
- package/skills/calimero-registry/rules/key-security.md +3 -2
- package/skills/calimero-registry/rules/sign-before-pack.md +5 -5
- package/skills/calimero-rust-sdk/SKILL.md +154 -44
- package/skills/calimero-rust-sdk/references/blob-api.md +119 -0
- package/skills/calimero-rust-sdk/references/event-handlers.md +122 -0
- package/skills/calimero-rust-sdk/references/events.md +2 -1
- package/skills/calimero-rust-sdk/references/examples.md +81 -29
- package/skills/calimero-rust-sdk/references/migrations.md +123 -0
- package/skills/calimero-rust-sdk/references/nested-crdts.md +113 -0
- package/skills/calimero-rust-sdk/references/private-storage.md +76 -34
- package/skills/calimero-rust-sdk/references/state-collections.md +106 -21
- package/skills/calimero-rust-sdk/references/user-and-frozen-storage.md +169 -0
- package/skills/calimero-rust-sdk/rules/app-macro-placement.md +5 -2
- package/skills/calimero-rust-sdk/rules/no-std-collections.md +5 -2
- package/skills/calimero-rust-sdk/rules/state-derives.md +45 -0
- package/skills/calimero-rust-sdk/rules/wasm-constraints.md +12 -10
- package/skills/calimero-sdk-js/SKILL.md +145 -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 +63 -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 +48 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Running `calimero-abi-codegen -i abi.json -o src/generated --client-name KvStoreClient` produces:
|
|
4
4
|
|
|
5
|
-
```
|
|
5
|
+
```text
|
|
6
6
|
src/generated/
|
|
7
7
|
├── types.ts
|
|
8
8
|
└── KvStoreClient.ts
|
|
@@ -36,7 +36,10 @@ import { CalimeroApp, Context } from '@calimero-network/calimero-client';
|
|
|
36
36
|
import type { Entry } from './types';
|
|
37
37
|
|
|
38
38
|
export class KvStoreClient {
|
|
39
|
-
constructor(
|
|
39
|
+
constructor(
|
|
40
|
+
private app: CalimeroApp,
|
|
41
|
+
private context: Context
|
|
42
|
+
) {}
|
|
40
43
|
|
|
41
44
|
async set(key: string, value: string): Promise<void> {
|
|
42
45
|
await this.app.mutate(this.context, 'set', { key, value });
|
|
@@ -55,7 +58,12 @@ export class KvStoreClient {
|
|
|
55
58
|
## Using the generated client
|
|
56
59
|
|
|
57
60
|
```typescript
|
|
58
|
-
import {
|
|
61
|
+
import {
|
|
62
|
+
CalimeroApp,
|
|
63
|
+
Context,
|
|
64
|
+
getJWTObject,
|
|
65
|
+
getStorageAppEndpointKey,
|
|
66
|
+
} from '@calimero-network/calimero-client';
|
|
59
67
|
import { KvStoreClient } from './generated/KvStoreClient';
|
|
60
68
|
|
|
61
69
|
const app = new CalimeroApp(getStorageAppEndpointKey(), getJWTObject()?.access_token);
|
|
@@ -65,7 +73,7 @@ const client = new KvStoreClient(app, context);
|
|
|
65
73
|
// Fully typed — IDE autocomplete works
|
|
66
74
|
await client.set('hello', 'world');
|
|
67
75
|
const value = await client.get('hello'); // string | null
|
|
68
|
-
const all = await client.entries();
|
|
76
|
+
const all = await client.entries(); // Entry[]
|
|
69
77
|
```
|
|
70
78
|
|
|
71
79
|
## Regenerating after app changes
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
# Rule: ABI manifest must have schema_version "wasm-abi/1"
|
|
2
2
|
|
|
3
|
-
The codegen tool rejects any ABI file that doesn't have the exact string `"wasm-abi/1"` as its
|
|
3
|
+
The codegen tool rejects any ABI file that doesn't have the exact string `"wasm-abi/1"` as its
|
|
4
|
+
`schema_version`.
|
|
4
5
|
|
|
5
6
|
## WRONG:
|
|
6
7
|
|
|
7
8
|
```json
|
|
8
9
|
{
|
|
9
10
|
"schema_version": "1",
|
|
10
|
-
"types": {},
|
|
11
|
+
"types": {},
|
|
12
|
+
"methods": [],
|
|
13
|
+
"events": []
|
|
11
14
|
}
|
|
12
15
|
```
|
|
13
16
|
|
|
14
17
|
```json
|
|
15
18
|
{
|
|
16
|
-
"types": {},
|
|
19
|
+
"types": {},
|
|
20
|
+
"methods": [],
|
|
21
|
+
"events": []
|
|
17
22
|
}
|
|
18
23
|
```
|
|
19
24
|
|
|
@@ -22,7 +27,9 @@ The codegen tool rejects any ABI file that doesn't have the exact string `"wasm-
|
|
|
22
27
|
```json
|
|
23
28
|
{
|
|
24
29
|
"schema_version": "wasm-abi/1",
|
|
25
|
-
"types": {},
|
|
30
|
+
"types": {},
|
|
31
|
+
"methods": [],
|
|
32
|
+
"events": []
|
|
26
33
|
}
|
|
27
34
|
```
|
|
28
35
|
|
|
@@ -20,9 +20,7 @@ The ABI validator rejects manifests where names clash — across methods, events
|
|
|
20
20
|
"types": {
|
|
21
21
|
"set": { "kind": "record", "fields": [] }
|
|
22
22
|
},
|
|
23
|
-
"methods": [
|
|
24
|
-
{ "name": "set", "params": [] }
|
|
25
|
-
]
|
|
23
|
+
"methods": [{ "name": "set", "params": [] }]
|
|
26
24
|
}
|
|
27
25
|
```
|
|
28
26
|
|
|
@@ -43,9 +41,7 @@ Map keys **must** be `{ "kind": "string" }`.
|
|
|
43
41
|
```json
|
|
44
42
|
{
|
|
45
43
|
"types": {},
|
|
46
|
-
"methods": [
|
|
47
|
-
{ "name": "get", "returns": { "$ref": "NonExistentType" } }
|
|
48
|
-
]
|
|
44
|
+
"methods": [{ "name": "get", "returns": { "$ref": "NonExistentType" } }]
|
|
49
45
|
}
|
|
50
46
|
```
|
|
51
47
|
|
|
@@ -1,44 +1,135 @@
|
|
|
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 using
|
|
3
|
+
You are helping a developer connect a **browser or Node.js frontend** to a Calimero node using
|
|
4
|
+
`@calimero-network/mero-react` (preferred) or `@calimero-network/calimero-client`.
|
|
5
|
+
|
|
6
|
+
> **NOT this skill** if the developer is building the application logic that _runs on the node_ in
|
|
7
|
+
> TypeScript — that is `calimero-sdk-js`. This skill is for the _client_ side: auth, RPC calls, and
|
|
8
|
+
> WebSocket subscriptions from a browser or React app.
|
|
4
9
|
|
|
5
10
|
## Package versions
|
|
6
11
|
|
|
7
|
-
| Package
|
|
8
|
-
|
|
|
9
|
-
| `@calimero-network/
|
|
10
|
-
| `@calimero-network/mero-js`
|
|
12
|
+
| Package | Notes |
|
|
13
|
+
| ----------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
14
|
+
| `@calimero-network/mero-react` | **Preferred for React apps.** Exports `MeroJs`, `useSubscription`, `MeroProvider`, hooks. |
|
|
15
|
+
| `@calimero-network/mero-js` | Core SDK. Zero deps. Used standalone in non-React contexts. |
|
|
16
|
+
| `@calimero-network/calimero-client` | Legacy client. Still works; new projects should prefer mero-react/mero-js. |
|
|
11
17
|
|
|
12
18
|
## Critical: mero-js v2 uses camelCase
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
All request field names changed from `snake_case` to `camelCase` in v2.
|
|
15
21
|
|
|
16
22
|
```typescript
|
|
17
|
-
// WRONG (v1
|
|
23
|
+
// WRONG (v1):
|
|
18
24
|
{ context_id: '...', context_identity: '...' }
|
|
19
25
|
|
|
20
|
-
// CORRECT (v2):
|
|
26
|
+
// CORRECT (v2 / mero-react):
|
|
21
27
|
{ contextId: '...', contextIdentity: '...' }
|
|
22
28
|
```
|
|
23
29
|
|
|
24
|
-
|
|
30
|
+
## React app pattern (mero-react) — recommended
|
|
25
31
|
|
|
26
|
-
|
|
32
|
+
### Install
|
|
27
33
|
|
|
28
34
|
```bash
|
|
29
|
-
pnpm add @calimero-network/
|
|
30
|
-
# or
|
|
31
|
-
pnpm add @calimero-network/mero-js
|
|
35
|
+
pnpm add @calimero-network/mero-react
|
|
32
36
|
```
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
### Setup provider
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { MeroProvider } from '@calimero-network/mero-react';
|
|
42
|
+
|
|
43
|
+
function App() {
|
|
44
|
+
return (
|
|
45
|
+
<MeroProvider nodeUrl="http://localhost:2428">
|
|
46
|
+
<YourApp />
|
|
47
|
+
</MeroProvider>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
35
51
|
|
|
36
|
-
|
|
37
|
-
2. Store tokens using the provided storage helpers (`setAppEndpointKey`, `setAccessToken`, etc.)
|
|
38
|
-
3. Call app methods via the `rpcClient` singleton using `rpcClient.execute()`
|
|
39
|
-
4. Subscribe to events via `WsSubscriptionsClient`
|
|
52
|
+
### Call app methods
|
|
40
53
|
|
|
41
|
-
|
|
54
|
+
Generated clients (from abi-codegen) import `MeroJs` from `@calimero-network/mero-react`:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { MeroJs } from '@calimero-network/mero-react';
|
|
58
|
+
|
|
59
|
+
export class KvClient {
|
|
60
|
+
constructor(
|
|
61
|
+
private mero: MeroJs,
|
|
62
|
+
private contextId: string,
|
|
63
|
+
private executorPublicKey: string
|
|
64
|
+
) {}
|
|
65
|
+
|
|
66
|
+
async set(key: string, value: string): Promise<void> {
|
|
67
|
+
await this.mero.rpc.execute({
|
|
68
|
+
contextId: this.contextId,
|
|
69
|
+
method: 'set',
|
|
70
|
+
argsJson: { key, value },
|
|
71
|
+
executorPublicKey: this.executorPublicKey,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async get(key: string): Promise<string | null> {
|
|
76
|
+
const response = await this.mero.rpc.execute({
|
|
77
|
+
contextId: this.contextId,
|
|
78
|
+
method: 'get',
|
|
79
|
+
argsJson: { key },
|
|
80
|
+
executorPublicKey: this.executorPublicKey,
|
|
81
|
+
});
|
|
82
|
+
return response as string | null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Subscribe to events (mero-react hook)
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { useSubscription } from '@calimero-network/mero-react';
|
|
91
|
+
|
|
92
|
+
// Subscribe to one or more contexts
|
|
93
|
+
useSubscription([contextId], (event: { contextId: string; data: unknown }) => {
|
|
94
|
+
// event.data may be:
|
|
95
|
+
// - { type: 'EventName', ...payload } for direct events
|
|
96
|
+
// - { events: [{ kind: string, data: unknown }] } for execution event batches
|
|
97
|
+
console.log('event:', event.data);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Subscribe to multiple contexts (e.g. game + lobby simultaneously)
|
|
101
|
+
useSubscription([gameContextId, lobbyContextId], (event) => {
|
|
102
|
+
console.log('from context:', event.contextId);
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Parsing execution event payloads
|
|
107
|
+
|
|
108
|
+
Events from `app::emit!()` arrive batched in an `events` array. Each entry has `kind` (the variant
|
|
109
|
+
name) and `data` (the payload, possibly as a byte array):
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
function decodeEventData(data: unknown): unknown {
|
|
113
|
+
// If data is a number array, it's JSON-encoded bytes
|
|
114
|
+
if (Array.isArray(data) && data.every((n) => typeof n === 'number')) {
|
|
115
|
+
return JSON.parse(new TextDecoder().decode(new Uint8Array(data)));
|
|
116
|
+
}
|
|
117
|
+
return data;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// In the subscription callback:
|
|
121
|
+
useSubscription([contextId], (event) => {
|
|
122
|
+
const payload = event.data as any;
|
|
123
|
+
if (Array.isArray(payload?.events)) {
|
|
124
|
+
for (const e of payload.events) {
|
|
125
|
+
const decoded = decodeEventData(e.data);
|
|
126
|
+
console.log(e.kind, decoded); // e.g. "Inserted", { key: "...", value: "..." }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Legacy calimero-client pattern
|
|
42
133
|
|
|
43
134
|
```typescript
|
|
44
135
|
import {
|
|
@@ -68,7 +159,7 @@ const response = await rpcClient.execute<{ key: string }, string | null>({
|
|
|
68
159
|
});
|
|
69
160
|
console.log(response.result?.output);
|
|
70
161
|
|
|
71
|
-
// 3. Subscribe to
|
|
162
|
+
// 3. Subscribe to events
|
|
72
163
|
const ws = new WsSubscriptionsClient(getAppEndpointKey()!, '/ws');
|
|
73
164
|
await ws.connect();
|
|
74
165
|
ws.subscribe([getContextId()!]);
|
|
@@ -81,7 +172,21 @@ ws.addCallback((event) => {
|
|
|
81
172
|
});
|
|
82
173
|
```
|
|
83
174
|
|
|
175
|
+
## Core workflow
|
|
176
|
+
|
|
177
|
+
1. On startup: read SSO tokens from URL hash (if opened by Desktop), otherwise check `localStorage`,
|
|
178
|
+
otherwise show login
|
|
179
|
+
2. Store tokens using storage helpers or the `MeroProvider` (handles this automatically)
|
|
180
|
+
3. Call app methods via the generated typed client or `mero.rpc.execute()`
|
|
181
|
+
4. Subscribe to events via `useSubscription` (React) or `WsSubscriptionsClient`
|
|
182
|
+
|
|
183
|
+
## Related skills
|
|
184
|
+
|
|
185
|
+
- **`calimero-core`** — JSON-RPC protocol spec, WebSocket event schemas, context/app/identity model
|
|
186
|
+
- **`calimero-desktop`** — SSO token passing from Calimero Desktop (URL hash flow)
|
|
187
|
+
- **`calimero-node`** — node setup if the developer is also running the node
|
|
188
|
+
|
|
84
189
|
## References
|
|
85
190
|
|
|
86
|
-
See `references/` for auth flow, RPC calls, WebSocket events, and SSO.
|
|
87
|
-
|
|
191
|
+
See `references/` for auth flow, RPC calls, WebSocket events, and SSO. See `rules/` for camelCase
|
|
192
|
+
API and token refresh requirements.
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
getContextId,
|
|
20
20
|
setExecutorPublicKey,
|
|
21
21
|
getExecutorPublicKey,
|
|
22
|
-
setContextAndIdentityFromJWT,
|
|
22
|
+
setContextAndIdentityFromJWT, // extracts + stores contextId + executorPublicKey from JWT
|
|
23
23
|
// App ID
|
|
24
24
|
setApplicationId,
|
|
25
25
|
getApplicationId,
|
|
@@ -27,9 +27,9 @@ import {
|
|
|
27
27
|
setAuthEndpointURL,
|
|
28
28
|
getAuthEndpointURL,
|
|
29
29
|
// Decoded JWT payload
|
|
30
|
-
getJWTObject,
|
|
30
|
+
getJWTObject, // returns { context_id, context_identity, exp, permissions, ... }
|
|
31
31
|
// Full auth config (throws if required fields are missing)
|
|
32
|
-
getAuthConfig,
|
|
32
|
+
getAuthConfig, // returns { appEndpointKey, contextId, executorPublicKey, jwtToken }
|
|
33
33
|
// Logout (clears all tokens + reloads)
|
|
34
34
|
clientLogout,
|
|
35
35
|
} from '@calimero-network/calimero-client';
|
|
@@ -44,10 +44,10 @@ When the app is opened by Desktop, tokens arrive in the URL hash. Store them:
|
|
|
44
44
|
```typescript
|
|
45
45
|
function initFromDesktopSSO(): boolean {
|
|
46
46
|
const hash = new URLSearchParams(window.location.hash.slice(1));
|
|
47
|
-
const accessToken
|
|
47
|
+
const accessToken = hash.get('access_token');
|
|
48
48
|
const refreshToken = hash.get('refresh_token');
|
|
49
|
-
const nodeUrl
|
|
50
|
-
const appId
|
|
49
|
+
const nodeUrl = hash.get('node_url');
|
|
50
|
+
const appId = hash.get('application_id');
|
|
51
51
|
|
|
52
52
|
if (!accessToken || !nodeUrl) return false;
|
|
53
53
|
|
|
@@ -72,7 +72,12 @@ function initFromDesktopSSO(): boolean {
|
|
|
72
72
|
## Manual login (no Desktop)
|
|
73
73
|
|
|
74
74
|
```typescript
|
|
75
|
-
import {
|
|
75
|
+
import {
|
|
76
|
+
setAppEndpointKey,
|
|
77
|
+
setAccessToken,
|
|
78
|
+
setRefreshToken,
|
|
79
|
+
setContextAndIdentityFromJWT,
|
|
80
|
+
} from '@calimero-network/calimero-client';
|
|
76
81
|
|
|
77
82
|
async function login(nodeUrl: string, accessToken: string, refreshToken: string) {
|
|
78
83
|
setAppEndpointKey(nodeUrl);
|
|
@@ -126,8 +131,11 @@ clientLogout();
|
|
|
126
131
|
|
|
127
132
|
```typescript
|
|
128
133
|
import {
|
|
129
|
-
setAppEndpointKey,
|
|
130
|
-
|
|
134
|
+
setAppEndpointKey,
|
|
135
|
+
setAccessToken,
|
|
136
|
+
setRefreshToken,
|
|
137
|
+
setApplicationId,
|
|
138
|
+
setContextAndIdentityFromJWT,
|
|
131
139
|
getAuthConfig,
|
|
132
140
|
} from '@calimero-network/calimero-client';
|
|
133
141
|
|
|
@@ -135,7 +143,7 @@ async function bootstrap() {
|
|
|
135
143
|
// Try SSO from Desktop hash
|
|
136
144
|
const hash = new URLSearchParams(window.location.hash.slice(1));
|
|
137
145
|
const accessToken = hash.get('access_token');
|
|
138
|
-
const nodeUrl
|
|
146
|
+
const nodeUrl = hash.get('node_url');
|
|
139
147
|
|
|
140
148
|
if (accessToken && nodeUrl) {
|
|
141
149
|
setAppEndpointKey(nodeUrl);
|
|
@@ -4,15 +4,11 @@ Calling application methods on a Calimero node.
|
|
|
4
4
|
|
|
5
5
|
## The `rpcClient` singleton
|
|
6
6
|
|
|
7
|
-
`@calimero-network/calimero-client` exports a pre-configured `rpcClient` singleton.
|
|
8
|
-
|
|
7
|
+
`@calimero-network/calimero-client` exports a pre-configured `rpcClient` singleton. Import it
|
|
8
|
+
directly — do not construct `JsonRpcClient` manually.
|
|
9
9
|
|
|
10
10
|
```typescript
|
|
11
|
-
import {
|
|
12
|
-
rpcClient,
|
|
13
|
-
getContextId,
|
|
14
|
-
getExecutorPublicKey,
|
|
15
|
-
} from '@calimero-network/calimero-client';
|
|
11
|
+
import { rpcClient, getContextId, getExecutorPublicKey } from '@calimero-network/calimero-client';
|
|
16
12
|
```
|
|
17
13
|
|
|
18
14
|
---
|
|
@@ -23,7 +19,9 @@ import {
|
|
|
23
19
|
const response = await rpcClient.execute<ArgsType, OutputType>({
|
|
24
20
|
contextId: getContextId()!,
|
|
25
21
|
method: 'methodName',
|
|
26
|
-
argsJson: {
|
|
22
|
+
argsJson: {
|
|
23
|
+
/* your args */
|
|
24
|
+
},
|
|
27
25
|
executorPublicKey: getExecutorPublicKey()!,
|
|
28
26
|
});
|
|
29
27
|
|
|
@@ -96,20 +94,20 @@ if (!response.error) {
|
|
|
96
94
|
|
|
97
95
|
## Error names
|
|
98
96
|
|
|
99
|
-
| name
|
|
100
|
-
|
|
101
|
-
| `FunctionCallError`
|
|
102
|
-
| `RpcExecutionError`
|
|
97
|
+
| name | meaning |
|
|
98
|
+
| --------------------- | -------------------------------------------------- |
|
|
99
|
+
| `FunctionCallError` | App method returned an error |
|
|
100
|
+
| `RpcExecutionError` | Node couldn't execute the method |
|
|
103
101
|
| `InvalidRequestError` | Malformed request (wrong context-id, missing args) |
|
|
104
|
-
| `AuthenticationError` | JWT expired, revoked, or missing
|
|
105
|
-
| `UnknownServerError`
|
|
102
|
+
| `AuthenticationError` | JWT expired, revoked, or missing |
|
|
103
|
+
| `UnknownServerError` | Unexpected server error |
|
|
106
104
|
|
|
107
105
|
---
|
|
108
106
|
|
|
109
107
|
## Error handling with 401
|
|
110
108
|
|
|
111
|
-
The HTTP client inside `rpcClient` automatically refreshes tokens on `401 token_expired`.
|
|
112
|
-
|
|
109
|
+
The HTTP client inside `rpcClient` automatically refreshes tokens on `401 token_expired`. For
|
|
110
|
+
`token_revoked` or `invalid_token` you need to re-authenticate:
|
|
113
111
|
|
|
114
112
|
```typescript
|
|
115
113
|
const response = await rpcClient.execute({ ... });
|
|
@@ -133,11 +131,7 @@ if (response.error) {
|
|
|
133
131
|
## Complete example: typed wrapper
|
|
134
132
|
|
|
135
133
|
```typescript
|
|
136
|
-
import {
|
|
137
|
-
rpcClient,
|
|
138
|
-
getContextId,
|
|
139
|
-
getExecutorPublicKey,
|
|
140
|
-
} from '@calimero-network/calimero-client';
|
|
134
|
+
import { rpcClient, getContextId, getExecutorPublicKey } from '@calimero-network/calimero-client';
|
|
141
135
|
|
|
142
136
|
async function setItem(key: string, value: string): Promise<void> {
|
|
143
137
|
const response = await rpcClient.execute<{ key: string; value: string }, void>({
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# SSO — Calimero Desktop Integration
|
|
2
2
|
|
|
3
|
-
When a user opens your app from Calimero Desktop, auth tokens are passed via the URL hash.
|
|
4
|
-
|
|
3
|
+
When a user opens your app from Calimero Desktop, auth tokens are passed via the URL hash. Reading
|
|
4
|
+
them lets users skip the manual login flow entirely.
|
|
5
5
|
|
|
6
6
|
## Hash parameters
|
|
7
7
|
|
|
8
|
-
| Parameter
|
|
9
|
-
|
|
|
10
|
-
| `access_token`
|
|
11
|
-
| `refresh_token`
|
|
12
|
-
| `node_url`
|
|
8
|
+
| Parameter | Description |
|
|
9
|
+
| ---------------- | ------------------------------------------ |
|
|
10
|
+
| `access_token` | JWT for authenticated node calls |
|
|
11
|
+
| `refresh_token` | Token to obtain a new access token |
|
|
12
|
+
| `node_url` | URL of the local node to connect to |
|
|
13
13
|
| `application_id` | The installed application's ID on the node |
|
|
14
14
|
|
|
15
15
|
## Reading from hash
|
|
@@ -68,5 +68,5 @@ async function initApp() {
|
|
|
68
68
|
|
|
69
69
|
## Important
|
|
70
70
|
|
|
71
|
-
Always fall back to manual login if the hash params are absent — the app must work
|
|
72
|
-
|
|
71
|
+
Always fall back to manual login if the hash params are absent — the app must work when opened
|
|
72
|
+
directly in a browser, not only from Desktop.
|
|
@@ -6,111 +6,117 @@ Subscribe to real-time events emitted by the application running on the node.
|
|
|
6
6
|
|
|
7
7
|
The node sends two kinds of events to subscribers:
|
|
8
8
|
|
|
9
|
-
| type
|
|
10
|
-
|
|
11
|
-
| `StateMutation`
|
|
12
|
-
| `ExecutionEvent` | App emitted `app::emit!()`
|
|
9
|
+
| type | when | payload |
|
|
10
|
+
| ---------------- | ----------------------------------- | ------------------------------ |
|
|
11
|
+
| `StateMutation` | Another member mutated shared state | `{ newRoot: string }` |
|
|
12
|
+
| `ExecutionEvent` | App emitted `app::emit!()` | `{ events: ExecutionEvent[] }` |
|
|
13
13
|
|
|
14
|
-
An `ExecutionEvent`
|
|
14
|
+
An `ExecutionEvent` entry: `{ kind: string; data: any }` where `kind` matches the Rust event variant
|
|
15
|
+
name and `data` may be a byte array (UTF-8 JSON) or a plain object.
|
|
15
16
|
|
|
16
17
|
---
|
|
17
18
|
|
|
18
|
-
##
|
|
19
|
+
## mero-react: useSubscription hook (preferred for React)
|
|
19
20
|
|
|
20
21
|
```typescript
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
import { useSubscription } from '@calimero-network/mero-react';
|
|
23
|
+
|
|
24
|
+
function MyComponent({ contextId }: { contextId: string }) {
|
|
25
|
+
useSubscription([contextId], (event: { contextId: string; data: unknown }) => {
|
|
26
|
+
const payload = event.data as any;
|
|
27
|
+
|
|
28
|
+
// ExecutionEvent: data has an `events` array
|
|
29
|
+
if (Array.isArray(payload?.events)) {
|
|
30
|
+
for (const e of payload.events) {
|
|
31
|
+
const data = decodeEventData(e.data);
|
|
32
|
+
switch (e.kind) {
|
|
33
|
+
case 'Inserted':
|
|
34
|
+
console.log('inserted', data);
|
|
35
|
+
break;
|
|
36
|
+
case 'Removed':
|
|
37
|
+
console.log('removed', data);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Byte-array payloads are JSON-encoded; decode them:
|
|
46
|
+
function decodeEventData(data: unknown): unknown {
|
|
47
|
+
if (Array.isArray(data) && data.every((n) => typeof n === 'number')) {
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(new TextDecoder().decode(new Uint8Array(data)));
|
|
50
|
+
} catch {
|
|
51
|
+
return data;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return data;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
26
57
|
|
|
27
|
-
|
|
28
|
-
const ws = new WsSubscriptionsClient(nodeUrl, '/ws');
|
|
58
|
+
## Subscribe to multiple contexts
|
|
29
59
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
console.log('Event received:', event);
|
|
60
|
+
```typescript
|
|
61
|
+
// Subscribe to game context + lobby context simultaneously
|
|
62
|
+
useSubscription([gameContextId, lobbyContextId], (event) => {
|
|
63
|
+
console.log('event from context:', event.contextId);
|
|
35
64
|
});
|
|
36
65
|
```
|
|
37
66
|
|
|
38
67
|
---
|
|
39
68
|
|
|
40
|
-
##
|
|
69
|
+
## Legacy calimero-client: WsSubscriptionsClient
|
|
41
70
|
|
|
42
71
|
```typescript
|
|
43
|
-
import
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
72
|
+
import {
|
|
73
|
+
WsSubscriptionsClient,
|
|
74
|
+
getAppEndpointKey,
|
|
75
|
+
getContextId,
|
|
47
76
|
} from '@calimero-network/calimero-client';
|
|
48
77
|
|
|
49
|
-
ws
|
|
78
|
+
const ws = new WsSubscriptionsClient(getAppEndpointKey()!, '/ws');
|
|
79
|
+
await ws.connect();
|
|
80
|
+
ws.subscribe([getContextId()!]);
|
|
81
|
+
ws.addCallback((event) => {
|
|
50
82
|
if (event.type === 'ExecutionEvent') {
|
|
51
83
|
for (const e of event.data.events) {
|
|
52
|
-
|
|
53
|
-
// e.data contains the event payload
|
|
54
|
-
switch (e.kind) {
|
|
55
|
-
case 'ItemAdded':
|
|
56
|
-
console.log('Item added:', e.data);
|
|
57
|
-
addItemToUI(e.data.key, e.data.value);
|
|
58
|
-
break;
|
|
59
|
-
case 'ItemRemoved':
|
|
60
|
-
removeItemFromUI(e.data.key);
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
84
|
+
console.log(e.kind, e.data);
|
|
63
85
|
}
|
|
64
86
|
}
|
|
65
|
-
|
|
66
87
|
if (event.type === 'StateMutation') {
|
|
67
|
-
// Another member changed shared state — refresh data from node
|
|
68
|
-
console.log('State root changed:', event.data.newRoot);
|
|
69
88
|
refreshDataFromNode();
|
|
70
89
|
}
|
|
71
90
|
});
|
|
72
91
|
```
|
|
73
92
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
## Subscribe to multiple contexts
|
|
77
|
-
|
|
78
|
-
```typescript
|
|
79
|
-
ws.subscribe([contextId1, contextId2]);
|
|
80
|
-
// Events include event.contextId to identify which context emitted them
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
---
|
|
84
|
-
|
|
85
|
-
## Cleanup
|
|
93
|
+
Cleanup:
|
|
86
94
|
|
|
87
95
|
```typescript
|
|
88
|
-
// Remove a specific callback
|
|
89
96
|
ws.removeCallback(myCallback);
|
|
90
|
-
|
|
91
|
-
// Unsubscribe from specific contexts
|
|
92
97
|
ws.unsubscribe([contextId]);
|
|
93
|
-
|
|
94
|
-
// Close connection entirely
|
|
95
98
|
ws.disconnect();
|
|
96
99
|
```
|
|
97
100
|
|
|
98
101
|
---
|
|
99
102
|
|
|
100
|
-
## React hook
|
|
103
|
+
## React hook for legacy client
|
|
101
104
|
|
|
102
105
|
```typescript
|
|
103
106
|
import { useEffect } from 'react';
|
|
104
|
-
import {
|
|
107
|
+
import {
|
|
108
|
+
WsSubscriptionsClient,
|
|
109
|
+
getAppEndpointKey,
|
|
110
|
+
getContextId,
|
|
111
|
+
} from '@calimero-network/calimero-client';
|
|
105
112
|
|
|
106
|
-
function useNodeEvents(onEvent: (event:
|
|
113
|
+
function useNodeEvents(onEvent: (event: any) => void) {
|
|
107
114
|
useEffect(() => {
|
|
108
115
|
const nodeUrl = getAppEndpointKey();
|
|
109
116
|
const contextId = getContextId();
|
|
110
117
|
if (!nodeUrl || !contextId) return;
|
|
111
118
|
|
|
112
119
|
const ws = new WsSubscriptionsClient(nodeUrl, '/ws');
|
|
113
|
-
|
|
114
120
|
ws.connect().then(() => {
|
|
115
121
|
ws.subscribe([contextId]);
|
|
116
122
|
ws.addCallback(onEvent);
|
|
@@ -124,3 +130,11 @@ function useNodeEvents(onEvent: (event: NodeEvent) => void) {
|
|
|
124
130
|
}, [onEvent]);
|
|
125
131
|
}
|
|
126
132
|
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Connection notes
|
|
137
|
+
|
|
138
|
+
- `WsSubscriptionsClient` does **not** reconnect automatically — handle reconnect manually
|
|
139
|
+
- `useSubscription` from `mero-react` manages the connection lifecycle for you
|
|
140
|
+
- WebSocket tokens are **not** auto-refreshed unlike `rpcClient` HTTP calls
|