@krischoichoi/channel-web 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -0
- package/lib/client.js +5005 -0
- package/lib/client.js.map +7 -0
- package/lib/host/routes-v2.d.ts +149 -0
- package/lib/host/routes-v2.d.ts.map +1 -0
- package/lib/host/routes-v2.js +524 -0
- package/lib/host/routes-v2.js.map +1 -0
- package/lib/host/routes.d.ts +87 -0
- package/lib/host/routes.d.ts.map +1 -0
- package/lib/host/routes.js +250 -0
- package/lib/host/routes.js.map +1 -0
- package/lib/host/security.d.ts +47 -0
- package/lib/host/security.d.ts.map +1 -0
- package/lib/host/security.js +78 -0
- package/lib/host/security.js.map +1 -0
- package/lib/index.d.ts +22 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +129 -0
- package/lib/index.js.map +1 -0
- package/lib/protocol.d.ts +152 -0
- package/lib/protocol.d.ts.map +1 -0
- package/lib/protocol.js +12 -0
- package/lib/protocol.js.map +1 -0
- package/package.json +73 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { errorBody } from './security.js';
|
|
2
|
+
import { authInputSchema } from './routes-v2.js';
|
|
3
|
+
/** The fixed four-channel catalog the dashboard always shows, mounted or not. */
|
|
4
|
+
export const CHANNEL_CATALOG = ['weixin', 'qq', 'dingtalk', 'lark'];
|
|
5
|
+
/** Human label for a channel id; falls back to the id itself. */
|
|
6
|
+
const CHANNEL_LABELS = {
|
|
7
|
+
weixin: 'Weixin',
|
|
8
|
+
qq: 'QQ',
|
|
9
|
+
dingtalk: 'DingTalk',
|
|
10
|
+
lark: 'Lark',
|
|
11
|
+
};
|
|
12
|
+
/** Sanitize a channel lastError to avoid leaking credentials. */
|
|
13
|
+
function safeLastError(error) {
|
|
14
|
+
if (!error)
|
|
15
|
+
return null;
|
|
16
|
+
const msg = typeof error === 'string' ? error : error instanceof Error ? error.message : String(error);
|
|
17
|
+
if (!msg)
|
|
18
|
+
return null;
|
|
19
|
+
return errorBody('REDACTED', msg).error.message;
|
|
20
|
+
}
|
|
21
|
+
export class ChannelApi {
|
|
22
|
+
channels;
|
|
23
|
+
now;
|
|
24
|
+
/** Host-side store of full auth challenges: never exposes payload. */
|
|
25
|
+
challenges = new Map();
|
|
26
|
+
constructor(channels, options = {}) {
|
|
27
|
+
this.channels = channels;
|
|
28
|
+
this.now = options.now ?? Date.now;
|
|
29
|
+
}
|
|
30
|
+
adapter(id) {
|
|
31
|
+
return this.channels.get(id);
|
|
32
|
+
}
|
|
33
|
+
projectCapabilities(c) {
|
|
34
|
+
if (!c)
|
|
35
|
+
return {};
|
|
36
|
+
const out = {};
|
|
37
|
+
for (const [k, v] of Object.entries(c)) {
|
|
38
|
+
if (v === undefined)
|
|
39
|
+
continue;
|
|
40
|
+
out[k] = typeof v === 'object' && v !== null ? JSON.stringify(v) : v;
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/** Map a ChannelHealth to the spec §23 status enum. */
|
|
45
|
+
mapStatus(health) {
|
|
46
|
+
if (health?.error)
|
|
47
|
+
return 'error';
|
|
48
|
+
if (!health)
|
|
49
|
+
return 'unconfigured';
|
|
50
|
+
switch (health.status) {
|
|
51
|
+
case 'ok':
|
|
52
|
+
return 'connected';
|
|
53
|
+
case 'degraded':
|
|
54
|
+
return 'degraded';
|
|
55
|
+
default:
|
|
56
|
+
// 'down' | 'unknown': authenticated but receive loop down → degraded,
|
|
57
|
+
// otherwise treat as unconfigured.
|
|
58
|
+
return health.authenticated === true ? 'degraded' : 'unconfigured';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async buildView(adapter) {
|
|
62
|
+
const health = adapter.getHealth ? await adapter.getHealth().catch(() => undefined) : undefined;
|
|
63
|
+
const authenticated = health?.authenticated === true;
|
|
64
|
+
// M1 has no managed lifecycle: a registered adapter is mounted/enabled.
|
|
65
|
+
// configured is inferred from adapter health.
|
|
66
|
+
const configured = authenticated;
|
|
67
|
+
return {
|
|
68
|
+
id: adapter.id,
|
|
69
|
+
enabled: true,
|
|
70
|
+
configured,
|
|
71
|
+
mounted: true,
|
|
72
|
+
status: this.mapStatus(health),
|
|
73
|
+
health: health ?? null,
|
|
74
|
+
capabilities: this.projectCapabilities(adapter.capabilities),
|
|
75
|
+
lastError: health?.error ? safeLastError(health.error) : null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** A neutral, unmounted view for a known catalog id with no live adapter. */
|
|
79
|
+
offlineView(id) {
|
|
80
|
+
return {
|
|
81
|
+
id,
|
|
82
|
+
enabled: false,
|
|
83
|
+
configured: false,
|
|
84
|
+
mounted: false,
|
|
85
|
+
status: 'unconfigured',
|
|
86
|
+
health: null,
|
|
87
|
+
capabilities: null,
|
|
88
|
+
lastError: null,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** GET /channels → ChannelView[] for the fixed four-channel catalog. */
|
|
92
|
+
async listChannels() {
|
|
93
|
+
const views = await Promise.all(CHANNEL_CATALOG.map((id) => {
|
|
94
|
+
const adapter = this.adapter(id);
|
|
95
|
+
return adapter ? this.buildView(adapter) : Promise.resolve(this.offlineView(id));
|
|
96
|
+
}));
|
|
97
|
+
return { status: 200, body: views };
|
|
98
|
+
}
|
|
99
|
+
/** GET /channels/:id → ChannelView. */
|
|
100
|
+
async getChannel(id) {
|
|
101
|
+
const adapter = this.adapter(id);
|
|
102
|
+
if (adapter)
|
|
103
|
+
return { status: 200, body: await this.buildView(adapter) };
|
|
104
|
+
// A known catalog id that is not mounted returns its offline view (200),
|
|
105
|
+
// not 404; truly unknown ids still 404.
|
|
106
|
+
if (CHANNEL_CATALOG.includes(id)) {
|
|
107
|
+
return { status: 200, body: this.offlineView(id) };
|
|
108
|
+
}
|
|
109
|
+
return { status: 404, body: errorBody('CHANNEL_NOT_FOUND', 'unknown channel: ' + id) };
|
|
110
|
+
}
|
|
111
|
+
/** POST /channels/:id/auth/start → PublicAuthChallenge. */
|
|
112
|
+
async startAuth(id) {
|
|
113
|
+
const adapter = this.adapter(id);
|
|
114
|
+
if (!adapter)
|
|
115
|
+
return { status: 404, body: errorBody('CHANNEL_NOT_FOUND', 'unknown channel: ' + id) };
|
|
116
|
+
if (!adapter.beginAuth) {
|
|
117
|
+
return { status: 400, body: errorBody('AUTH_NOT_SUPPORTED', 'channel does not support auth') };
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const full = await adapter.beginAuth();
|
|
121
|
+
if (!full || typeof full.id !== 'string') {
|
|
122
|
+
return { status: 500, body: errorBody('AUTH_BEGIN_FAILED', 'channel returned an invalid challenge') };
|
|
123
|
+
}
|
|
124
|
+
this.challenges.set(full.id, full);
|
|
125
|
+
const pub = {
|
|
126
|
+
id: full.id,
|
|
127
|
+
instruction: full.instruction,
|
|
128
|
+
qrUrl: full.qrUrl,
|
|
129
|
+
expiresAt: full.expiresAt,
|
|
130
|
+
};
|
|
131
|
+
return { status: 200, body: pub };
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
return { status: 500, body: errorBody('AUTH_BEGIN_FAILED', error instanceof Error ? error.message : String(error)) };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** Resolve a stored challenge or a structured error result. */
|
|
138
|
+
storedChallenge(id, challengeId) {
|
|
139
|
+
if (typeof challengeId !== 'string' || !challengeId) {
|
|
140
|
+
return { ok: false, result: { status: 400, body: errorBody('INVALID_CHALLENGE', 'challengeId is required') } };
|
|
141
|
+
}
|
|
142
|
+
const challenge = this.challenges.get(challengeId);
|
|
143
|
+
if (!challenge) {
|
|
144
|
+
return { ok: false, result: { status: 404, body: errorBody('CHALLENGE_NOT_FOUND', 'unknown or expired challenge') } };
|
|
145
|
+
}
|
|
146
|
+
if (challenge.expiresAt !== undefined && this.now() > challenge.expiresAt) {
|
|
147
|
+
this.challenges.delete(challengeId);
|
|
148
|
+
return { ok: false, result: { status: 410, body: errorBody('CHALLENGE_EXPIRED', 'challenge expired') } };
|
|
149
|
+
}
|
|
150
|
+
return { ok: true, challenge };
|
|
151
|
+
}
|
|
152
|
+
/** POST /channels/:id/auth/poll → PublicAuthPoll. */
|
|
153
|
+
async pollAuth(id, challengeId) {
|
|
154
|
+
const adapter = this.adapter(id);
|
|
155
|
+
if (!adapter)
|
|
156
|
+
return { status: 404, body: errorBody('CHANNEL_NOT_FOUND', 'unknown channel: ' + id) };
|
|
157
|
+
if (!adapter.pollAuth)
|
|
158
|
+
return { status: 400, body: errorBody('AUTH_NOT_SUPPORTED', 'channel does not support auth polling') };
|
|
159
|
+
const stored = this.storedChallenge(id, challengeId);
|
|
160
|
+
if (!stored.ok)
|
|
161
|
+
return stored.result;
|
|
162
|
+
try {
|
|
163
|
+
const poll = await adapter.pollAuth(stored.challenge);
|
|
164
|
+
const pub = {
|
|
165
|
+
state: poll.state,
|
|
166
|
+
detail: poll.detail,
|
|
167
|
+
prompt: derivePrompt(poll.state, poll.detail),
|
|
168
|
+
};
|
|
169
|
+
if (poll.state === 'authenticated' || poll.state === 'expired' || poll.state === 'failed') {
|
|
170
|
+
this.challenges.delete(stored.challenge.id);
|
|
171
|
+
}
|
|
172
|
+
return { status: 200, body: pub };
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
return { status: 500, body: errorBody('AUTH_POLL_FAILED', error instanceof Error ? error.message : String(error)) };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** POST /channels/:id/auth/input → PublicAuthPoll. */
|
|
179
|
+
async inputAuth(id, challengeId, input) {
|
|
180
|
+
const adapter = this.adapter(id);
|
|
181
|
+
if (!adapter)
|
|
182
|
+
return { status: 404, body: errorBody('CHANNEL_NOT_FOUND', 'unknown channel: ' + id) };
|
|
183
|
+
if (!adapter.submitAuthInput) {
|
|
184
|
+
return { status: 400, body: errorBody('AUTH_NOT_SUPPORTED', 'channel does not support auth input') };
|
|
185
|
+
}
|
|
186
|
+
if (!input || !authInputSchema.safeParse(input).success) {
|
|
187
|
+
return { status: 400, body: errorBody('INVALID_INPUT', 'input must be { kind: "verification-code", value: string }') };
|
|
188
|
+
}
|
|
189
|
+
const stored = this.storedChallenge(id, challengeId);
|
|
190
|
+
if (!stored.ok)
|
|
191
|
+
return stored.result;
|
|
192
|
+
try {
|
|
193
|
+
await adapter.submitAuthInput(stored.challenge, input);
|
|
194
|
+
// Call pollAuth as a method so adapters keep their `this` binding.
|
|
195
|
+
const poll = await adapter.pollAuth(stored.challenge);
|
|
196
|
+
const pub = {
|
|
197
|
+
state: poll.state,
|
|
198
|
+
detail: poll.detail,
|
|
199
|
+
prompt: derivePrompt(poll.state, poll.detail),
|
|
200
|
+
};
|
|
201
|
+
return { status: 200, body: pub };
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
return { status: 500, body: errorBody('AUTH_INPUT_FAILED', error instanceof Error ? error.message : String(error)) };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Match a path under `/dsh-channels/api/v1` (the prefix is stripped by the
|
|
209
|
+
* caller) and dispatch to the appropriate handler. Returns 404 for unknown routes.
|
|
210
|
+
*/
|
|
211
|
+
async handle(method, pathname, body) {
|
|
212
|
+
const clean = pathname.replace(/\/+$/, '');
|
|
213
|
+
if (method === 'GET' && clean === '/channels')
|
|
214
|
+
return this.listChannels();
|
|
215
|
+
if (method === 'GET') {
|
|
216
|
+
const one = /^\/channels\/([^/]+)$/.exec(clean);
|
|
217
|
+
if (one)
|
|
218
|
+
return this.getChannel(decodeURIComponent(one[1]));
|
|
219
|
+
}
|
|
220
|
+
if (method === 'POST') {
|
|
221
|
+
const start = /^\/channels\/([^/]+)\/auth\/start$/.exec(clean);
|
|
222
|
+
if (start)
|
|
223
|
+
return this.startAuth(decodeURIComponent(start[1]));
|
|
224
|
+
const poll = /^\/channels\/([^/]+)\/auth\/poll$/.exec(clean);
|
|
225
|
+
if (poll) {
|
|
226
|
+
const b = body;
|
|
227
|
+
return this.pollAuth(decodeURIComponent(poll[1]), b?.challengeId);
|
|
228
|
+
}
|
|
229
|
+
const input = /^\/channels\/([^/]+)\/auth\/input$/.exec(clean);
|
|
230
|
+
if (input) {
|
|
231
|
+
const b = (body ?? {});
|
|
232
|
+
return this.inputAuth(decodeURIComponent(input[1]), b?.challengeId, b?.input);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return { status: 404, body: errorBody('NOT_FOUND', 'no such endpoint') };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** Map an auth poll to a client face-hint string. */
|
|
239
|
+
function derivePrompt(state, detail) {
|
|
240
|
+
if (state === 'pending') {
|
|
241
|
+
const d = detail?.toLowerCase() ?? '';
|
|
242
|
+
if (d.includes('verify') || d.includes('code'))
|
|
243
|
+
return 'verify-code';
|
|
244
|
+
if (d.includes('scan'))
|
|
245
|
+
return d.includes('confirm') || d.includes('redirect') ? 'confirm' : 'waiting-scan';
|
|
246
|
+
return d.includes('confirm') ? 'confirm' : 'waiting-scan';
|
|
247
|
+
}
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/host/routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security helpers for the dsh-channels host API (M1). Pure, unit-testable.
|
|
3
|
+
*
|
|
4
|
+
* Rules (spec §2.2):
|
|
5
|
+
* - state-changing requests (POST) are loopback-only;
|
|
6
|
+
* - POST bodies must be application/json (else 415);
|
|
7
|
+
* - bodies are capped at BODY_LIMIT_BYTES (else 413);
|
|
8
|
+
* - error messages are sanitized so no token/secret leaks into a response.
|
|
9
|
+
*/
|
|
10
|
+
/** Upper bound for a request body payload (64 KiB). */
|
|
11
|
+
export declare const BODY_LIMIT_BYTES: number;
|
|
12
|
+
/**
|
|
13
|
+
* True when the socket remote address is a loopback address.
|
|
14
|
+
* Used to restrict state-changing API calls to the local machine.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isLoopbackAddress(address: string | undefined): boolean;
|
|
17
|
+
/** True when the Content-Type header is application/json (options ignored). */
|
|
18
|
+
export declare function isJsonContentType(contentType: string | undefined): boolean;
|
|
19
|
+
/** Structured result of reading + parsing a JSON request body. */
|
|
20
|
+
export type BodyResult<T> = {
|
|
21
|
+
ok: true;
|
|
22
|
+
value: T;
|
|
23
|
+
} | {
|
|
24
|
+
ok: false;
|
|
25
|
+
status: 413 | 415 | 400;
|
|
26
|
+
error: PublicErrorBody;
|
|
27
|
+
};
|
|
28
|
+
export interface PublicErrorBody {
|
|
29
|
+
error: {
|
|
30
|
+
code: string;
|
|
31
|
+
message: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export declare function errorBody(code: string, message: string): PublicErrorBody;
|
|
35
|
+
/**
|
|
36
|
+
* Read a JSON request body from a node:http IncomingMessage, respecting the
|
|
37
|
+
* 64 KiB cap. Never trusts caller-provided Content-Length; it reads the real
|
|
38
|
+
* stream to enforce the byte limit regardless of the header.
|
|
39
|
+
*/
|
|
40
|
+
export declare function readJsonBody<T>(req: AsyncIterable<Buffer | string>): Promise<BodyResult<T>>;
|
|
41
|
+
/**
|
|
42
|
+
* Best-effort sanitization of an error message so no secret/credential
|
|
43
|
+
* leaks into an API response. Values like 'token=...' / 'aeskey=...' are
|
|
44
|
+
* replaced with a redaction marker.
|
|
45
|
+
*/
|
|
46
|
+
export declare function sanitizeError(message: string): string;
|
|
47
|
+
//# sourceMappingURL=security.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.d.ts","sourceRoot":"","sources":["../../src/host/security.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,uDAAuD;AACvD,eAAO,MAAM,gBAAgB,QAAY,CAAC;AAI1C;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAGtE;AAED,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAI1E;AAED,kEAAkE;AAClE,MAAM,MAAM,UAAU,CAAC,CAAC,IACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GACtB;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IAAC,KAAK,EAAE,eAAe,CAAA;CAAE,CAAC;AAEnE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1C;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,eAAe,CAExE;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAAE,GAAG,EAAE,aAAa,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAuBjG;AAQD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAMrD"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security helpers for the dsh-channels host API (M1). Pure, unit-testable.
|
|
3
|
+
*
|
|
4
|
+
* Rules (spec §2.2):
|
|
5
|
+
* - state-changing requests (POST) are loopback-only;
|
|
6
|
+
* - POST bodies must be application/json (else 415);
|
|
7
|
+
* - bodies are capped at BODY_LIMIT_BYTES (else 413);
|
|
8
|
+
* - error messages are sanitized so no token/secret leaks into a response.
|
|
9
|
+
*/
|
|
10
|
+
/** Upper bound for a request body payload (64 KiB). */
|
|
11
|
+
export const BODY_LIMIT_BYTES = 64 * 1024;
|
|
12
|
+
const LOOPBACK = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
|
|
13
|
+
/**
|
|
14
|
+
* True when the socket remote address is a loopback address.
|
|
15
|
+
* Used to restrict state-changing API calls to the local machine.
|
|
16
|
+
*/
|
|
17
|
+
export function isLoopbackAddress(address) {
|
|
18
|
+
if (!address)
|
|
19
|
+
return false;
|
|
20
|
+
return LOOPBACK.has(address);
|
|
21
|
+
}
|
|
22
|
+
/** True when the Content-Type header is application/json (options ignored). */
|
|
23
|
+
export function isJsonContentType(contentType) {
|
|
24
|
+
if (!contentType)
|
|
25
|
+
return false;
|
|
26
|
+
const [media] = contentType.split(';').map((s) => s.trim());
|
|
27
|
+
return media?.toLowerCase() === 'application/json';
|
|
28
|
+
}
|
|
29
|
+
export function errorBody(code, message) {
|
|
30
|
+
return { error: { code, message: sanitizeError(message) } };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Read a JSON request body from a node:http IncomingMessage, respecting the
|
|
34
|
+
* 64 KiB cap. Never trusts caller-provided Content-Length; it reads the real
|
|
35
|
+
* stream to enforce the byte limit regardless of the header.
|
|
36
|
+
*/
|
|
37
|
+
export async function readJsonBody(req) {
|
|
38
|
+
const chunks = [];
|
|
39
|
+
let total = 0;
|
|
40
|
+
for await (const chunk of req) {
|
|
41
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
42
|
+
total += buf.length;
|
|
43
|
+
if (total > BODY_LIMIT_BYTES) {
|
|
44
|
+
return { ok: false, status: 413, error: errorBody('BODY_TOO_LARGE', 'request body exceeds 64 KiB limit') };
|
|
45
|
+
}
|
|
46
|
+
chunks.push(buf);
|
|
47
|
+
}
|
|
48
|
+
// An empty body is treated as an empty object; endpoints validate their
|
|
49
|
+
// own required fields (e.g. challengeId), so auth/start (no body) works.
|
|
50
|
+
if (total === 0) {
|
|
51
|
+
return { ok: true, value: {} };
|
|
52
|
+
}
|
|
53
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
54
|
+
try {
|
|
55
|
+
return { ok: true, value: JSON.parse(raw) };
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return { ok: false, status: 400, error: errorBody('INVALID_JSON', 'request body is not valid JSON') };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Regex capturing common credential-shaped fragments in an error message. */
|
|
62
|
+
const SECRET_PATTERNS = [
|
|
63
|
+
/(\b(?:token|aeskey|secret|password|verifycode|verify_code|verify code)\b\s*[:=]\s*)\S+/gi,
|
|
64
|
+
/\b(?:Illegal character code range [0-9a-f-]+)/gi,
|
|
65
|
+
];
|
|
66
|
+
/**
|
|
67
|
+
* Best-effort sanitization of an error message so no secret/credential
|
|
68
|
+
* leaks into an API response. Values like 'token=...' / 'aeskey=...' are
|
|
69
|
+
* replaced with a redaction marker.
|
|
70
|
+
*/
|
|
71
|
+
export function sanitizeError(message) {
|
|
72
|
+
let out = String(message ?? '');
|
|
73
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
74
|
+
out = out.replace(pattern, (_match, prefix) => (prefix ?? '') + '<redacted>');
|
|
75
|
+
}
|
|
76
|
+
return out.length > 500 ? out.slice(0, 500) + '…' : out;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=security.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.js","sourceRoot":"","sources":["../../src/host/security.ts"],"names":[],"mappings":"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"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @krischoichoi/channel-web — host (Node) side.
|
|
3
|
+
*
|
|
4
|
+
* Cordis plugin entry for a Harness host process. Registers TWO webServer
|
|
5
|
+
* prefix routes:
|
|
6
|
+
*
|
|
7
|
+
* - `/dsh-channels/api/v1` — the M1 read-only dashboard + adapter auth loop
|
|
8
|
+
* (compat; doc §34). Kept intact. Talks only to `ctx.channels`.
|
|
9
|
+
* - `/dsh-channels/api/v2` — the control-plane API (doc §28–§33) that
|
|
10
|
+
* delegates to the ChannelControlService (`ctx.channelControl`). When the
|
|
11
|
+
* control plugin is absent (standalone web profile) every v2 route returns
|
|
12
|
+
* 503 SERVICE_UNAVAILABLE so the profile still boots.
|
|
13
|
+
*
|
|
14
|
+
* The webServer / channels / channelControl services are injected via
|
|
15
|
+
* ctx.inject so this plugin does not hard-depend on the web profile. The host
|
|
16
|
+
* entry exports ONLY name + apply — no module-level inject export — which the
|
|
17
|
+
* bundle test asserts.
|
|
18
|
+
*/
|
|
19
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
20
|
+
export declare const name = "channel-web";
|
|
21
|
+
export declare function apply(ctx: Context): void;
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAMnD,eAAO,MAAM,IAAI,gBAAgB,CAAC;AA6FlC,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAoDxC"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { ChannelApi } from './host/routes.js';
|
|
2
|
+
import { ChannelApiV2 } from './host/routes-v2.js';
|
|
3
|
+
import { errorBody, isJsonContentType, isLoopbackAddress, readJsonBody } from './host/security.js';
|
|
4
|
+
export const name = 'channel-web';
|
|
5
|
+
function sendJson(res, status, body) {
|
|
6
|
+
// 204 No Content carries no response body.
|
|
7
|
+
if (status === 204 || body === undefined) {
|
|
8
|
+
res.writeHead(status);
|
|
9
|
+
res.end();
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const payload = JSON.stringify(body);
|
|
13
|
+
res.writeHead(status, {
|
|
14
|
+
'Content-Type': 'application/json',
|
|
15
|
+
'Content-Length': Buffer.byteLength(payload).toString(),
|
|
16
|
+
});
|
|
17
|
+
res.end(payload);
|
|
18
|
+
}
|
|
19
|
+
/** A read-only HTTP method — no loopback restriction (only mutations are gated). */
|
|
20
|
+
const MUTATION_METHODS = new Set(['POST', 'PATCH', 'PUT', 'DELETE']);
|
|
21
|
+
/** Unsafe non-empty JSON media check for mutation bodies (415 otherwise). */
|
|
22
|
+
function isMutationWithBody(method) {
|
|
23
|
+
return method === 'POST' || method === 'PATCH' || method === 'PUT';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build the v2 prefix handler. `control` is resolved lazily on every request
|
|
27
|
+
* so a control service that becomes available after webServer is picked up on
|
|
28
|
+
* the next request; when it is never available, every v2 request returns 503.
|
|
29
|
+
* The API wrapper is re-created whenever the control service identity changes
|
|
30
|
+
* (an HMR reload of the control plugin provides a NEW service object), so a
|
|
31
|
+
* request can never delegate to an unloaded provider.
|
|
32
|
+
*/
|
|
33
|
+
function makeV2Handler(resolveControl) {
|
|
34
|
+
let api;
|
|
35
|
+
let apiControl;
|
|
36
|
+
// Re-create the API wrapper whenever the control service identity changes
|
|
37
|
+
// (an HMR reload of the control plugin provides a NEW service object), so a
|
|
38
|
+
// request can never delegate to an unloaded provider.
|
|
39
|
+
function apiFor(control) {
|
|
40
|
+
if (apiControl === control && api)
|
|
41
|
+
return api;
|
|
42
|
+
api = new ChannelApiV2(control);
|
|
43
|
+
apiControl = control;
|
|
44
|
+
return api;
|
|
45
|
+
}
|
|
46
|
+
return async (req, res) => {
|
|
47
|
+
const url = req.url ?? '/';
|
|
48
|
+
const rawPath = url.split('?')[0] ?? '/';
|
|
49
|
+
const pathname = rawPath.replace(/^\/dsh-channels\/api\/v2/, '') || '/';
|
|
50
|
+
const method = (req.method ?? 'GET').toUpperCase();
|
|
51
|
+
// The control service is optional: without it every v2 route is 503 so the
|
|
52
|
+
// standalone web profile still boots.
|
|
53
|
+
const control = resolveControl();
|
|
54
|
+
if (!control) {
|
|
55
|
+
return sendJson(res, 503, errorBody('SERVICE_UNAVAILABLE', 'channel control is not available'));
|
|
56
|
+
}
|
|
57
|
+
const ready = apiFor(control);
|
|
58
|
+
if (MUTATION_METHODS.has(method) && !isLoopbackAddress(req.socket.remoteAddress)) {
|
|
59
|
+
return sendJson(res, 403, errorBody('FORBIDDEN', 'state-changing requests are loopback-only'));
|
|
60
|
+
}
|
|
61
|
+
if (isMutationWithBody(method)) {
|
|
62
|
+
if (!isJsonContentType(req.headers['content-type'])) {
|
|
63
|
+
return sendJson(res, 415, errorBody('UNSUPPORTED_MEDIA_TYPE', 'Content-Type must be application/json'));
|
|
64
|
+
}
|
|
65
|
+
const body = await readJsonBody(req);
|
|
66
|
+
if (!body.ok)
|
|
67
|
+
return sendJson(res, body.status, body.error);
|
|
68
|
+
const result = await ready.handle(method, pathname, body.value);
|
|
69
|
+
return sendJson(res, result.status, result.body);
|
|
70
|
+
}
|
|
71
|
+
if (method === 'GET' || method === 'DELETE') {
|
|
72
|
+
const result = await ready.handle(method, pathname, undefined);
|
|
73
|
+
return sendJson(res, result.status, result.body);
|
|
74
|
+
}
|
|
75
|
+
sendJson(res, 405, errorBody('METHOD_NOT_ALLOWED', 'method not allowed'));
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function apply(ctx) {
|
|
79
|
+
// v1 compat (doc §34): needs webServer + channels. The M1 auth start/poll/
|
|
80
|
+
// input talk to ctx.channels adapters as today.
|
|
81
|
+
ctx.inject(['webServer', 'channels'], (webCtx) => {
|
|
82
|
+
const server = webCtx.webServer;
|
|
83
|
+
const channels = webCtx.channels;
|
|
84
|
+
const api = new ChannelApi(channels);
|
|
85
|
+
return server.register({
|
|
86
|
+
kind: 'prefix',
|
|
87
|
+
path: '/dsh-channels/api/v1',
|
|
88
|
+
handler: async (req, res) => {
|
|
89
|
+
const url = req.url ?? '/';
|
|
90
|
+
const rawPath = url.split('?')[0] ?? '/';
|
|
91
|
+
const pathname = rawPath.replace(/^\/dsh-channels\/api\/v1/, '') || '/';
|
|
92
|
+
const method = (req.method ?? 'GET').toUpperCase();
|
|
93
|
+
if (method === 'POST' && !isLoopbackAddress(req.socket.remoteAddress)) {
|
|
94
|
+
return sendJson(res, 403, errorBody('FORBIDDEN', 'state-changing requests are loopback-only'));
|
|
95
|
+
}
|
|
96
|
+
if (method === 'POST' && !isJsonContentType(req.headers['content-type'])) {
|
|
97
|
+
return sendJson(res, 415, errorBody('UNSUPPORTED_MEDIA_TYPE', 'Content-Type must be application/json'));
|
|
98
|
+
}
|
|
99
|
+
if (method === 'POST') {
|
|
100
|
+
const body = await readJsonBody(req);
|
|
101
|
+
if (!body.ok)
|
|
102
|
+
return sendJson(res, body.status, body.error);
|
|
103
|
+
const result = await api.handle(method, pathname, body.value);
|
|
104
|
+
return sendJson(res, result.status, result.body);
|
|
105
|
+
}
|
|
106
|
+
if (method === 'GET') {
|
|
107
|
+
const result = await api.handle(method, pathname, undefined);
|
|
108
|
+
return sendJson(res, result.status, result.body);
|
|
109
|
+
}
|
|
110
|
+
sendJson(res, 405, errorBody('METHOD_NOT_ALLOWED', 'method not allowed'));
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
// v2 control-plane (doc §28–§33): needs webServer; channelControl is
|
|
115
|
+
// optional — when absent, requests return 503. The control service is read
|
|
116
|
+
// lazily via ctx.get() on every request (never cached in a module-level ref):
|
|
117
|
+
// ctx.get() resolves only the currently active provider, so an HMR unload of
|
|
118
|
+
// the control plugin immediately degrades to 503 instead of calling into an
|
|
119
|
+
// already-unloaded service object.
|
|
120
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
121
|
+
const server = webCtx.webServer;
|
|
122
|
+
return server.register({
|
|
123
|
+
kind: 'prefix',
|
|
124
|
+
path: '/dsh-channels/api/v2',
|
|
125
|
+
handler: makeV2Handler(() => ctx.get('channelControl')),
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|