@xpert-ai/plugin-spaces 0.0.0-stage → 0.3.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 (73) 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 +53 -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/catalog.d.ts +196 -0
  11. package/dist/catalog.js +140 -0
  12. package/dist/content-backup.d.ts +7 -0
  13. package/dist/content-backup.js +35 -0
  14. package/dist/content-coverage.d.ts +9 -0
  15. package/dist/content-coverage.js +12 -0
  16. package/dist/content-knowledge.d.ts +40 -0
  17. package/dist/content-knowledge.js +168 -0
  18. package/dist/content.d.ts +19 -0
  19. package/dist/content.js +47 -0
  20. package/dist/documents.d.ts +38 -0
  21. package/dist/documents.js +177 -0
  22. package/dist/domain.d.ts +95 -0
  23. package/dist/domain.js +23 -0
  24. package/dist/editors.d.ts +16 -0
  25. package/dist/editors.js +63 -0
  26. package/dist/entities.d.ts +91 -0
  27. package/dist/entities.js +318 -0
  28. package/dist/files.d.ts +79 -0
  29. package/dist/files.js +228 -0
  30. package/dist/imports.d.ts +83 -0
  31. package/dist/imports.js +139 -0
  32. package/dist/index.d.ts +6 -0
  33. package/dist/index.js +51 -0
  34. package/dist/item-catalog.d.ts +13 -0
  35. package/dist/item-catalog.js +71 -0
  36. package/dist/knowledge.d.ts +57 -0
  37. package/dist/knowledge.js +172 -0
  38. package/dist/middleware.d.ts +17 -0
  39. package/dist/middleware.js +82 -0
  40. package/dist/pages.d.ts +38 -0
  41. package/dist/pages.js +41 -0
  42. package/dist/proposal-id.d.ts +3 -0
  43. package/dist/proposal-id.js +11 -0
  44. package/dist/references.d.ts +22 -0
  45. package/dist/references.js +119 -0
  46. package/dist/remote/app.css +4 -0
  47. package/dist/remote/app.js +167 -0
  48. package/dist/resource-folders.d.ts +7 -0
  49. package/dist/resource-folders.js +21 -0
  50. package/dist/resource-provider.d.ts +39 -0
  51. package/dist/resource-provider.js +160 -0
  52. package/dist/spaces.d.ts +95 -0
  53. package/dist/spaces.js +233 -0
  54. package/dist/subpages.d.ts +24 -0
  55. package/dist/subpages.js +118 -0
  56. package/dist/view.d.ts +99 -0
  57. package/dist/view.js +259 -0
  58. package/docs/.mintignore +7 -0
  59. package/docs/AGENTS.md +3 -0
  60. package/docs/LICENSE +21 -0
  61. package/docs/README.md +3 -0
  62. package/docs/architecture.mdx +49 -0
  63. package/docs/collaboration.mdx +50 -0
  64. package/docs/docs.json +39 -0
  65. package/docs/feishu-document-import.mdx +118 -0
  66. package/docs/index.mdx +12 -0
  67. package/docs/insert-menu-plan.mdx +74 -0
  68. package/docs/math-subpages.mdx +62 -0
  69. package/docs/page-resources-and-connectors.mdx +103 -0
  70. package/docs/quickstart.mdx +17 -0
  71. package/migrations/20261023-document-import-origin.sql +4 -0
  72. package/migrations/20261023-subpages.sql +6 -0
  73. package/package.json +79 -4
@@ -0,0 +1,7 @@
1
+ import type { EntityManager } from 'typeorm';
2
+ import { SpaceItem } from './entities.js';
3
+ import { type SpaceActor } from './domain.js';
4
+ /** Folder deletion must not silently bypass a resource provider's lifecycle policy.
5
+ * Call under the Space lock when committing a change; the early validation gives
6
+ * the same error before the Markdown move coordinator retires editor rooms. */
7
+ export declare function assertFolderHasNoResources(manager: EntityManager, actor: SpaceActor, item: SpaceItem): Promise<void>;
@@ -0,0 +1,21 @@
1
+ import { ConflictException } from '@nestjs/common';
2
+ import { SpaceItem, SpaceResourcePlacement } from './entities.js';
3
+ import { scoped } from './domain.js';
4
+ /** Folder deletion must not silently bypass a resource provider's lifecycle policy.
5
+ * Call under the Space lock when committing a change; the early validation gives
6
+ * the same error before the Markdown move coordinator retires editor rooms. */
7
+ export async function assertFolderHasNoResources(manager, actor, item) {
8
+ if (item.kind !== 'folder')
9
+ return;
10
+ const folders = await manager.getRepository(SpaceItem).findBy({ ...scoped(actor), spaceId: item.spaceId, kind: 'folder' });
11
+ const inside = new Set([item.id]);
12
+ for (let i = 0; i <= folders.length; i++)
13
+ for (const folder of folders) {
14
+ if (folder.parentId && inside.has(folder.parentId))
15
+ inside.add(folder.id);
16
+ }
17
+ const placements = await manager.getRepository(SpaceResourcePlacement).findBy({ ...scoped(actor), spaceId: item.spaceId });
18
+ if (placements.some(placement => placement.parentId && inside.has(placement.parentId))) {
19
+ throw new ConflictException('spaces.folder_contains_resources');
20
+ }
21
+ }
@@ -0,0 +1,39 @@
1
+ import { type ContentResourceEditor, type ContentResourceRequest, type ContentPrincipal, type ContentPageProvider, type ContentLocationProvider, type ContentPublicationObserver, type ContentReferenceProvider, type IContentResourceProvider } from '@xpert-ai/plugin-sdk';
2
+ import { SpaceItemCatalog } from './item-catalog.js';
3
+ import { SpaceAccess } from './access.js';
4
+ import { SpaceFiles } from './files.js';
5
+ import { SpaceDocuments } from './documents.js';
6
+ import { SpaceKnowledge } from './knowledge.js';
7
+ import { SpaceReferences } from './references.js';
8
+ import { SpaceSubpages } from './subpages.js';
9
+ export declare class SpacesResourceProvider implements IContentResourceProvider {
10
+ private readonly access;
11
+ private readonly documents;
12
+ private readonly knowledge;
13
+ private readonly subpages;
14
+ private readonly referenceService;
15
+ private readonly files;
16
+ private readonly itemCatalog;
17
+ readonly title = "Spaces";
18
+ constructor(access: SpaceAccess, documents: SpaceDocuments, knowledge: SpaceKnowledge, subpages: SpaceSubpages, referenceService: SpaceReferences, files: SpaceFiles, itemCatalog: SpaceItemCatalog);
19
+ get catalog(): import("@xpert-ai/plugin-sdk").ContentCatalogProvider;
20
+ readonly editor: ContentResourceEditor;
21
+ readonly references: ContentReferenceProvider;
22
+ readonly pages: ContentPageProvider;
23
+ readonly publication: ContentPublicationObserver;
24
+ readonly locations: ContentLocationProvider;
25
+ validateLocationChange(input: Parameters<NonNullable<ContentLocationProvider['validateLocationChange']>>[0]): Promise<void>;
26
+ onLocationPrepared(input: Parameters<NonNullable<ContentLocationProvider['onLocationPrepared']>>[0]): Promise<void>;
27
+ onLocationChanged(input: Parameters<NonNullable<ContentLocationProvider['onLocationChanged']>>[0]): Promise<void>;
28
+ onDocumentPublished(input: Parameters<NonNullable<ContentPublicationObserver['onDocumentPublished']>>[0]): Promise<void>;
29
+ authorize(input: ContentResourceRequest & {
30
+ principal: ContentPrincipal;
31
+ }): Promise<{
32
+ providerKey: string;
33
+ resourceId: string;
34
+ title: string;
35
+ role: "editor" | "manager" | "owner" | "member";
36
+ canWrite: boolean;
37
+ canManage: boolean;
38
+ }>;
39
+ }
@@ -0,0 +1,160 @@
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
+ import { ConflictException, Injectable } from '@nestjs/common';
11
+ import { ContentResourceProvider } from '@xpert-ai/plugin-sdk';
12
+ import { SpaceItemCatalog } from './item-catalog.js';
13
+ import { Space, SpaceItem } from './entities.js';
14
+ import { SpaceAccess } from './access.js';
15
+ import { SpaceFiles } from './files.js';
16
+ import { SpaceDocuments } from './documents.js';
17
+ import { SpaceKnowledge } from './knowledge.js';
18
+ import { denied, scoped } from './domain.js';
19
+ import { SpaceReferences } from './references.js';
20
+ import { SpaceSubpages } from './subpages.js';
21
+ import { assertFolderHasNoResources } from './resource-folders.js';
22
+ let SpacesResourceProvider = class SpacesResourceProvider {
23
+ access;
24
+ documents;
25
+ knowledge;
26
+ subpages;
27
+ referenceService;
28
+ files;
29
+ itemCatalog;
30
+ title = 'Spaces';
31
+ constructor(access, documents, knowledge, subpages, referenceService, files, itemCatalog) {
32
+ this.access = access;
33
+ this.documents = documents;
34
+ this.knowledge = knowledge;
35
+ this.subpages = subpages;
36
+ this.referenceService = referenceService;
37
+ this.files = files;
38
+ this.itemCatalog = itemCatalog;
39
+ }
40
+ get catalog() { return this.itemCatalog.catalog; }
41
+ editor = {
42
+ describe: async (input) => {
43
+ const actor = { ...input.actor, projectId: input.projectId, assistantId: input.assistantId };
44
+ const { item, space } = await this.access.resource(actor, input.sourceResourceId, 'read');
45
+ if (!space.projectId || !item?.editorKey || !item.filePath || item.deleted)
46
+ throw denied();
47
+ return { editorKey: item.editorKey, projectId: space.projectId, path: item.filePath, title: item.title };
48
+ },
49
+ files: {
50
+ read: (input) => this.files.editorRead({ ...input.actor, projectId: input.projectId, assistantId: input.assistantId }, input.sourceResourceId),
51
+ write: (input) => this.files.editorWrite({ ...input.actor, projectId: input.projectId, assistantId: input.assistantId }, input.sourceResourceId, input.expectedRevision, input.buffer)
52
+ }
53
+ };
54
+ references = {
55
+ listReferences: (input) => this.referenceService.listReferences(input),
56
+ uploadReference: (input) => this.referenceService.uploadReference(input),
57
+ resolveReference: (input) => this.referenceService.resolveReference(input)
58
+ };
59
+ pages = {
60
+ pageHierarchy: (input) => this.subpages.hierarchy(input),
61
+ createChildPage: (input) => this.subpages.createChild(input)
62
+ };
63
+ publication = { onDocumentPublished: (input) => this.onDocumentPublished(input) };
64
+ locations = {
65
+ validateLocationChange: (input) => this.validateLocationChange(input),
66
+ onLocationPrepared: (input) => this.onLocationPrepared(input),
67
+ onLocationChanged: (input) => this.onLocationChanged(input)
68
+ };
69
+ async validateLocationChange(input) {
70
+ const actor = { ...input.actor, projectId: input.projectId, assistantId: input.assistantId };
71
+ const { item, space } = await this.access.resource(actor, input.source.resourceId, 'write', !input.deleted);
72
+ if (!item || space.projectId !== input.projectId || item.filePath !== input.path || item.version !== input.expectedSourceVersion)
73
+ throw new ConflictException('spaces.version_conflict');
74
+ if (item.deleted !== input.deleted)
75
+ await this.access.resource(actor, item.id, 'manage', !input.deleted);
76
+ if (input.deleted)
77
+ await assertFolderHasNoResources(this.access.items.manager, actor, item);
78
+ const parent = await this.access.resource(actor, input.parentResourceId ?? item.spaceId, 'write');
79
+ if (parent.space.id !== item.spaceId || (parent.item && parent.item.kind !== 'folder'))
80
+ throw denied();
81
+ let cursor = parent.item;
82
+ while (cursor) {
83
+ if (cursor.id === item.id)
84
+ throw new ConflictException('spaces.tree_cycle');
85
+ cursor = cursor.parentId ? await this.access.items.findOneByOrFail({ ...scoped(actor), id: cursor.parentId }) : null;
86
+ }
87
+ }
88
+ async onLocationPrepared(input) {
89
+ await this.access.items.manager.transaction(async (manager) => {
90
+ const repo = manager.getRepository(SpaceItem);
91
+ const item = await repo.findOneByOrFail({ ...scoped(input.actor), id: input.source.resourceId });
92
+ await manager.getRepository(Space).findOneOrFail({ where: { ...scoped(input.actor), id: item.spaceId }, lock: { mode: 'pessimistic_write' } });
93
+ if (input.deleted)
94
+ await assertFolderHasNoResources(manager, input.actor, item);
95
+ // Resource placement takes the same Space lock and rejects a pending ancestor.
96
+ // Once prepared, no resource can enter a folder while its durable move finishes.
97
+ await repo.update({ ...scoped(input.actor), id: item.id, version: input.expectedSourceVersion }, { syncStatus: 'pending', syncError: 'spaces.location_pending' });
98
+ });
99
+ }
100
+ async onLocationChanged(input) {
101
+ // Projection of a durable authorized operation: it must still finish if the initiating user leaves.
102
+ await this.access.items.manager.transaction(async (manager) => {
103
+ const repo = manager.getRepository(SpaceItem);
104
+ const item = await repo.findOneByOrFail({ ...scoped(input.actor), id: input.source.resourceId });
105
+ await manager.getRepository(Space).findOneOrFail({ where: { ...scoped(input.actor), id: item.spaceId, projectId: input.projectId }, lock: { mode: 'pessimistic_write' } });
106
+ const current = await repo.findOneByOrFail({ ...scoped(input.actor), id: item.id });
107
+ if (current.version === input.expectedSourceVersion + 1 && current.filePath === input.destination && current.deleted === input.deleted)
108
+ return;
109
+ if (current.version !== input.expectedSourceVersion || current.filePath !== input.path)
110
+ throw new ConflictException('spaces.version_conflict');
111
+ const descendants = await repo.findBy({ ...scoped(input.actor), spaceId: current.spaceId });
112
+ for (const child of descendants) {
113
+ if (!child.filePath?.startsWith(`${input.path}/`))
114
+ continue;
115
+ child.filePath = input.destination + child.filePath.slice(input.path.length);
116
+ child.deleted = input.deleted;
117
+ child.version++;
118
+ await repo.save(child);
119
+ }
120
+ current.filePath = input.destination;
121
+ current.title = input.title;
122
+ current.parentId = input.parentResourceId;
123
+ current.deleted = input.deleted;
124
+ current.version++;
125
+ current.syncStatus = 'ready';
126
+ current.syncError = null;
127
+ await repo.save(current);
128
+ });
129
+ }
130
+ async onDocumentPublished(input) {
131
+ const actor = { ...input.actor, projectId: input.projectId };
132
+ const { item, space } = await this.access.resource(actor, input.sourceResourceId, 'write');
133
+ if (!item || item.deleted || space.projectId !== input.projectId || item.filePath !== input.path)
134
+ return;
135
+ const disk = await this.documents.read(actor, space, item);
136
+ if (!disk || disk.sha256 !== input.hash)
137
+ return;
138
+ await this.access.items.update(item.id, { fileHash: input.hash, projectedSequence: input.sequence,
139
+ markdown: disk.buffer.toString('utf8'), syncStatus: 'ready', syncError: null });
140
+ await this.knowledge.sync(actor, space, item);
141
+ const indexed = await this.access.items.findOneByOrFail({ ...scoped(actor), id: item.id });
142
+ if (indexed.indexStatus === 'failed')
143
+ throw new Error('spaces.index_retry_required');
144
+ }
145
+ async authorize(input) {
146
+ const resourceId = input.resourceId;
147
+ const result = await this.access.resolve(input.principal, resourceId, input.operation);
148
+ // Resource IDs identify items. File paths only constrain that item's Project location.
149
+ if (input.filePath && input.filePath !== result.item?.filePath)
150
+ throw denied();
151
+ return { providerKey: 'spaces', resourceId, title: result.item?.title ?? result.space.title,
152
+ role: result.role, canWrite: result.canWrite, canManage: result.canManage };
153
+ }
154
+ };
155
+ SpacesResourceProvider = __decorate([
156
+ Injectable(),
157
+ ContentResourceProvider('spaces'),
158
+ __metadata("design:paramtypes", [SpaceAccess, SpaceDocuments, SpaceKnowledge, SpaceSubpages, SpaceReferences, SpaceFiles, SpaceItemCatalog])
159
+ ], SpacesResourceProvider);
160
+ export { SpacesResourceProvider };
@@ -0,0 +1,95 @@
1
+ import type { SpaceActor } from './domain.js';
2
+ import { SpaceArtifacts } from './artifacts.js';
3
+ import { Repository } from 'typeorm';
4
+ import { z } from 'zod/v3';
5
+ import { Space, SpaceItem } from './entities.js';
6
+ import { SpaceAccess } from './access.js';
7
+ import { SpaceDocuments } from './documents.js';
8
+ import { SpaceKnowledge } from './knowledge.js';
9
+ export declare const createItemSchema: z.ZodObject<{
10
+ spaceId: z.ZodString;
11
+ parentId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
12
+ title: z.ZodString;
13
+ kind: z.ZodEnum<["folder", "page", "file", "artifact"]>;
14
+ artifactId: z.ZodOptional<z.ZodString>;
15
+ artifactVersionId: z.ZodOptional<z.ZodString>;
16
+ content: z.ZodOptional<z.ZodString>;
17
+ }, "strict", z.ZodTypeAny, {
18
+ kind: "folder" | "page" | "file" | "artifact";
19
+ title: string;
20
+ spaceId: string;
21
+ parentId: string | null;
22
+ artifactId?: string | undefined;
23
+ artifactVersionId?: string | undefined;
24
+ content?: string | undefined;
25
+ }, {
26
+ kind: "folder" | "page" | "file" | "artifact";
27
+ title: string;
28
+ spaceId: string;
29
+ parentId?: string | null | undefined;
30
+ artifactId?: string | undefined;
31
+ artifactVersionId?: string | undefined;
32
+ content?: string | undefined;
33
+ }>;
34
+ export declare const editItemSchema: z.ZodObject<{
35
+ itemId: z.ZodString;
36
+ expectedVersion: z.ZodNumber;
37
+ title: z.ZodOptional<z.ZodString>;
38
+ parentId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
39
+ deleted: z.ZodOptional<z.ZodBoolean>;
40
+ }, "strict", z.ZodTypeAny, {
41
+ itemId: string;
42
+ expectedVersion: number;
43
+ title?: string | undefined;
44
+ deleted?: boolean | undefined;
45
+ parentId?: string | null | undefined;
46
+ }, {
47
+ itemId: string;
48
+ expectedVersion: number;
49
+ title?: string | undefined;
50
+ deleted?: boolean | undefined;
51
+ parentId?: string | null | undefined;
52
+ }>;
53
+ export declare class SpacesService {
54
+ private readonly access;
55
+ private readonly artifacts;
56
+ private readonly documents;
57
+ private readonly knowledge;
58
+ private readonly spaces;
59
+ constructor(access: SpaceAccess, artifacts: SpaceArtifacts, documents: SpaceDocuments, knowledge: SpaceKnowledge, spaces: Repository<Space>);
60
+ /** One extension record per authorized Project, shared by all connected Assistants. */
61
+ workspace(actor: SpaceActor): Promise<{
62
+ title: string;
63
+ projectId: string;
64
+ knowledgebaseId: string | null;
65
+ ownerId: string;
66
+ visibility: "personal" | "team";
67
+ version: number;
68
+ deleted: boolean;
69
+ id: string;
70
+ tenantId: string;
71
+ organizationId: string;
72
+ createdAt: Date;
73
+ updatedAt: Date;
74
+ }>;
75
+ list(actor: SpaceActor): Promise<{
76
+ root: Space | SpaceItem;
77
+ role: "editor" | "manager" | "owner" | "member";
78
+ canWrite: boolean;
79
+ canManage: boolean;
80
+ items: (SpaceItem & {
81
+ role: string;
82
+ })[];
83
+ }>;
84
+ tree(actor: SpaceActor, resourceId: string, trash?: boolean): Promise<{
85
+ root: Space | SpaceItem;
86
+ role: "editor" | "manager" | "owner" | "member";
87
+ canWrite: boolean;
88
+ canManage: boolean;
89
+ items: (SpaceItem & {
90
+ role: string;
91
+ })[];
92
+ }>;
93
+ createItem(actor: SpaceActor, input: z.infer<typeof createItemSchema>): Promise<SpaceItem>;
94
+ editItem(actor: SpaceActor, input: z.infer<typeof editItemSchema>): Promise<SpaceItem>;
95
+ }
package/dist/spaces.js ADDED
@@ -0,0 +1,233 @@
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 { SpaceArtifacts } from './artifacts.js';
14
+ import { ConflictException, Injectable } from '@nestjs/common';
15
+ import { InjectRepository } from '@nestjs/typeorm';
16
+ import { Repository } from 'typeorm';
17
+ import { randomUUID } from 'node:crypto';
18
+ import { z } from 'zod/v3';
19
+ import { Space, SpaceItem } from './entities.js';
20
+ import { SpaceAccess } from './access.js';
21
+ import { denied, id, kindSchema, scoped, title } from './domain.js';
22
+ import { isInternalSpacePath, SpaceDocuments } from './documents.js';
23
+ import { coveredByContent } from './content-coverage.js';
24
+ import { SpaceKnowledge } from './knowledge.js';
25
+ import { assertFolderHasNoResources } from './resource-folders.js';
26
+ export const createItemSchema = z.object({ spaceId: id, parentId: id.nullable().default(null), title,
27
+ kind: kindSchema, artifactId: id.optional(), artifactVersionId: id.optional(), content: z.string().max(100000).optional() }).strict();
28
+ export const editItemSchema = z.object({ itemId: id, expectedVersion: z.number().int().positive(),
29
+ title: title.optional(), parentId: id.nullable().optional(), deleted: z.boolean().optional() }).strict();
30
+ let SpacesService = class SpacesService {
31
+ access;
32
+ artifacts;
33
+ documents;
34
+ knowledge;
35
+ spaces;
36
+ constructor(access, artifacts, documents, knowledge, spaces) {
37
+ this.access = access;
38
+ this.artifacts = artifacts;
39
+ this.documents = documents;
40
+ this.knowledge = knowledge;
41
+ this.spaces = spaces;
42
+ }
43
+ /** One extension record per authorized Project, shared by all connected Assistants. */
44
+ async workspace(actor) {
45
+ const project = await this.access.project(actor);
46
+ const where = { ...scoped(actor), projectId: project.projectId };
47
+ await this.spaces.createQueryBuilder().insert().values({ ...where,
48
+ title: project.name ?? 'Space', ownerId: actor.userId, visibility: 'team'
49
+ }).orIgnore().execute();
50
+ const space = await this.spaces.findOneByOrFail(where);
51
+ if (space.deleted)
52
+ throw denied();
53
+ return { ...space, title: project.name ?? space.title };
54
+ }
55
+ async list(actor) {
56
+ const space = await this.workspace(actor);
57
+ return this.tree(actor, space.id);
58
+ }
59
+ async tree(actor, resourceId, trash = false) {
60
+ const principal = await this.access.principal(actor);
61
+ const root = await this.access.resolve(principal, resourceId, trash ? 'manage' : 'read');
62
+ const coverage = await this.documents.resourceCoverage(actor, root.space);
63
+ if (!trash)
64
+ await this.spaces.manager.transaction(async (manager) => {
65
+ await manager.getRepository(Space).findOneOrFail({ where: { ...scoped(actor), id: root.space.id }, lock: { mode: 'pessimistic_write' } });
66
+ await this.documents.discover(actor, root.space, root.canWrite, coverage);
67
+ });
68
+ const all = await this.access.items.find({ where: { ...scoped(actor), spaceId: root.space.id }, order: { createdAt: 'ASC' } });
69
+ const items = [];
70
+ const descendants = new Set([resourceId]);
71
+ // Traverse by identity, never by a path supplied by the client.
72
+ for (let round = 0; round <= all.length; round++) {
73
+ let changed = false;
74
+ for (const item of all)
75
+ if (!descendants.has(item.id) && (resourceId === root.space.id || (item.parentId && descendants.has(item.parentId)))) {
76
+ descendants.add(item.id);
77
+ changed = true;
78
+ }
79
+ if (!changed)
80
+ break;
81
+ }
82
+ for (const item of all) {
83
+ if (!descendants.has(item.id) || item.deleted !== trash ||
84
+ (!trash && item.filePath && (isInternalSpacePath(item.filePath) || coveredByContent(item.filePath, coverage))))
85
+ continue;
86
+ try {
87
+ const result = await this.access.resolve(principal, item.id, 'read', trash);
88
+ items.push({ ...item, role: result.role, parentId: item.id === resourceId ? null : item.parentId });
89
+ }
90
+ catch (error) {
91
+ if (!(error instanceof Error) || error.message !== 'spaces.access_denied')
92
+ throw error;
93
+ }
94
+ }
95
+ return { root: root.item ?? root.space, role: root.role, canWrite: root.canWrite, canManage: root.canManage, items };
96
+ }
97
+ async createItem(actor, input) {
98
+ const access = await this.access.resource(actor, input.parentId ?? input.spaceId, 'write');
99
+ if (access.space.id !== input.spaceId || (access.item && access.item.kind !== 'folder'))
100
+ throw denied();
101
+ if (input.kind !== 'page' && input.content !== undefined)
102
+ throw denied();
103
+ const { content, ...fields } = input;
104
+ {
105
+ const item = await this.access.items.save(this.access.items.create({
106
+ ...scoped(actor), ...fields, id: randomUUID(), createdById: actor.userId
107
+ }));
108
+ try {
109
+ await this.documents.initialize(actor, access.space, item, input.kind === 'page' ? Buffer.from(content ?? '', 'utf8') : undefined);
110
+ }
111
+ catch (error) {
112
+ await this.access.items.update(item.id, { deleted: true });
113
+ if (error && typeof error === 'object' && 'code' in error && error.code === '23505')
114
+ throw new ConflictException('spaces.file_name_conflict');
115
+ throw error;
116
+ }
117
+ if (input.kind === 'page') {
118
+ try {
119
+ item.artifactId = await this.artifacts.initializePage(actor, item.id, item.title);
120
+ }
121
+ catch (error) {
122
+ await this.access.items.update(item.id, { deleted: true });
123
+ throw error;
124
+ }
125
+ }
126
+ return item;
127
+ }
128
+ }
129
+ async editItem(actor, input) {
130
+ const result = await this.access.resource(actor, input.itemId, input.deleted !== undefined ? 'manage' : 'write', input.deleted === false);
131
+ if (!result.item)
132
+ throw denied();
133
+ if (result.space.projectId && result.item.filePath) {
134
+ const source = result.item;
135
+ const sourcePath = result.item.filePath;
136
+ const descendants = await this.access.items.findBy({ ...scoped(actor), spaceId: result.space.id });
137
+ if (source.editorKey === 'markdown.tiptap' || descendants.some((item) => item.editorKey === 'markdown.tiptap' && item.filePath?.startsWith(`${source.filePath}/`))) {
138
+ const target = Object.assign(new SpaceItem(), source, { title: input.title ?? source.title,
139
+ parentId: input.parentId === undefined ? source.parentId : input.parentId, deleted: input.deleted ?? source.deleted });
140
+ const destination = input.deleted === true ? `.space-trash/${source.id}/${sourcePath.split('/').pop()}` : await this.documents.path(actor, target);
141
+ await this.documents.content().move({ actor, assistantId: actor.assistantId, projectId: result.space.projectId,
142
+ source: { providerKey: 'spaces', resourceId: source.id }, path: sourcePath, destination,
143
+ title: target.title, parentResourceId: target.parentId, deleted: target.deleted, expectedSourceVersion: input.expectedVersion });
144
+ return this.access.items.findOneByOrFail({ ...scoped(actor), id: source.id });
145
+ }
146
+ }
147
+ let moved;
148
+ let changed;
149
+ let updated;
150
+ try {
151
+ updated = await this.spaces.manager.transaction(async (manager) => {
152
+ changed = [];
153
+ // A single Space lock prevents concurrent moves from introducing a cycle.
154
+ await manager.getRepository(Space).findOneOrFail({ where: { ...scoped(actor), id: result.space.id }, lock: { mode: 'pessimistic_write' } });
155
+ await this.access.resource(actor, input.itemId, input.deleted !== undefined ? 'manage' : 'write', input.deleted === false);
156
+ const repo = manager.getRepository(SpaceItem);
157
+ const item = await repo.findOneByOrFail({ ...scoped(actor), id: input.itemId });
158
+ if (input.deleted === true)
159
+ await assertFolderHasNoResources(manager, actor, item);
160
+ const previousPath = item.filePath;
161
+ if (item.version !== input.expectedVersion)
162
+ throw new ConflictException('spaces.version_conflict');
163
+ if (input.parentId !== undefined) {
164
+ const parent = await this.access.resource(actor, input.parentId ?? item.spaceId, 'write');
165
+ if (parent.space.id !== item.spaceId || (parent.item && parent.item.kind !== 'folder'))
166
+ throw denied();
167
+ let cursor = parent.item;
168
+ while (cursor) {
169
+ if (cursor.id === item.id)
170
+ throw new ConflictException('spaces.tree_cycle');
171
+ cursor = cursor.parentId ? await repo.findOneByOrFail({ ...scoped(actor), id: cursor.parentId }) : null;
172
+ }
173
+ item.parentId = input.parentId;
174
+ }
175
+ if (input.title !== undefined)
176
+ item.title = input.title;
177
+ if (input.deleted !== undefined)
178
+ item.deleted = input.deleted;
179
+ if (result.space.projectId && previousPath) {
180
+ const targetPath = input.deleted === true ? `.space-trash/${item.id}/${previousPath.split('/').pop()}`
181
+ : await this.documents.path(actor, item);
182
+ if (targetPath !== previousPath) {
183
+ await this.documents.api().move({ ...this.documents.scope(actor, result.space), path: previousPath, destination: targetPath });
184
+ moved = { path: targetPath, destination: previousPath };
185
+ const children = await repo.findBy({ ...scoped(actor), spaceId: item.spaceId });
186
+ for (const child of children) {
187
+ if (!child.filePath?.startsWith(`${previousPath}/`))
188
+ continue;
189
+ child.filePath = `${targetPath}${child.filePath.slice(previousPath.length)}`;
190
+ if (input.deleted !== undefined)
191
+ child.deleted = input.deleted;
192
+ child.version++;
193
+ await repo.save(child);
194
+ changed.push(child);
195
+ }
196
+ item.filePath = targetPath;
197
+ }
198
+ }
199
+ item.version++;
200
+ return repo.save(item);
201
+ });
202
+ }
203
+ catch (error) {
204
+ if (moved) {
205
+ try {
206
+ await this.documents.api().move({ ...this.documents.scope(actor, result.space), ...moved });
207
+ }
208
+ catch {
209
+ await this.access.items.update(input.itemId, { syncStatus: 'failed', syncError: 'spaces.move_recovery_required' });
210
+ }
211
+ }
212
+ throw error;
213
+ }
214
+ for (const item of [updated, ...changed]) {
215
+ if (item.editorKey === 'markdown.tiptap' && result.space.projectId && item.filePath) {
216
+ const scope = { actor, projectId: result.space.projectId, assistantId: actor.assistantId, resourceId: item.id };
217
+ if (input.deleted !== undefined)
218
+ await this.documents.content().setDeleted({ ...scope, deleted: input.deleted });
219
+ if (!item.deleted)
220
+ await this.documents.content().relocate({ ...scope, path: item.filePath, title: item.title });
221
+ }
222
+ await this.knowledge.sync(actor, result.space, item);
223
+ }
224
+ return updated;
225
+ }
226
+ };
227
+ SpacesService = __decorate([
228
+ Injectable(),
229
+ __param(4, InjectRepository(Space)),
230
+ __metadata("design:paramtypes", [SpaceAccess, SpaceArtifacts, SpaceDocuments,
231
+ SpaceKnowledge, Repository])
232
+ ], SpacesService);
233
+ export { SpacesService };
@@ -0,0 +1,24 @@
1
+ import type { ContentSourceInput } from '@xpert-ai/plugin-sdk';
2
+ import type { ContentPageHierarchy } from '@xpert-ai/contracts';
3
+ import { SpaceAccess } from './access.js';
4
+ import { SpaceDocuments } from './documents.js';
5
+ import { SpaceArtifacts } from './artifacts.js';
6
+ /** Logical hierarchy is metadata, never an ACL or a disguised file folder.
7
+ * Each create owns one stable resource/path. Retry resumes publication instead of duplicating a child.
8
+ * Parent rename/move/trash does not mutate independent child files; a restored parent reconnects them.
9
+ */
10
+ export declare class SpaceSubpages {
11
+ private readonly access;
12
+ private readonly documents;
13
+ private readonly artifacts;
14
+ constructor(access: SpaceAccess, documents: SpaceDocuments, artifacts: SpaceArtifacts);
15
+ private actor;
16
+ private parent;
17
+ hierarchy(input: ContentSourceInput): Promise<ContentPageHierarchy>;
18
+ createChild(input: ContentSourceInput & {
19
+ title: string;
20
+ requestId: string;
21
+ }): Promise<{
22
+ documentId: string;
23
+ }>;
24
+ }