@riceawa/dsh-lan-gateway 0.5.4 → 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 +37 -21
- package/lib/client.js +190 -146
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +129 -8
- package/lib/index.js +1424 -968
- package/package.json +26 -14
- package/src/auth.ts +7 -14
- package/src/client/index.ts +39 -29
- package/src/client/lan-gateway-card.tsx +93 -145
- package/src/config-fields.ts +137 -0
- package/src/gateway.ts +203 -240
- package/src/index.ts +401 -157
- package/src/login.ts +1 -17
- package/src/request-policy.ts +315 -0
- package/src/state.ts +16 -5
- package/src/tls.ts +23 -0
- package/src/upstream-session.ts +24 -8
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `lan-gateway` configuration field table: which settings keys exist, how
|
|
3
|
+
* each one is rendered, and how a draft string becomes a config value or a
|
|
4
|
+
* clear.
|
|
5
|
+
*
|
|
6
|
+
* The host and the browser card share this module. The host derives the key set
|
|
7
|
+
* its config route accepts and the set an empty value clears from it; the card
|
|
8
|
+
* renders its controls from it. A field therefore cannot be editable on one
|
|
9
|
+
* side and unknown on the other, which is what let the card silently rewrite
|
|
10
|
+
* keys it never showed.
|
|
11
|
+
*
|
|
12
|
+
* Because both halves import it, this module must stay dependency-free and
|
|
13
|
+
* side-effect-free: no schemastery (the host schema remains the validating
|
|
14
|
+
* authority, not this table), no node built-ins, no DOM, no I/O.
|
|
15
|
+
*
|
|
16
|
+
* @module @riceawa/dsh-lan-gateway/config-fields
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The wire shape of the `lan-gateway` config section, as the Settings card sees
|
|
21
|
+
* it. Deliberately narrower than the host's `Config`: `cookieName` and the
|
|
22
|
+
* retired `authRequired` are not card-editable. The config route applies a
|
|
23
|
+
* patch, so a key absent here is left untouched rather than reset.
|
|
24
|
+
*/
|
|
25
|
+
export interface LanGatewaySettings {
|
|
26
|
+
enabled?: boolean
|
|
27
|
+
gatewayPort?: number
|
|
28
|
+
dshTargetPort?: number
|
|
29
|
+
lanCidrs?: string[]
|
|
30
|
+
lanPasswordless?: boolean
|
|
31
|
+
cookieMaxAgeDays?: number
|
|
32
|
+
tlsEnabled?: boolean
|
|
33
|
+
tlsMode?: 'self-signed' | 'custom'
|
|
34
|
+
tlsCertPath?: string
|
|
35
|
+
tlsKeyPath?: string
|
|
36
|
+
tlsSelfSignedHosts?: string
|
|
37
|
+
tlsCertMaxAgeDays?: number
|
|
38
|
+
allowInsecurePlaintext?: boolean
|
|
39
|
+
trustedTerminator?: string
|
|
40
|
+
secureCookies?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A settings key this table knows how to edit. */
|
|
44
|
+
export type ConfigFieldKey = keyof LanGatewaySettings
|
|
45
|
+
|
|
46
|
+
/** How a field is rendered and parsed. */
|
|
47
|
+
export type FieldKind = 'boolean' | 'number' | 'text' | 'cidrs' | 'select' | 'tristate'
|
|
48
|
+
|
|
49
|
+
export interface FieldDef {
|
|
50
|
+
readonly field: ConfigFieldKey
|
|
51
|
+
readonly kind: FieldKind
|
|
52
|
+
/** An empty draft clears the key back to the composition layer. */
|
|
53
|
+
readonly optional?: boolean
|
|
54
|
+
/** The accepted values of a `select` field. */
|
|
55
|
+
readonly options?: readonly string[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The three states of a tri-state field, in display order. */
|
|
59
|
+
export const TRISTATE_OPTIONS = ['auto', 'true', 'false'] as const
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The editable settings, in display order. Adding a config key means adding it
|
|
63
|
+
* here (the host whitelist and the card's controls both follow), to the
|
|
64
|
+
* `Config` schema in `index.ts`, and to `listenerKey` when it changes listener
|
|
65
|
+
* behavior.
|
|
66
|
+
*/
|
|
67
|
+
export const FIELDS: readonly FieldDef[] = [
|
|
68
|
+
{ field: 'enabled', kind: 'boolean' },
|
|
69
|
+
{ field: 'gatewayPort', kind: 'number' },
|
|
70
|
+
{ field: 'dshTargetPort', kind: 'number', optional: true },
|
|
71
|
+
{ field: 'lanCidrs', kind: 'cidrs' },
|
|
72
|
+
{ field: 'lanPasswordless', kind: 'boolean' },
|
|
73
|
+
{ field: 'cookieMaxAgeDays', kind: 'number' },
|
|
74
|
+
{ field: 'tlsEnabled', kind: 'boolean' },
|
|
75
|
+
{ field: 'tlsMode', kind: 'select', options: ['self-signed', 'custom'] },
|
|
76
|
+
{ field: 'tlsSelfSignedHosts', kind: 'text' },
|
|
77
|
+
{ field: 'tlsCertPath', kind: 'text', optional: true },
|
|
78
|
+
{ field: 'tlsKeyPath', kind: 'text', optional: true },
|
|
79
|
+
{ field: 'tlsCertMaxAgeDays', kind: 'number' },
|
|
80
|
+
{ field: 'allowInsecurePlaintext', kind: 'boolean' },
|
|
81
|
+
{ field: 'trustedTerminator', kind: 'text', optional: true },
|
|
82
|
+
{ field: 'secureCookies', kind: 'tristate' },
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
/** Every settings key the config route accepts; anything else is ignored. */
|
|
86
|
+
export const CONFIG_FIELD_KEYS: ReadonlySet<string> = new Set(FIELDS.map(def => def.field as string))
|
|
87
|
+
|
|
88
|
+
/** Keys an empty submitted value clears back to the composition layer. */
|
|
89
|
+
export const OPTIONAL_CONFIG_KEYS: ReadonlySet<string> = new Set(
|
|
90
|
+
FIELDS.filter(def => def.optional === true).map(def => def.field as string),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
/** Render a stored value as draft text. */
|
|
94
|
+
export function formatValue(def: FieldDef, value: unknown): string {
|
|
95
|
+
switch (def.kind) {
|
|
96
|
+
case 'boolean': return value === true ? 'true' : 'false'
|
|
97
|
+
case 'number': return typeof value === 'number' ? String(value) : ''
|
|
98
|
+
case 'cidrs': return Array.isArray(value) ? value.join(', ') : ''
|
|
99
|
+
case 'select': return typeof value === 'string' ? value : (def.options?.[0] ?? '')
|
|
100
|
+
// Tri-state: an unset value is a distinct third state ("auto"), never "false".
|
|
101
|
+
case 'tristate': return value === true ? 'true' : value === false ? 'false' : 'auto'
|
|
102
|
+
case 'text': return typeof value === 'string' ? value : ''
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** One field's contribution to a config patch. */
|
|
107
|
+
export type Write = { kind: 'set'; value: unknown } | { kind: 'clear' }
|
|
108
|
+
|
|
109
|
+
/** Parse draft text into a value for the POST body; undefined blocks saving. */
|
|
110
|
+
export function parseValue(def: FieldDef, text: string): Write | undefined {
|
|
111
|
+
const trimmed = text.trim()
|
|
112
|
+
switch (def.kind) {
|
|
113
|
+
case 'boolean':
|
|
114
|
+
if (trimmed === 'true') return { kind: 'set', value: true }
|
|
115
|
+
if (trimmed === 'false') return { kind: 'set', value: false }
|
|
116
|
+
return undefined
|
|
117
|
+
case 'number':
|
|
118
|
+
if (trimmed === '') return def.optional ? { kind: 'clear' } : undefined
|
|
119
|
+
if (!/^\d+$/.test(trimmed)) return undefined
|
|
120
|
+
return { kind: 'set', value: Number(trimmed) }
|
|
121
|
+
case 'cidrs': {
|
|
122
|
+
const cidrs = trimmed.split(',').map(s => s.trim()).filter(s => s !== '')
|
|
123
|
+
return cidrs.length === 0 ? { kind: 'clear' } : { kind: 'set', value: cidrs }
|
|
124
|
+
}
|
|
125
|
+
case 'select':
|
|
126
|
+
return def.options?.includes(trimmed) ? { kind: 'set', value: trimmed } : undefined
|
|
127
|
+
case 'tristate':
|
|
128
|
+
// 'auto' clears the key so it re-inherits the composition layer (and the
|
|
129
|
+
// resolution rule), which is what an unset tri-state means.
|
|
130
|
+
if (trimmed === 'auto') return { kind: 'clear' }
|
|
131
|
+
if (trimmed === 'true') return { kind: 'set', value: true }
|
|
132
|
+
if (trimmed === 'false') return { kind: 'set', value: false }
|
|
133
|
+
return undefined
|
|
134
|
+
case 'text':
|
|
135
|
+
return trimmed === '' ? (def.optional ? { kind: 'clear' } : undefined) : { kind: 'set', value: trimmed }
|
|
136
|
+
}
|
|
137
|
+
}
|