@knotieaipro/openclaw-channel-knotie 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/LICENSE +21 -0
- package/README.md +144 -0
- package/dist/auth.d.ts +52 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +102 -0
- package/dist/auth.js.map +1 -0
- package/dist/channel.d.ts +64 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +128 -0
- package/dist/channel.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +25 -0
- package/dist/types.js.map +1 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Knotie AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# @knotie/openclaw-channel-knotie
|
|
2
|
+
|
|
3
|
+
> **OpenClaw plugin** — Secure portal chat channel that connects the [Knotie AI](https://knotie.ai) whitelabel portal to an [OpenClaw](https://openclaw.ai) agent running on a customer VPS.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What this plugin does
|
|
8
|
+
|
|
9
|
+
When you deploy OpenClaw through Knotie's VPS catalog, your customers get an agent that runs on *their* infrastructure — but you need a way for the Knotie whitelabel portal to chat with it securely over the public internet.
|
|
10
|
+
|
|
11
|
+
This plugin registers a hardened HTTP channel (`/knotie-channel`) on the OpenClaw instance. The Knotie portal's server-side proxy calls this channel to forward customer messages and receive agent replies — without any Tailscale or shared VPN required.
|
|
12
|
+
|
|
13
|
+
### Endpoints
|
|
14
|
+
|
|
15
|
+
| Method | Path | Auth | Description |
|
|
16
|
+
|--------|------|------|-------------|
|
|
17
|
+
| `GET` | `/knotie-channel/health` | None | Liveness check — used by the portal's status indicator |
|
|
18
|
+
| `POST` | `/knotie-channel/chat` | Bearer + HMAC | Send a message, receive an agent reply |
|
|
19
|
+
| `POST` | `/knotie-channel/clear-session` | Bearer + HMAC | Clear agent session history for a given session ID |
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Security model — 5 layers, defence-in-depth
|
|
24
|
+
|
|
25
|
+
The channel is designed to be exposed on a public VPS port without becoming a liability. Each layer independently limits what an attacker can do:
|
|
26
|
+
|
|
27
|
+
| Layer | Where | What it stops |
|
|
28
|
+
|-------|-------|---------------|
|
|
29
|
+
| **TLS** | nginx (self-signed cert) | Traffic interception — all data is encrypted in transit |
|
|
30
|
+
| **Silent 444 drop** | nginx | Port scanning — every path except `/knotie-channel/` returns no response; the port appears closed to scanners |
|
|
31
|
+
| **Knock header** (`X-Knotie-Gateway`) | nginx | Drive-by requests — nginx returns 444 (no response) if this per-instance secret is missing or wrong |
|
|
32
|
+
| **Bearer token** | This plugin | Credential brute-force — constant-time (`timingSafeEqual`) verification of the 32-byte hex shared secret |
|
|
33
|
+
| **HMAC request nonce** | This plugin | Replay attacks — every request must include `X-Knotie-Timestamp` + `X-Knotie-Nonce` + `X-Knotie-Signature` (HMAC-SHA256); the plugin rejects requests outside a ±5-minute window |
|
|
34
|
+
|
|
35
|
+
The knock header and HMAC secret are generated per-deployment (not shared across instances) and stored only in the Knotie DB — never exposed to the browser.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## About Knotie AI
|
|
40
|
+
|
|
41
|
+
[Knotie AI](https://knotie.ai) is a white-label AI platform built for agencies and developers who want to resell AI products under their own brand.
|
|
42
|
+
|
|
43
|
+
**What agencies get on Knotie:**
|
|
44
|
+
|
|
45
|
+
- One-click deploy templates for AI Receptionist, Voice SDR, Support Bot, Cloud Setup, and more — fully white-labeled under your domain
|
|
46
|
+
- A VPS marketplace where customers can deploy self-hosted AI tools (OpenClaw, n8n, Open WebUI, etc.) and manage them from your portal
|
|
47
|
+
- An AI Gateway (OpenAI-compatible, 50+ models) you can sell as a standalone product
|
|
48
|
+
- Multi-provider voice support: VAPI, Retell, ElevenLabs, LiveKit, Ultravox
|
|
49
|
+
- Built-in billing: Stripe Connect, credit system, metered usage — you set the margin
|
|
50
|
+
|
|
51
|
+
OpenClaw deployed through Knotie gets a fully automated setup: SSH deploy, nginx TLS proxy, this channel plugin, the customizer plugin, and all secrets generated and stored without any manual steps.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
openclaw plugin add @knotieaipro/openclaw-channel-knotie
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
> **Note:** When deploying OpenClaw through Knotie's VPS catalog, this plugin is installed and configured automatically as part of the deploy script. Manual installation is only needed for self-managed instances that you want to connect to a Knotie portal.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Configuration
|
|
66
|
+
|
|
67
|
+
| Variable | Required | Description |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `KNOTIE_CHANNEL_TOKEN` | Yes | 32-byte hex shared secret — generated by Knotie at deploy time |
|
|
70
|
+
|
|
71
|
+
The token is injected into `/etc/environment` and `/root/.openclaw/channel.env` during the catalog deploy so it survives daemon restarts.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Network topology
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
Customer browser
|
|
79
|
+
│
|
|
80
|
+
▼
|
|
81
|
+
Knotie whitelabel portal (Next.js)
|
|
82
|
+
│ POST /api/whitelabel/vps/instances/[id]/openclaw/chat
|
|
83
|
+
▼
|
|
84
|
+
Portal server-side proxy (Node.js)
|
|
85
|
+
│ HTTPS · Bearer token · Knock header · HMAC nonce
|
|
86
|
+
▼
|
|
87
|
+
VPS public IP : 18790 (nginx TLS proxy)
|
|
88
|
+
│ 444 drop on unknown paths
|
|
89
|
+
│ Rate-limited: 10 req/min, 3 concurrent
|
|
90
|
+
▼
|
|
91
|
+
loopback : 18789 (OpenClaw)
|
|
92
|
+
│ This plugin validates Bearer + HMAC
|
|
93
|
+
▼
|
|
94
|
+
Agent reply → reverse through the same chain → browser
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The customer connects their OpenClaw agent (and its control UI) via their own Tailscale network. The Knotie portal uses the public nginx channel — no shared VPN required.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## How the HMAC signature works
|
|
102
|
+
|
|
103
|
+
The portal signs every request before sending it:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
// Portal side (simplified)
|
|
107
|
+
const timestamp = String(Date.now());
|
|
108
|
+
const nonce = randomBytes(16).toString('hex');
|
|
109
|
+
const message = `${timestamp}:${nonce}`;
|
|
110
|
+
const signature = createHmac('sha256', channelToken).update(message).digest('hex');
|
|
111
|
+
|
|
112
|
+
headers['X-Knotie-Timestamp'] = timestamp;
|
|
113
|
+
headers['X-Knotie-Nonce'] = nonce;
|
|
114
|
+
headers['X-Knotie-Signature'] = signature;
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The plugin verifies:
|
|
118
|
+
1. All three headers are present
|
|
119
|
+
2. Timestamp is within ±5 minutes of the VPS clock
|
|
120
|
+
3. HMAC-SHA256 matches (constant-time comparison)
|
|
121
|
+
|
|
122
|
+
A captured request is useless after 5 minutes — even if the attacker has the exact headers.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Requirements
|
|
127
|
+
|
|
128
|
+
- OpenClaw ≥ 3.0.0
|
|
129
|
+
- Node.js ≥ 18 (ESM)
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT — see [LICENSE](./LICENSE)
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Links
|
|
140
|
+
|
|
141
|
+
- [Knotie AI Platform](https://knotie.ai) — white-label AI for agencies
|
|
142
|
+
- [OpenClaw](https://openclaw.ai) — self-hosted AI agent runtime
|
|
143
|
+
- [Knotie Partner Dashboard](https://app.knotie.ai) — manage your deployments
|
|
144
|
+
- [@knotieaipro/openclaw-customizer](https://www.npmjs.com/package/@knotieaipro/openclaw-customizer) — white-label branding plugin
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auth.ts — Token verification helpers for the Knotie channel.
|
|
3
|
+
*
|
|
4
|
+
* Multi-layer authentication:
|
|
5
|
+
*
|
|
6
|
+
* 1. Bearer token — `Authorization: Bearer <KNOTIE_CHANNEL_TOKEN>`
|
|
7
|
+
* Constant-time comparison (timingSafeEqual) prevents timing attacks.
|
|
8
|
+
*
|
|
9
|
+
* 2. HMAC request nonce — Layer 5 replay prevention.
|
|
10
|
+
* The portal signs every request with:
|
|
11
|
+
* X-Knotie-Timestamp : unix epoch milliseconds (string)
|
|
12
|
+
* X-Knotie-Nonce : 16-byte random hex
|
|
13
|
+
* X-Knotie-Signature : HMAC-SHA256(token, "<timestamp>:<nonce>")
|
|
14
|
+
* The plugin validates:
|
|
15
|
+
* - Timestamp is within ±5 minutes of server time.
|
|
16
|
+
* - HMAC matches (constant-time compare).
|
|
17
|
+
* This makes captured/intercepted requests expire within 5 minutes.
|
|
18
|
+
*
|
|
19
|
+
* Additional layers (handled outside this file):
|
|
20
|
+
* 3. nginx knock header (X-Knotie-Gateway) — anti-scanner, silent 444 drop.
|
|
21
|
+
* 4. nginx TLS (self-signed cert) — transport encryption.
|
|
22
|
+
* 5. nginx rate limiting — 10 req/min per IP.
|
|
23
|
+
*/
|
|
24
|
+
/** Resolve the channel token from env. Throws if missing. */
|
|
25
|
+
export declare function resolveChannelToken(): string;
|
|
26
|
+
/**
|
|
27
|
+
* Verify the Authorization header value against the configured channel token.
|
|
28
|
+
*
|
|
29
|
+
* @param authHeader Full "Authorization: Bearer ..." header value.
|
|
30
|
+
* @param expected The expected token (from resolveChannelToken).
|
|
31
|
+
* @returns true if valid, false otherwise.
|
|
32
|
+
*/
|
|
33
|
+
export declare function verifyBearerToken(authHeader: string | undefined, expected: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Verify the HMAC request-nonce signature (Layer 5 — replay prevention).
|
|
36
|
+
*
|
|
37
|
+
* The portal must include three headers on every authenticated request:
|
|
38
|
+
* X-Knotie-Timestamp : unix ms as a decimal string
|
|
39
|
+
* X-Knotie-Nonce : random hex value (ensures uniqueness within window)
|
|
40
|
+
* X-Knotie-Signature : HMAC-SHA256(channelToken, "<timestamp>:<nonce>")
|
|
41
|
+
*
|
|
42
|
+
* @param headers Request headers map (lowercase keys).
|
|
43
|
+
* @param token The channel token to derive the HMAC key from.
|
|
44
|
+
* @returns true if valid (correct signature AND timestamp within ±5 min).
|
|
45
|
+
*/
|
|
46
|
+
export declare function verifyRequestSignature(headers: Record<string, string | undefined>, token: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Generate an HMAC signature for a webhook payload.
|
|
49
|
+
* Used when the plugin pushes events back to the Knotie portal.
|
|
50
|
+
*/
|
|
51
|
+
export declare function signPayload(secret: string, payload: string): string;
|
|
52
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AASH,6DAA6D;AAC7D,wBAAgB,mBAAmB,IAAI,MAAM,CAS5C;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAW3F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,EAC3C,KAAK,EAAE,MAAM,GACZ,OAAO,CAwBT;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEnE"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auth.ts — Token verification helpers for the Knotie channel.
|
|
3
|
+
*
|
|
4
|
+
* Multi-layer authentication:
|
|
5
|
+
*
|
|
6
|
+
* 1. Bearer token — `Authorization: Bearer <KNOTIE_CHANNEL_TOKEN>`
|
|
7
|
+
* Constant-time comparison (timingSafeEqual) prevents timing attacks.
|
|
8
|
+
*
|
|
9
|
+
* 2. HMAC request nonce — Layer 5 replay prevention.
|
|
10
|
+
* The portal signs every request with:
|
|
11
|
+
* X-Knotie-Timestamp : unix epoch milliseconds (string)
|
|
12
|
+
* X-Knotie-Nonce : 16-byte random hex
|
|
13
|
+
* X-Knotie-Signature : HMAC-SHA256(token, "<timestamp>:<nonce>")
|
|
14
|
+
* The plugin validates:
|
|
15
|
+
* - Timestamp is within ±5 minutes of server time.
|
|
16
|
+
* - HMAC matches (constant-time compare).
|
|
17
|
+
* This makes captured/intercepted requests expire within 5 minutes.
|
|
18
|
+
*
|
|
19
|
+
* Additional layers (handled outside this file):
|
|
20
|
+
* 3. nginx knock header (X-Knotie-Gateway) — anti-scanner, silent 444 drop.
|
|
21
|
+
* 4. nginx TLS (self-signed cert) — transport encryption.
|
|
22
|
+
* 5. nginx rate limiting — 10 req/min per IP.
|
|
23
|
+
*/
|
|
24
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
25
|
+
const PLUGIN_NAME = '@knotie/openclaw-channel-knotie';
|
|
26
|
+
/** Maximum clock skew accepted between portal and VPS (milliseconds). */
|
|
27
|
+
const MAX_CLOCK_SKEW_MS = 5 * 60 * 1000; // 5 minutes
|
|
28
|
+
/** Resolve the channel token from env. Throws if missing. */
|
|
29
|
+
export function resolveChannelToken() {
|
|
30
|
+
const token = process.env['KNOTIE_CHANNEL_TOKEN']?.trim();
|
|
31
|
+
if (!token) {
|
|
32
|
+
throw new Error(`[${PLUGIN_NAME}] KNOTIE_CHANNEL_TOKEN is not set. ` +
|
|
33
|
+
'Set it in the gateway env (catalog deploy injects it automatically).');
|
|
34
|
+
}
|
|
35
|
+
return token;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Verify the Authorization header value against the configured channel token.
|
|
39
|
+
*
|
|
40
|
+
* @param authHeader Full "Authorization: Bearer ..." header value.
|
|
41
|
+
* @param expected The expected token (from resolveChannelToken).
|
|
42
|
+
* @returns true if valid, false otherwise.
|
|
43
|
+
*/
|
|
44
|
+
export function verifyBearerToken(authHeader, expected) {
|
|
45
|
+
if (!authHeader?.startsWith('Bearer '))
|
|
46
|
+
return false;
|
|
47
|
+
const provided = authHeader.slice('Bearer '.length).trim();
|
|
48
|
+
if (provided.length !== expected.length)
|
|
49
|
+
return false;
|
|
50
|
+
// timingSafeEqual requires equal-length Buffers
|
|
51
|
+
try {
|
|
52
|
+
return timingSafeEqual(Buffer.from(provided, 'utf8'), Buffer.from(expected, 'utf8'));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Verify the HMAC request-nonce signature (Layer 5 — replay prevention).
|
|
60
|
+
*
|
|
61
|
+
* The portal must include three headers on every authenticated request:
|
|
62
|
+
* X-Knotie-Timestamp : unix ms as a decimal string
|
|
63
|
+
* X-Knotie-Nonce : random hex value (ensures uniqueness within window)
|
|
64
|
+
* X-Knotie-Signature : HMAC-SHA256(channelToken, "<timestamp>:<nonce>")
|
|
65
|
+
*
|
|
66
|
+
* @param headers Request headers map (lowercase keys).
|
|
67
|
+
* @param token The channel token to derive the HMAC key from.
|
|
68
|
+
* @returns true if valid (correct signature AND timestamp within ±5 min).
|
|
69
|
+
*/
|
|
70
|
+
export function verifyRequestSignature(headers, token) {
|
|
71
|
+
const timestamp = headers['x-knotie-timestamp'];
|
|
72
|
+
const nonce = headers['x-knotie-nonce'];
|
|
73
|
+
const signature = headers['x-knotie-signature'];
|
|
74
|
+
if (!timestamp || !nonce || !signature)
|
|
75
|
+
return false;
|
|
76
|
+
// Reject timestamps outside the ±5-minute window (replay prevention).
|
|
77
|
+
const ts = parseInt(timestamp, 10);
|
|
78
|
+
if (isNaN(ts) || Math.abs(Date.now() - ts) > MAX_CLOCK_SKEW_MS)
|
|
79
|
+
return false;
|
|
80
|
+
// Derive expected HMAC — must match what the portal computed.
|
|
81
|
+
const message = `${timestamp}:${nonce}`;
|
|
82
|
+
const expected = createHmac('sha256', token).update(message, 'utf8').digest('hex');
|
|
83
|
+
// Constant-time comparison — both buffers must be the same length (hex strings).
|
|
84
|
+
try {
|
|
85
|
+
const sigBuf = Buffer.from(signature, 'hex');
|
|
86
|
+
const expBuf = Buffer.from(expected, 'hex');
|
|
87
|
+
if (sigBuf.length !== expBuf.length)
|
|
88
|
+
return false;
|
|
89
|
+
return timingSafeEqual(sigBuf, expBuf);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Generate an HMAC signature for a webhook payload.
|
|
97
|
+
* Used when the plugin pushes events back to the Knotie portal.
|
|
98
|
+
*/
|
|
99
|
+
export function signPayload(secret, payload) {
|
|
100
|
+
return createHmac('sha256', secret).update(payload, 'utf8').digest('hex');
|
|
101
|
+
}
|
|
102
|
+
//# 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":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* channel.ts — Knotie Portal channel registration for OpenClaw.
|
|
3
|
+
*
|
|
4
|
+
* Registers the channel with the OpenClaw plugin API. The channel exposes:
|
|
5
|
+
*
|
|
6
|
+
* GET /knotie-channel/health — liveness check (no auth required)
|
|
7
|
+
* POST /knotie-channel/chat — send a message, get a reply (auth required)
|
|
8
|
+
* POST /knotie-channel/clear-session — clear agent session history (auth required)
|
|
9
|
+
*
|
|
10
|
+
* The Knotie portal's server-side proxy (Next.js API route) calls these
|
|
11
|
+
* endpoints via the Tailscale MagicDNS HTTPS URL.
|
|
12
|
+
*
|
|
13
|
+
* OpenClaw plugin channel API shape (inferred from openclaw-channel-streamchat):
|
|
14
|
+
*
|
|
15
|
+
* api.registerChannel({
|
|
16
|
+
* id: string,
|
|
17
|
+
* name: string,
|
|
18
|
+
* description: string,
|
|
19
|
+
* routes: Array<{
|
|
20
|
+
* method: 'GET' | 'POST',
|
|
21
|
+
* path: string, // relative to /knotie-channel/
|
|
22
|
+
* handler: (req, ctx) => Promise<{ status: number; body: unknown }>
|
|
23
|
+
* }>
|
|
24
|
+
* })
|
|
25
|
+
*/
|
|
26
|
+
export declare const KNOTIE_CHANNEL_ID = "knotie-portal";
|
|
27
|
+
export declare const KNOTIE_CHANNEL_PATH = "/knotie-channel";
|
|
28
|
+
/** Minimal request/response types — matches OpenClaw channel route handler signature. */
|
|
29
|
+
interface ChannelRequest {
|
|
30
|
+
headers: Record<string, string | undefined>;
|
|
31
|
+
body?: unknown;
|
|
32
|
+
}
|
|
33
|
+
interface ChannelContext {
|
|
34
|
+
/** Send a message to the agent and await its response. */
|
|
35
|
+
sendMessage(opts: {
|
|
36
|
+
sessionId: string;
|
|
37
|
+
text: string;
|
|
38
|
+
userName?: string;
|
|
39
|
+
}): Promise<string>;
|
|
40
|
+
/** Clear the agent's session history for a given session ID. */
|
|
41
|
+
clearSession(sessionId: string): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
export declare function createKnotieChannel(): {
|
|
44
|
+
id: string;
|
|
45
|
+
name: string;
|
|
46
|
+
description: string;
|
|
47
|
+
routes: ({
|
|
48
|
+
method: "GET";
|
|
49
|
+
path: string;
|
|
50
|
+
handler: (_req: ChannelRequest, _ctx: ChannelContext) => Promise<{
|
|
51
|
+
status: number;
|
|
52
|
+
body: unknown;
|
|
53
|
+
}>;
|
|
54
|
+
} | {
|
|
55
|
+
method: "POST";
|
|
56
|
+
path: string;
|
|
57
|
+
handler: (req: ChannelRequest, ctx: ChannelContext) => Promise<{
|
|
58
|
+
status: number;
|
|
59
|
+
body: unknown;
|
|
60
|
+
}>;
|
|
61
|
+
})[];
|
|
62
|
+
};
|
|
63
|
+
export {};
|
|
64
|
+
//# sourceMappingURL=channel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAKH,eAAO,MAAM,iBAAiB,kBAAoB,CAAC;AACnD,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAGrD,yFAAyF;AACzF,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAC5C,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AACD,UAAU,cAAc;IACtB,0DAA0D;IAC1D,WAAW,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3F,gEAAgE;IAChE,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AAoBD,wBAAgB,mBAAmB;;;;;;;wBAWL,cAAc,QAAQ,cAAc;;;;;;;uBASrC,cAAc,OAAO,cAAc;;;;;EAgE/D"}
|
package/dist/channel.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* channel.ts — Knotie Portal channel registration for OpenClaw.
|
|
3
|
+
*
|
|
4
|
+
* Registers the channel with the OpenClaw plugin API. The channel exposes:
|
|
5
|
+
*
|
|
6
|
+
* GET /knotie-channel/health — liveness check (no auth required)
|
|
7
|
+
* POST /knotie-channel/chat — send a message, get a reply (auth required)
|
|
8
|
+
* POST /knotie-channel/clear-session — clear agent session history (auth required)
|
|
9
|
+
*
|
|
10
|
+
* The Knotie portal's server-side proxy (Next.js API route) calls these
|
|
11
|
+
* endpoints via the Tailscale MagicDNS HTTPS URL.
|
|
12
|
+
*
|
|
13
|
+
* OpenClaw plugin channel API shape (inferred from openclaw-channel-streamchat):
|
|
14
|
+
*
|
|
15
|
+
* api.registerChannel({
|
|
16
|
+
* id: string,
|
|
17
|
+
* name: string,
|
|
18
|
+
* description: string,
|
|
19
|
+
* routes: Array<{
|
|
20
|
+
* method: 'GET' | 'POST',
|
|
21
|
+
* path: string, // relative to /knotie-channel/
|
|
22
|
+
* handler: (req, ctx) => Promise<{ status: number; body: unknown }>
|
|
23
|
+
* }>
|
|
24
|
+
* })
|
|
25
|
+
*/
|
|
26
|
+
import { resolveChannelToken, verifyBearerToken, verifyRequestSignature } from './auth.js';
|
|
27
|
+
export const KNOTIE_CHANNEL_ID = 'knotie-portal';
|
|
28
|
+
export const KNOTIE_CHANNEL_PATH = '/knotie-channel';
|
|
29
|
+
const PLUGIN_VERSION = '0.1.0';
|
|
30
|
+
function json(status, body) {
|
|
31
|
+
return { status, body };
|
|
32
|
+
}
|
|
33
|
+
function unauthorized() {
|
|
34
|
+
return json(401, { error: 'unauthorized', message: 'Invalid or missing Bearer token.' });
|
|
35
|
+
}
|
|
36
|
+
function buildHealthPayload() {
|
|
37
|
+
const tailscaleMode = Boolean(process.env['KNOTIE_TS_HOSTNAME']);
|
|
38
|
+
return {
|
|
39
|
+
ok: true,
|
|
40
|
+
plugin: KNOTIE_CHANNEL_ID,
|
|
41
|
+
version: PLUGIN_VERSION,
|
|
42
|
+
mode: tailscaleMode ? 'tailscale' : 'local',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function createKnotieChannel() {
|
|
46
|
+
return {
|
|
47
|
+
id: KNOTIE_CHANNEL_ID,
|
|
48
|
+
name: 'Knotie Portal',
|
|
49
|
+
description: 'Connects the Knotie whitelabel portal chat interface to OpenClaw via Tailscale.',
|
|
50
|
+
routes: [
|
|
51
|
+
// ── Health ──────────────────────────────────────────────────────────────
|
|
52
|
+
{
|
|
53
|
+
method: 'GET',
|
|
54
|
+
path: '/health',
|
|
55
|
+
handler: async (_req, _ctx) => {
|
|
56
|
+
return json(200, buildHealthPayload());
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
// ── Chat ────────────────────────────────────────────────────────────────
|
|
60
|
+
{
|
|
61
|
+
method: 'POST',
|
|
62
|
+
path: '/chat',
|
|
63
|
+
handler: async (req, ctx) => {
|
|
64
|
+
let token;
|
|
65
|
+
try {
|
|
66
|
+
token = resolveChannelToken();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return json(503, { error: 'misconfigured', message: 'Channel token not set on server.' });
|
|
70
|
+
}
|
|
71
|
+
// Layer 1: Bearer token auth
|
|
72
|
+
if (!verifyBearerToken(req.headers['authorization'], token)) {
|
|
73
|
+
return unauthorized();
|
|
74
|
+
}
|
|
75
|
+
// Layer 5: HMAC nonce (replay prevention — 5-min window)
|
|
76
|
+
if (!verifyRequestSignature(req.headers, token)) {
|
|
77
|
+
return json(401, { error: 'unauthorized', message: 'Missing or invalid HMAC request signature.' });
|
|
78
|
+
}
|
|
79
|
+
const msg = req.body;
|
|
80
|
+
if (!msg?.sessionId || !msg?.text) {
|
|
81
|
+
return json(400, { error: 'bad_request', message: 'sessionId and text are required.' });
|
|
82
|
+
}
|
|
83
|
+
const reply = await ctx.sendMessage({
|
|
84
|
+
sessionId: msg.sessionId,
|
|
85
|
+
text: msg.text,
|
|
86
|
+
userName: msg.userName,
|
|
87
|
+
});
|
|
88
|
+
const response = {
|
|
89
|
+
sessionId: msg.sessionId,
|
|
90
|
+
reply,
|
|
91
|
+
streaming: false,
|
|
92
|
+
timestamp: new Date().toISOString(),
|
|
93
|
+
};
|
|
94
|
+
return json(200, response);
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
// ── Clear session ────────────────────────────────────────────────────────
|
|
98
|
+
{
|
|
99
|
+
method: 'POST',
|
|
100
|
+
path: '/clear-session',
|
|
101
|
+
handler: async (req, ctx) => {
|
|
102
|
+
let token;
|
|
103
|
+
try {
|
|
104
|
+
token = resolveChannelToken();
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return json(503, { error: 'misconfigured', message: 'Channel token not set on server.' });
|
|
108
|
+
}
|
|
109
|
+
// Layer 1: Bearer token auth
|
|
110
|
+
if (!verifyBearerToken(req.headers['authorization'], token)) {
|
|
111
|
+
return unauthorized();
|
|
112
|
+
}
|
|
113
|
+
// Layer 5: HMAC nonce (replay prevention — 5-min window)
|
|
114
|
+
if (!verifyRequestSignature(req.headers, token)) {
|
|
115
|
+
return json(401, { error: 'unauthorized', message: 'Missing or invalid HMAC request signature.' });
|
|
116
|
+
}
|
|
117
|
+
const body = req.body;
|
|
118
|
+
if (!body?.sessionId) {
|
|
119
|
+
return json(400, { error: 'bad_request', message: 'sessionId is required.' });
|
|
120
|
+
}
|
|
121
|
+
await ctx.clearSession(body.sessionId);
|
|
122
|
+
return json(200, { ok: true, sessionId: body.sessionId });
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=channel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel.js","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @knotie/openclaw-channel-knotie
|
|
3
|
+
*
|
|
4
|
+
* OpenClaw channel plugin that exposes a hardened HTTP API so the Knotie
|
|
5
|
+
* whitelabel portal can chat with OpenClaw over the public internet.
|
|
6
|
+
*
|
|
7
|
+
* ┌──────────────────────────────────────────────────────────────────────┐
|
|
8
|
+
* │ Knotie Portal (Next.js) — server-side proxy only │
|
|
9
|
+
* │ POST /api/whitelabel/vps/instances/[id]/openclaw/chat │
|
|
10
|
+
* │ ↕ HTTPS · Bearer token · Knock header · HMAC nonce │
|
|
11
|
+
* │ VPS nginx proxy (port 18790, self-signed TLS) │
|
|
12
|
+
* │ silent 444 on all paths except /knotie-channel/* │
|
|
13
|
+
* │ OpenClaw Gateway (loopback:18789) │
|
|
14
|
+
* │ GET /knotie-channel/health — liveness check (no auth) │
|
|
15
|
+
* │ POST /knotie-channel/chat — send message, get reply │
|
|
16
|
+
* │ POST /knotie-channel/clear-session — reset conversation │
|
|
17
|
+
* └──────────────────────────────────────────────────────────────────────┘
|
|
18
|
+
*
|
|
19
|
+
* Install on the VPS:
|
|
20
|
+
* openclaw plugin add @knotie/openclaw-channel-knotie
|
|
21
|
+
*
|
|
22
|
+
* Required env vars (injected during catalog deploy):
|
|
23
|
+
* KNOTIE_CHANNEL_TOKEN — 32-byte hex shared secret (auto-generated)
|
|
24
|
+
*/
|
|
25
|
+
export { createKnotieChannel, KNOTIE_CHANNEL_ID, KNOTIE_CHANNEL_PATH } from './channel.js';
|
|
26
|
+
export { resolveChannelToken, verifyBearerToken, verifyRequestSignature, signPayload } from './auth.js';
|
|
27
|
+
export type { KnotiePortalMessage, KnotieChannelResponse, KnotieChannelHealth, KnotieChannelConfig, } from './types.js';
|
|
28
|
+
declare const plugin: import("openclaw/plugin-sdk/plugin-entry").PluginDefinition;
|
|
29
|
+
export default plugin;
|
|
30
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAKH,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAC3F,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxG,YAAY,EACV,mBAAmB,EACnB,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAEpB,QAAA,MAAM,MAAM,6DAcV,CAAC;AAEH,eAAe,MAAM,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @knotie/openclaw-channel-knotie
|
|
3
|
+
*
|
|
4
|
+
* OpenClaw channel plugin that exposes a hardened HTTP API so the Knotie
|
|
5
|
+
* whitelabel portal can chat with OpenClaw over the public internet.
|
|
6
|
+
*
|
|
7
|
+
* ┌──────────────────────────────────────────────────────────────────────┐
|
|
8
|
+
* │ Knotie Portal (Next.js) — server-side proxy only │
|
|
9
|
+
* │ POST /api/whitelabel/vps/instances/[id]/openclaw/chat │
|
|
10
|
+
* │ ↕ HTTPS · Bearer token · Knock header · HMAC nonce │
|
|
11
|
+
* │ VPS nginx proxy (port 18790, self-signed TLS) │
|
|
12
|
+
* │ silent 444 on all paths except /knotie-channel/* │
|
|
13
|
+
* │ OpenClaw Gateway (loopback:18789) │
|
|
14
|
+
* │ GET /knotie-channel/health — liveness check (no auth) │
|
|
15
|
+
* │ POST /knotie-channel/chat — send message, get reply │
|
|
16
|
+
* │ POST /knotie-channel/clear-session — reset conversation │
|
|
17
|
+
* └──────────────────────────────────────────────────────────────────────┘
|
|
18
|
+
*
|
|
19
|
+
* Install on the VPS:
|
|
20
|
+
* openclaw plugin add @knotie/openclaw-channel-knotie
|
|
21
|
+
*
|
|
22
|
+
* Required env vars (injected during catalog deploy):
|
|
23
|
+
* KNOTIE_CHANNEL_TOKEN — 32-byte hex shared secret (auto-generated)
|
|
24
|
+
*/
|
|
25
|
+
import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry';
|
|
26
|
+
import { createKnotieChannel, KNOTIE_CHANNEL_ID } from './channel.js';
|
|
27
|
+
export { createKnotieChannel, KNOTIE_CHANNEL_ID, KNOTIE_CHANNEL_PATH } from './channel.js';
|
|
28
|
+
export { resolveChannelToken, verifyBearerToken, verifyRequestSignature, signPayload } from './auth.js';
|
|
29
|
+
const plugin = definePluginEntry({
|
|
30
|
+
id: KNOTIE_CHANNEL_ID,
|
|
31
|
+
name: 'KnotiePortalChannel',
|
|
32
|
+
description: 'Secure HTTP channel for the Knotie whitelabel portal to reach OpenClaw over the public internet.',
|
|
33
|
+
register(api) {
|
|
34
|
+
// Register the channel with OpenClaw's gateway HTTP server.
|
|
35
|
+
// The gateway will mount the channel routes at /knotie-channel/*.
|
|
36
|
+
api.registerChannel(createKnotieChannel());
|
|
37
|
+
// Log startup info so partners can confirm the channel is live
|
|
38
|
+
// (visible in `openclaw gateway logs`).
|
|
39
|
+
console.log('[knotie-channel] Channel registered. Listening at /knotie-channel/* (nginx proxy on port 18790).');
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
export default plugin;
|
|
43
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEtE,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAC3F,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAQxG,MAAM,MAAM,GAAG,iBAAiB,CAAC;IAC/B,EAAE,EAAW,iBAAiB;IAC9B,IAAI,EAAS,qBAAqB;IAClC,WAAW,EAAE,kGAAkG;IAE/G,QAAQ,CAAC,GAAG;QACV,4DAA4D;QAC5D,kEAAkE;QAClE,GAAG,CAAC,eAAe,CAAC,mBAAmB,EAAE,CAAC,CAAC;QAE3C,+DAA+D;QAC/D,wCAAwC;QACxC,OAAO,CAAC,GAAG,CAAC,kGAAkG,CAAC,CAAC;IAClH,CAAC;CACF,CAAC,CAAC;AAEH,eAAe,MAAM,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* types.ts — Shared types for the Knotie portal ↔ OpenClaw channel.
|
|
3
|
+
*
|
|
4
|
+
* Architecture overview:
|
|
5
|
+
*
|
|
6
|
+
* Knotie Whitelabel Portal (Next.js)
|
|
7
|
+
* │ POST /api/whitelabel/openclaw/chat (server-side proxy)
|
|
8
|
+
* │ ↕ via Tailscale MagicDNS HTTPS
|
|
9
|
+
* OpenClaw Gateway (port 18789, loopback only, served via tailscale serve)
|
|
10
|
+
* │ /knotie-channel/* endpoints registered by this plugin
|
|
11
|
+
* │
|
|
12
|
+
* OpenClaw Agent (local process)
|
|
13
|
+
*
|
|
14
|
+
* This plugin registers a lightweight HTTP channel on the OpenClaw gateway.
|
|
15
|
+
* The Knotie portal's server-side proxy route calls it — never the browser
|
|
16
|
+
* directly, so the Tailscale network key never leaves the server.
|
|
17
|
+
*
|
|
18
|
+
* Auth:
|
|
19
|
+
* Every request must carry:
|
|
20
|
+
* Authorization: Bearer <KNOTIE_CHANNEL_TOKEN>
|
|
21
|
+
* where KNOTIE_CHANNEL_TOKEN is the shared secret set during catalog deploy
|
|
22
|
+
* (same secret stored in the portal's env as OPENCLAW_CHANNEL_TOKEN).
|
|
23
|
+
*/
|
|
24
|
+
/** Sent by the Knotie portal to the channel endpoint. */
|
|
25
|
+
export interface KnotiePortalMessage {
|
|
26
|
+
/** Unique session/conversation ID — maps to an OpenClaw session. */
|
|
27
|
+
sessionId: string;
|
|
28
|
+
/** Display name for the user (shown in OpenClaw's context if available). */
|
|
29
|
+
userName?: string;
|
|
30
|
+
/** The chat message text. */
|
|
31
|
+
text: string;
|
|
32
|
+
/** Optional metadata forwarded as-is (e.g. customer ID, page context). */
|
|
33
|
+
metadata?: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
/** Returned synchronously from POST /knotie-channel/chat. */
|
|
36
|
+
export interface KnotieChannelResponse {
|
|
37
|
+
/** Echo of the session ID. */
|
|
38
|
+
sessionId: string;
|
|
39
|
+
/** The agent's reply text. */
|
|
40
|
+
reply: string;
|
|
41
|
+
/** Whether this response is still streaming (always false for HTTP mode). */
|
|
42
|
+
streaming: false;
|
|
43
|
+
/** ISO timestamp of the response. */
|
|
44
|
+
timestamp: string;
|
|
45
|
+
}
|
|
46
|
+
/** Returned from GET /knotie-channel/health. */
|
|
47
|
+
export interface KnotieChannelHealth {
|
|
48
|
+
ok: true;
|
|
49
|
+
plugin: string;
|
|
50
|
+
version: string;
|
|
51
|
+
mode: 'tailscale' | 'local';
|
|
52
|
+
}
|
|
53
|
+
/** Internal config resolved from env vars. */
|
|
54
|
+
export interface KnotieChannelConfig {
|
|
55
|
+
/** Auth token — must match KNOTIE_CHANNEL_TOKEN env var. */
|
|
56
|
+
channelToken: string;
|
|
57
|
+
/** Gateway port (default 18789). */
|
|
58
|
+
port: number;
|
|
59
|
+
/** Whether the gateway is accessible via Tailscale (informational). */
|
|
60
|
+
tailscaleMode: boolean;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,yDAAyD;AACzD,MAAM,WAAW,mBAAmB;IAClC,oEAAoE;IACpE,SAAS,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,6DAA6D;AAC7D,MAAM,WAAW,qBAAqB;IACpC,8BAA8B;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,SAAS,EAAE,KAAK,CAAC;IACjB,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,gDAAgD;AAChD,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,IAAI,CAAC;IACT,MAAM,EAAG,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAK,WAAW,GAAG,OAAO,CAAC;CAChC;AAED,8CAA8C;AAC9C,MAAM,WAAW,mBAAmB;IAClC,4DAA4D;IAC5D,YAAY,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,aAAa,EAAE,OAAO,CAAC;CACxB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* types.ts — Shared types for the Knotie portal ↔ OpenClaw channel.
|
|
3
|
+
*
|
|
4
|
+
* Architecture overview:
|
|
5
|
+
*
|
|
6
|
+
* Knotie Whitelabel Portal (Next.js)
|
|
7
|
+
* │ POST /api/whitelabel/openclaw/chat (server-side proxy)
|
|
8
|
+
* │ ↕ via Tailscale MagicDNS HTTPS
|
|
9
|
+
* OpenClaw Gateway (port 18789, loopback only, served via tailscale serve)
|
|
10
|
+
* │ /knotie-channel/* endpoints registered by this plugin
|
|
11
|
+
* │
|
|
12
|
+
* OpenClaw Agent (local process)
|
|
13
|
+
*
|
|
14
|
+
* This plugin registers a lightweight HTTP channel on the OpenClaw gateway.
|
|
15
|
+
* The Knotie portal's server-side proxy route calls it — never the browser
|
|
16
|
+
* directly, so the Tailscale network key never leaves the server.
|
|
17
|
+
*
|
|
18
|
+
* Auth:
|
|
19
|
+
* Every request must carry:
|
|
20
|
+
* Authorization: Bearer <KNOTIE_CHANNEL_TOKEN>
|
|
21
|
+
* where KNOTIE_CHANNEL_TOKEN is the shared secret set during catalog deploy
|
|
22
|
+
* (same secret stored in the portal's env as OPENCLAW_CHANNEL_TOKEN).
|
|
23
|
+
*/
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG"}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@knotieaipro/openclaw-channel-knotie",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenClaw channel plugin — secure 5-layer portal-to-agent channel for Knotie whitelabel deployments (TLS, HMAC nonces, silent 444 drop)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"dev": "tsc --watch",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"openclaw",
|
|
25
|
+
"plugin",
|
|
26
|
+
"channel",
|
|
27
|
+
"knotie",
|
|
28
|
+
"whitelabel",
|
|
29
|
+
"portal",
|
|
30
|
+
"security",
|
|
31
|
+
"hmac",
|
|
32
|
+
"nginx"
|
|
33
|
+
],
|
|
34
|
+
"author": "Knotie AI <hello@knotie-ai.pro>",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"openclaw": ">=3.0.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependenciesMeta": {
|
|
40
|
+
"openclaw": {
|
|
41
|
+
"optional": true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"typescript": "^5.0.0"
|
|
46
|
+
}
|
|
47
|
+
}
|