dsh-side-chat-plus 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +317 -0
  3. package/README.zh.md +261 -0
  4. package/cordis.patch.yml +8 -0
  5. package/dsh.plugin.json +16 -0
  6. package/lib/client-registry.js +2949 -0
  7. package/lib/client-registry.js.map +1 -0
  8. package/lib/client.js +2949 -0
  9. package/lib/client.js.map +1 -0
  10. package/lib/index.js +840 -0
  11. package/lib/types/client/api.d.ts +218 -0
  12. package/lib/types/client/attachments/AttachmentRail.d.ts +39 -0
  13. package/lib/types/client/attachments/DropOverlay.d.ts +18 -0
  14. package/lib/types/client/attachments/ImageLightbox.d.ts +20 -0
  15. package/lib/types/client/attachments/MessageImage.d.ts +38 -0
  16. package/lib/types/client/attachments/index.d.ts +18 -0
  17. package/lib/types/client/index.d.ts +6 -0
  18. package/lib/types/client/locales.d.ts +178 -0
  19. package/lib/types/context-types.d.ts +390 -0
  20. package/lib/types/index.d.ts +7 -0
  21. package/lib/types/settings-shared.d.ts +24 -0
  22. package/lib/types/trust-fence.d.ts +20 -0
  23. package/lib/types/wire.d.ts +25 -0
  24. package/package.json +114 -0
  25. package/src/client/api.ts +112 -0
  26. package/src/client/attachments/AttachmentRail.module.css +89 -0
  27. package/src/client/attachments/AttachmentRail.tsx +173 -0
  28. package/src/client/attachments/DropOverlay.module.css +38 -0
  29. package/src/client/attachments/DropOverlay.tsx +62 -0
  30. package/src/client/attachments/ImageLightbox.module.css +44 -0
  31. package/src/client/attachments/ImageLightbox.tsx +58 -0
  32. package/src/client/attachments/MessageImage.module.css +61 -0
  33. package/src/client/attachments/MessageImage.tsx +120 -0
  34. package/src/client/attachments/index.ts +19 -0
  35. package/src/client/client.module.css +1032 -0
  36. package/src/client/index.tsx +1966 -0
  37. package/src/client/layout.css +16 -0
  38. package/src/client/locales.ts +181 -0
  39. package/src/context-types.ts +384 -0
  40. package/src/css-modules.d.ts +10 -0
  41. package/src/index.ts +840 -0
  42. package/src/settings-shared.ts +33 -0
  43. package/src/trust-fence.ts +70 -0
  44. package/src/wire.ts +81 -0
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Shared side-chat preference vocabulary (types + constants), consumed by
3
+ * BOTH halves: the host registers the schemastery schema over these values
4
+ * (index.ts) and the client reads/writes them through the plugin's own
5
+ * fenced /sidechat settings routes. Kept free of schemastery so the browser
6
+ * bundle never pulls the schema runtime in.
7
+ */
8
+
9
+ /** The user-settings namespace holding the side-chat preferences. */
10
+ export const SUBCHAT_PREFS_NS = 'dsh-side-chat'
11
+
12
+ /** How a brought-back reply lands in the main conversation. */
13
+ export type BringMode = 'draft' | 'context'
14
+
15
+ /** User-facing side-chat preferences. */
16
+ export interface SubchatPrefs {
17
+ /** Whether the "look up workspace / parent when needed" switch defaults on. */
18
+ lookupDefault: boolean
19
+ /** Whether selecting text sends it immediately (true) or stages it as an attachment (false). */
20
+ sendImmediately: boolean
21
+ /** Extra prompt appended when the selection is sent immediately (empty = none). */
22
+ defaultPrompt: string
23
+ /** How brought-back content lands: into the composer draft, or as a collapsed context row. */
24
+ bringMode: BringMode
25
+ }
26
+
27
+ /** Fallback prefs used whenever the settings document is unreachable or malformed. */
28
+ export const SUBCHAT_PREFS_DEFAULTS: SubchatPrefs = {
29
+ lookupDefault: false,
30
+ sendImmediately: true,
31
+ defaultPrompt: '',
32
+ bringMode: 'draft',
33
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Browser-trust fence for the /sidechat routes, behaviorally identical to the
3
+ * /api gateway's fence (loopback or configured trusted authority; cross-site
4
+ * browser markers refuse). Self-contained copy, since the DSH connection
5
+ * package does not export these helpers.
6
+ */
7
+ import type { IncomingHttpHeaders } from 'node:http'
8
+
9
+ interface ApiTrustRequest {
10
+ headers: IncomingHttpHeaders
11
+ }
12
+
13
+ function header(headers: IncomingHttpHeaders, name: string): string | undefined {
14
+ const value = headers[name]
15
+ return typeof value === 'string' ? value : undefined
16
+ }
17
+
18
+ function parseAuthority(authority: string): URL | undefined {
19
+ try {
20
+ return new URL(`http://${authority}`)
21
+ } catch {
22
+ return undefined
23
+ }
24
+ }
25
+
26
+ /** Whether a normalized URL hostname names the local loopback authority. */
27
+ export function isLoopbackHostname(hostname: string): boolean {
28
+ if (hostname === 'localhost' || hostname === '[::1]') return true
29
+ const parts = hostname.split('.')
30
+ return parts.length === 4
31
+ && parts[0] === '127'
32
+ && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255)
33
+ }
34
+
35
+ function canonicalAuthority(entry: string, entryUrl: URL): string {
36
+ const port = entryUrl.port !== '' ? entryUrl.port : new URL(`https://${entry}`).port
37
+ return port === '' ? entryUrl.hostname : `${entryUrl.hostname}:${port}`
38
+ }
39
+
40
+ function isTrustedAuthority(hostUrl: URL, trustedHosts: readonly string[]): boolean {
41
+ return trustedHosts.some((entry) => {
42
+ const entryUrl = parseAuthority(entry)
43
+ if (entryUrl === undefined) return false
44
+ return canonicalAuthority(entry, entryUrl) === entryUrl.hostname
45
+ ? entryUrl.hostname === hostUrl.hostname
46
+ : entryUrl.host === hostUrl.host
47
+ })
48
+ }
49
+
50
+ /**
51
+ * Decide whether one sidechat request may reach the plugin routes.
52
+ * @param request - node HTTP request facts (headers).
53
+ * @param trustedHosts - non-loopback authorities this deployment serves.
54
+ * @returns true when the Host is ours (loopback or trusted) and browser markers are same-origin.
55
+ */
56
+ export function isTrustedApiRequest(request: ApiTrustRequest, trustedHosts: readonly string[]): boolean {
57
+ const host = header(request.headers, 'host')
58
+ if (host === undefined) return false
59
+ const hostUrl = parseAuthority(host)
60
+ if (hostUrl === undefined) return false
61
+ if (!isLoopbackHostname(hostUrl.hostname) && !isTrustedAuthority(hostUrl, trustedHosts)) return false
62
+ if (header(request.headers, 'sec-fetch-site') === 'cross-site') return false
63
+ const origin = header(request.headers, 'origin')
64
+ if (origin === undefined) return true
65
+ try {
66
+ return new URL(origin).host === hostUrl.host
67
+ } catch {
68
+ return false
69
+ }
70
+ }
package/src/wire.ts ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Wire helpers for the /sidechat JSON API: bounded body reading, response
3
+ * writing, and the shared error envelope. Mirrors the /sidebar wire helpers
4
+ * (loopback + trusted-host fence), kept self-contained so the plugin never
5
+ * depends on the DSH gateway internals.
6
+ */
7
+ import type { IncomingMessage, ServerResponse } from 'node:http'
8
+
9
+ /** One API failure with its wire code and HTTP status. */
10
+ export class SidechatError extends Error {
11
+ constructor(
12
+ readonly code: string,
13
+ message: string,
14
+ readonly status = 400,
15
+ ) {
16
+ super(message)
17
+ }
18
+ }
19
+
20
+ /** Body size bound of one JSON request. */
21
+ const MAX_BODY_BYTES = 1 << 20
22
+
23
+ /** Read and parse the JSON request body (bounded; malformed → bad-request). */
24
+ export async function readJsonBody(req: IncomingMessage): Promise<unknown> {
25
+ const chunks: Buffer[] = []
26
+ let total = 0
27
+ for await (const chunk of req) {
28
+ const buffer = typeof chunk === 'string' ? Buffer.from(chunk) : chunk
29
+ total += buffer.length
30
+ if (total > MAX_BODY_BYTES) {
31
+ throw new SidechatError('bad-request', 'request body too large')
32
+ }
33
+ chunks.push(buffer)
34
+ }
35
+ const text = Buffer.concat(chunks).toString('utf8')
36
+ if (text.trim() === '') return {}
37
+ try {
38
+ return JSON.parse(text) as unknown
39
+ } catch {
40
+ throw new SidechatError('bad-request', 'request body is not valid JSON')
41
+ }
42
+ }
43
+
44
+ /** Write a JSON response with the given status. */
45
+ export function writeJson(res: ServerResponse, status: number, body: unknown): void {
46
+ const payload = JSON.stringify(body)
47
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
48
+ res.end(payload)
49
+ }
50
+
51
+ /** Write the success envelope. */
52
+ export function writeOk(res: ServerResponse, value: unknown): void {
53
+ writeJson(res, 200, { ok: true, value })
54
+ }
55
+
56
+ /** Write the failure envelope for any thrown value (unknown → internal 500). */
57
+ export function writeError(res: ServerResponse, error: unknown): void {
58
+ if (error instanceof SidechatError) {
59
+ writeJson(res, error.status, { ok: false, error: { code: error.code, message: error.message } })
60
+ return
61
+ }
62
+ const message = error instanceof Error ? error.message : String(error)
63
+ writeJson(res, 500, { ok: false, error: { code: 'internal', message } })
64
+ }
65
+
66
+ /** Narrow an unknown payload value to a string, else throw bad-request. */
67
+ export function requireString(payload: unknown, key: string): string {
68
+ const record = payload as Record<string, unknown> | null
69
+ const value = record?.[key]
70
+ if (typeof value !== 'string' || value === '') {
71
+ throw new SidechatError('bad-request', `missing or invalid "${key}"`)
72
+ }
73
+ return value
74
+ }
75
+
76
+ /** Narrow an unknown payload value to a boolean (default false). */
77
+ export function optionalBoolean(payload: unknown, key: string): boolean {
78
+ const record = payload as Record<string, unknown> | null
79
+ const value = record?.[key]
80
+ return value === true
81
+ }