@xpert-ai/plugin-spaces 0.0.0-stage → 0.2.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 (61) hide show
  1. package/README.md +25 -2
  2. package/dist/access.d.ts +38 -0
  3. package/dist/access.js +96 -0
  4. package/dist/ai.d.ts +43 -0
  5. package/dist/ai.js +143 -0
  6. package/dist/artifacts.d.ts +9 -0
  7. package/dist/artifacts.js +40 -0
  8. package/dist/capability.d.ts +22 -0
  9. package/dist/capability.js +63 -0
  10. package/dist/content.d.ts +19 -0
  11. package/dist/content.js +47 -0
  12. package/dist/documents.d.ts +36 -0
  13. package/dist/documents.js +158 -0
  14. package/dist/domain.d.ts +95 -0
  15. package/dist/domain.js +23 -0
  16. package/dist/editors.d.ts +16 -0
  17. package/dist/editors.js +63 -0
  18. package/dist/entities.d.ts +68 -0
  19. package/dist/entities.js +251 -0
  20. package/dist/files.d.ts +79 -0
  21. package/dist/files.js +228 -0
  22. package/dist/imports.d.ts +83 -0
  23. package/dist/imports.js +139 -0
  24. package/dist/index.d.ts +6 -0
  25. package/dist/index.js +48 -0
  26. package/dist/knowledge.d.ts +43 -0
  27. package/dist/knowledge.js +166 -0
  28. package/dist/middleware.d.ts +17 -0
  29. package/dist/middleware.js +82 -0
  30. package/dist/pages.d.ts +38 -0
  31. package/dist/pages.js +41 -0
  32. package/dist/proposal-id.d.ts +3 -0
  33. package/dist/proposal-id.js +11 -0
  34. package/dist/references.d.ts +22 -0
  35. package/dist/references.js +119 -0
  36. package/dist/remote/app.css +4 -0
  37. package/dist/remote/app.js +167 -0
  38. package/dist/resource-provider.d.ts +36 -0
  39. package/dist/resource-provider.js +144 -0
  40. package/dist/spaces.d.ts +95 -0
  41. package/dist/spaces.js +228 -0
  42. package/dist/subpages.d.ts +24 -0
  43. package/dist/subpages.js +118 -0
  44. package/dist/view.d.ts +97 -0
  45. package/dist/view.js +226 -0
  46. package/docs/.mintignore +7 -0
  47. package/docs/AGENTS.md +3 -0
  48. package/docs/LICENSE +21 -0
  49. package/docs/README.md +3 -0
  50. package/docs/architecture.mdx +49 -0
  51. package/docs/collaboration.mdx +50 -0
  52. package/docs/docs.json +39 -0
  53. package/docs/feishu-document-import.mdx +118 -0
  54. package/docs/index.mdx +12 -0
  55. package/docs/insert-menu-plan.mdx +74 -0
  56. package/docs/math-subpages.mdx +62 -0
  57. package/docs/page-resources-and-connectors.mdx +103 -0
  58. package/docs/quickstart.mdx +17 -0
  59. package/migrations/20261023-document-import-origin.sql +4 -0
  60. package/migrations/20261023-subpages.sql +6 -0
  61. package/package.json +79 -4
@@ -0,0 +1,79 @@
1
+ import type { SpaceActor } from './domain.js';
2
+ import { type AgentMiddlewareRuntimeServiceApi } from '@xpert-ai/plugin-sdk';
3
+ import { Repository } from 'typeorm';
4
+ import { SpaceAccess } from './access.js';
5
+ import { SpaceItem, SpaceFileVersion } from './entities.js';
6
+ import { SpacePages } from './pages.js';
7
+ import { SpaceDocuments } from './documents.js';
8
+ import { SpaceKnowledge } from './knowledge.js';
9
+ import { SpacesService } from './spaces.js';
10
+ export declare class SpaceFiles {
11
+ private readonly access;
12
+ private readonly pages;
13
+ private readonly spaces;
14
+ private readonly documents;
15
+ private readonly knowledge;
16
+ private readonly versions;
17
+ private readonly runtime;
18
+ constructor(access: SpaceAccess, pages: SpacePages, spaces: SpacesService, documents: SpaceDocuments, knowledge: SpaceKnowledge, versions: Repository<SpaceFileVersion>, runtime: AgentMiddlewareRuntimeServiceApi);
19
+ put(actor: SpaceActor, itemId: string, buffer: Buffer, fileName: string, mimeType: string, revisionId?: string | null, updateCurrent?: boolean): Promise<SpaceFileVersion>;
20
+ /** Opaque file protocol. Editor plugins own parsing; this resource owns snapshots and CAS. */
21
+ editorRead(actor: SpaceActor, itemId: string): Promise<{
22
+ buffer: Buffer<ArrayBufferLike>;
23
+ revision: string;
24
+ }>;
25
+ private editorRevision;
26
+ editorWrite(actor: SpaceActor, itemId: string, expectedRevision: string, buffer: Buffer): Promise<{
27
+ revision: string;
28
+ }>;
29
+ readBytes(actor: SpaceActor, versionId: string): Promise<Buffer<ArrayBufferLike>>;
30
+ upload(actor: SpaceActor, spaceId: string, parentId: string | null, fileName: string, mimeType: string, buffer: Buffer): Promise<{
31
+ item: SpaceItem;
32
+ version: SpaceFileVersion;
33
+ }>;
34
+ get(actor: SpaceActor, versionId: string): Promise<SpaceFileVersion>;
35
+ list(actor: SpaceActor, itemId: string): Promise<SpaceFileVersion[]>;
36
+ exportPage(actor: SpaceActor, itemId: string): Promise<{
37
+ fileVersionId: string;
38
+ revisionId: string;
39
+ sequence: number;
40
+ artifactId: string;
41
+ versionId: string;
42
+ url: string;
43
+ }>;
44
+ collect(actor: SpaceActor, spaceId: string, parentId: string | null, artifactId: string, versionId: string): Promise<SpaceItem>;
45
+ artifactBytes(actor: SpaceActor, artifactId: string, versionId: string): Promise<{
46
+ artifact: import("@xpert-ai/plugin-sdk").ArtifactRecord;
47
+ version: import("@xpert-ai/plugin-sdk").ArtifactVersionRecord;
48
+ buffer: Buffer<ArrayBufferLike>;
49
+ }>;
50
+ available(actor: SpaceActor, page: number): Promise<{
51
+ items: {
52
+ id: string;
53
+ title: string | null | undefined;
54
+ versions: import("@xpert-ai/plugin-sdk").ArtifactVersionRecord[];
55
+ }[];
56
+ total: number;
57
+ page: number;
58
+ pageSize: number;
59
+ }>;
60
+ current(actor: SpaceActor, itemId: string): Promise<{
61
+ reference: {
62
+ userId: string;
63
+ projectId: string;
64
+ catalog: "projects";
65
+ source: "platform.workspace.files";
66
+ filePath: string;
67
+ scopeId: string;
68
+ workspacePath: string;
69
+ tenantId: string;
70
+ organizationId: string;
71
+ };
72
+ fileName: string;
73
+ mimeType: string;
74
+ }>;
75
+ openArtifact(actor: SpaceActor, itemId: string): Promise<{
76
+ artifact: import("@xpert-ai/plugin-sdk").ArtifactRecord;
77
+ version: import("@xpert-ai/plugin-sdk").ArtifactVersionRecord;
78
+ }>;
79
+ }
package/dist/files.js ADDED
@@ -0,0 +1,228 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ 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;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ import { BadRequestException, ConflictException, Inject, Injectable } from '@nestjs/common';
14
+ import { InjectRepository } from '@nestjs/typeorm';
15
+ import { ArtifactsRuntimeCapability, WorkspaceFilesRuntimeCapability, XPERT_AGENT_MIDDLEWARE_RUNTIME_TOKEN } from '@xpert-ai/plugin-sdk';
16
+ import { Repository } from 'typeorm';
17
+ import { createHash, randomUUID } from 'node:crypto';
18
+ import { SpaceAccess } from './access.js';
19
+ import { Space, SpaceItem, SpaceFileVersion } from './entities.js';
20
+ import { SpacePages } from './pages.js';
21
+ import { SpaceDocuments, contentHash } from './documents.js';
22
+ import { SpaceKnowledge } from './knowledge.js';
23
+ import { SpacesService } from './spaces.js';
24
+ import { denied, PLUGIN, scoped } from './domain.js';
25
+ let SpaceFiles = class SpaceFiles {
26
+ access;
27
+ pages;
28
+ spaces;
29
+ documents;
30
+ knowledge;
31
+ versions;
32
+ runtime;
33
+ constructor(access, pages, spaces, documents, knowledge, versions, runtime) {
34
+ this.access = access;
35
+ this.pages = pages;
36
+ this.spaces = spaces;
37
+ this.documents = documents;
38
+ this.knowledge = knowledge;
39
+ this.versions = versions;
40
+ this.runtime = runtime;
41
+ }
42
+ async put(actor, itemId, buffer, fileName, mimeType, revisionId = null, updateCurrent = true) {
43
+ const access = await this.access.resource(actor, itemId, 'write');
44
+ if (!access.space.projectId || !access.item || buffer.byteLength > 25 * 1024 * 1024)
45
+ throw denied();
46
+ const safeName = fileName.replace(/[\\/\u0000-\u001f]/gu, '_').replace(/^\.+/u, '_').slice(0, 180) || 'file';
47
+ const versionId = randomUUID();
48
+ // Space is a Project extension; current files and snapshots share its volume and ACL.
49
+ const folder = `.space-history/${itemId}/${versionId}`;
50
+ const scope = { ...scoped(actor), userId: actor.userId, catalog: 'projects',
51
+ projectId: access.space.projectId, scopeId: access.space.projectId };
52
+ const files = this.runtime.createScopedApi(scope).capabilities?.get(WorkspaceFilesRuntimeCapability);
53
+ if (!files)
54
+ throw new Error('spaces.files_unavailable');
55
+ const uploaded = await files.uploadBuffer({ ...scope, buffer, originalName: safeName, fileName: safeName, folder, mimeType });
56
+ const reference = { ...scope, source: 'platform.workspace.files',
57
+ filePath: uploaded.filePath, workspacePath: uploaded.filePath, originalName: safeName, mimeType, size: buffer.byteLength };
58
+ const version = await this.versions.save(this.versions.create({ ...scoped(actor), id: versionId, itemId,
59
+ createdById: actor.userId, reference, revisionId, sha256: createHash('sha256').update(buffer).digest('hex') }));
60
+ if (updateCurrent) {
61
+ await this.documents.write(actor, itemId, buffer);
62
+ await this.access.items.update({ ...scoped(actor), id: itemId }, { file: reference, currentFileVersionId: version.id });
63
+ await this.knowledge.sync(actor, access.space, access.item);
64
+ }
65
+ return version;
66
+ }
67
+ /** Opaque file protocol. Editor plugins own parsing; this resource owns snapshots and CAS. */
68
+ async editorRead(actor, itemId) {
69
+ const { item, space } = await this.access.resource(actor, itemId, 'read');
70
+ if (!item?.editorKey || !item.currentFileVersionId)
71
+ throw denied();
72
+ const disk = await this.documents.read(actor, space, item);
73
+ if (!disk || disk.sha256 !== item.fileHash)
74
+ throw new ConflictException('spaces.version_conflict');
75
+ return { buffer: disk.buffer, revision: this.editorRevision(item) };
76
+ }
77
+ editorRevision(item) {
78
+ // Metadata changes (move/trash/restore) invalidate a previously opened file as well.
79
+ return `${item.version}:${item.currentFileVersionId}:${item.fileHash}`;
80
+ }
81
+ async editorWrite(actor, itemId, expectedRevision, buffer) {
82
+ const access = await this.access.resource(actor, itemId, 'write');
83
+ const result = await this.access.items.manager.transaction(async (manager) => {
84
+ await manager.getRepository(Space).findOneOrFail({ where: { ...scoped(actor), id: access.space.id }, lock: { mode: 'pessimistic_write' } });
85
+ const repo = manager.getRepository(SpaceItem);
86
+ const item = await repo.findOneOrFail({ where: { ...scoped(actor), id: itemId }, lock: { mode: 'pessimistic_write' } });
87
+ await this.access.resource(actor, itemId, 'write');
88
+ if (!item.editorKey || !item.currentFileVersionId || !item.filePath || item.deleted)
89
+ throw denied();
90
+ if (this.editorRevision(item) !== expectedRevision)
91
+ throw new ConflictException('spaces.version_conflict');
92
+ const disk = await this.documents.read(actor, access.space, item);
93
+ if (!disk || disk.sha256 !== item.fileHash)
94
+ throw new ConflictException('spaces.version_conflict');
95
+ const version = await this.put(actor, itemId, buffer, item.filePath.split('/').pop() ?? item.title, item.file?.mimeType ?? 'application/octet-stream', null, false);
96
+ // ProjectFiles performs a second atomic hash check against edits made outside this resource.
97
+ await this.documents.api().write({ ...this.documents.scope(actor, access.space), path: item.filePath,
98
+ buffer, expectedHash: item.fileHash });
99
+ const updated = { ...item, currentFileVersionId: version.id, file: version.reference,
100
+ fileHash: contentHash(buffer), version: item.version + 1, syncStatus: 'ready', syncError: null };
101
+ await repo.update(item.id, { currentFileVersionId: updated.currentFileVersionId, file: updated.file,
102
+ fileHash: updated.fileHash, version: updated.version, syncStatus: updated.syncStatus, syncError: null });
103
+ return { revision: this.editorRevision(updated) };
104
+ });
105
+ await this.knowledge.sync(actor, access.space, await this.access.items.findOneByOrFail({ ...scoped(actor), id: itemId }));
106
+ return result;
107
+ }
108
+ async readBytes(actor, versionId) {
109
+ const version = await this.get(actor, versionId);
110
+ const files = this.runtime.createScopedApi(actor).capabilities?.get(WorkspaceFilesRuntimeCapability);
111
+ if (!files)
112
+ throw new Error('spaces.files_unavailable');
113
+ return (await files.readBuffer(version.reference)).buffer;
114
+ }
115
+ async upload(actor, spaceId, parentId, fileName, mimeType, buffer) {
116
+ await this.access.resource(actor, parentId ?? spaceId, 'write');
117
+ if (buffer.byteLength > 25 * 1024 * 1024)
118
+ throw new Error('spaces.file_too_large');
119
+ const item = await this.spaces.createItem(actor, { spaceId, parentId, title: fileName.slice(0, 200), kind: 'file' });
120
+ try {
121
+ return { item, version: await this.put(actor, item.id, buffer, fileName, mimeType) };
122
+ }
123
+ catch (error) {
124
+ await this.access.items.update({ ...scoped(actor), id: item.id }, { deleted: true });
125
+ throw error;
126
+ }
127
+ }
128
+ async get(actor, versionId) {
129
+ const version = await this.versions.findOneByOrFail({ ...scoped(actor), id: versionId });
130
+ await this.access.resource(actor, version.itemId, 'read');
131
+ return version;
132
+ }
133
+ async list(actor, itemId) {
134
+ await this.access.resource(actor, itemId, 'read');
135
+ return this.versions.find({ where: { ...scoped(actor), itemId }, order: { createdAt: 'DESC' } });
136
+ }
137
+ async exportPage(actor, itemId) {
138
+ const revision = await this.pages.checkpoint(actor, itemId, 'Export');
139
+ const access = await this.access.resource(actor, itemId, 'write');
140
+ const file = await this.put(actor, itemId, Buffer.from(revision.markdown || '\n', 'utf8'), `${access.item?.title ?? 'Page'}.md`, 'text/markdown', revision.id);
141
+ const artifacts = this.runtime.createScopedApi(actor).capabilities?.get(ArtifactsRuntimeCapability);
142
+ if (!artifacts)
143
+ throw new Error('spaces.artifacts_unavailable');
144
+ const artifact = await artifacts.createArtifact({ source: { pluginName: PLUGIN, resourceType: 'space-item', resourceId: itemId },
145
+ authorizationProviderKey: 'spaces', scope: { projectId: access.space.projectId, userId: null }, kind: 'markdown', title: access.item?.title });
146
+ const version = await artifacts.ensureArtifactVersion({ artifactId: artifact.id, idempotencyKey: `revision:${revision.id}`,
147
+ workspaceFileRef: file.reference, mimeType: 'text/markdown', fileName: file.reference.originalName, sha256: file.sha256,
148
+ sourceVersionId: revision.id });
149
+ const link = await artifacts.createArtifactLink({ artifactId: artifact.id, artifactVersionId: version.version.id,
150
+ versionMode: 'version', access: { mode: 'organization_all' }, presentation: { disposition: 'attachment', allowDownload: true } });
151
+ return { fileVersionId: file.id, revisionId: revision.id, sequence: revision.sequence, artifactId: artifact.id, versionId: version.version.id, url: link.publicUrl };
152
+ }
153
+ async collect(actor, spaceId, parentId, artifactId, versionId) {
154
+ await this.access.resource(actor, parentId ?? spaceId, 'write');
155
+ const { artifact, version, buffer } = await this.artifactBytes(actor, artifactId, versionId);
156
+ const item = await this.spaces.createItem(actor, { spaceId, parentId, title: version.fileName ?? artifact.title ?? 'Artifact', kind: 'artifact', artifactId, artifactVersionId: versionId });
157
+ try {
158
+ await this.put(actor, item.id, buffer, version.fileName ?? item.title, version.mimeType ?? 'application/octet-stream');
159
+ }
160
+ catch (error) {
161
+ await this.access.items.update(item.id, { deleted: true });
162
+ throw error;
163
+ }
164
+ return item;
165
+ }
166
+ async artifactBytes(actor, artifactId, versionId) {
167
+ const artifacts = this.runtime.createScopedApi(actor).capabilities?.get(ArtifactsRuntimeCapability);
168
+ if (!artifacts)
169
+ throw new Error('spaces.artifacts_unavailable');
170
+ const artifact = await artifacts.getArtifact(artifactId);
171
+ const version = (await artifacts.listArtifactVersions({ artifactId })).find((version) => version.id === versionId);
172
+ if (!version)
173
+ throw denied();
174
+ if (!version.workspaceFileRef)
175
+ throw new BadRequestException('spaces.artifact_file_unavailable');
176
+ // Collection can read another authorized project; its own ACL is checked by both
177
+ // Artifacts and WorkspaceFiles. The destination Project must not replace that scope.
178
+ const files = this.runtime.createScopedApi({ ...scoped(actor), userId: actor.userId }).capabilities?.get(WorkspaceFilesRuntimeCapability);
179
+ if (!files)
180
+ throw new Error('spaces.files_unavailable');
181
+ const bytes = await files.readBuffer(version.workspaceFileRef);
182
+ return { artifact, version, buffer: bytes.buffer };
183
+ }
184
+ async available(actor, page) {
185
+ await this.access.project(actor);
186
+ const api = this.runtime.createScopedApi(actor).capabilities?.require(ArtifactsRuntimeCapability);
187
+ if (!api)
188
+ throw new Error('spaces.artifacts_unavailable');
189
+ const result = await api.listArtifacts({ page, pageSize: 40 });
190
+ return { ...result, items: await Promise.all(result.items.map(async (artifact) => ({
191
+ id: artifact.id, title: artifact.title, versions: (await api.listArtifactVersions({ artifactId: artifact.id })).filter((version) => version.workspaceFileRef)
192
+ }))) };
193
+ }
194
+ async current(actor, itemId) {
195
+ const { item, space } = await this.access.resource(actor, itemId, 'read');
196
+ if (!space.projectId || !item?.filePath || item.kind === 'folder')
197
+ throw denied();
198
+ return { reference: { ...scoped(actor), userId: actor.userId, projectId: space.projectId,
199
+ catalog: 'projects', source: 'platform.workspace.files', filePath: item.filePath,
200
+ scopeId: space.projectId, workspacePath: item.filePath }, fileName: item.filePath.split('/').pop() ?? item.title,
201
+ mimeType: item.kind === 'page' ? 'text/markdown' : item.file?.mimeType ?? 'application/octet-stream' };
202
+ }
203
+ async openArtifact(actor, itemId) {
204
+ const access = await this.access.resource(actor, itemId, 'read');
205
+ if (!access.item?.artifactId)
206
+ throw denied();
207
+ // A collection is a reference. It never expands access to the original Artifact.
208
+ const artifacts = this.runtime.createScopedApi(actor).capabilities?.get(ArtifactsRuntimeCapability);
209
+ if (!artifacts)
210
+ throw new Error('spaces.artifacts_unavailable');
211
+ const artifact = await artifacts.getArtifact(access.item.artifactId);
212
+ const versions = await artifacts.listArtifactVersions({ artifactId: artifact.id });
213
+ const version = versions.find((entry) => entry.id === access.item?.artifactVersionId);
214
+ if (!version)
215
+ throw denied();
216
+ return { artifact, version };
217
+ }
218
+ };
219
+ SpaceFiles = __decorate([
220
+ Injectable(),
221
+ __param(5, InjectRepository(SpaceFileVersion)),
222
+ __param(6, Inject(XPERT_AGENT_MIDDLEWARE_RUNTIME_TOKEN)),
223
+ __metadata("design:paramtypes", [SpaceAccess, SpacePages, SpacesService,
224
+ SpaceDocuments,
225
+ SpaceKnowledge,
226
+ Repository, Object])
227
+ ], SpaceFiles);
228
+ export { SpaceFiles };
@@ -0,0 +1,83 @@
1
+ import { type RuntimeCapabilityRegistry } from '@xpert-ai/plugin-sdk';
2
+ import { z } from 'zod/v3';
3
+ import { SpaceAccess } from './access.js';
4
+ import { SpaceDocuments } from './documents.js';
5
+ import { SpaceArtifacts } from './artifacts.js';
6
+ import { type SpaceActor } from './domain.js';
7
+ export declare const sourceSelection: z.ZodObject<{
8
+ bindingId: z.ZodString;
9
+ locator: z.ZodString;
10
+ }, "strict", z.ZodTypeAny, {
11
+ bindingId: string;
12
+ locator: string;
13
+ }, {
14
+ bindingId: string;
15
+ locator: string;
16
+ }>;
17
+ export declare const importSelection: z.ZodObject<{
18
+ bindingId: z.ZodString;
19
+ locator: z.ZodString;
20
+ } & {
21
+ spaceId: z.ZodString;
22
+ parentId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
23
+ requestId: z.ZodString;
24
+ expectedRevision: z.ZodString;
25
+ expectedHash: z.ZodString;
26
+ confirmed: z.ZodLiteral<true>;
27
+ }, "strict", z.ZodTypeAny, {
28
+ spaceId: string;
29
+ parentId: string | null;
30
+ expectedHash: string;
31
+ bindingId: string;
32
+ locator: string;
33
+ requestId: string;
34
+ expectedRevision: string;
35
+ confirmed: true;
36
+ }, {
37
+ spaceId: string;
38
+ expectedHash: string;
39
+ bindingId: string;
40
+ locator: string;
41
+ requestId: string;
42
+ expectedRevision: string;
43
+ confirmed: true;
44
+ parentId?: string | null | undefined;
45
+ }>;
46
+ /** Encode remote plain text as literal Markdown paragraphs, never executable HTML. Encode colons to prevent autolinks from consuming Markdown escapes. */
47
+ export declare const literalMarkdown: (text: string) => string;
48
+ export declare class SpaceImports {
49
+ private readonly access;
50
+ private readonly documents;
51
+ private readonly artifacts;
52
+ private readonly runtime;
53
+ constructor(access: SpaceAccess, documents: SpaceDocuments, artifacts: SpaceArtifacts, runtime: RuntimeCapabilityRegistry);
54
+ private context;
55
+ private api;
56
+ sources(actor: SpaceActor): Promise<{
57
+ items: import("@xpert-ai/plugin-sdk").ConnectorDocumentConnection[];
58
+ }>;
59
+ search(actor: SpaceActor, input: {
60
+ bindingId: string;
61
+ query: string;
62
+ cursor?: string;
63
+ pageSize: number;
64
+ }): Promise<{
65
+ items: import("@xpert-ai/plugin-sdk").ConnectorDocumentEntry[];
66
+ nextCursor: string | null;
67
+ }>;
68
+ preview(actor: SpaceActor, input: z.infer<typeof sourceSelection>): Promise<{
69
+ provider: string;
70
+ revision: string;
71
+ contentHash: string;
72
+ format: "text/plain";
73
+ content: string;
74
+ warnings: string[];
75
+ id: string;
76
+ title: string;
77
+ url: string | null;
78
+ type: "document";
79
+ }>;
80
+ apply(actor: SpaceActor, input: z.infer<typeof importSelection>): Promise<{
81
+ resourceId: string;
82
+ }>;
83
+ }
@@ -0,0 +1,139 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ 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;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ import { BadRequestException, ConflictException, Inject, Injectable } from '@nestjs/common';
14
+ import { ConnectorDocumentsRuntimeCapability, XPERT_RUNTIME_CAPABILITIES_TOKEN } from '@xpert-ai/plugin-sdk';
15
+ import { z } from 'zod/v3';
16
+ import { SpaceAccess } from './access.js';
17
+ import { SpaceDocuments, contentHash, safeName } from './documents.js';
18
+ import { SpaceArtifacts } from './artifacts.js';
19
+ import { Space } from './entities.js';
20
+ import { denied, id, scoped } from './domain.js';
21
+ export const sourceSelection = z.object({ bindingId: id, locator: z.string().trim().min(1).max(2000) }).strict();
22
+ export const importSelection = sourceSelection.extend({ spaceId: id, parentId: id.nullable().default(null), requestId: id,
23
+ expectedRevision: z.string().min(1).max(200), expectedHash: z.string().regex(/^[a-f0-9]{64}$/u), confirmed: z.literal(true) }).strict();
24
+ const missing = (error) => error !== null && typeof error === 'object' && 'code' in error && error.code === 'ENOENT';
25
+ /** Encode remote plain text as literal Markdown paragraphs, never executable HTML. Encode colons to prevent autolinks from consuming Markdown escapes. */
26
+ export const literalMarkdown = (text) => text.replace(/\r\n?/gu, '\n').replace(/\u0000/gu, '')
27
+ .replace(/[&:\\`*_{}\[\]()#+.!<>|$~-]/gu, (char) => char === '&' || char === ':' ? `&#${char.codePointAt(0)};` : `\\${char}`).split('\n').join('\n\n');
28
+ let SpaceImports = class SpaceImports {
29
+ access;
30
+ documents;
31
+ artifacts;
32
+ runtime;
33
+ constructor(access, documents, artifacts, runtime) {
34
+ this.access = access;
35
+ this.documents = documents;
36
+ this.artifacts = artifacts;
37
+ this.runtime = runtime;
38
+ }
39
+ context(actor) {
40
+ if (!actor.assistantId || !actor.projectId)
41
+ throw denied();
42
+ return { actor, assistantId: actor.assistantId, projectId: actor.projectId };
43
+ }
44
+ api() { return this.runtime.require(ConnectorDocumentsRuntimeCapability); }
45
+ async sources(actor) {
46
+ await this.access.project(actor, actor.projectId, 'write');
47
+ return this.api().sources(this.context(actor));
48
+ }
49
+ async search(actor, input) {
50
+ await this.access.project(actor, actor.projectId, 'write');
51
+ return this.api().search({ ...this.context(actor), ...input });
52
+ }
53
+ async preview(actor, input) {
54
+ const sources = await this.sources(actor), source = sources.items.find((entry) => entry.bindingId === input.bindingId && entry.available);
55
+ if (!source)
56
+ throw denied();
57
+ const snapshot = await this.api().read({ ...this.context(actor), ...input });
58
+ if (snapshot.format !== 'text/plain')
59
+ throw new BadRequestException('spaces.import_format_unsupported');
60
+ return { ...snapshot, provider: source.provider };
61
+ }
62
+ async apply(actor, input) {
63
+ if (!input.confirmed)
64
+ throw new BadRequestException('spaces.import_confirmation_required');
65
+ const context = this.context(actor);
66
+ const target = async () => {
67
+ const result = await this.access.resource(actor, input.parentId ?? input.spaceId, 'write');
68
+ if (result.space.id !== input.spaceId || result.space.projectId !== context.projectId || (result.item && result.item.kind !== 'folder'))
69
+ throw denied();
70
+ return result;
71
+ };
72
+ const { space } = await target();
73
+ // A stable request binds all preview/target fields, but stores no credentials or raw pasted URL.
74
+ const requestHash = contentHash(Buffer.from(JSON.stringify({ ...input, confirmed: undefined, requestId: undefined })));
75
+ const existing = await this.access.items.findOneBy({ id: input.requestId });
76
+ const check = (item) => {
77
+ if (item.tenantId !== actor.tenantId || item.organizationId !== actor.organizationId || item.spaceId !== input.spaceId ||
78
+ item.createdById !== actor.userId || item.deleted || item.importOrigin?.requestHash !== requestHash)
79
+ throw new ConflictException('spaces.import_request_conflict');
80
+ };
81
+ if (existing) {
82
+ check(existing);
83
+ // A completed copy is a project resource: remote revocation never deletes it or prevents retry receipts.
84
+ await this.access.resource(actor, existing.id, 'write');
85
+ if (existing.editorKey === 'markdown.tiptap')
86
+ return { resourceId: existing.id };
87
+ }
88
+ const snapshot = await this.preview(actor, input);
89
+ if (snapshot.revision !== input.expectedRevision || snapshot.contentHash !== input.expectedHash)
90
+ throw new ConflictException('spaces.import_source_changed');
91
+ const text = `# ${literalMarkdown(snapshot.title)}\n\n${literalMarkdown(snapshot.content)}\n`;
92
+ await this.documents.content().validateImport({ ...context, modelKey: 'markdown.tiptap', markdown: text });
93
+ const buffer = Buffer.from(text), hash = contentHash(buffer);
94
+ // Serialize with discovery/moves. Persist the intent outside the lock transaction so an interrupted
95
+ // file/register step resumes the same ID and CAS-protected file rather than creating another copy.
96
+ return this.access.items.manager.transaction(async (manager) => {
97
+ await manager.getRepository(Space).findOneOrFail({ where: { ...scoped(actor), id: space.id }, lock: { mode: 'pessimistic_write' } });
98
+ const destination = await target();
99
+ let item = await this.access.items.findOneBy({ id: input.requestId });
100
+ if (item) {
101
+ check(item);
102
+ if (item.editorKey === 'markdown.tiptap')
103
+ return { resourceId: item.id };
104
+ }
105
+ else {
106
+ const prefix = destination.item?.filePath ? `${destination.item.filePath}/` : '';
107
+ item = await this.access.items.save(this.access.items.create({ ...scoped(actor), id: input.requestId, spaceId: space.id,
108
+ parentId: input.parentId, createdById: actor.userId, kind: 'page', title: snapshot.title,
109
+ filePath: `${prefix}${safeName(snapshot.title).slice(0, 100)}--${input.requestId}.md`, syncStatus: 'pending',
110
+ importOrigin: { provider: snapshot.provider, documentId: snapshot.id, url: snapshot.url, revision: snapshot.revision,
111
+ contentHash: snapshot.contentHash, format: snapshot.format, importedAt: new Date().toISOString(),
112
+ warnings: snapshot.warnings, bindingId: input.bindingId, requestHash } }));
113
+ }
114
+ const fileScope = { actor, projectId: context.projectId, path: item.filePath };
115
+ const disk = await this.documents.api().read(fileScope).catch((error) => { if (missing(error))
116
+ return null; throw error; });
117
+ if (disk && disk.sha256 !== (item.fileHash ?? hash))
118
+ throw new ConflictException('spaces.file_conflict');
119
+ if (!disk) {
120
+ if (item.fileHash)
121
+ throw new ConflictException('spaces.file_conflict');
122
+ await this.documents.api().write({ ...fileScope, buffer, expectedHash: null });
123
+ }
124
+ await this.access.items.update(item.id, { fileHash: disk?.sha256 ?? hash, syncStatus: 'ready' });
125
+ await this.documents.content().register({ ...context, resourceId: item.id, path: item.filePath, title: item.title,
126
+ modelKey: 'markdown.tiptap', source: { providerKey: 'spaces', resourceId: item.id } });
127
+ if (!item.artifactId)
128
+ await this.artifacts.initializePage(actor, item.id, item.title);
129
+ await this.access.items.update(item.id, { editorKey: 'markdown.tiptap' });
130
+ return { resourceId: item.id };
131
+ });
132
+ }
133
+ };
134
+ SpaceImports = __decorate([
135
+ Injectable(),
136
+ __param(3, Inject(XPERT_RUNTIME_CAPABILITIES_TOKEN)),
137
+ __metadata("design:paramtypes", [SpaceAccess, SpaceDocuments, SpaceArtifacts, Object])
138
+ ], SpaceImports);
139
+ export { SpaceImports };
@@ -0,0 +1,6 @@
1
+ import 'reflect-metadata';
2
+ import { type XpertPlugin } from '@xpert-ai/plugin-sdk';
3
+ export declare class SpacesPlugin {
4
+ }
5
+ declare const plugin: XpertPlugin;
6
+ export default plugin;
package/dist/index.js ADDED
@@ -0,0 +1,48 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ 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;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { SpaceImports } from './imports.js';
8
+ import 'reflect-metadata';
9
+ import { readFileSync } from 'node:fs';
10
+ const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
11
+ import { TypeOrmModule } from '@nestjs/typeorm';
12
+ import { XpertServerPlugin } from '@xpert-ai/plugin-sdk';
13
+ import { entities } from './entities.js';
14
+ import { SpaceAccess } from './access.js';
15
+ import { SpacesResourceProvider } from './resource-provider.js';
16
+ import { SpacesService } from './spaces.js';
17
+ import { SpacePages } from './pages.js';
18
+ import { SpaceAi } from './ai.js';
19
+ import { SpacesView } from './view.js';
20
+ import { SpaceEditors } from './editors.js';
21
+ import { SpacesMiddleware } from './middleware.js';
22
+ import { SpaceArtifacts } from './artifacts.js';
23
+ import { SpaceFiles } from './files.js';
24
+ import { SpaceDocuments } from './documents.js';
25
+ import { SpaceKnowledge } from './knowledge.js';
26
+ import { SpaceContent } from './content.js';
27
+ import { SpaceReferences } from './references.js';
28
+ import { SpaceSubpages } from './subpages.js';
29
+ import { SpacesCapabilityProvider } from './capability.js';
30
+ let SpacesPlugin = class SpacesPlugin {
31
+ };
32
+ SpacesPlugin = __decorate([
33
+ XpertServerPlugin({
34
+ imports: [TypeOrmModule.forFeature(entities)], entities,
35
+ providers: [SpacesCapabilityProvider, SpaceImports, SpaceReferences, SpaceSubpages, SpaceContent, SpaceAccess, SpaceKnowledge, SpaceDocuments, SpacesResourceProvider, SpacesService, SpacePages, SpaceAi, SpaceFiles, SpaceEditors, SpaceArtifacts, SpacesMiddleware, SpacesView]
36
+ })
37
+ ], SpacesPlugin);
38
+ export { SpacesPlugin };
39
+ const plugin = {
40
+ meta: {
41
+ name: manifest.name, version: manifest.version, author: 'XpertAI', level: manifest.xpert.plugin.level, artifactNamespace: manifest.xpert.plugin.artifactNamespace,
42
+ displayName: 'Spaces', description: 'Assistant collaboration capability for shared AI-maintained documents and artifacts',
43
+ category: 'middleware', targetApps: ['xpert'],
44
+ targetAppMeta: { xpert: { types: ['business-app', 'workbench-view', 'assistant-tool'], capabilities: ['spaces'], runtime: { middlewareProviders: ['spaces'], viewProviders: ['spaces.views'] } } }
45
+ },
46
+ register() { return { module: SpacesPlugin, global: true }; }
47
+ };
48
+ export default plugin;
@@ -0,0 +1,43 @@
1
+ import { type RuntimeCapabilityRegistry } from '@xpert-ai/plugin-sdk';
2
+ import { SpaceAccess } from './access.js';
3
+ import { type SpaceActor } from './domain.js';
4
+ import { Space, SpaceItem } from './entities.js';
5
+ export declare class SpaceKnowledge {
6
+ private readonly access;
7
+ private readonly runtime;
8
+ constructor(access: SpaceAccess, runtime: RuntimeCapabilityRegistry);
9
+ enable(actor: SpaceActor, spaceId: string): Promise<{
10
+ knowledgebaseId: string;
11
+ }>;
12
+ sync(actor: SpaceActor, space: Space, item: SpaceItem): Promise<void>;
13
+ refresh(actor: SpaceActor, spaceId: string): Promise<{
14
+ knowledgebaseId: string | null;
15
+ mode: string;
16
+ items: {
17
+ itemId: string;
18
+ title: string;
19
+ status: "pending" | "ready" | "failed" | "processing";
20
+ error: string | null;
21
+ document: import("@xpert-ai/plugin-sdk").KnowledgebaseDocumentRecord | null;
22
+ }[];
23
+ }>;
24
+ status(actor: SpaceActor, spaceId: string): Promise<{
25
+ knowledgebaseId: string | null;
26
+ mode: string;
27
+ items: {
28
+ itemId: string;
29
+ title: string;
30
+ status: "pending" | "ready" | "failed" | "processing";
31
+ error: string | null;
32
+ document: import("@xpert-ai/plugin-sdk").KnowledgebaseDocumentRecord | null;
33
+ }[];
34
+ }>;
35
+ search(actor: SpaceActor, spaceId: string, query: string): Promise<{
36
+ itemId: string;
37
+ title: string;
38
+ text: string;
39
+ filePath: string;
40
+ hash: string;
41
+ }[]>;
42
+ verify(actor: SpaceActor, itemId: string, hash: string): Promise<boolean>;
43
+ }