@sparkelf/dsh-plugin-dataops 0.1.0-rc.10

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/index.js ADDED
@@ -0,0 +1,499 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
3
+ import z from "@deepseek-ai/schemastery";
4
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
5
+ import * as McpClient from "@sparkelf/dsh-plugin-mcp-credentials";
6
+ //#region lib/types/index.js
7
+ /**
8
+ * Standalone DataOps MCP integration. It owns one persistent DSH target
9
+ * identity, the browser Authorization Code + PKCE handoff, and a generic
10
+ * authenticated MCP client for the target's immutable principal.
11
+ * @module @sparkelf/dsh-plugin-dataops
12
+ */
13
+ /** Cordis plugin name for the standalone DataOps integration. */
14
+ const name = "mcp-dataops";
15
+ /** Services required by the standalone authorization and MCP composition. */
16
+ const inject = [
17
+ "connection",
18
+ "credentials",
19
+ "webServer",
20
+ "tools"
21
+ ];
22
+ const CLIENT_ID = "deepseek-harness-plus";
23
+ const SCOPE = "openid dataops.mcp";
24
+ const INTEGRATION_PATH = "/integrations/dataops";
25
+ const STATUS_PATH = `${INTEGRATION_PATH}/status`;
26
+ const CONNECT_PATH = `${INTEGRATION_PATH}/connect`;
27
+ const CALLBACK_PATH = `${INTEGRATION_PATH}/callback`;
28
+ const DISCONNECT_PATH = `${INTEGRATION_PATH}/disconnect`;
29
+ const PENDING_TTL_MS = 600 * 1e3;
30
+ const TARGET_REF_PATTERN = /^[A-Za-z0-9_-]{32,128}$/u;
31
+ /** Schemastery parser for standalone DataOps integration configuration. */
32
+ const Config = z.object({
33
+ baseUrl: z.string().required(),
34
+ serverName: z.string().default("dataops"),
35
+ credentialRef: z.string().role("credential-ref").required(),
36
+ targetCredentialRef: z.string().role("credential-ref").required(),
37
+ callbackOrigin: z.string(),
38
+ toolCallTimeoutMs: z.number().min(1).default(6e4),
39
+ failOnStartupError: z.boolean().default(false)
40
+ });
41
+ function normalizeHttpOrigin(value, invalidMessage, componentMessage) {
42
+ if (!URL.canParse(value)) throw new Error(invalidMessage);
43
+ const url = new URL(value);
44
+ if (!["http:", "https:"].includes(url.protocol) || url.username !== "" || url.password !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "") throw new Error(componentMessage);
45
+ return url.origin;
46
+ }
47
+ function normalizeBaseUrl(value) {
48
+ return normalizeHttpOrigin(value, "mcp-dataops: baseUrl must be an absolute http(s) origin", "mcp-dataops: baseUrl must be an http(s) origin without path, query, credentials, or fragment");
49
+ }
50
+ function normalizeCallbackOrigin(value) {
51
+ return normalizeHttpOrigin(value, "mcp-dataops: callbackOrigin must be an absolute browser origin", "mcp-dataops: callbackOrigin must be an HTTP or HTTPS origin without path, query, credentials, or fragment");
52
+ }
53
+ function isLoopbackRequest(request) {
54
+ const address = request.socket.remoteAddress;
55
+ return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
56
+ }
57
+ function requireConnection(ctx, request, response) {
58
+ const rejection = ctx.connection.requestRejection(request);
59
+ if (rejection === void 0) return true;
60
+ response.writeHead(rejection);
61
+ response.end();
62
+ return false;
63
+ }
64
+ function requireLoopback(request, response) {
65
+ if (isLoopbackRequest(request)) return true;
66
+ response.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
67
+ response.end("DataOps authorization management accepts only DSH loopback ingress.");
68
+ return false;
69
+ }
70
+ function requireSameOriginBrowser(request, response) {
71
+ if (request.headers["sec-fetch-site"] === "same-origin") return true;
72
+ response.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
73
+ response.end("DataOps authorization management accepts only same-origin DSH browser requests.");
74
+ return false;
75
+ }
76
+ function requireMethod(request, response, method) {
77
+ if (request.method === method) return true;
78
+ response.writeHead(405, { allow: method });
79
+ response.end();
80
+ return false;
81
+ }
82
+ function sendJson(response, status, value) {
83
+ response.writeHead(status, {
84
+ "content-type": "application/json; charset=utf-8",
85
+ "cache-control": "no-store"
86
+ });
87
+ response.end(JSON.stringify(value));
88
+ }
89
+ function popupBridge(response, result, reason) {
90
+ const payload = JSON.stringify({
91
+ type: "dsh:dataops-oauth",
92
+ result,
93
+ reason
94
+ });
95
+ response.writeHead(200, {
96
+ "content-type": "text/html; charset=utf-8",
97
+ "cache-control": "no-store",
98
+ "referrer-policy": "no-referrer"
99
+ });
100
+ response.end(`<!doctype html><html><head><meta charset="utf-8"><title>DataOps</title></head><body><script>if(window.opener){window.opener.postMessage(${payload},window.location.origin);window.close()}else{window.location.replace('/')}<\/script></body></html>`);
101
+ }
102
+ function callbackOriginOf(request, response) {
103
+ const rawOrigin = new URL(request.url ?? CONNECT_PATH, "http://dsh.local").searchParams.get("origin") ?? "";
104
+ if (!URL.canParse(rawOrigin)) {
105
+ sendJson(response, 400, { error: "The DSH browser origin is invalid." });
106
+ return;
107
+ }
108
+ const candidate = new URL(rawOrigin);
109
+ const loopback = candidate.hostname === "127.0.0.1" || candidate.hostname === "localhost" || candidate.hostname === "[::1]";
110
+ const requestHost = request.headers.host?.trim().toLowerCase() ?? "";
111
+ if (!["http:", "https:"].includes(candidate.protocol) || candidate.username !== "" || candidate.password !== "" || candidate.pathname !== "/" || candidate.search !== "" || candidate.hash !== "" || !loopback || candidate.host.toLowerCase() !== requestHost) {
112
+ sendJson(response, 400, { error: "The DSH browser origin does not match this DSH host." });
113
+ return;
114
+ }
115
+ return candidate.origin;
116
+ }
117
+ function parseTargetRef(value) {
118
+ if (!TARGET_REF_PATTERN.test(value)) throw new Error("mcp-dataops: target credential must contain a 32-128 character base64url identifier");
119
+ return value;
120
+ }
121
+ function parseTokenResponse(value) {
122
+ if (!value || typeof value !== "object") throw new Error("DataOps token endpoint returned an invalid response");
123
+ const record = value;
124
+ if (typeof record.access_token !== "string" || record.access_token.length === 0 || record.token_type !== "Bearer" || record.scope !== SCOPE) throw new Error("DataOps token endpoint returned an invalid response");
125
+ return record;
126
+ }
127
+ function accessTokenExpiry(accessToken) {
128
+ const payload = accessToken.split(".")[1];
129
+ if (payload === void 0) throw new Error("DataOps access token does not contain a JWT payload");
130
+ let value;
131
+ try {
132
+ value = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
133
+ } catch (error) {
134
+ throw new Error("DataOps access token contains an invalid JWT payload", { cause: error });
135
+ }
136
+ if (!value || typeof value !== "object") throw new Error("DataOps access token JWT payload must be an object");
137
+ const expiresAt = value.exp;
138
+ if (typeof expiresAt !== "number" || !Number.isInteger(expiresAt)) throw new Error("DataOps access token JWT payload must contain an integer exp");
139
+ const delay = expiresAt * 1e3 - Date.now();
140
+ if (delay <= 0 || delay > MAX_TIMER_DELAY_MS) throw new Error("DataOps access token exp is outside the supported timer range");
141
+ return expiresAt * 1e3;
142
+ }
143
+ function parseAccountResponse(value) {
144
+ if (!value || typeof value !== "object") throw new Error("DataOps userinfo endpoint returned an invalid response");
145
+ const record = value;
146
+ if (typeof record.preferred_username !== "string" || typeof record.name !== "string" || typeof record.email !== "string") throw new Error("DataOps userinfo endpoint returned an invalid response");
147
+ return {
148
+ username: record.preferred_username,
149
+ displayName: record.name,
150
+ email: record.email
151
+ };
152
+ }
153
+ /**
154
+ * Compose the DataOps browser authorization routes and generic MCP child.
155
+ * @param ctx - Cordis context with credentials, Web routes, and tool registry.
156
+ * @param config - Standalone DataOps origin, credential references, and MCP settings.
157
+ * @returns Startup readiness after the persistent target and stored grant are accepted.
158
+ */
159
+ async function apply(ctx, config) {
160
+ const baseUrl = normalizeBaseUrl(config.baseUrl);
161
+ const callbackOrigin = config.callbackOrigin === void 0 ? void 0 : normalizeCallbackOrigin(config.callbackOrigin);
162
+ const accessRef = credentialRef(config.credentialRef);
163
+ const targetRefKey = credentialRef(config.targetCredentialRef);
164
+ const targetState = await ctx.credentials.describe(targetRefKey);
165
+ if (!targetState.configured) {
166
+ if (!targetState.writable) throw new Error("mcp-dataops: targetCredentialRef must be configured or use a writable credential source");
167
+ await ctx.credentials.set(targetRefKey, randomBytes(32).toString("base64url"));
168
+ }
169
+ const resolvedTarget = await ctx.credentials.resolve(targetRefKey);
170
+ if (resolvedTarget === void 0) throw new Error("mcp-dataops: target credential could not be resolved after initialization");
171
+ const targetRef = parseTargetRef(resolvedTarget.value);
172
+ const pending = /* @__PURE__ */ new Map();
173
+ const lifecycleAbort = new AbortController();
174
+ const activeOperations = /* @__PURE__ */ new Set();
175
+ let mcpFiber;
176
+ let grantGeneration = 0;
177
+ let accessExpiresAt = null;
178
+ const assertActive = () => {
179
+ if (lifecycleAbort.signal.aborted) throw new Error("mcp-dataops: plugin lifecycle is disposed");
180
+ };
181
+ const assertGrantCurrent = (expectedGeneration) => {
182
+ if (grantGeneration !== expectedGeneration) throw new Error("mcp-dataops: delegated grant operation was superseded");
183
+ };
184
+ const trackOperation = (operation) => {
185
+ activeOperations.add(operation);
186
+ operation.then(() => {
187
+ activeOperations.delete(operation);
188
+ }, () => {
189
+ activeOperations.delete(operation);
190
+ });
191
+ return operation;
192
+ };
193
+ const trackHandler = (handler) => (request, response) => trackOperation(handler(request, response));
194
+ const mcpConfig = () => ({
195
+ transport: "streamable-http",
196
+ serverName: config.serverName,
197
+ url: `${baseUrl}/api/ai/data-query/mcp`,
198
+ headers: {},
199
+ bearerTokenRef: accessRef,
200
+ toolCallTimeoutMs: config.toolCallTimeoutMs,
201
+ failOnStartupError: config.failOnStartupError
202
+ });
203
+ const isMcpMounted = () => mcpFiber !== void 0 && mcpFiber.uid !== null;
204
+ const ensureMcpMounted = async () => {
205
+ assertActive();
206
+ if (isMcpMounted()) return;
207
+ const fiber = ctx.plugin(McpClient, mcpConfig());
208
+ mcpFiber = fiber;
209
+ try {
210
+ await fiber.await();
211
+ assertActive();
212
+ } catch (error) {
213
+ ctx.logger.error("mcp-dataops: MCP client mount failed");
214
+ ctx.logger.error(error);
215
+ if (mcpFiber === fiber) mcpFiber = void 0;
216
+ try {
217
+ await fiber.dispose();
218
+ } catch (cleanupError) {
219
+ ctx.logger.error("mcp-dataops: MCP mount failure cleanup failed");
220
+ ctx.logger.error(cleanupError);
221
+ throw new AggregateError([error, cleanupError], "DataOps MCP mount and cleanup failed");
222
+ }
223
+ throw error;
224
+ }
225
+ };
226
+ const unmountMcp = async () => {
227
+ const fiber = mcpFiber;
228
+ mcpFiber = void 0;
229
+ if (fiber !== void 0 && fiber.uid !== null) await fiber.dispose();
230
+ };
231
+ ctx.effect(() => async () => {
232
+ pending.clear();
233
+ lifecycleAbort.abort();
234
+ await Promise.allSettled([...activeOperations]);
235
+ await unmountMcp();
236
+ }, "mcp-dataops: authorization lifecycle");
237
+ const credentialState = async () => {
238
+ const access = await ctx.credentials.describe(accessRef);
239
+ return {
240
+ configured: access.configured,
241
+ writable: access.writable
242
+ };
243
+ };
244
+ const fetchAccount = async (accessToken) => {
245
+ const response = await fetch(new URL("/api/auth/dsh/userinfo", baseUrl), {
246
+ headers: { authorization: `Bearer ${accessToken}` },
247
+ signal: lifecycleAbort.signal
248
+ });
249
+ if (response.status === 401 || response.status === 403) return null;
250
+ if (!response.ok) throw new Error(`DataOps userinfo lookup failed with HTTP ${String(response.status)}`);
251
+ return parseAccountResponse(await response.json());
252
+ };
253
+ const currentAccount = async () => {
254
+ const state = await credentialState();
255
+ if (!state.configured) return {
256
+ credential: state,
257
+ account: null,
258
+ authorizationAccepted: false
259
+ };
260
+ const resolved = await ctx.credentials.resolve(accessRef);
261
+ if (resolved === void 0) throw new Error("DataOps access credential is described as configured but cannot be resolved");
262
+ const account = await fetchAccount(resolved.value);
263
+ return {
264
+ credential: state,
265
+ account,
266
+ authorizationAccepted: account !== null && isMcpMounted()
267
+ };
268
+ };
269
+ const requestRevocation = async (token) => {
270
+ const response = await fetch(new URL("/api/auth/dsh/revoke", baseUrl), {
271
+ method: "POST",
272
+ headers: { "content-type": "application/x-www-form-urlencoded" },
273
+ body: new URLSearchParams({ token }),
274
+ signal: lifecycleAbort.signal
275
+ });
276
+ if (!response.ok) throw new Error(`DataOps token revocation failed with HTTP ${String(response.status)}`);
277
+ };
278
+ const revokeTokenResponse = (token) => requestRevocation(token.access_token);
279
+ const restoreCredential = async (ref, previous) => {
280
+ if (previous === void 0) await ctx.credentials.unset(ref);
281
+ else await ctx.credentials.set(ref, previous.value);
282
+ };
283
+ const storeAuthorization = async (token, expectedGeneration) => {
284
+ assertActive();
285
+ assertGrantCurrent(expectedGeneration);
286
+ const previousAccess = await ctx.credentials.resolve(accessRef);
287
+ const previousExpiresAt = accessExpiresAt;
288
+ assertActive();
289
+ assertGrantCurrent(expectedGeneration);
290
+ try {
291
+ await ctx.credentials.set(accessRef, token.access_token);
292
+ accessExpiresAt = accessTokenExpiry(token.access_token);
293
+ assertActive();
294
+ assertGrantCurrent(expectedGeneration);
295
+ } catch (error) {
296
+ ctx.logger.error("mcp-dataops: DataOps credential replacement failed");
297
+ ctx.logger.error(error);
298
+ try {
299
+ await restoreCredential(accessRef, previousAccess);
300
+ accessExpiresAt = previousExpiresAt;
301
+ } catch (rollbackError) {
302
+ ctx.logger.error("mcp-dataops: DataOps credential rollback failed");
303
+ ctx.logger.error(rollbackError);
304
+ throw new AggregateError([error, rollbackError], "DataOps credential replacement and rollback failed");
305
+ }
306
+ throw error;
307
+ }
308
+ return async () => {
309
+ await restoreCredential(accessRef, previousAccess);
310
+ accessExpiresAt = previousExpiresAt;
311
+ };
312
+ };
313
+ const acceptAuthorization = async (token, expectedGeneration) => {
314
+ const restore = await storeAuthorization(token, expectedGeneration);
315
+ try {
316
+ await ensureMcpMounted();
317
+ } catch (error) {
318
+ ctx.logger.error("mcp-dataops: DataOps authorization commit failed");
319
+ ctx.logger.error(error);
320
+ try {
321
+ await restore();
322
+ } catch (rollbackError) {
323
+ ctx.logger.error("mcp-dataops: DataOps authorization rollback failed");
324
+ ctx.logger.error(rollbackError);
325
+ throw new AggregateError([error, rollbackError], "DataOps authorization commit and rollback failed");
326
+ }
327
+ throw error;
328
+ }
329
+ };
330
+ if ((await credentialState()).configured) try {
331
+ const access = await ctx.credentials.resolve(accessRef);
332
+ if (access === void 0 || await fetchAccount(access.value) === null) throw new Error("Stored DataOps access credential was rejected by userinfo");
333
+ accessExpiresAt = accessTokenExpiry(access.value);
334
+ await ensureMcpMounted();
335
+ } catch (error) {
336
+ ctx.logger.warn("mcp-dataops: stored DataOps authorization could not be accepted");
337
+ ctx.logger.warn(error);
338
+ }
339
+ ctx.effect(() => ctx.webServer.register({
340
+ kind: "exact",
341
+ path: STATUS_PATH,
342
+ handler: trackHandler(async (request, response) => {
343
+ if (!requireConnection(ctx, request, response) || !requireLoopback(request, response) || !requireSameOriginBrowser(request, response) || !requireMethod(request, response, "GET")) return;
344
+ try {
345
+ const accountState = await currentAccount();
346
+ sendJson(response, 200, {
347
+ baseUrl,
348
+ serverName: config.serverName,
349
+ credentialConfigured: accountState.credential.configured,
350
+ credentialWritable: accountState.credential.writable,
351
+ authorizationAccepted: accountState.authorizationAccepted,
352
+ expiresAt: accountState.authorizationAccepted ? accessExpiresAt : null,
353
+ account: accountState.account
354
+ });
355
+ } catch (error) {
356
+ ctx.logger.warn("mcp-dataops: integration status lookup failed");
357
+ ctx.logger.warn(error);
358
+ sendJson(response, 502, { error: "Unable to read DataOps connection status." });
359
+ }
360
+ })
361
+ }), "mcp-dataops: status route");
362
+ ctx.effect(() => ctx.webServer.register({
363
+ kind: "exact",
364
+ path: CONNECT_PATH,
365
+ handler: trackHandler(async (request, response) => {
366
+ if (!requireConnection(ctx, request, response) || !requireLoopback(request, response) || !requireSameOriginBrowser(request, response) || !requireMethod(request, response, "GET")) return;
367
+ const state = await credentialState();
368
+ assertActive();
369
+ if (!state.writable) {
370
+ sendJson(response, 409, { error: "DataOps access credential must use a writable credential source." });
371
+ return;
372
+ }
373
+ pending.clear();
374
+ const browserOrigin = callbackOrigin ?? callbackOriginOf(request, response);
375
+ if (browserOrigin === void 0) return;
376
+ const stateValue = randomBytes(32).toString("base64url");
377
+ const verifier = randomBytes(32).toString("base64url");
378
+ const challenge = createHash("sha256").update(verifier, "ascii").digest("base64url");
379
+ const redirectUri = `${browserOrigin}${CALLBACK_PATH}`;
380
+ pending.set(stateValue, {
381
+ verifier,
382
+ redirectUri,
383
+ createdAt: Date.now()
384
+ });
385
+ const authorize = new URL("/api/auth/dsh/authorize", baseUrl);
386
+ authorize.searchParams.set("client_id", CLIENT_ID);
387
+ authorize.searchParams.set("target_ref", targetRef);
388
+ authorize.searchParams.set("redirect_uri", redirectUri);
389
+ authorize.searchParams.set("response_type", "code");
390
+ authorize.searchParams.set("state", stateValue);
391
+ authorize.searchParams.set("code_challenge", challenge);
392
+ authorize.searchParams.set("code_challenge_method", "S256");
393
+ authorize.searchParams.set("scope", SCOPE);
394
+ authorize.searchParams.set("prompt", "select_account");
395
+ response.writeHead(303, {
396
+ location: authorize.toString(),
397
+ "referrer-policy": "no-referrer"
398
+ });
399
+ response.end();
400
+ })
401
+ }), "mcp-dataops: connect route");
402
+ ctx.effect(() => ctx.webServer.register({
403
+ kind: "exact",
404
+ path: CALLBACK_PATH,
405
+ handler: trackHandler(async (request, response) => {
406
+ if (!requireConnection(ctx, request, response) || !requireLoopback(request, response) || !requireMethod(request, response, "GET")) return;
407
+ const callback = new URL(request.url ?? CALLBACK_PATH, "http://127.0.0.1");
408
+ const stateValue = callback.searchParams.get("state") ?? "";
409
+ const authorization = pending.get(stateValue);
410
+ pending.delete(stateValue);
411
+ if (authorization === void 0 || Date.now() - authorization.createdAt > PENDING_TTL_MS) {
412
+ ctx.logger.warn("mcp-dataops: authorization callback rejected missing or expired pending state");
413
+ popupBridge(response, "failed", "pending-state");
414
+ return;
415
+ }
416
+ if (callback.searchParams.get("error") !== null) {
417
+ popupBridge(response, "cancelled");
418
+ return;
419
+ }
420
+ const code = callback.searchParams.get("code") ?? "";
421
+ if (code === "") {
422
+ ctx.logger.warn("mcp-dataops: authorization callback rejected missing code");
423
+ popupBridge(response, "failed", "missing-code");
424
+ return;
425
+ }
426
+ const expectedGeneration = grantGeneration;
427
+ let token;
428
+ let failureReason = "token-service-failed";
429
+ try {
430
+ const tokenResponse = await fetch(new URL("/api/auth/dsh/token", baseUrl), {
431
+ method: "POST",
432
+ headers: { "content-type": "application/x-www-form-urlencoded" },
433
+ body: new URLSearchParams({
434
+ grant_type: "authorization_code",
435
+ code,
436
+ client_id: CLIENT_ID,
437
+ redirect_uri: authorization.redirectUri,
438
+ code_verifier: authorization.verifier
439
+ }),
440
+ signal: lifecycleAbort.signal
441
+ });
442
+ if (!tokenResponse.ok) {
443
+ if (tokenResponse.status === 400) failureReason = "token-request-rejected";
444
+ else if (tokenResponse.status === 401 || tokenResponse.status === 403) failureReason = "token-account-rejected";
445
+ else failureReason = "token-service-failed";
446
+ throw new Error(`DataOps token exchange failed with HTTP ${String(tokenResponse.status)}`);
447
+ }
448
+ failureReason = "token-response-invalid";
449
+ token = parseTokenResponse(await tokenResponse.json());
450
+ failureReason = "account-verification";
451
+ if (await fetchAccount(token.access_token) === null) throw new Error("DataOps access token was rejected by userinfo");
452
+ failureReason = "authorization-activation";
453
+ await acceptAuthorization(token, expectedGeneration);
454
+ } catch (error) {
455
+ ctx.logger.warn("mcp-dataops: DataOps authorization callback failed");
456
+ ctx.logger.warn(error);
457
+ if (token !== void 0) try {
458
+ await revokeTokenResponse(token);
459
+ } catch (revocationError) {
460
+ ctx.logger.warn("mcp-dataops: rejected DataOps authorization grant revocation failed");
461
+ ctx.logger.warn(revocationError);
462
+ }
463
+ popupBridge(response, "failed", failureReason);
464
+ return;
465
+ }
466
+ popupBridge(response, "connected");
467
+ })
468
+ }), "mcp-dataops: callback route");
469
+ ctx.effect(() => ctx.webServer.register({
470
+ kind: "exact",
471
+ path: DISCONNECT_PATH,
472
+ handler: trackHandler(async (request, response) => {
473
+ if (!requireConnection(ctx, request, response) || !requireLoopback(request, response) || !requireSameOriginBrowser(request, response) || !requireMethod(request, response, "POST")) return;
474
+ const state = await credentialState();
475
+ assertActive();
476
+ if (!state.writable) {
477
+ sendJson(response, 409, { error: "DataOps access credential must use a writable credential source." });
478
+ return;
479
+ }
480
+ grantGeneration += 1;
481
+ pending.clear();
482
+ try {
483
+ const access = await ctx.credentials.resolve(accessRef);
484
+ assertActive();
485
+ await unmountMcp();
486
+ if (access !== void 0) await requestRevocation(access.value);
487
+ await ctx.credentials.unset(accessRef);
488
+ accessExpiresAt = null;
489
+ sendJson(response, 200, { disconnected: true });
490
+ } catch (error) {
491
+ ctx.logger.warn("mcp-dataops: DataOps disconnect failed");
492
+ ctx.logger.warn(error);
493
+ sendJson(response, 502, { error: "Unable to disconnect DataOps." });
494
+ }
495
+ })
496
+ }), "mcp-dataops: disconnect route");
497
+ }
498
+ //#endregion
499
+ export { Config, apply, inject, name };
@@ -0,0 +1,16 @@
1
+ //#region lib/types/invariant.js
2
+ const PACKAGE_NAME = "@sparkelf/dsh-plugin-dataops";
3
+ /** Cordis plugin name for the DataOps invariant companion. */
4
+ const name = "mcp-dataops-invariant";
5
+ /** Services required to register the package invariant installer. */
6
+ const inject = ["invariants"];
7
+ /** No runtime invariant: the package owns browser OAuth handoff and a child mcp-client fiber, not a separate durable projection. */
8
+ const install = () => {};
9
+ /**
10
+ * Register the package's explained empty invariant installer.
11
+ * @param ctx - Cordis context with the invariant registry.
12
+ * @returns The registration disposer.
13
+ */
14
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
15
+ //#endregion
16
+ export { apply, inject, name };
@@ -0,0 +1,12 @@
1
+ import type { PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots';
2
+ import type { DataOpsInjectedProps } from './contract.ts';
3
+ import { createDataOpsStore } from './store.ts';
4
+ /** Full props for the frame-wide DataOps authorization prompt. */
5
+ export type DataOpsExpiryModalProps = PropsRuntime<'shell.overlay'> & PropsStore<ReturnType<typeof createDataOpsStore>> & DataOpsInjectedProps;
6
+ /**
7
+ * Render the frame-wide expired-sign-in prompt and own its browser lifecycle.
8
+ * @param props - Root-scoped overlay owner, shared store, and injected callbacks.
9
+ * @returns The modal contribution, or nothing before injection or expiry.
10
+ */
11
+ export declare function DataOpsExpiryModal(props: DataOpsExpiryModalProps): import("react").JSX.Element | null;
12
+ //# sourceMappingURL=DataOpsExpiryModal.d.ts.map
@@ -0,0 +1,12 @@
1
+ import type { PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots';
2
+ import type { DataOpsInjectedProps } from './contract.ts';
3
+ import { createDataOpsStore } from './store.ts';
4
+ /** Full props for the DataOps Settings contribution. */
5
+ export type DataOpsSectionProps = PropsRuntime<'settings.section'> & PropsStore<ReturnType<typeof createDataOpsStore>> & DataOpsInjectedProps;
6
+ /**
7
+ * Render the DataOps connection state and authorization controls.
8
+ * @param props - Root-scoped Settings owner, store, and injected callbacks.
9
+ * @returns The localized Settings section, or nothing before injection.
10
+ */
11
+ export declare function DataOpsSection(props: DataOpsSectionProps): import("react").JSX.Element | null;
12
+ //# sourceMappingURL=DataOpsSection.d.ts.map
@@ -0,0 +1,18 @@
1
+ import type { InjectFace } from '@deepseek-ai/dsh-client-ui-slots';
2
+ import type { en } from './locales.ts';
3
+ /** Callbacks and localized copy shared by both DataOps Client entries. */
4
+ export interface DataOpsClientInjected {
5
+ /** Translate one DataOps UI message key. */
6
+ t: (key: keyof typeof en) => string;
7
+ /** Start the always-mounted status and OAuth browser lifecycle. */
8
+ start: () => () => void;
9
+ /** Refresh the authoritative Host status. */
10
+ reload: () => void;
11
+ /** Open the real DataOps OAuth popup from a user gesture. */
12
+ openAuthorization: () => void;
13
+ /** Disconnect the delegated grant and report whether it completed. */
14
+ disconnect: () => Promise<boolean>;
15
+ }
16
+ /** Optional injected face while the slot registration is activating. */
17
+ export type DataOpsInjectedProps = Partial<InjectFace<DataOpsClientInjected>>;
18
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1,33 @@
1
+ import type { DataOpsActions } from './store.ts';
2
+ /** Own browser status refresh, OAuth popup handoff, and expiry notification timing. */
3
+ export declare class DataOpsController {
4
+ private actions;
5
+ private authorizationPopup;
6
+ private expiryTimer;
7
+ /**
8
+ * Attach the root-scoped store actions supplied by a slot registration.
9
+ * @param actions - Bound writes for the shared DataOps Client store.
10
+ */
11
+ attach(actions: DataOpsActions): void;
12
+ private writes;
13
+ private clearExpiryTimer;
14
+ private scheduleExpiry;
15
+ /**
16
+ * Refresh the authoritative Host projection and schedule its known expiry.
17
+ * @returns A promise settled after the shared status reflects the response.
18
+ */
19
+ load(): Promise<void>;
20
+ /**
21
+ * Start the always-mounted browser lifecycle.
22
+ * @returns A disposer for browser listeners, popup ownership, and expiry timing.
23
+ */
24
+ start(): () => void;
25
+ /** Open the real DataOps authorization handoff from a user gesture. */
26
+ openAuthorization(): void;
27
+ /**
28
+ * Revoke the current grant and refresh shared state.
29
+ * @returns Whether the disconnect completed and Settings may collapse confirmation.
30
+ */
31
+ disconnect(): Promise<boolean>;
32
+ }
33
+ //# sourceMappingURL=controller.d.ts.map
@@ -0,0 +1,12 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { type DataOpsKey } from './locales.ts';
3
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
4
+ interface LocaleNamespaceMap {
5
+ 'settings.dataops': DataOpsKey;
6
+ }
7
+ }
8
+ /** Client services required for the Settings and frame-overlay contributions. */
9
+ export declare const inject: string[];
10
+ /** Register the shared DataOps lifecycle, Settings section, and expiry modal. */
11
+ export declare function apply(ctx: Context): void;
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,39 @@
1
+ /** English DataOps Settings copy. */
2
+ export declare const en: {
3
+ readonly nav: "DataOps";
4
+ readonly title: "DataOps";
5
+ readonly close: "Close";
6
+ readonly later: "Later";
7
+ readonly connected: "Connected";
8
+ readonly loginExpired: "Sign-in expired";
9
+ readonly loginExpiredHint: "Sign in again to continue using DataOps tools.";
10
+ readonly notConnected: "Not connected";
11
+ readonly connectionFailed: "Connection failed";
12
+ readonly managedByAdministrator: "Managed by administrator";
13
+ readonly connect: "Connect DataOps";
14
+ readonly reauthorize: "Authorize again";
15
+ readonly signInAgain: "Sign in again";
16
+ readonly disconnect: "Disconnect";
17
+ readonly confirmDisconnect: "Disconnect this DataOps account?";
18
+ readonly confirmDisconnectDetail: "You’ll need to connect DataOps again before using this account.";
19
+ readonly keepConnected: "Keep connected";
20
+ readonly confirm: "Disconnect";
21
+ readonly retry: "Retry";
22
+ readonly loading: "Checking connection…";
23
+ readonly popupBlocked: "The authorization window could not be opened. Allow pop-ups for this page and try again.";
24
+ readonly connectFailed: "DataOps connection failed. Try again.";
25
+ readonly authorizationExpired: "The DataOps authorization request expired. Connect again.";
26
+ readonly authorizationResponseInvalid: "DataOps returned an incomplete authorization response. Connect again.";
27
+ readonly tokenRequestRejected: "DataOps rejected the authorization request. Connect again.";
28
+ readonly tokenAccountRejected: "DataOps rejected the selected account authorization. Sign in again.";
29
+ readonly tokenServiceFailed: "The DataOps authorization service failed. Try again.";
30
+ readonly tokenResponseInvalid: "DataOps returned an invalid token response. Connect again.";
31
+ readonly accountVerificationFailed: "The authorized DataOps account could not be verified. Try again.";
32
+ readonly authorizationActivationFailed: "The DataOps authorization could not be activated. Try again.";
33
+ readonly disconnectFailed: "Unable to disconnect DataOps. Try again.";
34
+ };
35
+ /** Stable key set shared by every DataOps Settings locale. */
36
+ export type DataOpsKey = keyof typeof en;
37
+ /** Chinese DataOps Settings copy. */
38
+ export declare const zh: Record<DataOpsKey, string>;
39
+ //# sourceMappingURL=locales.d.ts.map