@brew-cms/api 0.1.0 → 0.2.2
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/dist/errors.d.ts +13 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +29 -0
- package/dist/errors.js.map +1 -0
- package/dist/idempotency.d.ts +13 -0
- package/dist/idempotency.d.ts.map +1 -0
- package/dist/idempotency.js +26 -0
- package/dist/idempotency.js.map +1 -0
- package/{src/index.ts → dist/index.d.ts} +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js.map +1 -0
- package/dist/router.d.ts +34 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +436 -0
- package/dist/router.js.map +1 -0
- package/dist/validators.d.ts +155 -0
- package/dist/validators.d.ts.map +1 -0
- package/dist/validators.js +57 -0
- package/dist/validators.js.map +1 -0
- package/package.json +9 -5
- package/src/__tests__/api.test.ts +0 -416
- package/src/errors.ts +0 -42
- package/src/idempotency.ts +0 -41
- package/src/router.ts +0 -538
- package/src/validators.ts +0 -63
- package/tsconfig.json +0 -14
package/src/errors.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { DomainError } from '@brew-cms/core';
|
|
2
|
-
|
|
3
|
-
export interface ApiErrorEnvelope {
|
|
4
|
-
error: {
|
|
5
|
-
code: string;
|
|
6
|
-
message: string;
|
|
7
|
-
requestId?: string;
|
|
8
|
-
details?: unknown;
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function formatErrorResponse(
|
|
13
|
-
err: unknown,
|
|
14
|
-
requestId?: string
|
|
15
|
-
): { status: number; body: ApiErrorEnvelope } {
|
|
16
|
-
if (err instanceof DomainError) {
|
|
17
|
-
return {
|
|
18
|
-
status: err.statusCode,
|
|
19
|
-
body: {
|
|
20
|
-
error: {
|
|
21
|
-
code: err.code,
|
|
22
|
-
message: err.message,
|
|
23
|
-
requestId,
|
|
24
|
-
details: err.details,
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Fallback for unexpected internal errors: NEVER expose raw stack traces or internal SQL details
|
|
31
|
-
const fallbackMessage = err instanceof Error ? err.message : 'An unexpected internal error occurred.';
|
|
32
|
-
return {
|
|
33
|
-
status: 500,
|
|
34
|
-
body: {
|
|
35
|
-
error: {
|
|
36
|
-
code: 'INTERNAL_ERROR',
|
|
37
|
-
message: fallbackMessage,
|
|
38
|
-
requestId,
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
}
|
package/src/idempotency.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
export interface IdempotencyRecord {
|
|
2
|
-
key: string;
|
|
3
|
-
actorId: string;
|
|
4
|
-
responseStatus: number;
|
|
5
|
-
responseBody: unknown;
|
|
6
|
-
expiresAt: Date;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export class InMemoryIdempotencyStore {
|
|
10
|
-
private cache = new Map<string, IdempotencyRecord>();
|
|
11
|
-
|
|
12
|
-
get(key: string, actorId: string): IdempotencyRecord | null {
|
|
13
|
-
const compositeKey = `${actorId}:${key}`;
|
|
14
|
-
const record = this.cache.get(compositeKey);
|
|
15
|
-
if (!record) return null;
|
|
16
|
-
|
|
17
|
-
if (record.expiresAt < new Date()) {
|
|
18
|
-
this.cache.delete(compositeKey);
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
return record;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
set(
|
|
25
|
-
key: string,
|
|
26
|
-
actorId: string,
|
|
27
|
-
responseStatus: number,
|
|
28
|
-
responseBody: unknown,
|
|
29
|
-
ttlSeconds: number = 3600
|
|
30
|
-
): void {
|
|
31
|
-
const compositeKey = `${actorId}:${key}`;
|
|
32
|
-
const expiresAt = new Date(Date.now() + ttlSeconds * 1000);
|
|
33
|
-
this.cache.set(compositeKey, {
|
|
34
|
-
key,
|
|
35
|
-
actorId,
|
|
36
|
-
responseStatus,
|
|
37
|
-
responseBody,
|
|
38
|
-
expiresAt,
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
}
|
package/src/router.ts
DELETED
|
@@ -1,538 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Actor,
|
|
3
|
-
DocumentService,
|
|
4
|
-
WorkflowService,
|
|
5
|
-
AgentService,
|
|
6
|
-
AuditService,
|
|
7
|
-
TaxonomyService,
|
|
8
|
-
RevisionRepository,
|
|
9
|
-
MediaProvider,
|
|
10
|
-
MediaAssetRepository,
|
|
11
|
-
AgentRepository,
|
|
12
|
-
} from '@brew-cms/core';
|
|
13
|
-
import type {
|
|
14
|
-
SourceRegistry,
|
|
15
|
-
IndexingService,
|
|
16
|
-
HybridRetrievalService,
|
|
17
|
-
CanonicalSourceResolver,
|
|
18
|
-
} from '@brew-cms/intelligence';
|
|
19
|
-
import { compileContent } from '@brew-cms/content';
|
|
20
|
-
import { formatErrorResponse } from './errors.js';
|
|
21
|
-
import { InMemoryIdempotencyStore } from './idempotency.js';
|
|
22
|
-
import {
|
|
23
|
-
CreateDocumentRequestSchema,
|
|
24
|
-
UpdateDocumentRequestSchema,
|
|
25
|
-
PublishDocumentRequestSchema,
|
|
26
|
-
ScheduleDocumentRequestSchema,
|
|
27
|
-
CreateTopicRequestSchema,
|
|
28
|
-
CreateTagRequestSchema,
|
|
29
|
-
CreateSeriesRequestSchema,
|
|
30
|
-
} from './validators.js';
|
|
31
|
-
|
|
32
|
-
export interface ApiRequest {
|
|
33
|
-
method: string;
|
|
34
|
-
path: string;
|
|
35
|
-
query?: Record<string, string | undefined>;
|
|
36
|
-
headers?: Record<string, string | undefined>;
|
|
37
|
-
body?: unknown;
|
|
38
|
-
actor: Actor;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface ApiResponse {
|
|
42
|
-
status: number;
|
|
43
|
-
headers?: Record<string, string>;
|
|
44
|
-
body: unknown;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface ApiContext {
|
|
48
|
-
documentService: DocumentService;
|
|
49
|
-
workflowService: WorkflowService;
|
|
50
|
-
agentService: AgentService;
|
|
51
|
-
auditService: AuditService;
|
|
52
|
-
taxonomyService: TaxonomyService;
|
|
53
|
-
revisionRepo: RevisionRepository;
|
|
54
|
-
mediaProvider?: MediaProvider;
|
|
55
|
-
mediaRepo?: MediaAssetRepository;
|
|
56
|
-
agentRepo?: AgentRepository;
|
|
57
|
-
idempotencyStore?: InMemoryIdempotencyStore;
|
|
58
|
-
sourceRegistry?: SourceRegistry;
|
|
59
|
-
indexingService?: IndexingService;
|
|
60
|
-
retrievalService?: HybridRetrievalService;
|
|
61
|
-
canonicalResolver?: CanonicalSourceResolver;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export async function handleApiRequest(
|
|
65
|
-
req: ApiRequest,
|
|
66
|
-
ctx: ApiContext
|
|
67
|
-
): Promise<ApiResponse> {
|
|
68
|
-
const idempotencyKey = req.headers?.['idempotency-key'];
|
|
69
|
-
const idempotencyStore = ctx.idempotencyStore;
|
|
70
|
-
|
|
71
|
-
// 1. Idempotency Check for mutations
|
|
72
|
-
if (idempotencyKey && req.method !== 'GET' && idempotencyStore) {
|
|
73
|
-
const cached = idempotencyStore.get(idempotencyKey, req.actor.id);
|
|
74
|
-
if (cached) {
|
|
75
|
-
return {
|
|
76
|
-
status: cached.responseStatus,
|
|
77
|
-
headers: { 'X-Cache-Lookup': 'HIT', 'Idempotency-Key': idempotencyKey },
|
|
78
|
-
body: cached.responseBody,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
const response = await dispatchRoute(req, ctx);
|
|
85
|
-
|
|
86
|
-
// Save in idempotency store if mutation
|
|
87
|
-
if (idempotencyKey && req.method !== 'GET' && idempotencyStore && response.status < 400) {
|
|
88
|
-
idempotencyStore.set(idempotencyKey, req.actor.id, response.status, response.body);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return response;
|
|
92
|
-
} catch (err: any) {
|
|
93
|
-
const errorFormatted = formatErrorResponse(err, req.headers?.['x-request-id']);
|
|
94
|
-
return {
|
|
95
|
-
status: errorFormatted.status,
|
|
96
|
-
headers: { 'Content-Type': 'application/json' },
|
|
97
|
-
body: errorFormatted.body,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async function dispatchRoute(req: ApiRequest, ctx: ApiContext): Promise<ApiResponse> {
|
|
103
|
-
const { path: rawPath, method, actor } = req;
|
|
104
|
-
const urlPath = rawPath.replace(/\/$/, '');
|
|
105
|
-
|
|
106
|
-
// 1. Health Endpoints
|
|
107
|
-
if (urlPath === '/api/health/live' && method === 'GET') {
|
|
108
|
-
return { status: 200, body: { status: 'live', timestamp: new Date().toISOString() } };
|
|
109
|
-
}
|
|
110
|
-
if (urlPath === '/api/health/ready' && method === 'GET') {
|
|
111
|
-
return { status: 200, body: { status: 'ready', database: 'connected', timestamp: new Date().toISOString() } };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// 2. Documents
|
|
115
|
-
if (urlPath === '/api/v1/documents') {
|
|
116
|
-
if (method === 'GET') {
|
|
117
|
-
const result = await ctx.documentService.listDocuments({
|
|
118
|
-
type: req.query?.type as any,
|
|
119
|
-
status: req.query?.status as any,
|
|
120
|
-
query: req.query?.q,
|
|
121
|
-
limit: req.query?.limit ? Number(req.query.limit) : undefined,
|
|
122
|
-
});
|
|
123
|
-
return { status: 200, body: result };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (method === 'POST') {
|
|
127
|
-
const validated = CreateDocumentRequestSchema.parse(req.body);
|
|
128
|
-
const compiled = compileContent(validated.sourceMarkdown, {
|
|
129
|
-
frontmatterDefaults: validated.frontmatter,
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
const res = await ctx.documentService.createDraft(actor, {
|
|
133
|
-
type: validated.type,
|
|
134
|
-
slug: validated.slug,
|
|
135
|
-
title: validated.title,
|
|
136
|
-
excerpt: validated.excerpt,
|
|
137
|
-
sourceMarkdown: compiled.sourceMarkdown,
|
|
138
|
-
frontmatter: compiled.frontmatter,
|
|
139
|
-
contentIr: compiled.contentIr,
|
|
140
|
-
contentHash: compiled.contentHash,
|
|
141
|
-
compilerVersion: compiled.compilerVersion,
|
|
142
|
-
wordCount: compiled.wordCount,
|
|
143
|
-
readingTimeSeconds: compiled.readingTimeSeconds,
|
|
144
|
-
seo: validated.seo,
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
return { status: 201, body: res };
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Document by ID: /api/v1/documents/:id
|
|
152
|
-
const docIdMatch = urlPath.match(/^\/api\/v1\/documents\/([^/]+)$/);
|
|
153
|
-
if (docIdMatch) {
|
|
154
|
-
const id = docIdMatch[1];
|
|
155
|
-
if (method === 'GET') {
|
|
156
|
-
const doc = await ctx.documentService.getDocument(id);
|
|
157
|
-
return { status: 200, body: doc };
|
|
158
|
-
}
|
|
159
|
-
if (method === 'PUT') {
|
|
160
|
-
const validated = UpdateDocumentRequestSchema.parse(req.body);
|
|
161
|
-
let compiled: ReturnType<typeof compileContent> | undefined;
|
|
162
|
-
if (validated.sourceMarkdown !== undefined) {
|
|
163
|
-
compiled = compileContent(validated.sourceMarkdown, {
|
|
164
|
-
frontmatterDefaults: validated.frontmatter,
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const res = await ctx.documentService.updateDraft(actor, id, {
|
|
169
|
-
title: validated.title,
|
|
170
|
-
slug: validated.slug,
|
|
171
|
-
excerpt: validated.excerpt,
|
|
172
|
-
sourceMarkdown: compiled?.sourceMarkdown,
|
|
173
|
-
frontmatter: compiled?.frontmatter,
|
|
174
|
-
contentIr: compiled?.contentIr,
|
|
175
|
-
contentHash: compiled?.contentHash,
|
|
176
|
-
compilerVersion: compiled?.compilerVersion,
|
|
177
|
-
wordCount: compiled?.wordCount,
|
|
178
|
-
readingTimeSeconds: compiled?.readingTimeSeconds,
|
|
179
|
-
seo: validated.seo,
|
|
180
|
-
changeSummary: validated.changeSummary,
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
return { status: 200, body: res };
|
|
184
|
-
}
|
|
185
|
-
if (method === 'DELETE') {
|
|
186
|
-
await ctx.documentService.deleteDocument(actor, id);
|
|
187
|
-
return { status: 200, body: { success: true, id } };
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Revisions for document: /api/v1/documents/:id/revisions
|
|
192
|
-
const revMatch = urlPath.match(/^\/api\/v1\/documents\/([^/]+)\/revisions$/);
|
|
193
|
-
if (revMatch && method === 'GET') {
|
|
194
|
-
const id = revMatch[1];
|
|
195
|
-
const revisions = await ctx.revisionRepo.listByDocumentId(id);
|
|
196
|
-
return { status: 200, body: { revisions } };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Publish: /api/v1/documents/:id/publish
|
|
200
|
-
const publishMatch = urlPath.match(/^\/api\/v1\/documents\/([^/]+)\/publish$/);
|
|
201
|
-
if (publishMatch && method === 'POST') {
|
|
202
|
-
const id = publishMatch[1];
|
|
203
|
-
const validated = PublishDocumentRequestSchema.parse(req.body || {});
|
|
204
|
-
const res = await ctx.workflowService.publish(actor, id, validated.revisionId);
|
|
205
|
-
return { status: 200, body: res };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Submit for review: /api/v1/documents/:id/submit
|
|
209
|
-
const submitMatch = urlPath.match(/^\/api\/v1\/documents\/([^/]+)\/submit$/);
|
|
210
|
-
if (submitMatch && method === 'POST') {
|
|
211
|
-
const id = submitMatch[1];
|
|
212
|
-
const revisionId = (req.body as any)?.revisionId;
|
|
213
|
-
const res = await ctx.workflowService.submitForReview(actor, id, revisionId);
|
|
214
|
-
return { status: 200, body: res };
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Approve: /api/v1/documents/:id/approve
|
|
218
|
-
const approveMatch = urlPath.match(/^\/api\/v1\/documents\/([^/]+)\/approve$/);
|
|
219
|
-
if (approveMatch && method === 'POST') {
|
|
220
|
-
const id = approveMatch[1];
|
|
221
|
-
const revisionId = (req.body as any)?.revisionId;
|
|
222
|
-
const res = await ctx.workflowService.approve(actor, id, revisionId);
|
|
223
|
-
return { status: 200, body: res };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Request Changes: /api/v1/documents/:id/request-changes
|
|
227
|
-
const reqChangesMatch = urlPath.match(/^\/api\/v1\/documents\/([^/]+)\/request-changes$/);
|
|
228
|
-
if (reqChangesMatch && method === 'POST') {
|
|
229
|
-
const id = reqChangesMatch[1];
|
|
230
|
-
const reason = (req.body as any)?.reason;
|
|
231
|
-
const res = await ctx.workflowService.requestChanges(actor, id, reason);
|
|
232
|
-
return { status: 200, body: res };
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Unpublish: /api/v1/documents/:id/unpublish
|
|
236
|
-
const unpublishMatch = urlPath.match(/^\/api\/v1\/documents\/([^/]+)\/unpublish$/);
|
|
237
|
-
if (unpublishMatch && method === 'POST') {
|
|
238
|
-
const id = unpublishMatch[1];
|
|
239
|
-
const res = await ctx.workflowService.unpublish(actor, id);
|
|
240
|
-
return { status: 200, body: res };
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Archive: /api/v1/documents/:id/archive
|
|
244
|
-
const archiveMatch = urlPath.match(/^\/api\/v1\/documents\/([^/]+)\/archive$/);
|
|
245
|
-
if (archiveMatch && method === 'POST') {
|
|
246
|
-
const id = archiveMatch[1];
|
|
247
|
-
const res = await ctx.workflowService.archive(actor, id);
|
|
248
|
-
return { status: 200, body: res };
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Restore Revision: /api/v1/documents/:id/restore
|
|
252
|
-
const restoreMatch = urlPath.match(/^\/api\/v1\/documents\/([^/]+)\/restore$/);
|
|
253
|
-
if (restoreMatch && method === 'POST') {
|
|
254
|
-
const id = restoreMatch[1];
|
|
255
|
-
const targetRevId = (req.body as any)?.revisionId;
|
|
256
|
-
const changeSummary = (req.body as any)?.changeSummary;
|
|
257
|
-
const res = await ctx.workflowService.restoreRevision(actor, id, targetRevId, changeSummary);
|
|
258
|
-
return { status: 200, body: res };
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Schedule: /api/v1/documents/:id/schedule
|
|
262
|
-
const scheduleMatch = urlPath.match(/^\/api\/v1\/documents\/([^/]+)\/schedule$/);
|
|
263
|
-
if (scheduleMatch && method === 'POST') {
|
|
264
|
-
const id = scheduleMatch[1];
|
|
265
|
-
const validated = ScheduleDocumentRequestSchema.parse(req.body);
|
|
266
|
-
const res = await ctx.workflowService.schedule(
|
|
267
|
-
actor,
|
|
268
|
-
id,
|
|
269
|
-
new Date(validated.scheduledAt),
|
|
270
|
-
validated.revisionId
|
|
271
|
-
);
|
|
272
|
-
return { status: 200, body: res };
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Taxonomies: Topics
|
|
276
|
-
if (urlPath === '/api/v1/topics') {
|
|
277
|
-
if (method === 'GET') {
|
|
278
|
-
const items = await ctx.taxonomyService.listTopics();
|
|
279
|
-
return { status: 200, body: { items } };
|
|
280
|
-
}
|
|
281
|
-
if (method === 'POST') {
|
|
282
|
-
const validated = CreateTopicRequestSchema.parse(req.body);
|
|
283
|
-
const res = await ctx.taxonomyService.createTopic({
|
|
284
|
-
id: `top_${validated.slug}`,
|
|
285
|
-
...validated,
|
|
286
|
-
});
|
|
287
|
-
return { status: 201, body: res };
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Taxonomies: Tags
|
|
292
|
-
if (urlPath === '/api/v1/tags') {
|
|
293
|
-
if (method === 'GET') {
|
|
294
|
-
const items = await ctx.taxonomyService.listTags();
|
|
295
|
-
return { status: 200, body: { items } };
|
|
296
|
-
}
|
|
297
|
-
if (method === 'POST') {
|
|
298
|
-
const validated = CreateTagRequestSchema.parse(req.body);
|
|
299
|
-
const res = await ctx.taxonomyService.createTag({
|
|
300
|
-
id: `tag_${validated.slug}`,
|
|
301
|
-
...validated,
|
|
302
|
-
});
|
|
303
|
-
return { status: 201, body: res };
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Taxonomies: Series
|
|
308
|
-
if (urlPath === '/api/v1/series') {
|
|
309
|
-
if (method === 'GET') {
|
|
310
|
-
const items = await ctx.taxonomyService.listSeries();
|
|
311
|
-
return { status: 200, body: { items } };
|
|
312
|
-
}
|
|
313
|
-
if (method === 'POST') {
|
|
314
|
-
const validated = CreateSeriesRequestSchema.parse(req.body);
|
|
315
|
-
const res = await ctx.taxonomyService.createSeries({
|
|
316
|
-
id: `ser_${validated.slug}`,
|
|
317
|
-
...validated,
|
|
318
|
-
});
|
|
319
|
-
return { status: 201, body: res };
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Media: /api/v1/media
|
|
324
|
-
if (urlPath === '/api/v1/media') {
|
|
325
|
-
if (method === 'GET') {
|
|
326
|
-
const items = ctx.mediaRepo
|
|
327
|
-
? await ctx.mediaRepo.list({
|
|
328
|
-
mimeType: req.query?.mimeType,
|
|
329
|
-
limit: req.query?.limit ? Number(req.query.limit) : 50,
|
|
330
|
-
})
|
|
331
|
-
: [];
|
|
332
|
-
return { status: 200, body: { items } };
|
|
333
|
-
}
|
|
334
|
-
if (method === 'POST') {
|
|
335
|
-
const body = req.body as any;
|
|
336
|
-
if (!ctx.mediaRepo) {
|
|
337
|
-
return { status: 500, body: { error: { code: 'NO_MEDIA_REPO', message: 'Media repository not configured.' } } };
|
|
338
|
-
}
|
|
339
|
-
let stored: any = null;
|
|
340
|
-
if (ctx.mediaProvider && body.contentBase64) {
|
|
341
|
-
stored = await ctx.mediaProvider.put({
|
|
342
|
-
filename: body.filename,
|
|
343
|
-
mimeType: body.mimeType,
|
|
344
|
-
content: Buffer.from(body.contentBase64, 'base64'),
|
|
345
|
-
prefix: body.prefix,
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
const asset = await ctx.mediaRepo.create({
|
|
349
|
-
id: body.id || `med_${Date.now()}`,
|
|
350
|
-
provider: stored?.provider || 'local',
|
|
351
|
-
providerKey: stored?.providerKey || body.providerKey || body.filename,
|
|
352
|
-
mimeType: body.mimeType || 'application/octet-stream',
|
|
353
|
-
sizeBytes: stored?.sizeBytes || body.sizeBytes || 0,
|
|
354
|
-
width: body.width ?? null,
|
|
355
|
-
height: body.height ?? null,
|
|
356
|
-
altText: body.altText ?? null,
|
|
357
|
-
caption: body.caption ?? null,
|
|
358
|
-
metadata: body.metadata ?? null,
|
|
359
|
-
createdBy: actor.id,
|
|
360
|
-
});
|
|
361
|
-
return { status: 201, body: asset };
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Audit Events
|
|
366
|
-
if (urlPath === '/api/v1/audit' && method === 'GET') {
|
|
367
|
-
const events = await ctx.auditService.listEvents({
|
|
368
|
-
resourceType: req.query?.resourceType,
|
|
369
|
-
resourceId: req.query?.resourceId,
|
|
370
|
-
actorId: req.query?.actorId,
|
|
371
|
-
limit: req.query?.limit ? Number(req.query.limit) : 50,
|
|
372
|
-
});
|
|
373
|
-
return { status: 200, body: { items: events } };
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Agents: /api/v1/agents
|
|
377
|
-
if (urlPath === '/api/v1/agents') {
|
|
378
|
-
if (method === 'GET') {
|
|
379
|
-
const agents = await ctx.agentService.listAgents();
|
|
380
|
-
return { status: 200, body: { items: agents } };
|
|
381
|
-
}
|
|
382
|
-
if (method === 'POST') {
|
|
383
|
-
const body = req.body as any;
|
|
384
|
-
const agent = await ctx.agentService.registerAgent(actor, {
|
|
385
|
-
id: body.id,
|
|
386
|
-
name: body.name,
|
|
387
|
-
description: body.description,
|
|
388
|
-
scopes: body.scopes || [],
|
|
389
|
-
});
|
|
390
|
-
return { status: 201, body: agent };
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Agent Action Runs: /api/v1/agent/runs
|
|
395
|
-
if (urlPath === '/api/v1/agent/runs') {
|
|
396
|
-
if (method === 'GET') {
|
|
397
|
-
const runs = ctx.agentRepo
|
|
398
|
-
? await ctx.agentRepo.listActionRuns({ limit: req.query?.limit ? Number(req.query.limit) : 50 })
|
|
399
|
-
: [];
|
|
400
|
-
return { status: 200, body: { items: runs } };
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Agent Action Run Approval: /api/v1/agent/runs/:id/approve
|
|
405
|
-
const approveRunMatch = urlPath.match(/^\/api\/v1\/agent\/runs\/([^/]+)\/approve$/);
|
|
406
|
-
if (approveRunMatch && method === 'POST') {
|
|
407
|
-
const runId = approveRunMatch[1];
|
|
408
|
-
const res = await ctx.agentService.approveActionRun(actor, runId, (req.body as any)?.reason);
|
|
409
|
-
return { status: 200, body: res };
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// Agent Action Run Rejection: /api/v1/agent/runs/:id/reject
|
|
413
|
-
const rejectRunMatch = urlPath.match(/^\/api\/v1\/agent\/runs\/([^/]+)\/reject$/);
|
|
414
|
-
if (rejectRunMatch && method === 'POST') {
|
|
415
|
-
const runId = rejectRunMatch[1];
|
|
416
|
-
const res = await ctx.agentService.rejectActionRun(actor, runId, (req.body as any)?.reason);
|
|
417
|
-
return { status: 200, body: res };
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Intelligence: Status / Sources
|
|
421
|
-
if (urlPath === '/api/v1/intelligence/status' && method === 'GET') {
|
|
422
|
-
const registry = ctx.sourceRegistry ?? ctx.indexingService?.registry;
|
|
423
|
-
return {
|
|
424
|
-
status: 200,
|
|
425
|
-
body: {
|
|
426
|
-
status: 'operational',
|
|
427
|
-
hasIndexingService: Boolean(ctx.indexingService),
|
|
428
|
-
hasRetrievalService: Boolean(ctx.retrievalService),
|
|
429
|
-
hasCanonicalResolver: Boolean(ctx.canonicalResolver),
|
|
430
|
-
sources: registry
|
|
431
|
-
? registry.list().map((s) => ({
|
|
432
|
-
id: s.sourceId,
|
|
433
|
-
capabilities: s.capabilities,
|
|
434
|
-
}))
|
|
435
|
-
: [],
|
|
436
|
-
},
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Intelligence: Hybrid Retrieval
|
|
441
|
-
if (urlPath === '/api/v1/intelligence/search' && method === 'GET') {
|
|
442
|
-
if (!ctx.retrievalService) {
|
|
443
|
-
return {
|
|
444
|
-
status: 503,
|
|
445
|
-
body: {
|
|
446
|
-
error: {
|
|
447
|
-
code: 'INTELLIGENCE_UNAVAILABLE',
|
|
448
|
-
message: 'Retrieval service is not configured.',
|
|
449
|
-
},
|
|
450
|
-
},
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
const q = req.query?.q;
|
|
454
|
-
if (!q || typeof q !== 'string') {
|
|
455
|
-
return {
|
|
456
|
-
status: 400,
|
|
457
|
-
body: {
|
|
458
|
-
error: {
|
|
459
|
-
code: 'INVALID_QUERY',
|
|
460
|
-
message: "Query parameter 'q' is required.",
|
|
461
|
-
},
|
|
462
|
-
},
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
const limit = req.query?.limit ? Number(req.query.limit) : 10;
|
|
466
|
-
const minScore = req.query?.minScore ? Number(req.query.minScore) : undefined;
|
|
467
|
-
const sourceId = req.query?.sourceId;
|
|
468
|
-
|
|
469
|
-
const results = await ctx.retrievalService.search({
|
|
470
|
-
query: q,
|
|
471
|
-
limit,
|
|
472
|
-
minScore,
|
|
473
|
-
sourceId,
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
const shouldResolve = req.query?.resolve === 'true' && ctx.canonicalResolver;
|
|
477
|
-
if (shouldResolve && ctx.canonicalResolver) {
|
|
478
|
-
const resolved = await ctx.canonicalResolver.resolveMany(results);
|
|
479
|
-
return {
|
|
480
|
-
status: 200,
|
|
481
|
-
body: {
|
|
482
|
-
query: q,
|
|
483
|
-
total: results.length,
|
|
484
|
-
items: resolved.map((r) => ({
|
|
485
|
-
reference: r.reference,
|
|
486
|
-
canonical: r.content,
|
|
487
|
-
error: r.error,
|
|
488
|
-
})),
|
|
489
|
-
},
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
return {
|
|
494
|
-
status: 200,
|
|
495
|
-
body: {
|
|
496
|
-
query: q,
|
|
497
|
-
total: results.length,
|
|
498
|
-
items: results,
|
|
499
|
-
},
|
|
500
|
-
};
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// Intelligence: Indexing
|
|
504
|
-
if (urlPath === '/api/v1/intelligence/index' && method === 'POST') {
|
|
505
|
-
if (!ctx.indexingService) {
|
|
506
|
-
return {
|
|
507
|
-
status: 503,
|
|
508
|
-
body: {
|
|
509
|
-
error: {
|
|
510
|
-
code: 'INTELLIGENCE_UNAVAILABLE',
|
|
511
|
-
message: 'Indexing service is not configured.',
|
|
512
|
-
},
|
|
513
|
-
},
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
const body = (req.body || {}) as { sourceId?: string; contentId?: string; revisionId?: string };
|
|
517
|
-
if (body.sourceId && body.contentId) {
|
|
518
|
-
const result = await ctx.indexingService.indexDocument(body.sourceId, body.contentId, body.revisionId);
|
|
519
|
-
return { status: 200, body: { success: true, result } };
|
|
520
|
-
} else if (body.sourceId) {
|
|
521
|
-
const summary = await ctx.indexingService.syncSource(body.sourceId);
|
|
522
|
-
return { status: 200, body: { success: true, summary } };
|
|
523
|
-
} else {
|
|
524
|
-
const summaries = await ctx.indexingService.syncAll();
|
|
525
|
-
return { status: 200, body: { success: true, summaries } };
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
return {
|
|
530
|
-
status: 404,
|
|
531
|
-
body: {
|
|
532
|
-
error: {
|
|
533
|
-
code: 'ROUTE_NOT_FOUND',
|
|
534
|
-
message: `Endpoint '${method} ${urlPath}' was not found.`,
|
|
535
|
-
},
|
|
536
|
-
},
|
|
537
|
-
};
|
|
538
|
-
}
|
package/src/validators.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
export const CreateDocumentRequestSchema = z.object({
|
|
4
|
-
type: z.enum(['post', 'page']),
|
|
5
|
-
slug: z.string().min(1),
|
|
6
|
-
title: z.string().min(1),
|
|
7
|
-
excerpt: z.string().optional().nullable(),
|
|
8
|
-
sourceMarkdown: z.string(),
|
|
9
|
-
frontmatter: z.record(z.unknown()).optional(),
|
|
10
|
-
seo: z
|
|
11
|
-
.object({
|
|
12
|
-
metaTitle: z.string().optional(),
|
|
13
|
-
metaDescription: z.string().optional(),
|
|
14
|
-
ogImage: z.string().optional(),
|
|
15
|
-
noIndex: z.boolean().optional(),
|
|
16
|
-
})
|
|
17
|
-
.optional()
|
|
18
|
-
.nullable(),
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
export const UpdateDocumentRequestSchema = z.object({
|
|
22
|
-
title: z.string().optional(),
|
|
23
|
-
slug: z.string().optional(),
|
|
24
|
-
excerpt: z.string().optional().nullable(),
|
|
25
|
-
sourceMarkdown: z.string().optional(),
|
|
26
|
-
frontmatter: z.record(z.unknown()).optional(),
|
|
27
|
-
seo: z
|
|
28
|
-
.object({
|
|
29
|
-
metaTitle: z.string().optional(),
|
|
30
|
-
metaDescription: z.string().optional(),
|
|
31
|
-
ogImage: z.string().optional(),
|
|
32
|
-
noIndex: z.boolean().optional(),
|
|
33
|
-
})
|
|
34
|
-
.optional()
|
|
35
|
-
.nullable(),
|
|
36
|
-
changeSummary: z.string().optional().nullable(),
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
export const PublishDocumentRequestSchema = z.object({
|
|
40
|
-
revisionId: z.string().optional(),
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
export const ScheduleDocumentRequestSchema = z.object({
|
|
44
|
-
scheduledAt: z.string().datetime(),
|
|
45
|
-
revisionId: z.string().optional(),
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
export const CreateTopicRequestSchema = z.object({
|
|
49
|
-
slug: z.string().min(1),
|
|
50
|
-
name: z.string().min(1),
|
|
51
|
-
description: z.string().optional().nullable(),
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
export const CreateTagRequestSchema = z.object({
|
|
55
|
-
slug: z.string().min(1),
|
|
56
|
-
name: z.string().min(1),
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
export const CreateSeriesRequestSchema = z.object({
|
|
60
|
-
slug: z.string().min(1),
|
|
61
|
-
name: z.string().min(1),
|
|
62
|
-
description: z.string().optional().nullable(),
|
|
63
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../tsconfig.base.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"rootDir": "src",
|
|
5
|
-
"outDir": "dist"
|
|
6
|
-
},
|
|
7
|
-
"references": [
|
|
8
|
-
{ "path": "../core" },
|
|
9
|
-
{ "path": "../content" },
|
|
10
|
-
{ "path": "../policy" },
|
|
11
|
-
{ "path": "../intelligence" }
|
|
12
|
-
],
|
|
13
|
-
"include": ["src/**/*"]
|
|
14
|
-
}
|