dsh-github-router 0.3.1 → 0.4.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/lib/config.js CHANGED
@@ -1,116 +1,167 @@
1
- /**
2
- * Settings schema and runtime-option resolution for dsh-github-router.
3
- *
4
- * The settings section (`dsh-github-router` namespace) is registered through
5
- * the official settings seam (`ctx.settings.register`, see `lib/settings.js`);
6
- * the Settings UI renders a plugin card for it (Settings → Plugins, DSH ≥
7
- * 0.1.0-rc.7). Secrets are declared with `.role('secret')` so they are
8
- * redacted on every wire boundary and rendered as write-only inputs.
9
- *
10
- * The schema is deliberately FLAT: the client settings scope writes scalar
11
- * fields by name (`scope.set(field, value)`), so nested objects cannot be
12
- * edited from the card. `resolveOptions` projects the flat section into the
13
- * nested runtime shape the core aggregators use.
14
- * @module dsh-github-router/config
15
- */
16
- import z from '@deepseek-ai/schemastery'
17
-
18
- /**
19
- * Settings namespace carrying the plugin's configuration.
20
- *
21
- * Since DSH 0.1.2-alpha.4 the namespace is a plain lowercase-hyphenated
22
- * string: `SettingsNamespace` is a type-only export and the runtime
23
- * `settingsNamespace()` helper was removed from the public API, with the
24
- * pattern check internalised in `ctx.settings.register`.
25
- */
26
- export const NAMESPACE = 'dsh-github-router'
27
-
28
- /** Composition/settings schema. All fields optional with safe defaults. */
29
- export const Config = z.object({
30
- /** Literal GitHub token (redacted on wire). Prefer tokenEnv. */
31
- token: z.string().role('secret'),
32
- /** Environment variable / credential ref naming the token. */
33
- tokenEnv: z.string().role('credential-ref').default('GITHUB_TOKEN'),
34
- /** Proxy URL for proxy routes. '' = inherit ambient proxy env; 'direct' = never proxy. */
35
- proxy: z.string().default(''),
36
- directTimeoutMs: z.number().min(1000).max(60000).default(8000),
37
- proxyTimeoutMs: z.number().min(1000).max(60000).default(15000),
38
- /** Retries for idempotent GETs on 429/5xx. */
39
- retries: z.number().min(0).max(3).default(1),
40
- routesApi: z.boolean().default(true),
41
- routesGh: z.boolean().default(true),
42
- routesGit: z.boolean().default(true),
43
- routesHtml: z.boolean().default(true),
44
- /** Raw-content mirrors: OFF by default — the user must opt in. */
45
- routesMirror: z.boolean().default(false),
46
- /** Raw-content mirror base URLs, e.g. ["https://ghproxy.net"]. Empty = none. */
47
- mirrors: z.array(z.string()).default([]),
48
- /** PR/issue metadata cache TTL. */
49
- cacheTtlMeta: z.number().min(0).default(300),
50
- /** Immutable-ish content (files, commits, diffs at a sha) cache TTL. */
51
- cacheTtlContent: z.number().min(0).default(86400),
52
- /** Cap for every response body read by this plugin, in bytes. */
53
- maxBytes: z.number().min(16384).max(8388608).default(1048576),
54
- /** Local repo paths granted for read-only git-route reads (log/diff/show only). */
55
- repos: z.array(z.string()).default([]),
56
- /** Plugin-owned git fetch cache dir. '' = <DSH_HOME>/storages/dsh-github-router/git. */
57
- gitCacheDir: z.string().default(''),
58
- })
59
-
60
- const PROXY_ENV_NAMES = ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy']
61
-
62
- /**
63
- * Resolve one settings/composition section into fully-defaulted runtime
64
- * options. A thunk-producing source is consumed at call time so a settings
65
- * change applies to the NEXT tool call.
66
- * @param section - the resolved settings section (already schema-defaulted).
67
- */
68
- export function resolveOptions(section) {
69
- const s = section ?? {}
70
- const proxy =
71
- typeof s.proxy === 'string' && s.proxy.trim().length > 0
72
- ? s.proxy.trim()
73
- : undefined
74
- const routes = {
75
- api: s.routesApi !== false,
76
- gh: s.routesGh !== false,
77
- git: s.routesGit !== false,
78
- html: s.routesHtml !== false,
79
- mirror: s.routesMirror === true,
80
- }
81
- const mirrors = Array.isArray(s.mirrors)
82
- ? s.mirrors.filter((m) => typeof m === 'string' && m.trim().length > 0).map((m) => m.trim())
83
- : []
84
- const repos = Array.isArray(s.repos)
85
- ? s.repos.filter((r) => typeof r === 'string' && r.trim().length > 0).map((r) => r.trim())
86
- : []
87
- return {
88
- token: typeof s.token === 'string' && s.token.length > 0 ? s.token : undefined,
89
- tokenEnv: typeof s.tokenEnv === 'string' && s.tokenEnv.length > 0 ? s.tokenEnv : 'GITHUB_TOKEN',
90
- proxy, // undefined = ambient env decides; 'direct' = never proxy
91
- directTimeoutMs: Number.isFinite(s.directTimeoutMs) ? s.directTimeoutMs : 8000,
92
- proxyTimeoutMs: Number.isFinite(s.proxyTimeoutMs) ? s.proxyTimeoutMs : 15000,
93
- retries: Number.isFinite(s.retries) ? Math.min(3, Math.max(0, s.retries)) : 1,
94
- routes,
95
- mirrors,
96
- cacheTtlSeconds: {
97
- meta: Number.isFinite(s.cacheTtlMeta) ? s.cacheTtlMeta : 300,
98
- content: Number.isFinite(s.cacheTtlContent) ? s.cacheTtlContent : 86400,
99
- },
100
- maxBytes: Number.isFinite(s.maxBytes) ? Math.min(8388608, Math.max(16384, s.maxBytes)) : 1048576,
101
- repos,
102
- gitCacheDir: typeof s.gitCacheDir === 'string' && s.gitCacheDir.trim().length > 0 ? s.gitCacheDir.trim() : undefined,
103
- }
104
- }
105
-
106
- /** The proxy URL one route attempt uses: explicit override > ambient env > none. */
107
- export function effectiveProxy(options, explicit) {
108
- if (explicit !== undefined && explicit !== null && explicit !== '') return explicit
109
- if (options.proxy === 'direct') return undefined
110
- if (options.proxy !== undefined && options.proxy !== '') return options.proxy
111
- for (const name of PROXY_ENV_NAMES) {
112
- const value = process.env[name]
113
- if (value && value.length > 0) return value
114
- }
115
- return undefined
116
- }
1
+ /**
2
+ * Plugin configuration schema and runtime-option resolution for
3
+ * dsh-github-router.
4
+ *
5
+ * The schema IS the settings surface. DSH reads the `Config` schema a plugin
6
+ * exports from its Loader entry, serves that entry's id
7
+ * (`dsh-github-router`) as the settings namespace to the browser, derives the
8
+ * configuration form from the schema, validates every write against it, and
9
+ * redacts `.role('secret')` fields on every wire boundary. There is no
10
+ * registration call anymore — `lib/settings.js` only reads the live values.
11
+ *
12
+ * Every field is `.volatile()`, which is what makes it editable from the
13
+ * configuration page AND live: the framework hands the plugin a Volatile
14
+ * reference whose `.get()` is the current value (user layer over composition
15
+ * layer over schema default), and a saved write applies to the NEXT tool call.
16
+ *
17
+ * The schema is deliberately FLAT: the plugin's card writes scalar fields by
18
+ * name, so nested objects cannot be edited from it. `resolveOptions` projects
19
+ * the flat section into the nested runtime shape the core aggregators use.
20
+ * @module dsh-github-router/config
21
+ */
22
+ import z from '@deepseek-ai/schemastery'
23
+
24
+ /**
25
+ * Settings namespace carrying the plugin's configuration.
26
+ *
27
+ * Since DSH 0.1.7-rc.2 the namespace is the Loader entry id the bundle patch
28
+ * declares (`cordis.patch.yml` inserts the row as `dsh-github-router`); the
29
+ * browser half addresses the same string through `ctx.configForms.get(...)`
30
+ * and keys its Plugins-page card with it.
31
+ */
32
+ export const NAMESPACE = 'dsh-github-router'
33
+
34
+ /** Composition/settings schema. All fields optional with safe defaults. */
35
+ export const Config = z.object({
36
+ /** Literal GitHub token (redacted on wire). Prefer tokenEnv. */
37
+ token: z.string().role('secret').volatile(),
38
+ /** Environment variable / credential ref naming the token. */
39
+ tokenEnv: z.string().role('credential-ref').default('GITHUB_TOKEN').volatile(),
40
+ /** Proxy URL for proxy routes. '' = inherit ambient proxy env; 'direct' = never proxy. */
41
+ proxy: z.string().default('').volatile(),
42
+ directTimeoutMs: z.number().min(1000).max(60000).default(8000).volatile(),
43
+ proxyTimeoutMs: z.number().min(1000).max(60000).default(15000).volatile(),
44
+ /** Retries for idempotent GETs on 429/5xx. */
45
+ retries: z.number().min(0).max(3).default(1).volatile(),
46
+ routesApi: z.boolean().default(true).volatile(),
47
+ routesGh: z.boolean().default(true).volatile(),
48
+ routesGit: z.boolean().default(true).volatile(),
49
+ routesHtml: z.boolean().default(true).volatile(),
50
+ /** Raw-content mirrors: OFF by default — the user must opt in. */
51
+ routesMirror: z.boolean().default(false).volatile(),
52
+ /** Raw-content mirror base URLs, e.g. ["https://ghproxy.net"]. Empty = none. */
53
+ mirrors: z.array(z.string()).default([]).volatile(),
54
+ /** PR/issue metadata cache TTL. */
55
+ cacheTtlMeta: z.number().min(0).default(300).volatile(),
56
+ /** Immutable-ish content (files, commits, diffs at a sha) cache TTL. */
57
+ cacheTtlContent: z.number().min(0).default(86400).volatile(),
58
+ /** Cap for every response body read by this plugin, in bytes. */
59
+ maxBytes: z.number().min(16384).max(8388608).default(1048576).volatile(),
60
+ /** Local repo paths granted for read-only git-route reads (log/diff/show only). */
61
+ repos: z.array(z.string()).default([]).volatile(),
62
+ /** Plugin-owned git fetch cache dir. '' = <DSH_HOME>/storages/dsh-github-router/git. */
63
+ gitCacheDir: z.string().default('').volatile(),
64
+ })
65
+
66
+ /** Every schema field, in the order the configuration card renders them. */
67
+ const CONFIG_FIELDS = [
68
+ 'token',
69
+ 'tokenEnv',
70
+ 'proxy',
71
+ 'directTimeoutMs',
72
+ 'proxyTimeoutMs',
73
+ 'retries',
74
+ 'routesApi',
75
+ 'routesGh',
76
+ 'routesGit',
77
+ 'routesHtml',
78
+ 'routesMirror',
79
+ 'mirrors',
80
+ 'cacheTtlMeta',
81
+ 'cacheTtlContent',
82
+ 'maxBytes',
83
+ 'repos',
84
+ 'gitCacheDir',
85
+ ]
86
+
87
+ /**
88
+ * Read one live configuration section out of the config the framework passes
89
+ * to `apply(ctx, config)`.
90
+ *
91
+ * A `.volatile()` field arrives as a Volatile reference, so its current value
92
+ * is read with `.get()` at the moment of the call; a plain value (a hand-built
93
+ * options object, or a unit test) passes through unchanged. Reading per call is
94
+ * what makes a settings write apply to the next tool call.
95
+ * @param config - the resolved plugin config, volatile references or plain values.
96
+ * @returns a flat section usable by {@link resolveOptions}.
97
+ */
98
+ export function liveSection(config) {
99
+ const source = config ?? {}
100
+ const section = {}
101
+ for (const field of CONFIG_FIELDS) {
102
+ const value = source[field]
103
+ section[field] =
104
+ value !== null && typeof value === 'object' && typeof value.get === 'function'
105
+ ? value.get()
106
+ : value
107
+ }
108
+ return section
109
+ }
110
+
111
+ const PROXY_ENV_NAMES = ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy']
112
+
113
+ /**
114
+ * Resolve one settings/composition section into fully-defaulted runtime
115
+ * options. A thunk-producing source is consumed at call time so a settings
116
+ * change applies to the NEXT tool call.
117
+ * @param section - the resolved settings section (already schema-defaulted).
118
+ */
119
+ export function resolveOptions(section) {
120
+ const s = section ?? {}
121
+ const proxy =
122
+ typeof s.proxy === 'string' && s.proxy.trim().length > 0
123
+ ? s.proxy.trim()
124
+ : undefined
125
+ const routes = {
126
+ api: s.routesApi !== false,
127
+ gh: s.routesGh !== false,
128
+ git: s.routesGit !== false,
129
+ html: s.routesHtml !== false,
130
+ mirror: s.routesMirror === true,
131
+ }
132
+ const mirrors = Array.isArray(s.mirrors)
133
+ ? s.mirrors.filter((m) => typeof m === 'string' && m.trim().length > 0).map((m) => m.trim())
134
+ : []
135
+ const repos = Array.isArray(s.repos)
136
+ ? s.repos.filter((r) => typeof r === 'string' && r.trim().length > 0).map((r) => r.trim())
137
+ : []
138
+ return {
139
+ token: typeof s.token === 'string' && s.token.length > 0 ? s.token : undefined,
140
+ tokenEnv: typeof s.tokenEnv === 'string' && s.tokenEnv.length > 0 ? s.tokenEnv : 'GITHUB_TOKEN',
141
+ proxy, // undefined = ambient env decides; 'direct' = never proxy
142
+ directTimeoutMs: Number.isFinite(s.directTimeoutMs) ? s.directTimeoutMs : 8000,
143
+ proxyTimeoutMs: Number.isFinite(s.proxyTimeoutMs) ? s.proxyTimeoutMs : 15000,
144
+ retries: Number.isFinite(s.retries) ? Math.min(3, Math.max(0, s.retries)) : 1,
145
+ routes,
146
+ mirrors,
147
+ cacheTtlSeconds: {
148
+ meta: Number.isFinite(s.cacheTtlMeta) ? s.cacheTtlMeta : 300,
149
+ content: Number.isFinite(s.cacheTtlContent) ? s.cacheTtlContent : 86400,
150
+ },
151
+ maxBytes: Number.isFinite(s.maxBytes) ? Math.min(8388608, Math.max(16384, s.maxBytes)) : 1048576,
152
+ repos,
153
+ gitCacheDir: typeof s.gitCacheDir === 'string' && s.gitCacheDir.trim().length > 0 ? s.gitCacheDir.trim() : undefined,
154
+ }
155
+ }
156
+
157
+ /** The proxy URL one route attempt uses: explicit override > ambient env > none. */
158
+ export function effectiveProxy(options, explicit) {
159
+ if (explicit !== undefined && explicit !== null && explicit !== '') return explicit
160
+ if (options.proxy === 'direct') return undefined
161
+ if (options.proxy !== undefined && options.proxy !== '') return options.proxy
162
+ for (const name of PROXY_ENV_NAMES) {
163
+ const value = process.env[name]
164
+ if (value && value.length > 0) return value
165
+ }
166
+ return undefined
167
+ }