@xpert-ai/plugin-live-dashboard 0.0.0-stage → 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.
- package/.xpertai-plugin/plugin.json +8 -0
- package/README.md +108 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +11 -0
- package/dist/lib/agent.schema.d.ts +568 -0
- package/dist/lib/agent.schema.js +52 -0
- package/dist/lib/dashboard-icon.d.ts +5 -0
- package/dist/lib/dashboard-icon.js +8 -0
- package/dist/lib/dashboard-navigation.d.ts +38 -0
- package/dist/lib/dashboard-navigation.js +18 -0
- package/dist/lib/dashboard-queries.d.ts +8 -0
- package/dist/lib/dashboard-queries.js +39 -0
- package/dist/lib/dashboard-receipt.d.ts +11 -0
- package/dist/lib/dashboard-receipt.js +35 -0
- package/dist/lib/dashboard.schema.d.ts +816 -0
- package/dist/lib/dashboard.schema.js +74 -0
- package/dist/lib/dashboard.service.d.ts +243 -0
- package/dist/lib/dashboard.service.js +185 -0
- package/dist/lib/document-libraries.d.ts +8 -0
- package/dist/lib/document-libraries.js +41 -0
- package/dist/lib/document-package.d.ts +41 -0
- package/dist/lib/document-package.js +25 -0
- package/dist/lib/editing.d.ts +403 -0
- package/dist/lib/editing.js +114 -0
- package/dist/lib/filter-evidence.d.ts +71 -0
- package/dist/lib/filter-evidence.js +60 -0
- package/dist/lib/html-guidance.d.ts +1 -0
- package/dist/lib/html-guidance.js +4 -0
- package/dist/lib/html-validation.d.ts +19 -0
- package/dist/lib/html-validation.js +136 -0
- package/dist/lib/middleware.d.ts +10 -0
- package/dist/lib/middleware.js +225 -0
- package/dist/lib/plugin.d.ts +2 -0
- package/dist/lib/plugin.js +22 -0
- package/dist/lib/presentation.provider.d.ts +122 -0
- package/dist/lib/presentation.provider.js +72 -0
- package/dist/lib/remote-entry.d.ts +4 -0
- package/dist/lib/remote-entry.js +12 -0
- package/dist/lib/renderer.d.ts +58 -0
- package/dist/lib/renderer.js +32 -0
- package/dist/lib/resource-card.d.ts +21 -0
- package/dist/lib/resource-card.js +33 -0
- package/dist/lib/save-evidence.d.ts +24 -0
- package/dist/lib/save-evidence.js +99 -0
- package/dist/lib/sharing.service.d.ts +93 -0
- package/dist/lib/sharing.service.js +71 -0
- package/dist/lib/view.provider.d.ts +235 -0
- package/dist/lib/view.provider.js +103 -0
- package/dist/libraries/ECHARTS-LICENSE.txt +222 -0
- package/dist/libraries/ECHARTS-NOTICE.txt +5 -0
- package/dist/libraries/THREE-LICENSE.txt +21 -0
- package/dist/libraries/catalog.json +1 -0
- package/dist/remote/app.css +1 -0
- package/dist/remote/app.js +253 -0
- package/dist/remote/build-manifest.json +63 -0
- package/dist/remote/renderer-source.json +5 -0
- package/dist/remote/renderer.html +4653 -0
- package/dist/sandbox-actions/action.json +10 -0
- package/dist/sandbox-actions/bundle/host.js +149 -0
- package/dist/sandbox-actions/bundle/runner.mjs +15 -0
- package/dist/sandbox-actions/bundle/validate.mjs +140 -0
- package/dist/validation/host-entry.d.ts +1 -0
- package/dist/validation/host-entry.js +6 -0
- package/docs/AGENTS.md +9 -0
- package/docs/acceptance.md +89 -0
- package/docs/agent-acceptance.md +96 -0
- package/docs/chat-resource-cards.mdx +60 -0
- package/docs/docs.json +49 -0
- package/docs/drafts-acceptance.mdx +42 -0
- package/docs/drafts.mdx +35 -0
- package/docs/editing.mdx +63 -0
- package/docs/evolution-acceptance.mdx +98 -0
- package/docs/file-authoring-acceptance.mdx +62 -0
- package/docs/file-authoring.mdx +49 -0
- package/docs/file-datasets-acceptance.mdx +65 -0
- package/docs/fixed-build-multimodel-acceptance.mdx +129 -0
- package/docs/frontend-libraries-acceptance.mdx +86 -0
- package/docs/frontend-libraries.mdx +57 -0
- package/docs/html-acceptance.md +86 -0
- package/docs/html-mode.md +76 -0
- package/docs/index.mdx +47 -0
- package/docs/retry-regression-acceptance.mdx +112 -0
- package/docs/team-sharing-acceptance.mdx +66 -0
- package/docs/team-sharing.mdx +44 -0
- package/docs/verification.mdx +91 -0
- package/examples/assistant-middleware.yaml +28 -0
- package/examples/data-xpert-sales.json +118 -0
- package/examples/postgres-sales.json +93 -0
- package/index.cjs +1 -0
- package/package.json +73 -3
- package/skills/live-dashboard/SKILL.md +71 -0
- package/skills/live-dashboard/agents/xpertai.yaml +21 -0
- package/skills/live-dashboard/references/data-validation.md +15 -0
- package/skills/live-dashboard/references/html-lifecycle.md +129 -0
- package/skills/live-dashboard/references/html-runtime.md +53 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.DashboardPresentationProvider = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const node_crypto_1 = require("node:crypto");
|
|
18
|
+
const plugin_sdk_1 = require("@xpert-ai/plugin-sdk");
|
|
19
|
+
const dashboard_service_1 = require("./dashboard.service");
|
|
20
|
+
const document_package_1 = require("./document-package");
|
|
21
|
+
const remote_entry_1 = require("./remote-entry");
|
|
22
|
+
const dashboard_schema_1 = require("./dashboard.schema");
|
|
23
|
+
const dashboard_queries_1 = require("./dashboard-queries");
|
|
24
|
+
const renderer_1 = require("./renderer");
|
|
25
|
+
let DashboardPresentationProvider = class DashboardPresentationProvider {
|
|
26
|
+
constructor(runtime) {
|
|
27
|
+
this.runtime = runtime;
|
|
28
|
+
this.operations = ['query', 'query-binding'];
|
|
29
|
+
}
|
|
30
|
+
getEntry() { return (0, remote_entry_1.dashboardRemoteEntry)(true); }
|
|
31
|
+
async read(context) {
|
|
32
|
+
const saved = this.open(context);
|
|
33
|
+
return { meta: { saved, dashboards: [{ artifactId: saved.artifactId, versionId: saved.versionId, title: saved.title }], sources: [], versions: [] } };
|
|
34
|
+
}
|
|
35
|
+
async query(context, request) {
|
|
36
|
+
const saved = this.open(context);
|
|
37
|
+
const parsed = (request.operation === 'query-binding' ? dashboard_schema_1.queryBindingSchema : dashboard_schema_1.querySchema).safeParse(request.input);
|
|
38
|
+
if (!parsed.success)
|
|
39
|
+
throw new common_1.BadRequestException('DASHBOARD_INVALID_QUERY');
|
|
40
|
+
const input = parsed.data;
|
|
41
|
+
if (input.artifactId !== saved.artifactId || input.versionId !== saved.versionId || input.draftRevision !== undefined)
|
|
42
|
+
throw new common_1.ForbiddenException('DASHBOARD_SHARED_VERSION_REQUIRED');
|
|
43
|
+
if (request.operation === 'query-binding' && 'bindingId' in input && typeof input.bindingId === 'string')
|
|
44
|
+
return (0, dashboard_queries_1.queryDashboardBinding)(this.runtime, context.actor, saved.manifest, input.bindingId, input.controls);
|
|
45
|
+
if (request.operation !== 'query')
|
|
46
|
+
throw new common_1.ForbiddenException('DASHBOARD_ACTION_UNSUPPORTED');
|
|
47
|
+
const datasets = await (0, dashboard_queries_1.queryDashboard)(this.runtime, context.actor, saved.manifest, input.controls);
|
|
48
|
+
return { datasets, ...(saved.manifest.presentation ? {} : { payload: (0, renderer_1.toDataAnalyticsPayload)(saved.manifest, datasets) }) };
|
|
49
|
+
}
|
|
50
|
+
open(context) {
|
|
51
|
+
const { artifact, version, content } = context;
|
|
52
|
+
if (artifact.pluginName !== dashboard_service_1.PLUGIN_NAME || artifact.resourceType !== 'live-dashboard' || artifact.id !== version.artifactId ||
|
|
53
|
+
artifact.tenantId !== context.actor.tenantId || artifact.organizationId !== context.actor.organizationId)
|
|
54
|
+
throw new common_1.ForbiddenException('DASHBOARD_ACCESS_DENIED');
|
|
55
|
+
if (version.mimeType !== 'application/json' || content.length > document_package_1.MAX_DASHBOARD_PACKAGE_BYTES)
|
|
56
|
+
throw new common_1.BadRequestException('DASHBOARD_VERSION_UNAVAILABLE');
|
|
57
|
+
if (!version.sha256 || (0, node_crypto_1.createHash)('sha256').update(content).digest('hex') !== version.sha256)
|
|
58
|
+
throw new common_1.BadRequestException('DASHBOARD_VERSION_CONTENT_CHANGED');
|
|
59
|
+
const { manifest, dependencies } = (0, document_package_1.decodeDashboardPackage)(content);
|
|
60
|
+
const prefix = artifact.projectId ? `project/${artifact.projectId}/` : artifact.xpertId ? `agent/${artifact.xpertId}/` : null;
|
|
61
|
+
if (!prefix || artifact.resourceId !== `${prefix}${manifest.id}`)
|
|
62
|
+
throw new common_1.BadRequestException('DASHBOARD_IDENTITY_MISMATCH');
|
|
63
|
+
return { artifactId: artifact.id, versionId: version.id, versionNumber: version.versionNumber,
|
|
64
|
+
title: version.title ?? manifest.title, isDraft: false, manifest, dependencies };
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
exports.DashboardPresentationProvider = DashboardPresentationProvider;
|
|
68
|
+
exports.DashboardPresentationProvider = DashboardPresentationProvider = __decorate([
|
|
69
|
+
(0, plugin_sdk_1.ArtifactPresentationProvider)({ pluginName: dashboard_service_1.PLUGIN_NAME, resourceType: 'live-dashboard' }),
|
|
70
|
+
__param(0, (0, common_1.Inject)(plugin_sdk_1.XPERT_RUNTIME_CAPABILITIES_TOKEN)),
|
|
71
|
+
__metadata("design:paramtypes", [Object])
|
|
72
|
+
], DashboardPresentationProvider);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dashboardRemoteEntry = dashboardRemoteEntry;
|
|
4
|
+
const promises_1 = require("node:fs/promises");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const plugin_sdk_1 = require("@xpert-ai/plugin-sdk");
|
|
7
|
+
async function dashboardRemoteEntry(readOnly = false) {
|
|
8
|
+
const [appScript, appCss, renderer] = await Promise.all([(0, promises_1.readFile)((0, node_path_1.join)(__dirname, '../remote/app.js'), 'utf8'), (0, promises_1.readFile)((0, node_path_1.join)(__dirname, '../remote/app.css'), 'utf8'), (0, promises_1.readFile)((0, node_path_1.join)(__dirname, '../remote/renderer.html'), 'utf8')]);
|
|
9
|
+
return { html: (0, plugin_sdk_1.renderRemoteReactIframeHtml)({ title: 'Live dashboards', reactUmd: '', reactDomUmd: '',
|
|
10
|
+
appScript: `window.__DASHBOARD_READ_ONLY__=${readOnly};window.__ANALYTICS_RENDERER_HTML__=${JSON.stringify(renderer).replace(/</g, '\\u003c')};\n${appScript}`, appCss }),
|
|
11
|
+
contentType: 'text/html; charset=utf-8' };
|
|
12
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { AnalyticalDataset } from '@xpert-ai/contracts';
|
|
2
|
+
import type { DashboardManifest } from './dashboard.schema';
|
|
3
|
+
/** The only Data Analytics v1 translation. Providers never see chart encoding syntax. */
|
|
4
|
+
export declare function toDataAnalyticsPayload(manifest: DashboardManifest, datasets: AnalyticalDataset[]): {
|
|
5
|
+
ok: boolean;
|
|
6
|
+
widget_type: string;
|
|
7
|
+
surface: string;
|
|
8
|
+
sources: never[];
|
|
9
|
+
manifest: {
|
|
10
|
+
version: number;
|
|
11
|
+
surface: string;
|
|
12
|
+
title: string;
|
|
13
|
+
cards: never[];
|
|
14
|
+
filters: never[];
|
|
15
|
+
sources: never[];
|
|
16
|
+
charts: {
|
|
17
|
+
id: string;
|
|
18
|
+
title: string;
|
|
19
|
+
type: "bar" | "line" | "area" | "pie" | "scatter";
|
|
20
|
+
dataset: string;
|
|
21
|
+
encodings: {
|
|
22
|
+
color?: {
|
|
23
|
+
field: string;
|
|
24
|
+
} | undefined;
|
|
25
|
+
x: {
|
|
26
|
+
field: string;
|
|
27
|
+
};
|
|
28
|
+
y: {
|
|
29
|
+
field: string;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
}[];
|
|
33
|
+
tables: {
|
|
34
|
+
id: string;
|
|
35
|
+
title: string;
|
|
36
|
+
dataset: string;
|
|
37
|
+
columns: {
|
|
38
|
+
field: string;
|
|
39
|
+
label: string;
|
|
40
|
+
}[];
|
|
41
|
+
}[];
|
|
42
|
+
blocks: ({
|
|
43
|
+
id: string;
|
|
44
|
+
type: string;
|
|
45
|
+
chartId: string;
|
|
46
|
+
} | {
|
|
47
|
+
id: string;
|
|
48
|
+
type: string;
|
|
49
|
+
tableId: string;
|
|
50
|
+
})[];
|
|
51
|
+
};
|
|
52
|
+
snapshot: {
|
|
53
|
+
version: number;
|
|
54
|
+
generatedAt: string;
|
|
55
|
+
status: string;
|
|
56
|
+
datasets: any;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toDataAnalyticsPayload = toDataAnalyticsPayload;
|
|
4
|
+
/** The only Data Analytics v1 translation. Providers never see chart encoding syntax. */
|
|
5
|
+
function toDataAnalyticsPayload(manifest, datasets) {
|
|
6
|
+
const snapshot = Object.fromEntries(datasets.flatMap((dataset) => [
|
|
7
|
+
[dataset.datasetId, dataset.rows],
|
|
8
|
+
...(!manifest.charts.some((chart) => chart.datasetId === dataset.datasetId) ? [] : [[`__plot.${dataset.datasetId}`, dataset.rows.map((row) => Object.fromEntries(dataset.fields.map((field) => [field.id, manifest.charts.some((chart) => chart.datasetId === dataset.datasetId && [chart.x, chart.y].includes(field.id)) ? renderValue(row[field.id], field.logicalType) : row[field.id]])))]])
|
|
9
|
+
]));
|
|
10
|
+
const payload = {
|
|
11
|
+
ok: true, widget_type: 'artifact', surface: 'dashboard', sources: [],
|
|
12
|
+
manifest: { version: 1, surface: 'dashboard', title: manifest.title, cards: [], filters: [], sources: [],
|
|
13
|
+
charts: manifest.charts.map((chart) => ({ id: chart.id, title: chart.title, type: chart.type, dataset: `__plot.${chart.datasetId}`,
|
|
14
|
+
encodings: { x: { field: chart.x }, y: { field: chart.y }, ...(chart.color ? { color: { field: chart.color } } : {}) } })),
|
|
15
|
+
tables: manifest.tables.map((table) => ({ id: table.id, title: table.title, dataset: table.datasetId,
|
|
16
|
+
columns: table.fields.map((id) => ({ field: id, label: datasets.find((dataset) => dataset.datasetId === table.datasetId)?.fields.find((field) => field.id === id)?.label ?? id })) })),
|
|
17
|
+
blocks: [...manifest.charts.map((chart) => ({ id: `chart.${chart.id}`, type: 'chart', chartId: chart.id })),
|
|
18
|
+
...manifest.tables.map((table) => ({ id: `table.${table.id}`, type: 'table', tableId: table.id }))] },
|
|
19
|
+
snapshot: { version: 1, generatedAt: new Date().toISOString(), status: datasets.some((dataset) => dataset.completeness.truncated) ? 'partial' : 'ready', datasets: snapshot }
|
|
20
|
+
};
|
|
21
|
+
if (new TextEncoder().encode(JSON.stringify(payload)).byteLength > 3_000_000)
|
|
22
|
+
throw new Error('DASHBOARD_RESULT_TOO_LARGE');
|
|
23
|
+
return payload;
|
|
24
|
+
}
|
|
25
|
+
function renderValue(value, type) {
|
|
26
|
+
if (value === null || (type !== 'int64' && type !== 'decimal'))
|
|
27
|
+
return value;
|
|
28
|
+
// Dataset retains exact strings. Refuse values that the existing numeric renderer cannot represent safely.
|
|
29
|
+
if (typeof value !== 'string' || !Number.isFinite(Number(value)) || Math.abs(Number(value)) > Number.MAX_SAFE_INTEGER || value.replace(/[-.]/g, '').replace(/^0+/, '').length > 15)
|
|
30
|
+
throw new Error('DASHBOARD_NUMERIC_PRECISION_UNSUPPORTED');
|
|
31
|
+
return Number(value);
|
|
32
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
2
|
+
import type { ConversationResourceCard, XpertResolvedViewHostContext } from '@xpert-ai/contracts';
|
|
3
|
+
type DashboardCard = {
|
|
4
|
+
artifactId: string;
|
|
5
|
+
title: string;
|
|
6
|
+
} & ({
|
|
7
|
+
state: 'draft';
|
|
8
|
+
revision: number;
|
|
9
|
+
} | {
|
|
10
|
+
state: 'published';
|
|
11
|
+
versionNumber: number;
|
|
12
|
+
versionId?: string;
|
|
13
|
+
});
|
|
14
|
+
/** Snapshot only. Opening a draft resolves current state; explicit historical reads pin a version. */
|
|
15
|
+
export declare function dashboardResourceCard(scope: XpertResolvedViewHostContext, saved: DashboardCard): ConversationResourceCard;
|
|
16
|
+
export declare function emitDashboardCard(scope: XpertResolvedViewHostContext, saved: DashboardCard, config?: RunnableConfig): Promise<{
|
|
17
|
+
status: "emitted";
|
|
18
|
+
} | {
|
|
19
|
+
status: "unavailable";
|
|
20
|
+
}>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dashboardResourceCard = dashboardResourceCard;
|
|
4
|
+
exports.emitDashboardCard = emitDashboardCard;
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
const plugin_sdk_1 = require("@xpert-ai/plugin-sdk");
|
|
7
|
+
const dashboard_navigation_1 = require("./dashboard-navigation");
|
|
8
|
+
const dashboard_icon_1 = require("./dashboard-icon");
|
|
9
|
+
const logger = new common_1.Logger('DashboardResourceCard');
|
|
10
|
+
/** Snapshot only. Opening a draft resolves current state; explicit historical reads pin a version. */
|
|
11
|
+
function dashboardResourceCard(scope, saved) {
|
|
12
|
+
return {
|
|
13
|
+
resource: { namespace: 'live_dashboard', type: 'live-dashboard', id: saved.artifactId, artifactId: saved.artifactId },
|
|
14
|
+
title: saved.title,
|
|
15
|
+
description: saved.state === 'draft' ? `Dashboard · Draft r${saved.revision}` : `Dashboard · v${saved.versionNumber}`,
|
|
16
|
+
icon: dashboard_icon_1.dashboardIcon,
|
|
17
|
+
open: {
|
|
18
|
+
...(scope.hostType === 'project' ? { target: 'assistant.project', projectId: scope.hostId } : { target: 'workbench.view' }),
|
|
19
|
+
viewKey: dashboard_navigation_1.DASHBOARD_VIEW_KEY, selectionId: saved.artifactId,
|
|
20
|
+
...(saved.state === 'published' && saved.versionId ? { parameters: { versionId: saved.versionId } } : {})
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
async function emitDashboardCard(scope, saved, config) {
|
|
25
|
+
try {
|
|
26
|
+
await (0, plugin_sdk_1.emitResourceCard)(dashboardResourceCard(scope, saved), config);
|
|
27
|
+
return { status: 'emitted' };
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
logger.warn('Dashboard operation succeeded; resource card delivery failed');
|
|
31
|
+
return { status: 'unavailable' };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type WorkspaceFilesApi } from '@xpert-ai/plugin-sdk';
|
|
2
|
+
type SaveAttempt = {
|
|
3
|
+
token: number;
|
|
4
|
+
generations: Map<string, number>;
|
|
5
|
+
supersedesPaths?: string[];
|
|
6
|
+
confirmation?: 'source' | 'draft';
|
|
7
|
+
};
|
|
8
|
+
/** A domain receipt covers only the exact edited file bytes consumed by that save. */
|
|
9
|
+
export declare function dashboardSaveEvidence(files: () => WorkspaceFilesApi): {
|
|
10
|
+
opened(path: string, artifactId: string): void;
|
|
11
|
+
has(path: string): boolean;
|
|
12
|
+
write(path: string): void;
|
|
13
|
+
trackWrite<T>(path: string, run: () => Promise<T>): Promise<T>;
|
|
14
|
+
beginSave(supersedesPaths?: string[], confirmation?: "source" | "draft"): {
|
|
15
|
+
token: number;
|
|
16
|
+
generations: Map<string, number>;
|
|
17
|
+
supersedesPaths: string[];
|
|
18
|
+
confirmation: "source" | "draft";
|
|
19
|
+
};
|
|
20
|
+
accept(save: SaveAttempt, output: unknown): Promise<void>;
|
|
21
|
+
readonly pending: boolean;
|
|
22
|
+
readonly pendingPaths: string[];
|
|
23
|
+
};
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dashboardSaveEvidence = dashboardSaveEvidence;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const plugin_sdk_1 = require("@xpert-ai/plugin-sdk");
|
|
6
|
+
const dashboard_receipt_1 = require("./dashboard-receipt");
|
|
7
|
+
/** A domain receipt covers only the exact edited file bytes consumed by that save. */
|
|
8
|
+
function dashboardSaveEvidence(files) {
|
|
9
|
+
const saves = (0, plugin_sdk_1.createMutationEvidence)();
|
|
10
|
+
const edits = new Map();
|
|
11
|
+
const activeWrites = new Map();
|
|
12
|
+
const abandonCandidates = (save, savedPath) => {
|
|
13
|
+
for (const path of save.supersedesPaths ?? []) {
|
|
14
|
+
const candidate = edits.get(path);
|
|
15
|
+
if (path !== savedPath && candidate && !candidate.artifactId && !activeWrites.has(path) && candidate.generation === save.generations.get(path))
|
|
16
|
+
candidate.pending = false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const fingerprint = async (path) => {
|
|
20
|
+
try {
|
|
21
|
+
return (0, node_crypto_1.createHash)('sha256').update((await files().readRuntimeBuffer(path)).buffer).digest('hex');
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
} // An unreadable file cannot establish that a mutation was a no-op.
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
opened(path, artifactId) { edits.set(path, { artifactId, generation: 0, pending: false }); },
|
|
29
|
+
has(path) { return edits.has(path); },
|
|
30
|
+
write(path) {
|
|
31
|
+
const edit = edits.get(path) ?? { generation: 0, pending: false };
|
|
32
|
+
edits.set(path, { ...edit, generation: edit.generation + 1, pending: true });
|
|
33
|
+
},
|
|
34
|
+
async trackWrite(path, run) {
|
|
35
|
+
// A receipt cannot confirm bytes while any writer may still change this path.
|
|
36
|
+
activeWrites.set(path, (activeWrites.get(path) ?? 0) + 1);
|
|
37
|
+
try {
|
|
38
|
+
const before = await fingerprint(path), previous = edits.get(path);
|
|
39
|
+
const edit = { ...previous, generation: (previous?.generation ?? 0) + 1, pending: true };
|
|
40
|
+
edits.set(path, edit);
|
|
41
|
+
try {
|
|
42
|
+
return await run();
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
// File tools may return a domain error with status=success. Verify bytes rather than
|
|
46
|
+
// guessing from tool names/error text; preserve earlier or concurrent unsaved edits.
|
|
47
|
+
if (before !== undefined && before === await fingerprint(path) && edits.get(path) === edit) {
|
|
48
|
+
if (previous)
|
|
49
|
+
edits.set(path, previous);
|
|
50
|
+
else
|
|
51
|
+
edits.delete(path);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
const remaining = activeWrites.get(path) - 1;
|
|
57
|
+
if (remaining)
|
|
58
|
+
activeWrites.set(path, remaining);
|
|
59
|
+
else
|
|
60
|
+
activeWrites.delete(path);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
beginSave(supersedesPaths = [], confirmation = 'source') {
|
|
64
|
+
for (const path of supersedesPaths) {
|
|
65
|
+
const edit = edits.get(path);
|
|
66
|
+
if (!edit?.pending || edit.artifactId || activeWrites.has(path))
|
|
67
|
+
throw new Error('DASHBOARD_SUPERSEDE_INVALID: only idle, unsaved creation candidates from this run may be abandoned');
|
|
68
|
+
}
|
|
69
|
+
return { token: saves.begin(), generations: new Map([...edits].map(([path, edit]) => [path, edit.generation])), supersedesPaths, confirmation };
|
|
70
|
+
},
|
|
71
|
+
async accept(save, output) {
|
|
72
|
+
const receipt = (0, dashboard_receipt_1.createdDashboard)(output);
|
|
73
|
+
if (!receipt)
|
|
74
|
+
return;
|
|
75
|
+
saves.accept(save.token, receipt);
|
|
76
|
+
const source = (0, dashboard_receipt_1.savedDashboardSource)(output);
|
|
77
|
+
if (!source) {
|
|
78
|
+
// A standard dashboard has no HTML source; its domain receipt can explicitly
|
|
79
|
+
// abandon creation prototypes without claiming those files were persisted.
|
|
80
|
+
if (save.confirmation === 'draft')
|
|
81
|
+
abandonCandidates(save);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const edit = edits.get(source.path);
|
|
85
|
+
if (!edit || activeWrites.has(source.path) || save.generations.get(source.path) !== edit.generation || edit.artifactId && edit.artifactId !== receipt.artifactId)
|
|
86
|
+
return;
|
|
87
|
+
const current = await files().readRuntimeBuffer(source.path);
|
|
88
|
+
if (!activeWrites.has(source.path) && edits.get(source.path) === edit && (0, node_crypto_1.createHash)('sha256').update(current.buffer).digest('hex') === source.sha256) {
|
|
89
|
+
edit.pending = false;
|
|
90
|
+
edit.artifactId = receipt.artifactId;
|
|
91
|
+
// Explicit replacement applies only after exact-byte confirmation. Do not infer intent
|
|
92
|
+
// from names/titles, delete files, or clear edits made concurrently with this save.
|
|
93
|
+
abandonCandidates(save, source.path);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
get pending() { return saves.pending || activeWrites.size > 0 || [...edits.values()].some(edit => edit.pending); },
|
|
97
|
+
get pendingPaths() { return [...new Set([...activeWrites.keys(), ...[...edits].filter(([, edit]) => edit.pending).map(([path]) => path)])]; }
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { z } from 'zod/v3';
|
|
2
|
+
import type { XpertResolvedViewHostContext } from '@xpert-ai/contracts';
|
|
3
|
+
import { type RuntimeCapabilityRegistry } from '@xpert-ai/plugin-sdk';
|
|
4
|
+
import { DashboardService } from './dashboard.service';
|
|
5
|
+
export declare const shareInput: z.ZodEffects<z.ZodObject<{
|
|
6
|
+
artifactId: z.ZodString;
|
|
7
|
+
versionMode: z.ZodEnum<["latest", "version"]>;
|
|
8
|
+
versionId: z.ZodOptional<z.ZodString>;
|
|
9
|
+
}, "strict", z.ZodTypeAny, {
|
|
10
|
+
artifactId: string;
|
|
11
|
+
versionMode: "version" | "latest";
|
|
12
|
+
versionId?: string | undefined;
|
|
13
|
+
}, {
|
|
14
|
+
artifactId: string;
|
|
15
|
+
versionMode: "version" | "latest";
|
|
16
|
+
versionId?: string | undefined;
|
|
17
|
+
}>, {
|
|
18
|
+
artifactId: string;
|
|
19
|
+
versionMode: "version" | "latest";
|
|
20
|
+
versionId?: string | undefined;
|
|
21
|
+
}, {
|
|
22
|
+
artifactId: string;
|
|
23
|
+
versionMode: "version" | "latest";
|
|
24
|
+
versionId?: string | undefined;
|
|
25
|
+
}>;
|
|
26
|
+
export declare class DashboardSharingService {
|
|
27
|
+
private readonly dashboards;
|
|
28
|
+
private readonly runtime;
|
|
29
|
+
constructor(dashboards: DashboardService, runtime: RuntimeCapabilityRegistry);
|
|
30
|
+
inspect(context: XpertResolvedViewHostContext, artifactId: string, versionId?: string): Promise<{
|
|
31
|
+
published: {
|
|
32
|
+
current: boolean;
|
|
33
|
+
artifactId: string;
|
|
34
|
+
versionId: string | null | undefined;
|
|
35
|
+
versionNumber: number | undefined;
|
|
36
|
+
title: string | null | undefined;
|
|
37
|
+
updatedAt: string | Date | undefined;
|
|
38
|
+
} | null;
|
|
39
|
+
issues: {
|
|
40
|
+
code: string;
|
|
41
|
+
bindingId?: string;
|
|
42
|
+
}[];
|
|
43
|
+
link: {
|
|
44
|
+
status: import("@xpert-ai/contracts").ArtifactLinkStatus;
|
|
45
|
+
versionMode: import("@xpert-ai/contracts").ArtifactLinkVersionMode;
|
|
46
|
+
versionId: string | null | undefined;
|
|
47
|
+
url: string | undefined;
|
|
48
|
+
expiresAt: string | Date | null | undefined;
|
|
49
|
+
} | null;
|
|
50
|
+
}>;
|
|
51
|
+
share(context: XpertResolvedViewHostContext, input: z.infer<typeof shareInput>): Promise<{
|
|
52
|
+
published: {
|
|
53
|
+
current: boolean;
|
|
54
|
+
artifactId: string;
|
|
55
|
+
versionId: string | null | undefined;
|
|
56
|
+
versionNumber: number | undefined;
|
|
57
|
+
title: string | null | undefined;
|
|
58
|
+
updatedAt: string | Date | undefined;
|
|
59
|
+
} | null;
|
|
60
|
+
issues: {
|
|
61
|
+
code: string;
|
|
62
|
+
bindingId?: string;
|
|
63
|
+
}[];
|
|
64
|
+
link: {
|
|
65
|
+
status: import("@xpert-ai/contracts").ArtifactLinkStatus;
|
|
66
|
+
versionMode: import("@xpert-ai/contracts").ArtifactLinkVersionMode;
|
|
67
|
+
versionId: string | null | undefined;
|
|
68
|
+
url: string | undefined;
|
|
69
|
+
expiresAt: string | Date | null | undefined;
|
|
70
|
+
} | null;
|
|
71
|
+
}>;
|
|
72
|
+
revoke(context: XpertResolvedViewHostContext, artifactId: string): Promise<{
|
|
73
|
+
published: {
|
|
74
|
+
current: boolean;
|
|
75
|
+
artifactId: string;
|
|
76
|
+
versionId: string | null | undefined;
|
|
77
|
+
versionNumber: number | undefined;
|
|
78
|
+
title: string | null | undefined;
|
|
79
|
+
updatedAt: string | Date | undefined;
|
|
80
|
+
} | null;
|
|
81
|
+
issues: {
|
|
82
|
+
code: string;
|
|
83
|
+
bindingId?: string;
|
|
84
|
+
}[];
|
|
85
|
+
link: {
|
|
86
|
+
status: import("@xpert-ai/contracts").ArtifactLinkStatus;
|
|
87
|
+
versionMode: import("@xpert-ai/contracts").ArtifactLinkVersionMode;
|
|
88
|
+
versionId: string | null | undefined;
|
|
89
|
+
url: string | undefined;
|
|
90
|
+
expiresAt: string | Date | null | undefined;
|
|
91
|
+
} | null;
|
|
92
|
+
}>;
|
|
93
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.DashboardSharingService = exports.shareInput = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const v3_1 = require("zod/v3");
|
|
18
|
+
const plugin_sdk_1 = require("@xpert-ai/plugin-sdk");
|
|
19
|
+
const dashboard_service_1 = require("./dashboard.service");
|
|
20
|
+
exports.shareInput = v3_1.z.object({ artifactId: v3_1.z.string().uuid(), versionMode: v3_1.z.enum(['latest', 'version']), versionId: v3_1.z.string().uuid().optional() }).strict()
|
|
21
|
+
.refine(value => value.versionMode !== 'version' || !!value.versionId, 'A fixed share requires a published version');
|
|
22
|
+
const SHARE_KEY = 'team-readonly';
|
|
23
|
+
let DashboardSharingService = class DashboardSharingService {
|
|
24
|
+
constructor(dashboards, runtime) {
|
|
25
|
+
this.dashboards = dashboards;
|
|
26
|
+
this.runtime = runtime;
|
|
27
|
+
}
|
|
28
|
+
async inspect(context, artifactId, versionId) {
|
|
29
|
+
const versions = await this.dashboards.history(context, artifactId);
|
|
30
|
+
const selected = versionId ? versions.find(version => version.versionId === versionId) : versions.find(version => version.current);
|
|
31
|
+
const link = await this.runtime.require(plugin_sdk_1.ArtifactsRuntimeCapability).getArtifactShare({ artifactId, shareKey: SHARE_KEY });
|
|
32
|
+
const issues = [];
|
|
33
|
+
if (!selected?.versionId)
|
|
34
|
+
issues.push({ code: 'DASHBOARD_PUBLISH_BEFORE_SHARING' });
|
|
35
|
+
else {
|
|
36
|
+
const saved = await this.dashboards.open(context, artifactId, selected.versionId);
|
|
37
|
+
const sources = await this.dashboards.sources(context);
|
|
38
|
+
for (const binding of saved.manifest.bindings) {
|
|
39
|
+
const provider = sources.find(source => source.id === binding.dataSourceId)?.providers.find(provider => provider.id === binding.providerId);
|
|
40
|
+
if (!provider)
|
|
41
|
+
issues.push({ code: 'DASHBOARD_SOURCE_UNAVAILABLE', bindingId: binding.id });
|
|
42
|
+
else if (provider.sharing?.mode === 'owner_only')
|
|
43
|
+
issues.push({ code: provider.sharing.reasonCode ?? 'DASHBOARD_PRIVATE_DEPENDENCY', bindingId: binding.id });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { published: selected ?? null, issues, link: link ? { status: link.status, versionMode: link.versionMode,
|
|
47
|
+
versionId: link.artifactVersionId, url: link.presentationUrl, expiresAt: link.expiresAt } : null };
|
|
48
|
+
}
|
|
49
|
+
async share(context, input) {
|
|
50
|
+
const state = await this.inspect(context, input.artifactId, input.versionMode === 'version' ? input.versionId : undefined);
|
|
51
|
+
if (state.issues.length || !state.published?.versionId)
|
|
52
|
+
throw new common_1.BadRequestException({ code: 'DASHBOARD_SHARE_BLOCKED', issues: state.issues });
|
|
53
|
+
const result = await this.runtime.require(plugin_sdk_1.ArtifactsRuntimeCapability).ensureArtifactShare({ artifactId: input.artifactId, shareKey: SHARE_KEY,
|
|
54
|
+
versionMode: input.versionMode, ...(input.versionMode === 'version' ? { artifactVersionId: state.published.versionId } : {}),
|
|
55
|
+
access: { mode: 'organization_all' }, presentation: { allowDownload: false, disposition: 'inline', safeHtmlProfile: 'strict' } });
|
|
56
|
+
if (!result.link.presentationUrl)
|
|
57
|
+
throw new common_1.BadRequestException('DASHBOARD_PRESENTATION_HOST_REQUIRED');
|
|
58
|
+
return this.inspect(context, input.artifactId, input.versionMode === 'version' ? input.versionId : undefined);
|
|
59
|
+
}
|
|
60
|
+
async revoke(context, artifactId) {
|
|
61
|
+
await this.dashboards.history(context, artifactId);
|
|
62
|
+
await this.runtime.require(plugin_sdk_1.ArtifactsRuntimeCapability).revokeArtifactShare({ artifactId, shareKey: SHARE_KEY });
|
|
63
|
+
return this.inspect(context, artifactId);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
exports.DashboardSharingService = DashboardSharingService;
|
|
67
|
+
exports.DashboardSharingService = DashboardSharingService = __decorate([
|
|
68
|
+
(0, common_1.Injectable)(),
|
|
69
|
+
__param(1, (0, common_1.Inject)(plugin_sdk_1.XPERT_RUNTIME_CAPABILITIES_TOKEN)),
|
|
70
|
+
__metadata("design:paramtypes", [dashboard_service_1.DashboardService, Object])
|
|
71
|
+
], DashboardSharingService);
|