@calimero-network/agent-skills 0.1.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 +238 -0
- package/SKILL.md +43 -0
- package/package.json +43 -0
- package/scripts/install.js +116 -0
- package/scripts/test.js +67 -0
- package/skills/calimero-abi-codegen/SKILL.md +61 -0
- package/skills/calimero-abi-codegen/references/abi-format.md +105 -0
- package/skills/calimero-abi-codegen/references/generated-output.md +89 -0
- package/skills/calimero-abi-codegen/references/programmatic-api.md +53 -0
- package/skills/calimero-abi-codegen/rules/schema-version.md +36 -0
- package/skills/calimero-abi-codegen/rules/unique-names.md +52 -0
- package/skills/calimero-client-js/SKILL.md +44 -0
- package/skills/calimero-client-js/references/auth.md +58 -0
- package/skills/calimero-client-js/references/rpc-calls.md +75 -0
- package/skills/calimero-client-js/references/sso.md +67 -0
- package/skills/calimero-client-js/references/websocket-events.md +66 -0
- package/skills/calimero-client-js/rules/camelcase-api.md +37 -0
- package/skills/calimero-client-js/rules/token-refresh.md +41 -0
- package/skills/calimero-client-py/SKILL.md +53 -0
- package/skills/calimero-client-py/references/api.md +147 -0
- package/skills/calimero-client-py/references/auth.md +72 -0
- package/skills/calimero-client-py/rules/async-usage.md +52 -0
- package/skills/calimero-client-py/rules/stable-node-name.md +39 -0
- package/skills/calimero-desktop/SKILL.md +47 -0
- package/skills/calimero-desktop/references/sso-integration.md +92 -0
- package/skills/calimero-desktop/rules/sso-fallback.md +33 -0
- package/skills/calimero-merobox/SKILL.md +55 -0
- package/skills/calimero-merobox/references/ci-integration.md +52 -0
- package/skills/calimero-merobox/references/workflow-files.md +70 -0
- package/skills/calimero-merobox/rules/docker-required.md +26 -0
- package/skills/calimero-node/SKILL.md +34 -0
- package/skills/calimero-node/references/context-lifecycle.md +59 -0
- package/skills/calimero-node/references/meroctl-commands.md +80 -0
- package/skills/calimero-node/rules/app-vs-context.md +33 -0
- package/skills/calimero-registry/SKILL.md +51 -0
- package/skills/calimero-registry/references/bundle-and-push.md +69 -0
- package/skills/calimero-registry/references/manifest-format.md +63 -0
- package/skills/calimero-registry/references/mero-sign.md +75 -0
- package/skills/calimero-registry/rules/key-security.md +41 -0
- package/skills/calimero-registry/rules/sign-before-pack.md +25 -0
- package/skills/calimero-rust-sdk/SKILL.md +73 -0
- package/skills/calimero-rust-sdk/references/events.md +53 -0
- package/skills/calimero-rust-sdk/references/examples.md +70 -0
- package/skills/calimero-rust-sdk/references/private-storage.md +56 -0
- package/skills/calimero-rust-sdk/references/state-collections.md +50 -0
- package/skills/calimero-rust-sdk/rules/app-macro-placement.md +34 -0
- package/skills/calimero-rust-sdk/rules/no-std-collections.md +27 -0
- package/skills/calimero-rust-sdk/rules/wasm-constraints.md +35 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Generated Output
|
|
2
|
+
|
|
3
|
+
Running `calimero-abi-codegen -i abi.json -o src/generated --client-name KvStoreClient` produces:
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
src/generated/
|
|
7
|
+
├── types.ts
|
|
8
|
+
└── KvStoreClient.ts
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## types.ts
|
|
12
|
+
|
|
13
|
+
Contains all TypeScript types matching the ABI types, plus event payload types:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// Generated from abi.json — do not edit manually
|
|
17
|
+
|
|
18
|
+
export interface Entry {
|
|
19
|
+
key: string;
|
|
20
|
+
value: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Event union type
|
|
24
|
+
export type AppEvent =
|
|
25
|
+
| { type: 'ItemSet'; payload: Entry }
|
|
26
|
+
| { type: 'ItemRemoved'; payload: { key: string } };
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## KvStoreClient.ts
|
|
30
|
+
|
|
31
|
+
Contains the typed client class with a method per ABI function:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// Generated from abi.json — do not edit manually
|
|
35
|
+
import { CalimeroApp, Context } from '@calimero-network/calimero-client';
|
|
36
|
+
import type { Entry } from './types';
|
|
37
|
+
|
|
38
|
+
export class KvStoreClient {
|
|
39
|
+
constructor(private app: CalimeroApp, private context: Context) {}
|
|
40
|
+
|
|
41
|
+
async set(key: string, value: string): Promise<void> {
|
|
42
|
+
await this.app.mutate(this.context, 'set', { key, value });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async get(key: string): Promise<string | null> {
|
|
46
|
+
return this.app.query(this.context, 'get', { key });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async entries(): Promise<Entry[]> {
|
|
50
|
+
return this.app.query(this.context, 'entries', {});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Using the generated client
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { CalimeroApp, Context, getJWTObject, getStorageAppEndpointKey } from '@calimero-network/calimero-client';
|
|
59
|
+
import { KvStoreClient } from './generated/KvStoreClient';
|
|
60
|
+
|
|
61
|
+
const app = new CalimeroApp(getStorageAppEndpointKey(), getJWTObject()?.access_token);
|
|
62
|
+
const context = new Context('your-context-id');
|
|
63
|
+
const client = new KvStoreClient(app, context);
|
|
64
|
+
|
|
65
|
+
// Fully typed — IDE autocomplete works
|
|
66
|
+
await client.set('hello', 'world');
|
|
67
|
+
const value = await client.get('hello'); // string | null
|
|
68
|
+
const all = await client.entries(); // Entry[]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Regenerating after app changes
|
|
72
|
+
|
|
73
|
+
When you update your Rust app and the ABI changes, re-run codegen:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# After rebuilding your WASM app (which regenerates abi.json):
|
|
77
|
+
npx calimero-abi-codegen -i abi.json -o src/generated --client-name KvStoreClient
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Add this to your build script so it stays in sync automatically:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"scripts": {
|
|
85
|
+
"codegen": "calimero-abi-codegen -i abi.json -o src/generated --client-name KvStoreClient",
|
|
86
|
+
"build": "npm run codegen && tsc"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Programmatic API
|
|
2
|
+
|
|
3
|
+
Use the library directly in Node.js scripts for custom code generation pipelines.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @calimero-network/abi-codegen
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Parse an ABI
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { loadAbiManifestFromFile, parseAbiManifest } from '@calimero-network/abi-codegen/parse';
|
|
15
|
+
|
|
16
|
+
// From file
|
|
17
|
+
const manifest = loadAbiManifestFromFile('./abi.json');
|
|
18
|
+
|
|
19
|
+
// From already-parsed JSON
|
|
20
|
+
const json = JSON.parse(fs.readFileSync('./abi.json', 'utf8'));
|
|
21
|
+
const manifest = parseAbiManifest(json);
|
|
22
|
+
// Throws if invalid — schema_version wrong, dangling refs, duplicate names, etc.
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Generate types and client
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { generateTypes } from '@calimero-network/abi-codegen/generate/types';
|
|
29
|
+
import { generateClient } from '@calimero-network/abi-codegen/generate/client';
|
|
30
|
+
import { deriveClientNameFromPath } from '@calimero-network/abi-codegen';
|
|
31
|
+
|
|
32
|
+
const manifest = loadAbiManifestFromFile('./abi.json');
|
|
33
|
+
|
|
34
|
+
// Generate types.ts content
|
|
35
|
+
const typesContent = generateTypes(manifest);
|
|
36
|
+
|
|
37
|
+
// Generate client content
|
|
38
|
+
const clientName = deriveClientNameFromPath('kv_store.wasm'); // → "KvStore"
|
|
39
|
+
const clientContent = generateClient(manifest, clientName);
|
|
40
|
+
|
|
41
|
+
// Write to disk
|
|
42
|
+
fs.writeFileSync('src/generated/types.ts', typesContent);
|
|
43
|
+
fs.writeFileSync(`src/generated/${clientName}.ts`, clientContent);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Export paths
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { deriveClientNameFromPath } from '@calimero-network/abi-codegen';
|
|
50
|
+
import { loadAbiManifestFromFile, parseAbiManifest } from '@calimero-network/abi-codegen/parse';
|
|
51
|
+
import { generateTypes } from '@calimero-network/abi-codegen/generate/types';
|
|
52
|
+
import { generateClient } from '@calimero-network/abi-codegen/generate/client';
|
|
53
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Rule: ABI manifest must have schema_version "wasm-abi/1"
|
|
2
|
+
|
|
3
|
+
The codegen tool rejects any ABI file that doesn't have the exact string `"wasm-abi/1"` as its `schema_version`.
|
|
4
|
+
|
|
5
|
+
## WRONG:
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"schema_version": "1",
|
|
10
|
+
"types": {}, "methods": [], "events": []
|
|
11
|
+
}
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"types": {}, "methods": [], "events": []
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## CORRECT:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"schema_version": "wasm-abi/1",
|
|
25
|
+
"types": {}, "methods": [], "events": []
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Validate before generating
|
|
30
|
+
|
|
31
|
+
Use `--validate` to check an ABI file without generating code — useful as a CI step:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx calimero-abi-codegen --validate -i abi.json
|
|
35
|
+
# exits 0 if valid, non-zero if invalid
|
|
36
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Rule: Method, event, and type names must all be unique
|
|
2
|
+
|
|
3
|
+
The ABI validator rejects manifests where names clash — across methods, events, and types.
|
|
4
|
+
|
|
5
|
+
## WRONG — duplicate method names:
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"methods": [
|
|
10
|
+
{ "name": "get", "params": [...] },
|
|
11
|
+
{ "name": "get", "params": [...] }
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## WRONG — type name same as method name:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"types": {
|
|
21
|
+
"set": { "kind": "record", "fields": [] }
|
|
22
|
+
},
|
|
23
|
+
"methods": [
|
|
24
|
+
{ "name": "set", "params": [] }
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## WRONG — map key is not string:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"kind": "map",
|
|
34
|
+
"key": { "kind": "u64" },
|
|
35
|
+
"value": { "kind": "string" }
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Map keys **must** be `{ "kind": "string" }`.
|
|
40
|
+
|
|
41
|
+
## WRONG — dangling $ref:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"types": {},
|
|
46
|
+
"methods": [
|
|
47
|
+
{ "name": "get", "returns": { "$ref": "NonExistentType" } }
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Every `$ref` must point to a name defined in the `types` object.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# calimero-client-js — Agent Instructions
|
|
2
|
+
|
|
3
|
+
You are helping a developer connect a **browser or Node.js frontend** to a Calimero node using `@calimero-network/calimero-client` or `@calimero-network/mero-js`.
|
|
4
|
+
|
|
5
|
+
## Package versions
|
|
6
|
+
|
|
7
|
+
| Package | Version | Notes |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `@calimero-network/calimero-client` | latest | Stable client for browser/Node — auth, RPC, WebSocket |
|
|
10
|
+
| `@calimero-network/mero-js` | `>=2.0.0-beta.1` | v2 API — all request fields are **camelCase** |
|
|
11
|
+
|
|
12
|
+
## Critical: mero-js v2 uses camelCase
|
|
13
|
+
|
|
14
|
+
v2 changed all request field names from `snake_case` to `camelCase`.
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
// WRONG (v1 / old):
|
|
18
|
+
{ context_id: '...', context_identity: '...' }
|
|
19
|
+
|
|
20
|
+
// CORRECT (v2):
|
|
21
|
+
{ contextId: '...', contextIdentity: '...' }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This applies to `GenerateClientKeyRequest` and all other request types.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pnpm add @calimero-network/calimero-client
|
|
30
|
+
# or
|
|
31
|
+
pnpm add @calimero-network/mero-js
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Core workflow
|
|
35
|
+
|
|
36
|
+
1. Read auth tokens (from URL hash if opened by Desktop, otherwise prompt login)
|
|
37
|
+
2. Initialize client with `node_url` + tokens
|
|
38
|
+
3. Call app methods via JSON-RPC
|
|
39
|
+
4. Subscribe to events via WebSocket
|
|
40
|
+
|
|
41
|
+
## References
|
|
42
|
+
|
|
43
|
+
See `references/` for auth flow, RPC calls, WebSocket events, and SSO.
|
|
44
|
+
See `rules/` for camelCase API and token refresh requirements.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Authentication
|
|
2
|
+
|
|
3
|
+
## Login flow
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { setupWalletSelector } from '@near-wallet-selector/core';
|
|
7
|
+
import { ClientLogin } from '@calimero-network/calimero-client';
|
|
8
|
+
|
|
9
|
+
const client = new ClientLogin({ nodeUrl: 'http://localhost:2428' });
|
|
10
|
+
|
|
11
|
+
// 1. Generate a client key for this device
|
|
12
|
+
const { contextId, contextIdentity } = await client.generateClientKey({
|
|
13
|
+
contextId: 'your-context-id',
|
|
14
|
+
contextIdentity: 'your-identity',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// 2. Login — returns access_token + refresh_token
|
|
18
|
+
const tokens = await client.login({
|
|
19
|
+
contextId,
|
|
20
|
+
contextIdentity,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// 3. Store tokens
|
|
24
|
+
localStorage.setItem('access_token', tokens.access_token);
|
|
25
|
+
localStorage.setItem('refresh_token', tokens.refresh_token);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Token storage
|
|
29
|
+
|
|
30
|
+
Tokens should be stored in `localStorage` or `sessionStorage`. The client library reads
|
|
31
|
+
them automatically if you use the provided storage helpers:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { getStorageAppEndpointKey, getJWTObject } from '@calimero-network/calimero-client';
|
|
35
|
+
|
|
36
|
+
const nodeUrl = getStorageAppEndpointKey();
|
|
37
|
+
const jwt = getJWTObject();
|
|
38
|
+
// jwt.access_token, jwt.refresh_token
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Checking auth status
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { getJWTObject } from '@calimero-network/calimero-client';
|
|
45
|
+
|
|
46
|
+
function isLoggedIn(): boolean {
|
|
47
|
+
const jwt = getJWTObject();
|
|
48
|
+
return !!(jwt?.access_token);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Logout
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
localStorage.removeItem('calimero_jwt');
|
|
56
|
+
localStorage.removeItem('calimero_node_url');
|
|
57
|
+
// redirect to login
|
|
58
|
+
```
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# RPC Calls
|
|
2
|
+
|
|
3
|
+
Calling application methods on a Calimero node.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import {
|
|
9
|
+
JsonRpcClient,
|
|
10
|
+
getStorageAppEndpointKey,
|
|
11
|
+
getJWTObject,
|
|
12
|
+
} from '@calimero-network/calimero-client';
|
|
13
|
+
|
|
14
|
+
const nodeUrl = getStorageAppEndpointKey() ?? 'http://localhost:2428';
|
|
15
|
+
const jwt = getJWTObject();
|
|
16
|
+
|
|
17
|
+
const client = new JsonRpcClient(nodeUrl, '/jsonrpc', jwt?.access_token);
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Calling a mutation (changes state)
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
const response = await client.mutate<{ key: string; value: string }, void>({
|
|
24
|
+
contextId: 'your-context-id',
|
|
25
|
+
method: 'set',
|
|
26
|
+
argsJson: { key: 'hello', value: 'world' },
|
|
27
|
+
executorPublicKey: jwt?.executor_public_key ?? '',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (response.error) {
|
|
31
|
+
console.error(response.error);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Calling a view (read-only)
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
const response = await client.query<{ key: string }, string | null>({
|
|
39
|
+
contextId: 'your-context-id',
|
|
40
|
+
method: 'get',
|
|
41
|
+
argsJson: { key: 'hello' },
|
|
42
|
+
executorPublicKey: jwt?.executor_public_key ?? '',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
console.log(response.result?.output); // "world"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Response shape
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
interface RpcResponse<T> {
|
|
52
|
+
result?: {
|
|
53
|
+
output: T;
|
|
54
|
+
};
|
|
55
|
+
error?: {
|
|
56
|
+
code: number;
|
|
57
|
+
message: string;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Error handling
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
const response = await client.query({ ... });
|
|
66
|
+
|
|
67
|
+
if (response.error) {
|
|
68
|
+
if (response.error.code === 401) {
|
|
69
|
+
// token expired — refresh and retry
|
|
70
|
+
await refreshTokens();
|
|
71
|
+
return callAgain();
|
|
72
|
+
}
|
|
73
|
+
throw new Error(response.error.message);
|
|
74
|
+
}
|
|
75
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# SSO — Calimero Desktop Integration
|
|
2
|
+
|
|
3
|
+
When a user opens your app from Calimero Desktop, auth tokens are passed via the URL hash.
|
|
4
|
+
Reading them lets users skip the manual login flow entirely.
|
|
5
|
+
|
|
6
|
+
## Hash parameters
|
|
7
|
+
|
|
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
|
+
| `application_id` | The installed application's ID on the node |
|
|
14
|
+
|
|
15
|
+
## Reading from hash
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
function readSSOParams(): {
|
|
19
|
+
accessToken: string | null;
|
|
20
|
+
refreshToken: string | null;
|
|
21
|
+
nodeUrl: string | null;
|
|
22
|
+
applicationId: string | null;
|
|
23
|
+
} {
|
|
24
|
+
const hash = window.location.hash.slice(1); // remove leading #
|
|
25
|
+
const params = new URLSearchParams(hash);
|
|
26
|
+
return {
|
|
27
|
+
accessToken: params.get('access_token'),
|
|
28
|
+
refreshToken: params.get('refresh_token'),
|
|
29
|
+
nodeUrl: params.get('node_url'),
|
|
30
|
+
applicationId: params.get('application_id'),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Using SSO tokens on app startup
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
async function initApp() {
|
|
39
|
+
const sso = readSSOParams();
|
|
40
|
+
|
|
41
|
+
if (sso.accessToken && sso.nodeUrl) {
|
|
42
|
+
// Opened from Desktop — store tokens and skip login
|
|
43
|
+
localStorage.setItem('calimero_node_url', sso.nodeUrl);
|
|
44
|
+
localStorage.setItem('calimero_jwt', JSON.stringify({
|
|
45
|
+
access_token: sso.accessToken,
|
|
46
|
+
refresh_token: sso.refreshToken,
|
|
47
|
+
}));
|
|
48
|
+
// clear hash so tokens aren't in browser history
|
|
49
|
+
history.replaceState(null, '', window.location.pathname);
|
|
50
|
+
renderApp();
|
|
51
|
+
} else {
|
|
52
|
+
// No SSO — show manual login
|
|
53
|
+
renderLogin();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
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
|
+
## Important
|
|
65
|
+
|
|
66
|
+
Always fall back to manual login if the hash params are absent — the app must work
|
|
67
|
+
when opened directly in a browser, not only from Desktop.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# WebSocket Events
|
|
2
|
+
|
|
3
|
+
Subscribe to real-time events emitted by the application running on the node.
|
|
4
|
+
|
|
5
|
+
## Connect and subscribe
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { WsSubscriptionsClient } from '@calimero-network/calimero-client';
|
|
9
|
+
|
|
10
|
+
const nodeUrl = getStorageAppEndpointKey() ?? 'http://localhost:2428';
|
|
11
|
+
const jwt = getJWTObject();
|
|
12
|
+
|
|
13
|
+
const ws = new WsSubscriptionsClient(nodeUrl, '/ws', jwt?.access_token);
|
|
14
|
+
|
|
15
|
+
ws.subscribe([contextId], (event) => {
|
|
16
|
+
console.log('Event received:', event);
|
|
17
|
+
// event.data contains the serialized app event payload
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Handling specific event types
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
interface AppEvent {
|
|
25
|
+
type: 'ItemAdded' | 'ItemRemoved' | 'MemberJoined';
|
|
26
|
+
key?: string;
|
|
27
|
+
value?: string;
|
|
28
|
+
identity?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
ws.subscribe([contextId], (event) => {
|
|
32
|
+
const appEvent = event.data as AppEvent;
|
|
33
|
+
|
|
34
|
+
switch (appEvent.type) {
|
|
35
|
+
case 'ItemAdded':
|
|
36
|
+
addItemToUI(appEvent.key!, appEvent.value!);
|
|
37
|
+
break;
|
|
38
|
+
case 'ItemRemoved':
|
|
39
|
+
removeItemFromUI(appEvent.key!);
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Cleanup
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// Unsubscribe when component unmounts
|
|
49
|
+
ws.unsubscribe([contextId]);
|
|
50
|
+
|
|
51
|
+
// Or close connection entirely
|
|
52
|
+
ws.close();
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## React hook pattern
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const ws = new WsSubscriptionsClient(nodeUrl, '/ws', token);
|
|
60
|
+
ws.subscribe([contextId], handleEvent);
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
ws.unsubscribe([contextId]);
|
|
64
|
+
};
|
|
65
|
+
}, [contextId, token]);
|
|
66
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Rule: mero-js v2 uses camelCase — not snake_case
|
|
2
|
+
|
|
3
|
+
`@calimero-network/mero-js` v2 (`>=2.0.0-beta.1`) changed all request object field names from `snake_case` to `camelCase`.
|
|
4
|
+
|
|
5
|
+
## WRONG (v1 / old code):
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
await client.generateClientKey({
|
|
9
|
+
context_id: contextId, // ✗
|
|
10
|
+
context_identity: identity, // ✗
|
|
11
|
+
});
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## CORRECT (v2):
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
await client.generateClientKey({
|
|
18
|
+
contextId: contextId, // ✓
|
|
19
|
+
contextIdentity: identity, // ✓
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Affected types
|
|
24
|
+
|
|
25
|
+
- `GenerateClientKeyRequest` — `contextId`, `contextIdentity`
|
|
26
|
+
- All other request types in the v2 API
|
|
27
|
+
|
|
28
|
+
## Why this matters
|
|
29
|
+
|
|
30
|
+
The v2 mero-js package does not alias the old snake_case keys. Passing `context_id` silently
|
|
31
|
+
sends `undefined` to the server, which returns a cryptic error rather than a clear "wrong field name" message.
|
|
32
|
+
|
|
33
|
+
## How to detect if a codebase is on v1 or v2
|
|
34
|
+
|
|
35
|
+
Check `package.json`:
|
|
36
|
+
- `"@calimero-network/mero-js": "^2.0.0-beta.1"` or higher → v2, use camelCase
|
|
37
|
+
- `"@calimero-network/calimero-client": "..."` → original client, check its docs
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Rule: Always handle 401 with token refresh
|
|
2
|
+
|
|
3
|
+
Access tokens expire. Any RPC call or WebSocket connection can return `401 Unauthorized`. Never let this surface as an unhandled error.
|
|
4
|
+
|
|
5
|
+
## Pattern
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
async function callWithRefresh<T>(callFn: () => Promise<T>): Promise<T> {
|
|
9
|
+
try {
|
|
10
|
+
return await callFn();
|
|
11
|
+
} catch (err: any) {
|
|
12
|
+
if (err?.code === 401 || err?.status === 401) {
|
|
13
|
+
await refreshAccessToken();
|
|
14
|
+
return callFn(); // retry once
|
|
15
|
+
}
|
|
16
|
+
throw err;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function refreshAccessToken() {
|
|
21
|
+
const jwt = getJWTObject();
|
|
22
|
+
if (!jwt?.refresh_token) {
|
|
23
|
+
// No refresh token — redirect to login
|
|
24
|
+
window.location.href = '/login';
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const newTokens = await client.refreshToken(jwt.refresh_token);
|
|
29
|
+
localStorage.setItem('calimero_jwt', JSON.stringify(newTokens));
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Why
|
|
34
|
+
|
|
35
|
+
Access tokens are short-lived by design. Without refresh handling, users will see
|
|
36
|
+
random authentication errors mid-session. The refresh token is longer-lived and
|
|
37
|
+
should be used to silently re-authenticate.
|
|
38
|
+
|
|
39
|
+
## What to do if refresh also fails
|
|
40
|
+
|
|
41
|
+
Redirect to login. Do not retry the refresh indefinitely.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# calimero-client-py — Agent Instructions
|
|
2
|
+
|
|
3
|
+
You are helping a developer use the **Calimero Python client** (`calimero-client-py`) to
|
|
4
|
+
interact with a Calimero node from Python — automation scripts, backend services, or CLI tools.
|
|
5
|
+
|
|
6
|
+
## What it is
|
|
7
|
+
|
|
8
|
+
`calimero-client-py` is a Python package built with PyO3 (Rust bindings). It provides:
|
|
9
|
+
- Full async API for managing contexts, applications, identities, blobs
|
|
10
|
+
- Automatic JWT token caching and refresh (tokens stored in `~/.merobox/auth_cache/`)
|
|
11
|
+
- A CLI for common node operations
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install calimero-client-py
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Build from source (requires Rust 1.70+ and maturin):**
|
|
20
|
+
```bash
|
|
21
|
+
pip install maturin
|
|
22
|
+
git clone https://github.com/calimero-network/calimero-client-py
|
|
23
|
+
cd calimero-client-py
|
|
24
|
+
maturin develop --release
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Minimal working example
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import asyncio
|
|
31
|
+
from calimero_client_py import create_connection, create_client
|
|
32
|
+
|
|
33
|
+
async def main():
|
|
34
|
+
connection = create_connection(
|
|
35
|
+
api_url="http://localhost:2428",
|
|
36
|
+
node_name="my-local-node" # must be stable — see rules/
|
|
37
|
+
)
|
|
38
|
+
client = create_client(connection)
|
|
39
|
+
contexts = await client.list_contexts()
|
|
40
|
+
print(contexts)
|
|
41
|
+
|
|
42
|
+
asyncio.run(main())
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Critical: `node_name` must be stable
|
|
46
|
+
|
|
47
|
+
`node_name` is used to derive the token cache filename. If it changes between runs,
|
|
48
|
+
the client loses its stored tokens and will need to re-authenticate.
|
|
49
|
+
|
|
50
|
+
## References
|
|
51
|
+
|
|
52
|
+
See `references/` for the full API, authentication flow, and CLI commands.
|
|
53
|
+
See `rules/` for the `node_name` stability requirement and async usage.
|