@sxl-studio/bridge 1.2.0 → 1.3.1
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 +122 -48
- package/dist/audit.d.ts +21 -0
- package/dist/audit.js +24 -0
- package/dist/audit.js.map +1 -0
- package/dist/auth.d.ts +18 -0
- package/dist/auth.js +39 -0
- package/dist/auth.js.map +1 -0
- package/dist/http-api.d.ts +13 -0
- package/dist/http-api.js +101 -5
- package/dist/http-api.js.map +1 -1
- package/dist/idempotency.d.ts +13 -0
- package/dist/idempotency.js +47 -0
- package/dist/idempotency.js.map +1 -0
- package/dist/mcp-factory.d.ts +1 -1
- package/dist/mcp-factory.js +19 -3
- package/dist/mcp-factory.js.map +1 -1
- package/dist/mcp-server.d.ts +1 -1
- package/dist/mcp-server.js +1 -1
- package/dist/mcp-server.js.map +1 -1
- package/dist/sxl-mcp-instructions.d.ts +1 -1
- package/dist/sxl-mcp-instructions.js +123 -23
- package/dist/sxl-mcp-instructions.js.map +1 -1
- package/dist/tools/catalogue-bootstrap.d.ts +9 -0
- package/dist/tools/catalogue-bootstrap.js +168 -0
- package/dist/tools/catalogue-bootstrap.js.map +1 -0
- package/dist/tools/composition.d.ts +1 -1
- package/dist/tools/composition.js +30 -2
- package/dist/tools/composition.js.map +1 -1
- package/dist/tools/data.d.ts +1 -1
- package/dist/tools/diagnostics.d.ts +1 -1
- package/dist/tools/diagnostics.js +5 -1
- package/dist/tools/diagnostics.js.map +1 -1
- package/dist/tools/figma-nodes.d.ts +1 -1
- package/dist/tools/figma-rc-extended.d.ts +1 -1
- package/dist/tools/figma-url.d.ts +20 -0
- package/dist/tools/figma-url.js +52 -0
- package/dist/tools/figma-url.js.map +1 -0
- package/dist/tools/git.d.ts +1 -1
- package/dist/tools/meta.d.ts +10 -0
- package/dist/tools/meta.js +36 -0
- package/dist/tools/meta.js.map +1 -0
- package/dist/tools/orchestration.d.ts +11 -0
- package/dist/tools/orchestration.js +112 -0
- package/dist/tools/orchestration.js.map +1 -0
- package/dist/tools/registry.d.ts +22 -0
- package/dist/tools/registry.js +26 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/resources.d.ts +17 -0
- package/dist/tools/resources.js +92 -0
- package/dist/tools/resources.js.map +1 -0
- package/dist/tools/shared.d.ts +20 -0
- package/dist/tools/shared.js +61 -0
- package/dist/tools/shared.js.map +1 -0
- package/dist/tools/styles.d.ts +8 -0
- package/dist/tools/styles.js +55 -0
- package/dist/tools/styles.js.map +1 -0
- package/dist/tools/tokens.d.ts +2 -2
- package/dist/tools/tokens.js +65 -34
- package/dist/tools/tokens.js.map +1 -1
- package/dist/tools/variables.d.ts +11 -0
- package/dist/tools/variables.js +55 -0
- package/dist/tools/variables.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,86 +5,93 @@ Node.js process that connects **Cursor (or any MCP client)** and **HTTP clients*
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
7
|
1. **WebSocket** — plugin UI iframe connects here and forwards `remote-command` / results.
|
|
8
|
-
2. **MCP (stdio)** — Cursor
|
|
9
|
-
3. **HTTP** — same command queue for scripts
|
|
8
|
+
2. **MCP (Streamable HTTP + stdio)** — Cursor / Claude Desktop / any MCP client calls typed tools (`export_variables`, `export_composition_json`, `get_codegen`, `compose_from_url`, …); each tool maps to one plugin command or an orchestration that chains several.
|
|
9
|
+
3. **HTTP** — same command queue for scripts, CI, curl, idempotency-safe retries, audit log.
|
|
10
10
|
|
|
11
|
-
Official [Figma MCP](https://developers.figma.com/docs/figma-mcp-server/) does not run your plugin code. Use **both**: Figma MCP for file/design context; **this bridge** for SXL-specific operations (compositions, token export, mappings,
|
|
11
|
+
Official [Figma MCP](https://developers.figma.com/docs/figma-mcp-server/) does not run your plugin code. Use **both**: Figma MCP for file/design context; **this bridge** for SXL-specific operations (compositions, token export, mappings, Git, Dev Mode codegen).
|
|
12
12
|
|
|
13
13
|
### Figma MCP vs SXL Bridge (product split)
|
|
14
14
|
|
|
15
|
-
Figma documents the hosted and desktop MCP in the Help Center: [Guide to the Figma MCP server](https://help.figma.com/hc/en-us/articles/32132100833559-Guide-to-the-Figma-MCP-server). **Remote** MCP is the default in their guidance; some client features (for example write-to-canvas flows in their compatibility matrix) are oriented toward **remote** MCP rather than a local-only server.
|
|
16
|
-
|
|
17
15
|
| Need | Use |
|
|
18
16
|
|------|-----|
|
|
19
|
-
| Native file operations where Figma’s MCP tools are enough (context, many canvas writes, variables in supported flows) | **Official Figma MCP** (often remote `https://mcp.figma.com/mcp`
|
|
20
|
-
| Plugin-only workflows: compositions, plugin token/Git APIs, whitelisted node helpers
|
|
17
|
+
| Native file operations where Figma’s MCP tools are enough (context, many canvas writes, variables in supported flows) | **Official Figma MCP** (often remote `https://mcp.figma.com/mcp`) |
|
|
18
|
+
| Plugin-only workflows: compositions, plugin token/Git APIs, whitelisted node helpers, Dev Mode codegen parity | **SXL Studio Bridge** + plugin Remote Connect |
|
|
21
19
|
|
|
22
|
-
Bridge forwards `commandType` + `payload` over WebSocket; **execution is always in the plugin** (whitelist + handlers). Adding an MCP tool in Bridge without the matching plugin handler will
|
|
20
|
+
Bridge forwards `commandType` + `payload` over WebSocket; **execution is always in the plugin** (whitelist + handlers). Adding an MCP tool in Bridge without the matching plugin handler will return `not_whitelisted`.
|
|
23
21
|
|
|
24
22
|
### Version compatibility (npm Bridge ⟷ plugin)
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
1.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
| `@sxl-studio/bridge` (npm) | SXL Studio plugin |
|
|
33
|
-
|---------------------------|-------------------|
|
|
34
|
-
| `1.0.x` | Same-era plugin with base remote node tools (`create_frame`, `set_node_fill`, …) |
|
|
35
|
-
| `1.1.x` | Plugin build that includes `set_node_fill_variable` and extended `create_frame` / `create_text` (`parentId`, Auto Layout–related optional fields) |
|
|
36
|
-
| `1.2.x` | Plugin build that includes Remote module split (`dispatch/`, `canvas/`, `sxl/`), MCP `instructions`, base canvas tools (`create_rectangle`, `set_auto_layout`, `duplicate_subtree`, `create_component_instance`, `apply_documentation_payload`), plus **extended** Remote Connect canvas tools registered from `src/tools/figma-rc-extended.ts`. Bridge **1.2.0+** reads `package.json` for startup version, aligns **timeouts** with plugin `COMMAND_TIMEOUTS`, and validates **`POST /api/command`** bodies (`commandType` + object `payload`). |
|
|
24
|
+
| `@sxl-studio/bridge` (npm) | SXL Studio plugin | Highlights |
|
|
25
|
+
|---------------------------|-------------------|------------|
|
|
26
|
+
| `1.0.x` | same-era | Base remote node tools (`create_frame`, `set_node_fill`, …) |
|
|
27
|
+
| `1.1.x` | same-era | `set_node_fill_variable`, extended `create_frame` / `create_text` (`parentId`, Auto Layout fields) |
|
|
28
|
+
| `1.2.x` | same-era | Remote module split (`dispatch/`, `canvas/`, `sxl/`), MCP instructions, base canvas tools, validated `POST /api/command`, aligned `COMMAND_TIMEOUTS` |
|
|
29
|
+
| **`1.3.x`** | **`2.1.0+`** | Dev Mode codegen bridge (`get_codegen`, `export_composition_json`), **full token workspace CRUD** (`create_token_file`, `delete_token_file`, `move_token_file`, `save_tokens_config`, upsert `save_token_file`), **Variables CRUD** (modes, scopes, codeSyntax), **Styles CRUD**, **orchestration** (`compose_from_url`, `generate_code_from_url`, `document_component`), **MCP Resources** (`sxl://…`), typed errors (`EDITOR_MODE_READONLY` + structured envelope), `BRIDGE_AUTH_TOKEN`, **Idempotency-Key** on `/api/command`, `/api/log` audit trail, `/api/tools` + `list_tools` meta-tool, editor-mode write-guard with typed propagation. |
|
|
37
30
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
### Remote node API strategy
|
|
41
|
-
|
|
42
|
-
New canvas automation is shipped as **small atomic** remote commands (variable-bound fills, optional `parentId`, Auto Layout fields) so Bridge and HTTP clients stay in sync with the whitelist. A single high-level command such as `generate_palette_documentation` remains a possible future optimization if you want fewer round-trips for one specific workflow.
|
|
31
|
+
Treat feature alignment by changelog or release tag, not npm name alone.
|
|
43
32
|
|
|
44
33
|
## Ports
|
|
45
34
|
|
|
46
|
-
**One TCP port** (`BRIDGE_PORT`, default **`37830`**
|
|
35
|
+
**One TCP port** (`BRIDGE_PORT`, default **`37830`**): plugin **WebSocket**, **HTTP REST** (`/api/*`), and **MCP Streamable HTTP** (`/mcp`) share it. Override with `BRIDGE_PORT=3100 npm start` if needed (update plugin build + `mcp.json` to match).
|
|
47
36
|
|
|
48
37
|
| On port `BRIDGE_PORT` | URL (default) |
|
|
49
38
|
|----------------------|---------------|
|
|
50
39
|
| Plugin UI | `ws://127.0.0.1:37830` |
|
|
51
|
-
| REST | `http://127.0.0.1:37830/api/status
|
|
40
|
+
| REST | `http://127.0.0.1:37830/api/{status,session,tools,log,command,disconnect}` |
|
|
52
41
|
| Cursor MCP | `http://127.0.0.1:37830/mcp` |
|
|
53
42
|
|
|
43
|
+
### Optional: shared-secret auth
|
|
44
|
+
|
|
45
|
+
Set `BRIDGE_AUTH_TOKEN` in the environment and every `/api/*` and `/mcp` request must send `Authorization: Bearer <token>`. Requests without the header return `401 BRIDGE_UNAUTHORIZED`. Useful when running Bridge on a shared machine, or behind a local proxy.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
BRIDGE_AUTH_TOKEN=s3cret npm start
|
|
49
|
+
```
|
|
50
|
+
|
|
54
51
|
### Cursor MCP (recommended)
|
|
55
52
|
|
|
56
|
-
|
|
53
|
+
Add `http://127.0.0.1:37830/mcp` to `.cursor/mcp.json`. Start Bridge **first** (`cd Utils/bridge && npm run build && npm start`). Then enable Remote Connect in the SXL Studio plugin.
|
|
57
54
|
|
|
58
|
-
Official **Figma MCP** (`http://127.0.0.1:3845/mcp`) reads design context from the file; it does **not** run SXL Studio plugin commands.
|
|
55
|
+
Official **Figma MCP** (`http://127.0.0.1:3845/mcp`) reads design context from the file; it does **not** run SXL Studio plugin commands.
|
|
59
56
|
|
|
60
57
|
## Run locally
|
|
61
58
|
|
|
62
59
|
```bash
|
|
63
|
-
cd
|
|
60
|
+
cd Utils/bridge
|
|
64
61
|
npm install
|
|
65
62
|
npm run build
|
|
66
63
|
node dist/index.js
|
|
67
64
|
```
|
|
68
65
|
|
|
69
|
-
If you see
|
|
66
|
+
If you see `EADDRINUSE`, pick a free port: `BRIDGE_PORT=39999 npm start`, then use `http://127.0.0.1:39999/mcp` in Cursor and rebuild the plugin so `remoteBridge.ts` default matches.
|
|
70
67
|
|
|
71
|
-
The repo
|
|
68
|
+
The repo `.cursor/mcp.json` points Cursor at `http://127.0.0.1:37830/mcp`. Bridge must run **before** opening Agent chat.
|
|
72
69
|
|
|
73
|
-
**MCP + stdout:** the Model Context Protocol uses **stdout** for JSON-RPC. Do not use `console.log` in this process (it corrupts the stream). Use
|
|
70
|
+
**MCP + stdout:** the Model Context Protocol uses **stdout** for JSON-RPC. Do not use `console.log` in this process (it corrupts the stream). Use `console.error` for diagnostics.
|
|
74
71
|
|
|
75
72
|
## MCP configuration (Cursor)
|
|
76
73
|
|
|
77
74
|
Example `.cursor/mcp.json`:
|
|
78
75
|
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"mcpServers": {
|
|
79
|
+
"sxl-studio": {
|
|
80
|
+
"url": "http://127.0.0.1:37830/mcp"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
stdio variant (if preferred):
|
|
87
|
+
|
|
79
88
|
```json
|
|
80
89
|
{
|
|
81
90
|
"mcpServers": {
|
|
82
91
|
"sxl-studio": {
|
|
83
92
|
"command": "node",
|
|
84
|
-
"args": ["/absolute/path/to/repo/
|
|
85
|
-
"env": {
|
|
86
|
-
"BRIDGE_PORT": "37830"
|
|
87
|
-
}
|
|
93
|
+
"args": ["/absolute/path/to/repo/Utils/bridge/dist/index.js"],
|
|
94
|
+
"env": { "BRIDGE_PORT": "37830" }
|
|
88
95
|
}
|
|
89
96
|
}
|
|
90
97
|
}
|
|
@@ -92,31 +99,59 @@ Example `.cursor/mcp.json`:
|
|
|
92
99
|
|
|
93
100
|
## HTTP API examples
|
|
94
101
|
|
|
95
|
-
Requires the plugin open in Figma, Remote Connect enabled, and
|
|
102
|
+
Requires the plugin open in Figma, Remote Connect enabled, and Bridge running.
|
|
96
103
|
|
|
97
|
-
|
|
104
|
+
### Status
|
|
98
105
|
|
|
99
106
|
```bash
|
|
100
107
|
curl -s http://127.0.0.1:37830/api/status
|
|
101
108
|
```
|
|
102
109
|
|
|
103
|
-
|
|
110
|
+
Returns `ports`, `mcpStreamableHttp`, `session`, `queueLength`, `currentCommand`, `authEnabled`.
|
|
111
|
+
|
|
112
|
+
### Tool catalogue
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
curl -s 'http://127.0.0.1:37830/api/tools?category=tokens'
|
|
116
|
+
curl -s 'http://127.0.0.1:37830/api/tools?devModeReadableOnly=true'
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Returns entries `{ name, category, description, requiresDesignMode, requiresComposition }`. MCP clients can call the equivalent `list_tools` meta-tool.
|
|
120
|
+
|
|
121
|
+
### Audit trail
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
curl -s 'http://127.0.0.1:37830/api/log?limit=50'
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Ring buffer of executed commands: `commandType`, `status`, `durationMs`, `payloadKeys`, `idempotencyKey`, `timestamp`.
|
|
128
|
+
|
|
129
|
+
### Read token file (after `list_token_files`)
|
|
104
130
|
|
|
105
131
|
```bash
|
|
106
132
|
curl -s -X POST http://127.0.0.1:37830/api/command \
|
|
107
133
|
-H "Content-Type: application/json" \
|
|
134
|
+
-H "Authorization: Bearer $BRIDGE_AUTH_TOKEN" \
|
|
135
|
+
-H "Idempotency-Key: 01HXYZ1234" \
|
|
108
136
|
-d '{"commandType":"get_token_file_content","payload":{"fileId":"tf_..."}}'
|
|
109
137
|
```
|
|
110
138
|
|
|
111
|
-
|
|
139
|
+
Repeat requests with the same `Idempotency-Key` (within 10 minutes) return the cached response without touching the plugin — safe for retries.
|
|
140
|
+
|
|
141
|
+
### Save token file (upsert)
|
|
112
142
|
|
|
113
143
|
```bash
|
|
114
144
|
curl -s -X POST http://127.0.0.1:37830/api/command \
|
|
115
145
|
-H "Content-Type: application/json" \
|
|
116
|
-
-d '{
|
|
146
|
+
-d '{
|
|
147
|
+
"commandType": "save_token_file",
|
|
148
|
+
"payload": { "name": "buttons", "folder": "components", "content": "{\"$type\":\"composition\"}" }
|
|
149
|
+
}'
|
|
117
150
|
```
|
|
118
151
|
|
|
119
|
-
|
|
152
|
+
If a file with that `name`+`folder` exists, it is updated. Otherwise it is created.
|
|
153
|
+
|
|
154
|
+
### Export composition JSON from current selection
|
|
120
155
|
|
|
121
156
|
```bash
|
|
122
157
|
curl -s -X POST http://127.0.0.1:37830/api/command \
|
|
@@ -126,15 +161,54 @@ curl -s -X POST http://127.0.0.1:37830/api/command \
|
|
|
126
161
|
|
|
127
162
|
Optional `payload.nodeId` selects a specific node instead of the first selected node.
|
|
128
163
|
|
|
164
|
+
### Typed errors
|
|
165
|
+
|
|
166
|
+
Failures return a structured envelope:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"status": "failed",
|
|
171
|
+
"error": "{\"code\":\"EDITOR_MODE_READONLY\",\"message\":\"Command \\\"create_frame\\\" requires Figma Design mode.\",\"retryable\":false,\"details\":{\"editorType\":\"dev\",\"mode\":\"default\",\"isDevMode\":true}}"
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Known codes: `EDITOR_MODE_READONLY`, `BRIDGE_TIMEOUT`, `BRIDGE_CANCELLED`, `BRIDGE_UNAUTHORIZED`, `BRIDGE_ERROR`. MCP tools decode the envelope before returning to the agent.
|
|
176
|
+
|
|
129
177
|
## MCP tools (summary)
|
|
130
178
|
|
|
131
|
-
Registered in `src/mcp-
|
|
179
|
+
Registered in `src/mcp-factory.ts` via `tools/*.ts`:
|
|
180
|
+
|
|
181
|
+
- **Diagnostics / meta:** `get_plugin_status`, `is_dev_mode`, `get_selection_summary`, `get_drift_status`, `list_tools`.
|
|
182
|
+
- **Tokens / workspace:** `list_token_files`, `get_token_file_content`, `save_token_file` (upsert), `create_token_file`, `delete_token_file`, `move_token_file`, `rename_token_file`, `get_tokens_config`, `save_tokens_config`, `export_variables`, `reset_diff*`, `reapply_token_bindings`, `cross_file_sync_*`, `get_applied_tokens`.
|
|
183
|
+
- **Composition:** `export_composition_json`, `get_codegen`, `list_compositions`, `generate_composition`, `apply_composition`, `preview_composition`, `check_composition_linked`, `inspect_selection`.
|
|
184
|
+
- **Variables:** `get_variables`, `create_variable_collection`, `create_variable`, `bind_variable`, `rename_variable`, `delete_variable`, `rename_variable_collection`, `delete_variable_collection`, `add_variable_mode`, `remove_variable_mode`, `rename_variable_mode`, `set_variable_mode_value`, `set_variable_scopes`, `set_variable_code_syntax`.
|
|
185
|
+
- **Styles:** `get_local_styles`, `create_paint_style`, `create_text_style`, `create_effect_style`, `set_text_style`, `set_effect_style`, `set_stroke_style`, `set_fill_style`, `import_style_by_key`.
|
|
186
|
+
- **Data / nodes:** `apply_mapping`, `apply_all_mappings`, `count_apply_targets`, `generate_instances`, `apply_image`, plus direct node helpers in `data.ts` / `figma-nodes.ts` (`set_node_fill_variable`, `create_rectangle`, `set_auto_layout`, `duplicate_subtree`, `create_component_instance`, `apply_documentation_payload`, extended `create_frame` / `create_text`).
|
|
187
|
+
- **Git:** `git_pull`, `git_hard_pull`, `git_push`.
|
|
188
|
+
- **Orchestration:** `generate_code_from_url`, `compose_from_url`, `document_component`.
|
|
189
|
+
|
|
190
|
+
### MCP Resources
|
|
191
|
+
|
|
192
|
+
| URI | Contents |
|
|
193
|
+
|-----|----------|
|
|
194
|
+
| `sxl://figma/status` | Plugin status snapshot |
|
|
195
|
+
| `sxl://figma/selection` | Current Figma selection |
|
|
196
|
+
| `sxl://tokens/config` | Raw `config.json` |
|
|
197
|
+
| `sxl://tokens/files` | Token file index |
|
|
198
|
+
| `sxl://tokens/files/{fileId}` | Raw token file body |
|
|
199
|
+
| `sxl://compositions/index` | Composition JSON index |
|
|
200
|
+
|
|
201
|
+
Cursor / Claude can attach these as context without calling tools.
|
|
202
|
+
|
|
203
|
+
## Dev Mode support
|
|
204
|
+
|
|
205
|
+
| Context | Reads | Canvas writes | Token/config/git writes |
|
|
206
|
+
|---------|-------|---------------|--------------------------|
|
|
207
|
+
| Figma Design | ✔︎ | ✔︎ | ✔︎ |
|
|
208
|
+
| Figma Dev Mode (UI, `mode: "default"`) | ✔︎ | ✘ (plugin returns `EDITOR_MODE_READONLY`) | ✔︎ |
|
|
209
|
+
| Figma Dev Mode (Inspect → Code sandbox) | read-only codegen callback; Remote Connect unavailable | ✘ | ✘ |
|
|
132
210
|
|
|
133
|
-
|
|
134
|
-
- **Composition:** `generate_composition`, `apply_composition`, `preview_composition`, `check_composition_linked`, `inspect_selection`, `export_composition_json`
|
|
135
|
-
- **Data / nodes:** `apply_mapping`, `apply_all_mappings`, `count_apply_targets`, `generate_instances`, `apply_image`, direct node helpers in `data.ts` / `figma-nodes.ts` (including `set_node_fill_variable`, `create_rectangle`, `set_auto_layout`, `duplicate_subtree`, `create_component_instance`, `apply_documentation_payload`, extended `create_frame` / `create_text`)
|
|
136
|
-
- **Git:** `git_pull`, `git_hard_pull`, `git_push`
|
|
137
|
-
- **Diagnostics:** plugin status, selection, drift, Code Connect, etc.
|
|
211
|
+
`get_codegen` (Bridge) and the Dev Mode Inspect → Code panel (plugin) share the exact same `routeCodegenGenerate` pipeline. Output is byte-identical.
|
|
138
212
|
|
|
139
213
|
## Development
|
|
140
214
|
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit ring buffer for Bridge commands.
|
|
3
|
+
*
|
|
4
|
+
* Every command dispatched via POST /api/command is recorded with its
|
|
5
|
+
* commandType, status, duration and a short payload-keys preview (no values
|
|
6
|
+
* — to avoid leaking secrets). GET /api/log returns the most recent entries.
|
|
7
|
+
* This is in-memory, per-process, and capped to 200 entries.
|
|
8
|
+
*/
|
|
9
|
+
import type { CommandResult } from "./types.js";
|
|
10
|
+
export type AuditEntry = {
|
|
11
|
+
at: number;
|
|
12
|
+
commandId: string;
|
|
13
|
+
commandType: string;
|
|
14
|
+
status: CommandResult["status"];
|
|
15
|
+
durationMs: number | null;
|
|
16
|
+
error: string | null;
|
|
17
|
+
payloadKeys: string[];
|
|
18
|
+
idempotencyKey: string | null;
|
|
19
|
+
};
|
|
20
|
+
export declare function recordAudit(entry: AuditEntry): void;
|
|
21
|
+
export declare function getAuditSnapshot(limit?: number): AuditEntry[];
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit ring buffer for Bridge commands.
|
|
3
|
+
*
|
|
4
|
+
* Every command dispatched via POST /api/command is recorded with its
|
|
5
|
+
* commandType, status, duration and a short payload-keys preview (no values
|
|
6
|
+
* — to avoid leaking secrets). GET /api/log returns the most recent entries.
|
|
7
|
+
* This is in-memory, per-process, and capped to 200 entries.
|
|
8
|
+
*/
|
|
9
|
+
const BUFFER_SIZE = 200;
|
|
10
|
+
const buffer = [];
|
|
11
|
+
export function recordAudit(entry) {
|
|
12
|
+
buffer.push(entry);
|
|
13
|
+
if (buffer.length > BUFFER_SIZE) {
|
|
14
|
+
buffer.splice(0, buffer.length - BUFFER_SIZE);
|
|
15
|
+
}
|
|
16
|
+
// Structured log line for operators tailing the console.
|
|
17
|
+
// eslint-disable-next-line no-console
|
|
18
|
+
console.error(`[Audit] ${new Date(entry.at).toISOString()} ${entry.commandType} -> ${entry.status}${typeof entry.durationMs === "number" ? ` (${entry.durationMs}ms)` : ""}${entry.error ? ` — ${entry.error}` : ""}`);
|
|
19
|
+
}
|
|
20
|
+
export function getAuditSnapshot(limit = 50) {
|
|
21
|
+
const clamped = Math.max(1, Math.min(limit, BUFFER_SIZE));
|
|
22
|
+
return buffer.slice(-clamped).reverse();
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAeH,MAAM,WAAW,GAAG,GAAG,CAAC;AACxB,MAAM,MAAM,GAAiB,EAAE,CAAC;AAEhC,MAAM,UAAU,WAAW,CAAC,KAAiB;IAC3C,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACnB,IAAI,MAAM,CAAC,MAAM,GAAG,WAAW,EAAE,CAAC;QAChC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,GAAG,WAAW,CAAC,CAAC;IAChD,CAAC;IACD,yDAAyD;IACzD,sCAAsC;IACtC,OAAO,CAAC,KAAK,CACX,WAAW,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,IAAI,KAAK,CAAC,WAAW,OAAO,KAAK,CAAC,MAAM,GACjF,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,UAAU,KAAK,CAAC,CAAC,CAAC,EACtE,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAC5C,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAK,GAAG,EAAE;IACzC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC;IAC1D,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,CAAC;AAC1C,CAAC"}
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge authentication — optional shared-token gate.
|
|
3
|
+
*
|
|
4
|
+
* Activated when BRIDGE_AUTH_TOKEN env var is set. Requests must then carry
|
|
5
|
+
* `Authorization: Bearer <token>` (case-insensitive header name, case-sensitive token).
|
|
6
|
+
* Applies to both REST /api/* and MCP Streamable HTTP at /mcp.
|
|
7
|
+
*
|
|
8
|
+
* Disabled by default to preserve zero-config "run and connect Cursor" UX.
|
|
9
|
+
*/
|
|
10
|
+
import type { IncomingMessage } from "node:http";
|
|
11
|
+
export declare function getBridgeAuthToken(): string | null;
|
|
12
|
+
export declare function checkAuth(req: IncomingMessage): {
|
|
13
|
+
ok: true;
|
|
14
|
+
} | {
|
|
15
|
+
ok: false;
|
|
16
|
+
reason: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function isAuthEnabled(): boolean;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge authentication — optional shared-token gate.
|
|
3
|
+
*
|
|
4
|
+
* Activated when BRIDGE_AUTH_TOKEN env var is set. Requests must then carry
|
|
5
|
+
* `Authorization: Bearer <token>` (case-insensitive header name, case-sensitive token).
|
|
6
|
+
* Applies to both REST /api/* and MCP Streamable HTTP at /mcp.
|
|
7
|
+
*
|
|
8
|
+
* Disabled by default to preserve zero-config "run and connect Cursor" UX.
|
|
9
|
+
*/
|
|
10
|
+
export function getBridgeAuthToken() {
|
|
11
|
+
const raw = process.env["BRIDGE_AUTH_TOKEN"];
|
|
12
|
+
if (!raw)
|
|
13
|
+
return null;
|
|
14
|
+
const trimmed = raw.trim();
|
|
15
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
16
|
+
}
|
|
17
|
+
export function checkAuth(req) {
|
|
18
|
+
const token = getBridgeAuthToken();
|
|
19
|
+
if (!token)
|
|
20
|
+
return { ok: true };
|
|
21
|
+
const header = req.headers["authorization"] ??
|
|
22
|
+
req.headers["Authorization"];
|
|
23
|
+
if (!header) {
|
|
24
|
+
return { ok: false, reason: "Missing Authorization header" };
|
|
25
|
+
}
|
|
26
|
+
const [scheme, ...rest] = header.split(" ");
|
|
27
|
+
if (scheme?.toLowerCase() !== "bearer") {
|
|
28
|
+
return { ok: false, reason: "Authorization header must use Bearer scheme" };
|
|
29
|
+
}
|
|
30
|
+
const provided = rest.join(" ").trim();
|
|
31
|
+
if (provided !== token) {
|
|
32
|
+
return { ok: false, reason: "Invalid bearer token" };
|
|
33
|
+
}
|
|
34
|
+
return { ok: true };
|
|
35
|
+
}
|
|
36
|
+
export function isAuthEnabled() {
|
|
37
|
+
return getBridgeAuthToken() !== null;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,MAAM,UAAU,kBAAkB;IAChC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAC7C,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,GAAoB;IAC5C,MAAM,KAAK,GAAG,kBAAkB,EAAE,CAAC;IACnC,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IAChC,MAAM,MAAM,GACT,GAAG,CAAC,OAAO,CAAC,eAAe,CAAwB;QACnD,GAAG,CAAC,OAAO,CAAC,eAAe,CAAmC,CAAC;IAClE,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,8BAA8B,EAAE,CAAC;IAC/D,CAAC;IACD,MAAM,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC5C,IAAI,MAAM,EAAE,WAAW,EAAE,KAAK,QAAQ,EAAE,CAAC;QACvC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,6CAA6C,EAAE,CAAC;IAC9E,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACvC,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;QACvB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC;IACvD,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,OAAO,kBAAkB,EAAE,KAAK,IAAI,CAAC;AACvC,CAAC"}
|
package/dist/http-api.d.ts
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Also serves MCP Streamable HTTP at GET|POST /mcp (recommended for Cursor — same pattern
|
|
5
5
|
* as Figma's official MCP: add server URL in Cursor settings).
|
|
6
|
+
*
|
|
7
|
+
* Endpoints:
|
|
8
|
+
* GET /api/status — server + plugin session snapshot
|
|
9
|
+
* GET /api/session — detailed session info
|
|
10
|
+
* GET /api/tools — authoritative catalogue of MCP tools
|
|
11
|
+
* GET /api/log?limit=N — audit trail of recent commands (ring buffer)
|
|
12
|
+
* POST /api/command — execute a plugin command (with optional Idempotency-Key)
|
|
13
|
+
* POST /api/disconnect — close the current plugin socket
|
|
14
|
+
* ANY /mcp — MCP Streamable HTTP
|
|
15
|
+
*
|
|
16
|
+
* Security:
|
|
17
|
+
* When BRIDGE_AUTH_TOKEN is set, all /api/* and /mcp requests require
|
|
18
|
+
* `Authorization: Bearer <token>`.
|
|
6
19
|
*/
|
|
7
20
|
import type { BridgeWSServer } from "./ws-server.js";
|
|
8
21
|
export declare function startHttpApi(bridgePort: number, wsServer: BridgeWSServer): Promise<void>;
|
package/dist/http-api.js
CHANGED
|
@@ -3,11 +3,29 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Also serves MCP Streamable HTTP at GET|POST /mcp (recommended for Cursor — same pattern
|
|
5
5
|
* as Figma's official MCP: add server URL in Cursor settings).
|
|
6
|
+
*
|
|
7
|
+
* Endpoints:
|
|
8
|
+
* GET /api/status — server + plugin session snapshot
|
|
9
|
+
* GET /api/session — detailed session info
|
|
10
|
+
* GET /api/tools — authoritative catalogue of MCP tools
|
|
11
|
+
* GET /api/log?limit=N — audit trail of recent commands (ring buffer)
|
|
12
|
+
* POST /api/command — execute a plugin command (with optional Idempotency-Key)
|
|
13
|
+
* POST /api/disconnect — close the current plugin socket
|
|
14
|
+
* ANY /mcp — MCP Streamable HTTP
|
|
15
|
+
*
|
|
16
|
+
* Security:
|
|
17
|
+
* When BRIDGE_AUTH_TOKEN is set, all /api/* and /mcp requests require
|
|
18
|
+
* `Authorization: Bearer <token>`.
|
|
6
19
|
*/
|
|
7
20
|
import { createServer } from "node:http";
|
|
8
|
-
import { StreamableHTTPServerTransport } from "
|
|
9
|
-
import {
|
|
21
|
+
import { StreamableHTTPServerTransport } from "../node_modules/@modelcontextprotocol/sdk/dist/cjs/server/streamableHttp.js";
|
|
22
|
+
import { getAuditSnapshot, recordAudit } from "./audit.js";
|
|
23
|
+
import { checkAuth, isAuthEnabled } from "./auth.js";
|
|
10
24
|
import { parseHttpCommandBody } from "./command-http.js";
|
|
25
|
+
import { getIdempotent, putIdempotent } from "./idempotency.js";
|
|
26
|
+
import { createBridgeMcpServer } from "./mcp-factory.js";
|
|
27
|
+
import { bootstrapBridgeToolCatalogue } from "./tools/catalogue-bootstrap.js";
|
|
28
|
+
import { getBridgeToolCatalogue } from "./tools/registry.js";
|
|
11
29
|
function readBody(req) {
|
|
12
30
|
return new Promise((resolve, reject) => {
|
|
13
31
|
let body = "";
|
|
@@ -23,7 +41,7 @@ function sendJson(res, status, data) {
|
|
|
23
41
|
"Content-Type": "application/json",
|
|
24
42
|
"Access-Control-Allow-Origin": "*",
|
|
25
43
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
26
|
-
"Access-Control-Allow-Headers": "Content-Type, Accept, Authorization, Mcp-Session-Id, mcp-protocol-version",
|
|
44
|
+
"Access-Control-Allow-Headers": "Content-Type, Accept, Authorization, Mcp-Session-Id, mcp-protocol-version, Idempotency-Key",
|
|
27
45
|
});
|
|
28
46
|
res.end(JSON.stringify(data, null, 2));
|
|
29
47
|
}
|
|
@@ -35,9 +53,19 @@ function pathnameOnly(reqUrl) {
|
|
|
35
53
|
return "/";
|
|
36
54
|
}
|
|
37
55
|
}
|
|
56
|
+
function searchParamsOf(reqUrl) {
|
|
57
|
+
try {
|
|
58
|
+
return new URL(reqUrl || "/", "http://127.0.0.1").searchParams;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return new URLSearchParams();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
38
64
|
export async function startHttpApi(bridgePort, wsServer) {
|
|
39
65
|
const queue = wsServer.commandQueue;
|
|
40
66
|
const session = wsServer.session;
|
|
67
|
+
// Keep catalogue + MCP server init symmetric for both stdio and HTTP.
|
|
68
|
+
bootstrapBridgeToolCatalogue();
|
|
41
69
|
const mcpHttpServer = createBridgeMcpServer(queue);
|
|
42
70
|
const mcpTransport = new StreamableHTTPServerTransport({
|
|
43
71
|
sessionIdGenerator: undefined,
|
|
@@ -56,11 +84,21 @@ export async function startHttpApi(bridgePort, wsServer) {
|
|
|
56
84
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
57
85
|
"Access-Control-Allow-Headers": typeof reqHdr === "string" && reqHdr.length > 0
|
|
58
86
|
? reqHdr
|
|
59
|
-
: "Content-Type, Accept, Authorization, Mcp-Session-Id, mcp-protocol-version",
|
|
87
|
+
: "Content-Type, Accept, Authorization, Mcp-Session-Id, mcp-protocol-version, Idempotency-Key",
|
|
60
88
|
});
|
|
61
89
|
res.end();
|
|
62
90
|
return;
|
|
63
91
|
}
|
|
92
|
+
// Optional shared-secret auth (BRIDGE_AUTH_TOKEN).
|
|
93
|
+
const auth = checkAuth(req);
|
|
94
|
+
if (!auth.ok) {
|
|
95
|
+
sendJson(res, 401, {
|
|
96
|
+
code: "BRIDGE_UNAUTHORIZED",
|
|
97
|
+
message: auth.reason,
|
|
98
|
+
retryable: false,
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
64
102
|
try {
|
|
65
103
|
// MCP Streamable HTTP (Cursor, same idea as Figma MCP URL in docs)
|
|
66
104
|
if (pathname === "/mcp") {
|
|
@@ -91,7 +129,7 @@ export async function startHttpApi(bridgePort, wsServer) {
|
|
|
91
129
|
queueLength: queue.queueLength,
|
|
92
130
|
currentCommand: queue.currentCommand,
|
|
93
131
|
};
|
|
94
|
-
sendJson(res, 200, status);
|
|
132
|
+
sendJson(res, 200, { ...status, authEnabled: isAuthEnabled() });
|
|
95
133
|
return;
|
|
96
134
|
}
|
|
97
135
|
// GET /api/session
|
|
@@ -99,6 +137,31 @@ export async function startHttpApi(bridgePort, wsServer) {
|
|
|
99
137
|
sendJson(res, 200, session.info);
|
|
100
138
|
return;
|
|
101
139
|
}
|
|
140
|
+
// GET /api/tools — authoritative catalogue (same as list_tools MCP tool)
|
|
141
|
+
if (req.method === "GET" && pathname === "/api/tools") {
|
|
142
|
+
const sp = searchParamsOf(req.url || "/");
|
|
143
|
+
const category = sp.get("category");
|
|
144
|
+
const devModeReadableOnly = sp.get("devModeReadableOnly") === "true";
|
|
145
|
+
let entries = [...getBridgeToolCatalogue()];
|
|
146
|
+
if (category)
|
|
147
|
+
entries = entries.filter((e) => e.category === category);
|
|
148
|
+
if (devModeReadableOnly)
|
|
149
|
+
entries = entries.filter((e) => !e.requiresDesignMode);
|
|
150
|
+
sendJson(res, 200, {
|
|
151
|
+
count: entries.length,
|
|
152
|
+
categories: Array.from(new Set(entries.map((e) => e.category))),
|
|
153
|
+
tools: entries,
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// GET /api/log — recent audit entries (ring buffer)
|
|
158
|
+
if (req.method === "GET" && pathname === "/api/log") {
|
|
159
|
+
const sp = searchParamsOf(req.url || "/");
|
|
160
|
+
const rawLimit = parseInt(sp.get("limit") ?? "", 10);
|
|
161
|
+
const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? rawLimit : 50;
|
|
162
|
+
sendJson(res, 200, { entries: getAuditSnapshot(limit) });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
102
165
|
// POST /api/command
|
|
103
166
|
if (req.method === "POST" && pathname === "/api/command") {
|
|
104
167
|
if (!session.connected) {
|
|
@@ -111,7 +174,37 @@ export async function startHttpApi(bridgePort, wsServer) {
|
|
|
111
174
|
sendJson(res, parsed.status, { error: parsed.error });
|
|
112
175
|
return;
|
|
113
176
|
}
|
|
177
|
+
// Idempotency: header wins, body fallback.
|
|
178
|
+
const headerKey = req.headers["idempotency-key"] ??
|
|
179
|
+
req.headers["Idempotency-Key"] ??
|
|
180
|
+
null;
|
|
181
|
+
const bodyKeyRaw = JSON.parse(body || "{}")?.idempotencyKey;
|
|
182
|
+
const idempotencyKey = (typeof headerKey === "string" && headerKey.length > 0 ? headerKey : null) ??
|
|
183
|
+
(typeof bodyKeyRaw === "string" && bodyKeyRaw.length > 0 ? bodyKeyRaw : null);
|
|
184
|
+
if (idempotencyKey) {
|
|
185
|
+
const cached = getIdempotent(idempotencyKey);
|
|
186
|
+
if (cached) {
|
|
187
|
+
sendJson(res, 200, { ...cached, idempotent: "hit" });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const startedAt = Date.now();
|
|
114
192
|
const result = await queue.execute(parsed.commandType, parsed.payload);
|
|
193
|
+
recordAudit({
|
|
194
|
+
at: startedAt,
|
|
195
|
+
commandId: result.commandId,
|
|
196
|
+
commandType: parsed.commandType,
|
|
197
|
+
status: result.status,
|
|
198
|
+
durationMs: typeof result.durationMs === "number"
|
|
199
|
+
? result.durationMs
|
|
200
|
+
: Date.now() - startedAt,
|
|
201
|
+
error: result.error ?? null,
|
|
202
|
+
payloadKeys: Object.keys(parsed.payload ?? {}),
|
|
203
|
+
idempotencyKey,
|
|
204
|
+
});
|
|
205
|
+
if (idempotencyKey) {
|
|
206
|
+
putIdempotent(idempotencyKey, result);
|
|
207
|
+
}
|
|
115
208
|
sendJson(res, 200, result);
|
|
116
209
|
return;
|
|
117
210
|
}
|
|
@@ -139,6 +232,9 @@ export async function startHttpApi(bridgePort, wsServer) {
|
|
|
139
232
|
});
|
|
140
233
|
server.listen(bridgePort, "127.0.0.1", () => {
|
|
141
234
|
console.error(`[Bridge] Listening on http://127.0.0.1:${bridgePort} (REST /api/*, MCP /mcp, plugin ws://127.0.0.1:${bridgePort})`);
|
|
235
|
+
if (isAuthEnabled()) {
|
|
236
|
+
console.error("[Bridge] Auth enabled — require Authorization: Bearer <BRIDGE_AUTH_TOKEN>");
|
|
237
|
+
}
|
|
142
238
|
resolve();
|
|
143
239
|
});
|
|
144
240
|
});
|
package/dist/http-api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-api.js","sourceRoot":"","sources":["../src/http-api.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"http-api.js","sourceRoot":"","sources":["../src/http-api.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotency cache for POST /api/command.
|
|
3
|
+
*
|
|
4
|
+
* Clients may pass an `Idempotency-Key` HTTP header (or top-level
|
|
5
|
+
* `idempotencyKey` in the JSON body). Repeating the same key within the TTL
|
|
6
|
+
* window returns the original response without re-dispatching the command to
|
|
7
|
+
* the plugin — avoiding duplicate side effects on network retries.
|
|
8
|
+
*
|
|
9
|
+
* Cache is in-memory, per-process, and intentionally small.
|
|
10
|
+
*/
|
|
11
|
+
import type { CommandResult } from "./types.js";
|
|
12
|
+
export declare function getIdempotent(key: string): CommandResult | null;
|
|
13
|
+
export declare function putIdempotent(key: string, result: CommandResult): void;
|