dsh-draw 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.
Files changed (104) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE +201 -0
  3. package/README.es.md +194 -0
  4. package/README.hi.md +194 -0
  5. package/README.md +194 -0
  6. package/README.pt.md +194 -0
  7. package/README.zh.md +194 -0
  8. package/SECURITY.md +39 -0
  9. package/THIRD_PARTY_NOTICES.md +21 -0
  10. package/cordis.patch.yml +48 -0
  11. package/lib/client.js +4787 -0
  12. package/lib/client.js.map +1 -0
  13. package/lib/index.js +1429 -0
  14. package/lib/typert.host.js +26 -0
  15. package/lib/types/client/DrawResultCard.d.ts +58 -0
  16. package/lib/types/client/DrawResultCard.d.ts.map +1 -0
  17. package/lib/types/client/DrawResultCard.js +48 -0
  18. package/lib/types/client/DrawSettingsTab.d.ts +31 -0
  19. package/lib/types/client/DrawSettingsTab.d.ts.map +1 -0
  20. package/lib/types/client/DrawSettingsTab.js +129 -0
  21. package/lib/types/client/index.d.ts +35 -0
  22. package/lib/types/client/index.d.ts.map +1 -0
  23. package/lib/types/client/index.js +98 -0
  24. package/lib/types/client/locales.d.ts +14 -0
  25. package/lib/types/client/locales.d.ts.map +1 -0
  26. package/lib/types/client/locales.js +57 -0
  27. package/lib/types/client/present.d.ts +80 -0
  28. package/lib/types/client/present.d.ts.map +1 -0
  29. package/lib/types/client/present.js +86 -0
  30. package/lib/types/client/remote.d.ts +268 -0
  31. package/lib/types/client/remote.d.ts.map +1 -0
  32. package/lib/types/client/remote.js +15 -0
  33. package/lib/types/client/styles.d.ts +11 -0
  34. package/lib/types/client/styles.d.ts.map +1 -0
  35. package/lib/types/client/styles.js +43 -0
  36. package/lib/types/config.d.ts +160 -0
  37. package/lib/types/config.d.ts.map +1 -0
  38. package/lib/types/config.js +230 -0
  39. package/lib/types/drawer.d.ts +114 -0
  40. package/lib/types/drawer.d.ts.map +1 -0
  41. package/lib/types/drawer.js +138 -0
  42. package/lib/types/engine.d.ts +58 -0
  43. package/lib/types/engine.d.ts.map +1 -0
  44. package/lib/types/engine.js +135 -0
  45. package/lib/types/http.d.ts +89 -0
  46. package/lib/types/http.d.ts.map +1 -0
  47. package/lib/types/http.js +127 -0
  48. package/lib/types/index.d.ts +43 -0
  49. package/lib/types/index.d.ts.map +1 -0
  50. package/lib/types/index.js +78 -0
  51. package/lib/types/quota.d.ts +69 -0
  52. package/lib/types/quota.d.ts.map +1 -0
  53. package/lib/types/quota.js +56 -0
  54. package/lib/types/router.d.ts +141 -0
  55. package/lib/types/router.d.ts.map +1 -0
  56. package/lib/types/router.js +207 -0
  57. package/lib/types/sanitize.d.ts +40 -0
  58. package/lib/types/sanitize.d.ts.map +1 -0
  59. package/lib/types/sanitize.js +103 -0
  60. package/lib/types/service.d.ts +59 -0
  61. package/lib/types/service.d.ts.map +1 -0
  62. package/lib/types/service.js +131 -0
  63. package/lib/types/session-events.d.ts +66 -0
  64. package/lib/types/session-events.d.ts.map +1 -0
  65. package/lib/types/session-events.js +32 -0
  66. package/lib/types/tool.d.ts +30 -0
  67. package/lib/types/tool.d.ts.map +1 -0
  68. package/lib/types/tool.js +131 -0
  69. package/lib/types/translate.d.ts +64 -0
  70. package/lib/types/translate.d.ts.map +1 -0
  71. package/lib/types/translate.js +56 -0
  72. package/lib/types/typert.host.d.ts +250 -0
  73. package/lib/types/typert.host.d.ts.map +1 -0
  74. package/lib/types/typert.host.js +23 -0
  75. package/lib/types/version.d.ts +10 -0
  76. package/lib/types/version.d.ts.map +1 -0
  77. package/lib/types/version.js +9 -0
  78. package/lib/types/wire.d.ts +699 -0
  79. package/lib/types/wire.d.ts.map +1 -0
  80. package/lib/types/wire.js +273 -0
  81. package/lib/wire-Cc4JZ3jR.js +4370 -0
  82. package/package.json +179 -0
  83. package/src/client/DrawResultCard.tsx +100 -0
  84. package/src/client/DrawSettingsTab.tsx +159 -0
  85. package/src/client/index.ts +123 -0
  86. package/src/client/locales.ts +84 -0
  87. package/src/client/present.ts +137 -0
  88. package/src/client/remote.ts +44 -0
  89. package/src/client/styles.ts +44 -0
  90. package/src/config.ts +358 -0
  91. package/src/drawer.ts +234 -0
  92. package/src/engine.ts +182 -0
  93. package/src/http.ts +161 -0
  94. package/src/index.ts +93 -0
  95. package/src/quota.ts +98 -0
  96. package/src/router.ts +309 -0
  97. package/src/sanitize.ts +113 -0
  98. package/src/service.ts +169 -0
  99. package/src/session-events.ts +70 -0
  100. package/src/tool.ts +145 -0
  101. package/src/translate.ts +101 -0
  102. package/src/typert.host.ts +25 -0
  103. package/src/version.ts +10 -0
  104. package/src/wire.ts +417 -0
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Engine routing with health-aware fallback: the configured chain is walked
3
+ * top-down, every engine is attempted at most once per call, and consecutive
4
+ * failures push an engine into cooldown so a broken engine stops eating the
5
+ * request budget. The router is a plain class (not a Cordis Service): it is
6
+ * plugin-owned state, not a published capability.
7
+ *
8
+ * @module dsh-draw/router
9
+ */
10
+ import type { ResolvedConfig, ResolvedEngineConfig } from './config.ts';
11
+ import { type EngineDeps, type ProducedImage } from './engine.ts';
12
+ import { type StandardImageRequest } from './translate.ts';
13
+ /** One recorded attempt against one engine, success or failure. */
14
+ export interface AttemptView {
15
+ /** Engine id. */
16
+ engine: string;
17
+ /** Failure phase for a failed attempt; absent on success. */
18
+ phase?: 'credential' | 'request' | 'parse';
19
+ /** Stable machine code: `unconfigured`, `auth`, `http`, `parse`, `disabled`, `cooldown`. */
20
+ code: string;
21
+ /** Display-safe failure detail; absent on success. */
22
+ message?: string;
23
+ /** HTTP status when one existed. */
24
+ status?: number;
25
+ }
26
+ /** A successful routed generation. */
27
+ export interface RouterSuccess {
28
+ /** Discriminant. */
29
+ ok: true;
30
+ /** Engine id that produced the images. */
31
+ engine: string;
32
+ /** Engine model name. */
33
+ model: string;
34
+ /** Standard size vocabulary value of the request. */
35
+ size: string;
36
+ /** Produced images. */
37
+ images: readonly ProducedImage[];
38
+ /** Whether an earlier engine in the chain failed first. */
39
+ fallbackUsed: boolean;
40
+ /** Every attempt in chain order. */
41
+ attempts: readonly AttemptView[];
42
+ }
43
+ /** A routed generation where every usable engine failed or was skipped. */
44
+ export interface RouterFailure {
45
+ /** Discriminant. */
46
+ ok: false;
47
+ /** Every attempt in chain order. */
48
+ attempts: readonly AttemptView[];
49
+ }
50
+ /** Router result. */
51
+ export type RouterResult = RouterSuccess | RouterFailure;
52
+ /** One engine's health state for the settings panel. */
53
+ export interface EngineStatus {
54
+ /** Engine id. */
55
+ engineId: string;
56
+ /** Consecutive failures since the last success. */
57
+ consecutiveFailures: number;
58
+ /** Epoch ms until which the engine is in cooldown; `null` = not cooling down. */
59
+ cooldownUntil: number | null;
60
+ /** Display-safe last failure detail; `null` = none recorded. */
61
+ lastError: string | null;
62
+ /** HTTP status of the last failure, when one existed. */
63
+ lastStatus: number | null;
64
+ }
65
+ /** One probe outcome (never mutates routing health). */
66
+ export interface ProbeOutcome {
67
+ /** Engine id. */
68
+ engineId: string;
69
+ /** Whether the endpoint answered with any HTTP status. */
70
+ reachable: boolean;
71
+ /** HTTP status when one existed. */
72
+ httpStatus: number | null;
73
+ /** Sanitized base URL probed (the models listing endpoint). */
74
+ target: string;
75
+ /** Display-safe result note. */
76
+ note: string;
77
+ /** Whether the credential was resolvable at probe time. */
78
+ credentialConfigured: boolean;
79
+ }
80
+ /** Router construction options. */
81
+ export interface RouterOptions {
82
+ /** Consecutive failures before an engine enters cooldown. */
83
+ failureThreshold: number;
84
+ /** Cooldown length in milliseconds. */
85
+ cooldownMs: number;
86
+ /** Clock override for tests. */
87
+ now?: () => number;
88
+ }
89
+ /**
90
+ * The engine chain with per-engine health and cooldown. All mutations are
91
+ * synchronous bookkeeping guarded by `generate`'s single-writer path (the
92
+ * tool is not concurrency-safe, so generations serialize).
93
+ */
94
+ export declare class EngineRouter {
95
+ private readonly config;
96
+ private readonly health;
97
+ private readonly now;
98
+ /**
99
+ * @param config - resolved plugin configuration (engine order and bounds).
100
+ * @param options - failure threshold, cooldown, and clock.
101
+ */
102
+ constructor(config: ResolvedConfig, options: RouterOptions);
103
+ /**
104
+ * Route one standardized request through the configured chain. The chain
105
+ * order is the config array order, except an explicit `request.engine`
106
+ * promotes that engine to the front; every engine is attempted at most
107
+ * once, and an engine in cooldown or without a resolved credential is
108
+ * skipped with a recorded attempt.
109
+ *
110
+ * @param request - normalized standard request.
111
+ * @param deps - transport and credential resolution.
112
+ * @param signal - caller cancellation.
113
+ * @returns success with images, or the complete failure record.
114
+ */
115
+ generate(request: StandardImageRequest, deps: EngineDeps, signal?: AbortSignal): Promise<RouterResult>;
116
+ /** One engine's current health snapshot. */
117
+ statusOf(engineId: string): EngineStatus | undefined;
118
+ /** Health snapshots for every configured engine in chain order. */
119
+ statuses(): readonly EngineStatus[];
120
+ /**
121
+ * Probe one engine with a cheap authenticated `GET {baseUrl}/models` call.
122
+ * The probe reports reachability and credential validity without mutating
123
+ * routing health — it is a settings-panel check, not the router's memory.
124
+ *
125
+ * @param engine - engine to probe.
126
+ * @param deps - transport and credential resolution.
127
+ * @returns the probe outcome.
128
+ */
129
+ probe(engine: ResolvedEngineConfig, deps: EngineDeps): Promise<ProbeOutcome>;
130
+ /** Chain order: an explicit engine override first, then config order minus the override. */
131
+ private chainOrder;
132
+ /** Why an engine may not even be attempted: disabled, cooling down. */
133
+ private skipReason;
134
+ /** Translate the standard request against one engine (the pure translate step). */
135
+ private translate;
136
+ /** Record a success: reset consecutive failures and cooldown. */
137
+ private recordSuccess;
138
+ /** Record a failure: bump the counter, trip cooldown at the threshold, and build the attempt view. */
139
+ private recordFailure;
140
+ }
141
+ //# sourceMappingURL=router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/router.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Engine routing with health-aware fallback: the configured chain is walked
3
+ * top-down, every engine is attempted at most once per call, and consecutive
4
+ * failures push an engine into cooldown so a broken engine stops eating the
5
+ * request budget. The router is a plain class (not a Cordis Service): it is
6
+ * plugin-owned state, not a published capability.
7
+ *
8
+ * @module dsh-draw/router
9
+ */
10
+ import { callEngine, EngineCallError } from './engine.js';
11
+ import { sanitizeError, sanitizeText, sanitizeUrl } from './sanitize.js';
12
+ import { translateRequest } from './translate.js';
13
+ /**
14
+ * The engine chain with per-engine health and cooldown. All mutations are
15
+ * synchronous bookkeeping guarded by `generate`'s single-writer path (the
16
+ * tool is not concurrency-safe, so generations serialize).
17
+ */
18
+ export class EngineRouter {
19
+ config;
20
+ health = new Map();
21
+ now;
22
+ /**
23
+ * @param config - resolved plugin configuration (engine order and bounds).
24
+ * @param options - failure threshold, cooldown, and clock.
25
+ */
26
+ constructor(config, options) {
27
+ this.config = config;
28
+ this.now = options.now ?? Date.now;
29
+ for (const engine of config.engines) {
30
+ this.health.set(engine.id, { consecutiveFailures: 0, cooldownUntil: null, lastError: null, lastStatus: null });
31
+ }
32
+ }
33
+ /**
34
+ * Route one standardized request through the configured chain. The chain
35
+ * order is the config array order, except an explicit `request.engine`
36
+ * promotes that engine to the front; every engine is attempted at most
37
+ * once, and an engine in cooldown or without a resolved credential is
38
+ * skipped with a recorded attempt.
39
+ *
40
+ * @param request - normalized standard request.
41
+ * @param deps - transport and credential resolution.
42
+ * @param signal - caller cancellation.
43
+ * @returns success with images, or the complete failure record.
44
+ */
45
+ async generate(request, deps, signal) {
46
+ const ordered = this.chainOrder(request.engine);
47
+ const attempts = [];
48
+ let tried = 0;
49
+ for (const engine of ordered) {
50
+ if (tried > 0)
51
+ signal?.throwIfAborted();
52
+ const skip = this.skipReason(engine);
53
+ if (skip !== undefined) {
54
+ attempts.push({ engine: engine.id, code: skip.code, message: skip.message });
55
+ continue;
56
+ }
57
+ tried += 1;
58
+ try {
59
+ const images = await callEngine(engine, this.translate(engine, request), deps, signal);
60
+ this.recordSuccess(engine.id);
61
+ attempts.push({ engine: engine.id, code: 'ok' });
62
+ return {
63
+ ok: true,
64
+ engine: engine.id,
65
+ model: engine.model,
66
+ size: request.size ?? 'square',
67
+ images,
68
+ fallbackUsed: tried > 1,
69
+ attempts,
70
+ };
71
+ }
72
+ catch (error) {
73
+ const view = this.recordFailure(engine.id, error);
74
+ attempts.push(view);
75
+ if (view.phase === 'credential' || view.code === 'auth' || view.code === 'parse') {
76
+ // Credential and schema failures are deterministic for this engine —
77
+ // continue the chain so a healthy next engine still serves the call.
78
+ continue;
79
+ }
80
+ if (signal !== undefined && signal.aborted)
81
+ throw signal.reason;
82
+ continue;
83
+ }
84
+ }
85
+ return { ok: false, attempts };
86
+ }
87
+ /** One engine's current health snapshot. */
88
+ statusOf(engineId) {
89
+ const health = this.health.get(engineId);
90
+ if (health === undefined)
91
+ return undefined;
92
+ return {
93
+ engineId,
94
+ consecutiveFailures: health.consecutiveFailures,
95
+ cooldownUntil: health.cooldownUntil,
96
+ lastError: health.lastError,
97
+ lastStatus: health.lastStatus,
98
+ };
99
+ }
100
+ /** Health snapshots for every configured engine in chain order. */
101
+ statuses() {
102
+ return this.config.engines
103
+ .map(engine => this.statusOf(engine.id))
104
+ .filter((status) => status !== undefined);
105
+ }
106
+ /**
107
+ * Probe one engine with a cheap authenticated `GET {baseUrl}/models` call.
108
+ * The probe reports reachability and credential validity without mutating
109
+ * routing health — it is a settings-panel check, not the router's memory.
110
+ *
111
+ * @param engine - engine to probe.
112
+ * @param deps - transport and credential resolution.
113
+ * @returns the probe outcome.
114
+ */
115
+ async probe(engine, deps) {
116
+ const target = sanitizeUrl(`${engine.baseUrl}/models`);
117
+ const credential = await deps.resolveCredential(engine.apiKeyRef);
118
+ if (credential === undefined) {
119
+ return { engineId: engine.id, reachable: false, httpStatus: null, target, note: `credential reference ${engine.apiKeyRef} is not configured`, credentialConfigured: false };
120
+ }
121
+ try {
122
+ const response = await deps.transport.request({
123
+ method: 'GET',
124
+ url: `${engine.baseUrl}/models`,
125
+ headers: { authorization: `Bearer ${credential}` },
126
+ });
127
+ if (response.status === 401 || response.status === 403) {
128
+ return { engineId: engine.id, reachable: true, httpStatus: response.status, target, note: `endpoint answered but rejected the credential (HTTP ${response.status})`, credentialConfigured: true };
129
+ }
130
+ if (response.status === 404 || response.status === 405 || response.status === 501) {
131
+ return { engineId: engine.id, reachable: true, httpStatus: response.status, target, note: `endpoint answered (HTTP ${response.status}); the models listing may be absent but generation can still work`, credentialConfigured: true };
132
+ }
133
+ if (response.status < 200 || response.status >= 300) {
134
+ return { engineId: engine.id, reachable: true, httpStatus: response.status, target, note: `endpoint answered with HTTP ${response.status}`, credentialConfigured: true };
135
+ }
136
+ return { engineId: engine.id, reachable: true, httpStatus: response.status, target, note: 'endpoint reachable and credential accepted', credentialConfigured: true };
137
+ }
138
+ catch (error) {
139
+ return { engineId: engine.id, reachable: false, httpStatus: null, target, note: sanitizeError(error), credentialConfigured: true };
140
+ }
141
+ }
142
+ /** Chain order: an explicit engine override first, then config order minus the override. */
143
+ chainOrder(override) {
144
+ const engines = [...this.config.engines];
145
+ if (override === undefined || override === this.config.defaultEngine) {
146
+ const index = engines.findIndex(engine => engine.id === this.config.defaultEngine);
147
+ if (index > 0) {
148
+ const [preferred] = engines.splice(index, 1);
149
+ engines.unshift(preferred);
150
+ }
151
+ return engines;
152
+ }
153
+ const index = engines.findIndex(engine => engine.id === override);
154
+ if (index < 0) {
155
+ // Unknown override: fall back to the configured chain (the override
156
+ // intent is recorded nowhere else; the tool output names the engine).
157
+ return engines;
158
+ }
159
+ const [preferred] = engines.splice(index, 1);
160
+ engines.unshift(preferred);
161
+ return engines;
162
+ }
163
+ /** Why an engine may not even be attempted: disabled, cooling down. */
164
+ skipReason(engine) {
165
+ if (!engine.enabled)
166
+ return { code: 'disabled', message: `engine "${engine.id}" is disabled` };
167
+ const health = this.health.get(engine.id);
168
+ const now = this.now();
169
+ const cooldown = health?.cooldownUntil;
170
+ if (cooldown !== null && cooldown !== undefined && cooldown > now) {
171
+ return { code: 'cooldown', message: `engine "${engine.id}" is cooling down after repeated failures` };
172
+ }
173
+ return undefined;
174
+ }
175
+ /** Translate the standard request against one engine (the pure translate step). */
176
+ translate(engine, request) {
177
+ return translateRequest(engine, request);
178
+ }
179
+ /** Record a success: reset consecutive failures and cooldown. */
180
+ recordSuccess(engineId) {
181
+ const health = this.health.get(engineId);
182
+ if (health === undefined)
183
+ return;
184
+ health.consecutiveFailures = 0;
185
+ health.cooldownUntil = null;
186
+ health.lastError = null;
187
+ health.lastStatus = null;
188
+ }
189
+ /** Record a failure: bump the counter, trip cooldown at the threshold, and build the attempt view. */
190
+ recordFailure(engineId, error) {
191
+ const health = this.health.get(engineId);
192
+ const engineError = error instanceof EngineCallError ? error : undefined;
193
+ const phase = engineError?.phase ?? 'request';
194
+ const code = engineError?.code ?? 'http';
195
+ const message = sanitizeText(sanitizeError(error));
196
+ const status = engineError?.status;
197
+ if (health !== undefined) {
198
+ health.consecutiveFailures += 1;
199
+ health.lastError = message;
200
+ health.lastStatus = status ?? null;
201
+ if (health.consecutiveFailures >= this.config.failureThreshold) {
202
+ health.cooldownUntil = this.now() + this.config.cooldownMs;
203
+ }
204
+ }
205
+ return { engine: engineId, phase, code, message, ...(status !== undefined ? { status } : {}) };
206
+ }
207
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Display sanitization for dsh-draw surfaces. Everything shown to a human —
3
+ * the tool result text, the settings panel snapshot, probe details, engine
4
+ * errors — passes through these pure functions so URL credentials, API keys,
5
+ * bearer tokens, and JWTs never reach a display. Secrets themselves are
6
+ * credential references; this module redacts what can still leak through
7
+ * configured URLs and provider error text.
8
+ *
9
+ * @module dsh-draw/sanitize
10
+ */
11
+ /** Replacement for every redacted credential value. */
12
+ export declare const REDACTED = "***";
13
+ /**
14
+ * Redact a URL for display: userinfo password, credential query values, and
15
+ * credential fragment pairs. Query keys are read through `URLSearchParams`,
16
+ * so percent-encoded key names are decoded before matching. Unparseable
17
+ * inputs fall back to pattern redaction (whole userinfo, credential query
18
+ * pairs, credential fragment pairs) instead of throwing.
19
+ *
20
+ * @param url - candidate URL text.
21
+ * @returns display-safe URL text.
22
+ */
23
+ export declare function sanitizeUrl(url: string): string;
24
+ /**
25
+ * Redact credential-shaped fragments from free text: header lines, bearer
26
+ * tokens, raw JWTs, embedded query pairs, and quoted token values.
27
+ *
28
+ * @param text - candidate display text.
29
+ * @returns display-safe text.
30
+ */
31
+ export declare function sanitizeText(text: string): string;
32
+ /**
33
+ * Stringify an arbitrary thrown value safely and redact it for display.
34
+ * Never throws: unrenderable values degrade to a fixed marker.
35
+ *
36
+ * @param error - thrown value from an engine call or probe.
37
+ * @returns display-safe error text.
38
+ */
39
+ export declare function sanitizeError(error: unknown): string;
40
+ //# sourceMappingURL=sanitize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,uDAAuD;AACvD,eAAO,MAAM,QAAQ,QAAQ,CAAA;AAmC7B;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAgB/C;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAajD;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAQpD"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Display sanitization for dsh-draw surfaces. Everything shown to a human —
3
+ * the tool result text, the settings panel snapshot, probe details, engine
4
+ * errors — passes through these pure functions so URL credentials, API keys,
5
+ * bearer tokens, and JWTs never reach a display. Secrets themselves are
6
+ * credential references; this module redacts what can still leak through
7
+ * configured URLs and provider error text.
8
+ *
9
+ * @module dsh-draw/sanitize
10
+ */
11
+ /** Replacement for every redacted credential value. */
12
+ export const REDACTED = '***';
13
+ /** Query/field keys whose values are credentials regardless of their name casing. */
14
+ const CREDENTIAL_KEY = /^(?:access[_-]?token|api[_-]?key|apikey|auth|authorization|client[_-]?secret|key|password|passwd|pwd|secret|sig|signature|token)$/iu;
15
+ /** Credential keys for the unparseable-URL fallback and embedded-text scans. */
16
+ const CREDENTIAL_KEY_SOURCE = '(?:access[_-]?token|api[_-]?key|apikey|auth(?:orization)?|client[_-]?secret|key|passw(?:or)?d|passwd|pwd|secret|sig(?:nature)?|token)';
17
+ /** Whole userinfo before `@` (unparseable URLs only — parsed URLs redact just the password). */
18
+ const USERINFO = /([a-z][a-z0-9+.-]*:\/\/)([^/@\s]+)@/giu;
19
+ /** `?key=value` / `&key=value` credential pairs inside arbitrary text. */
20
+ const QUERY_CREDENTIAL = new RegExp(`([?&](?:[^=&#\\s]*${CREDENTIAL_KEY_SOURCE}[^=&#\\s]*)=)[^&#\\s]*`, 'giu');
21
+ /** `#key=value` credential pairs in URL fragments and arbitrary text. */
22
+ const FRAGMENT_CREDENTIAL = new RegExp(`(#[^=&#\\s]*${CREDENTIAL_KEY_SOURCE}[^=&#\\s]*=)[^&#\\s]*`, 'giu');
23
+ /** `Authorization: <value>`-style header lines in arbitrary text (quoted value first). */
24
+ const HEADER_CREDENTIAL_QUOTED = new RegExp(`(\\b${CREDENTIAL_KEY_SOURCE}\\s*[:=]\\s*["'])[^"']*(["'])`, 'giu');
25
+ /** `Authorization: <value>`-style header lines with unquoted values. */
26
+ const HEADER_CREDENTIAL_BARE = new RegExp(`(\\b${CREDENTIAL_KEY_SOURCE}\\s*[:=]\\s*)[^\\s,;)\\]}]+`, 'giu');
27
+ /** Environment-variable-shaped credentials (`GITHUB_TOKEN=…`) in spawn errors. */
28
+ const ENV_VAR_CREDENTIAL = /\b[A-Za-z0-9_]*(?:TOKEN|API[_-]?KEY|SECRET|PASSWORD|PASSWD)[A-Za-z0-9_]*\s*=\s*[^\s,;)\]}]+/gu;
29
+ /** Bearer tokens, including the `Bearer ` keyword and the token itself. */
30
+ const BEARER = /(bearer)\s+[A-Za-z0-9._~+/=-]+/giu;
31
+ /** Quoted JSON-ish `"token": "value"` pairs in arbitrary text. */
32
+ const QUOTED_CREDENTIAL = new RegExp(`(["'](?:access[_-]?token|api[_-]?key|client[_-]?secret|secret|token)["']\\s*[:=]\\s*["'])[^"']*(["'])`, 'giu');
33
+ /** Raw JWT bodies, wherever they appear. */
34
+ const JWT = /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}\b/gu;
35
+ /**
36
+ * Redact a URL for display: userinfo password, credential query values, and
37
+ * credential fragment pairs. Query keys are read through `URLSearchParams`,
38
+ * so percent-encoded key names are decoded before matching. Unparseable
39
+ * inputs fall back to pattern redaction (whole userinfo, credential query
40
+ * pairs, credential fragment pairs) instead of throwing.
41
+ *
42
+ * @param url - candidate URL text.
43
+ * @returns display-safe URL text.
44
+ */
45
+ export function sanitizeUrl(url) {
46
+ let parsed;
47
+ try {
48
+ parsed = new URL(url);
49
+ }
50
+ catch {
51
+ return url
52
+ .replace(USERINFO, '$1***@')
53
+ .replace(QUERY_CREDENTIAL, `$1${REDACTED}`)
54
+ .replace(FRAGMENT_CREDENTIAL, `$1${REDACTED}`);
55
+ }
56
+ if (parsed.password !== '')
57
+ parsed.password = REDACTED;
58
+ for (const key of [...parsed.searchParams.keys()]) {
59
+ if (CREDENTIAL_KEY.test(key))
60
+ parsed.searchParams.set(key, REDACTED);
61
+ }
62
+ if (parsed.hash !== '')
63
+ parsed.hash = parsed.hash.replace(FRAGMENT_CREDENTIAL, `$1${REDACTED}`);
64
+ return parsed.toString();
65
+ }
66
+ /**
67
+ * Redact credential-shaped fragments from free text: header lines, bearer
68
+ * tokens, raw JWTs, embedded query pairs, and quoted token values.
69
+ *
70
+ * @param text - candidate display text.
71
+ * @returns display-safe text.
72
+ */
73
+ export function sanitizeText(text) {
74
+ return text
75
+ .replace(BEARER, `$1 ${REDACTED}`)
76
+ .replace(HEADER_CREDENTIAL_QUOTED, `$1${REDACTED}$2`)
77
+ .replace(HEADER_CREDENTIAL_BARE, `$1${REDACTED}`)
78
+ .replace(ENV_VAR_CREDENTIAL, value => {
79
+ const equals = value.indexOf('=');
80
+ return equals < 0 ? value : `${value.slice(0, equals)}=${REDACTED}`;
81
+ })
82
+ .replace(QUOTED_CREDENTIAL, `$1${REDACTED}$2`)
83
+ .replace(QUERY_CREDENTIAL, `$1${REDACTED}`)
84
+ .replace(FRAGMENT_CREDENTIAL, `$1${REDACTED}`)
85
+ .replace(JWT, REDACTED);
86
+ }
87
+ /**
88
+ * Stringify an arbitrary thrown value safely and redact it for display.
89
+ * Never throws: unrenderable values degrade to a fixed marker.
90
+ *
91
+ * @param error - thrown value from an engine call or probe.
92
+ * @returns display-safe error text.
93
+ */
94
+ export function sanitizeError(error) {
95
+ let text;
96
+ try {
97
+ text = typeof error === 'string' ? error : String(error);
98
+ }
99
+ catch {
100
+ text = '<unrenderable error>';
101
+ }
102
+ return sanitizeText(text);
103
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The `draw` host service: the Typert Remote namespace the settings panel and
3
+ * the result card consume (`draw/status`, `draw/probe`, `draw/setCredential`,
4
+ * `draw/unsetCredential`, `draw/regenerate`). Status snapshots are read-only;
5
+ * credential writes go through the official `ctx.credentials` seam (values
6
+ * never enter a log or a snapshot); regenerate re-runs the full drawer path
7
+ * so a panel regeneration is as durable and quota-accounted as a tool call.
8
+ *
9
+ * @module dsh-draw/service
10
+ */
11
+ import type { Context } from '@deepseek-ai/cordis';
12
+ import { type CredentialProvider } from '@deepseek-ai/dsh-credentials';
13
+ import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
14
+ import type { ResolvedConfig } from './config.ts';
15
+ import type { Drawer } from './drawer.ts';
16
+ import type { EngineRouter } from './router.ts';
17
+ import { type CredentialActionResult, type DrawProbeResult, type DrawRegenerateResult, type DrawStatusSnapshot } from './wire.ts';
18
+ declare module '@deepseek-ai/cordis' {
19
+ interface Context {
20
+ /** Image-generation host service (this package). */
21
+ draw: DrawService;
22
+ }
23
+ }
24
+ /** Bindings the service reads at call time (hot-swappable optional seams). */
25
+ export interface DrawServiceOptions {
26
+ /** Resolved plugin configuration. */
27
+ config: ResolvedConfig;
28
+ /** The engine router (health state for the status snapshot). */
29
+ router: EngineRouter;
30
+ /** The generation drawer (regenerate path). */
31
+ drawer: Drawer;
32
+ /** Per-call credential service; undefined = credential actions degrade. */
33
+ credentials: CredentialProvider | undefined;
34
+ }
35
+ /**
36
+ * The `draw` Typert Remote service.
37
+ */
38
+ export declare class DrawService extends TypertRemoteService {
39
+ /** Per-call bindings (replaced on plugin reload). */
40
+ options: DrawServiceOptions;
41
+ /**
42
+ * @param ctx - the mounting context.
43
+ * @param options - runtime bindings.
44
+ */
45
+ constructor(ctx: Context, options: DrawServiceOptions);
46
+ /** Resolve one engine's credential view for the status snapshot. */
47
+ private credentialView;
48
+ /** Read-only panel snapshot: engine chain, health, credential facts, quota. */
49
+ status(): Promise<DrawStatusSnapshot>;
50
+ /** Probe one engine's connectivity without mutating routing health. */
51
+ probe(engineId: string): Promise<DrawProbeResult>;
52
+ /** Store one API key under the engine's credential reference (credentials seam). */
53
+ setCredential(engineId: string, value: string): Promise<CredentialActionResult>;
54
+ /** Remove a stored API key for the engine's credential reference. */
55
+ unsetCredential(engineId: string): Promise<CredentialActionResult>;
56
+ /** Re-run a generation from the result card through the full drawer path. */
57
+ regenerate(sessionId: string, args: unknown): Promise<DrawRegenerateResult>;
58
+ }
59
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAiB,KAAK,kBAAkB,EAAE,MAAM,8BAA8B,CAAA;AAIrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAA;AAEtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACjD,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,aAAa,CAAA;AAEtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE/C,OAAO,EAIL,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACxB,MAAM,WAAW,CAAA;AAElB,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,oDAAoD;QACpD,IAAI,EAAE,WAAW,CAAA;KAClB;CACF;AAED,8EAA8E;AAC9E,MAAM,WAAW,kBAAkB;IACjC,qCAAqC;IACrC,MAAM,EAAE,cAAc,CAAA;IACtB,gEAAgE;IAChE,MAAM,EAAE,YAAY,CAAA;IACpB,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAA;IACd,2EAA2E;IAC3E,WAAW,EAAE,kBAAkB,GAAG,SAAS,CAAA;CAC5C;AAED;;GAEG;AACH,qBAAa,WAAY,SAAQ,mBAAmB;IAClD,qDAAqD;IACrD,OAAO,EAAE,kBAAkB,CAAA;IAE3B;;;OAGG;IACH,YAAY,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,kBAAkB,EAGpD;IAED,oEAAoE;YACtD,cAAc;IAW5B,+EAA+E;IACzE,MAAM,IAAI,OAAO,CAAC,kBAAkB,CAAC,CA0B1C;IAED,uEAAuE;IACjE,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAUtD;IAED,oFAAoF;IAC9E,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAQpF;IAED,qEAAqE;IAC/D,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAOvE;IAED,6EAA6E;IACvE,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAQhF;CACF"}