@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 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
@@ -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"}
@@ -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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"}
@@ -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"}
@@ -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
+ }