@bsb/registry 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/.bsb/clients/service-bsb-registry.d.ts +348 -982
- package/lib/.bsb/clients/service-bsb-registry.d.ts.map +1 -1
- package/lib/.bsb/clients/service-bsb-registry.js +287 -291
- package/lib/.bsb/clients/service-bsb-registry.js.map +1 -1
- package/lib/plugins/service-bsb-registry/auth.d.ts +2 -2
- package/lib/plugins/service-bsb-registry/auth.d.ts.map +1 -1
- package/lib/plugins/service-bsb-registry/auth.js +5 -9
- package/lib/plugins/service-bsb-registry/auth.js.map +1 -1
- package/lib/plugins/service-bsb-registry/db/file.d.ts +2 -2
- package/lib/plugins/service-bsb-registry/db/file.d.ts.map +1 -1
- package/lib/plugins/service-bsb-registry/db/file.js +3 -40
- package/lib/plugins/service-bsb-registry/db/file.js.map +1 -1
- package/lib/plugins/service-bsb-registry/db/index.d.ts +1 -1
- package/lib/plugins/service-bsb-registry/db/index.d.ts.map +1 -1
- package/lib/plugins/service-bsb-registry/db/index.js +3 -6
- package/lib/plugins/service-bsb-registry/db/index.js.map +1 -1
- package/lib/plugins/service-bsb-registry/index.d.ts +377 -1020
- package/lib/plugins/service-bsb-registry/index.d.ts.map +1 -1
- package/lib/plugins/service-bsb-registry/index.js +58 -95
- package/lib/plugins/service-bsb-registry/index.js.map +1 -1
- package/lib/plugins/service-bsb-registry/types.d.ts +197 -517
- package/lib/plugins/service-bsb-registry/types.d.ts.map +1 -1
- package/lib/plugins/service-bsb-registry/types.js +164 -167
- package/lib/plugins/service-bsb-registry/types.js.map +1 -1
- package/lib/plugins/service-bsb-registry-ui/http-server.d.ts +3 -2
- package/lib/plugins/service-bsb-registry-ui/http-server.d.ts.map +1 -1
- package/lib/plugins/service-bsb-registry-ui/http-server.js +241 -181
- package/lib/plugins/service-bsb-registry-ui/http-server.js.map +1 -1
- package/lib/plugins/service-bsb-registry-ui/index.d.ts +27 -27
- package/lib/plugins/service-bsb-registry-ui/index.d.ts.map +1 -1
- package/lib/plugins/service-bsb-registry-ui/index.js +22 -26
- package/lib/plugins/service-bsb-registry-ui/index.js.map +1 -1
- package/lib/plugins/service-bsb-registry-ui/static/llms.txt +36 -0
- package/lib/schemas/service-bsb-registry-ui.json +1 -46
- package/lib/schemas/service-bsb-registry-ui.plugin.json +2 -47
- package/lib/schemas/service-bsb-registry.json +1903 -1693
- package/lib/schemas/service-bsb-registry.plugin.json +1 -47
- package/package.json +5 -8
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Registry UI & API HTTP Server (Event-Driven with Handlebars)
|
|
4
3
|
*
|
|
@@ -6,56 +5,49 @@
|
|
|
6
5
|
* using content negotiation (Accept header).
|
|
7
6
|
* Communicates with registry core via typed BsbRegistryClient.
|
|
8
7
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as fsp from 'node:fs/promises';
|
|
11
|
+
import { pipeline } from 'node:stream/promises';
|
|
12
|
+
import Fastify from 'fastify';
|
|
13
|
+
import fastifyCors from '@fastify/cors';
|
|
14
|
+
import fastifyMultipart from '@fastify/multipart';
|
|
15
|
+
import fastifyStatic from '@fastify/static';
|
|
16
|
+
import fastifyView from '@fastify/view';
|
|
17
|
+
import handlebars from 'handlebars';
|
|
18
|
+
import { marked } from 'marked';
|
|
19
|
+
import * as av from '@anyvali/js';
|
|
20
|
+
function objectSchema(shape) {
|
|
21
|
+
return av.object(shape, { unknownKeys: 'strip' });
|
|
22
|
+
}
|
|
23
|
+
function createValidator(schema, options) {
|
|
24
|
+
return {
|
|
25
|
+
safeParse(input) {
|
|
26
|
+
const normalized = options?.normalize ? options.normalize(input) : input;
|
|
27
|
+
const result = schema.safeParse(normalized);
|
|
28
|
+
if (!result.success) {
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
const extraIssues = options?.extraIssues?.(result.data) ?? [];
|
|
32
|
+
if (extraIssues.length > 0) {
|
|
33
|
+
return {
|
|
34
|
+
success: false,
|
|
35
|
+
issues: extraIssues,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
},
|
|
40
40
|
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const fastify_1 = __importDefault(require("fastify"));
|
|
52
|
-
const cors_1 = __importDefault(require("@fastify/cors"));
|
|
53
|
-
const multipart_1 = __importDefault(require("@fastify/multipart"));
|
|
54
|
-
const static_1 = __importDefault(require("@fastify/static"));
|
|
55
|
-
const view_1 = __importDefault(require("@fastify/view"));
|
|
56
|
-
const handlebars_1 = __importDefault(require("handlebars"));
|
|
57
|
-
const marked_1 = require("marked");
|
|
58
|
-
const zod_1 = require("zod");
|
|
41
|
+
}
|
|
42
|
+
function mapObjectInput(input, transform) {
|
|
43
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
44
|
+
return input;
|
|
45
|
+
}
|
|
46
|
+
return transform({ ...input });
|
|
47
|
+
}
|
|
48
|
+
function emptyStringToUndefined(value) {
|
|
49
|
+
return value === '' ? undefined : value;
|
|
50
|
+
}
|
|
59
51
|
// ============================================================================
|
|
60
52
|
// Zod Validation Schemas — all external input validated at the boundary
|
|
61
53
|
// ============================================================================
|
|
@@ -70,149 +62,206 @@ const semverPattern = /^\d{1,5}\.\d{1,5}\.\d{1,5}(-[a-zA-Z0-9.]+)?$/;
|
|
|
70
62
|
const majorMinorPattern = /^\d{1,5}\.\d{1,5}$/;
|
|
71
63
|
// Package name: npm-style scoped or unscoped
|
|
72
64
|
const packageNamePattern = /^(@[a-zA-Z0-9_-]+\/)?[a-zA-Z0-9._-]+$/;
|
|
73
|
-
const slugField =
|
|
74
|
-
const semverField =
|
|
75
|
-
const languageEnum =
|
|
76
|
-
const categoryEnum =
|
|
77
|
-
const visibilityEnum =
|
|
78
|
-
const safeString = (max) =>
|
|
79
|
-
const safeStringRequired = (min, max) =>
|
|
80
|
-
const optionalNonEmpty = (schema) => zod_1.z.preprocess((value) => value === '' ? undefined : value, schema.optional());
|
|
65
|
+
const slugField = av.string().minLength(1).maxLength(100).pattern(slugPattern.source);
|
|
66
|
+
const semverField = av.string().minLength(5).maxLength(50).pattern(semverPattern.source);
|
|
67
|
+
const languageEnum = av.enum_(['nodejs', 'csharp', 'go', 'java', 'python']);
|
|
68
|
+
const categoryEnum = av.enum_(['service', 'observable', 'events', 'config']);
|
|
69
|
+
const visibilityEnum = av.enum_(['public', 'private']);
|
|
70
|
+
const safeString = (max) => av.string().maxLength(max).pattern(safeAscii.source);
|
|
71
|
+
const safeStringRequired = (min, max) => av.string().minLength(min).maxLength(max).pattern(safeAscii.source);
|
|
81
72
|
// ---- Route param schemas ----
|
|
82
|
-
const OrgParamsSchema =
|
|
73
|
+
const OrgParamsSchema = createValidator(objectSchema({
|
|
83
74
|
org: slugField,
|
|
84
|
-
});
|
|
85
|
-
const PluginDetailParamsSchema =
|
|
75
|
+
}));
|
|
76
|
+
const PluginDetailParamsSchema = createValidator(objectSchema({
|
|
86
77
|
org: slugField,
|
|
87
78
|
name: slugField,
|
|
88
|
-
});
|
|
89
|
-
const PluginVersionParamsSchema =
|
|
79
|
+
}));
|
|
80
|
+
const PluginVersionParamsSchema = createValidator(objectSchema({
|
|
90
81
|
org: slugField,
|
|
91
82
|
name: slugField,
|
|
92
83
|
version: semverField,
|
|
93
|
-
});
|
|
94
|
-
const PluginTypesParamsSchema =
|
|
84
|
+
}));
|
|
85
|
+
const PluginTypesParamsSchema = createValidator(objectSchema({
|
|
95
86
|
org: slugField,
|
|
96
87
|
name: slugField,
|
|
97
88
|
version: semverField,
|
|
98
89
|
language: languageEnum,
|
|
99
|
-
});
|
|
90
|
+
}));
|
|
100
91
|
const packageLookupIdPattern = /^[A-Za-z0-9@._\-/:]+$/;
|
|
101
|
-
const PackageLookupParamsSchema =
|
|
92
|
+
const PackageLookupParamsSchema = createValidator(objectSchema({
|
|
102
93
|
language: languageEnum,
|
|
103
|
-
'*':
|
|
104
|
-
|
|
94
|
+
'*': av.string().minLength(1).maxLength(300).pattern(packageLookupIdPattern.source),
|
|
95
|
+
}), {
|
|
96
|
+
normalize: (input) => mapObjectInput(input, (value) => {
|
|
97
|
+
const packageId = value['*'];
|
|
98
|
+
if (typeof packageId !== 'string') {
|
|
105
99
|
return value;
|
|
100
|
+
}
|
|
106
101
|
try {
|
|
107
|
-
return
|
|
102
|
+
return {
|
|
103
|
+
...value,
|
|
104
|
+
'*': decodeURIComponent(packageId).trim(),
|
|
105
|
+
};
|
|
108
106
|
}
|
|
109
107
|
catch {
|
|
110
|
-
return
|
|
108
|
+
return {
|
|
109
|
+
...value,
|
|
110
|
+
'*': '__INVALID_PACKAGE_ID__',
|
|
111
|
+
};
|
|
111
112
|
}
|
|
112
|
-
}
|
|
113
|
+
}),
|
|
113
114
|
});
|
|
114
115
|
// ---- Query string schemas ----
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
})
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
})
|
|
116
|
+
const BrowseQuerySchema = createValidator(objectSchema({
|
|
117
|
+
page: av.optional(av.int32().coerce({ from: 'string' }).min(1).max(10000)),
|
|
118
|
+
query: av.optional(safeString(200)),
|
|
119
|
+
category: av.optional(categoryEnum),
|
|
120
|
+
language: av.optional(languageEnum),
|
|
121
|
+
limit: av.optional(av.int32().coerce({ from: 'string' }).min(1).max(100)),
|
|
122
|
+
offset: av.optional(av.int32().coerce({ from: 'string' }).min(0).max(100000)),
|
|
123
|
+
}), {
|
|
124
|
+
normalize: (input) => mapObjectInput(input, (value) => ({
|
|
125
|
+
...value,
|
|
126
|
+
query: emptyStringToUndefined(value.query),
|
|
127
|
+
category: emptyStringToUndefined(value.category),
|
|
128
|
+
language: emptyStringToUndefined(value.language),
|
|
129
|
+
})),
|
|
130
|
+
});
|
|
131
|
+
const VersionsQuerySchema = createValidator(objectSchema({
|
|
132
|
+
majorMinor: av.optional(av.string().maxLength(11).pattern(majorMinorPattern.source)),
|
|
133
|
+
}));
|
|
134
|
+
const MatchQuerySchema = createValidator(objectSchema({
|
|
135
|
+
version: av.string().minLength(1).maxLength(20).pattern(safeAscii.source),
|
|
136
|
+
}));
|
|
137
|
+
const DocsQuerySchema = createValidator(objectSchema({
|
|
138
|
+
index: av.optional(av.int32().coerce({ from: 'string' }).min(0).max(100)),
|
|
139
|
+
}));
|
|
133
140
|
// ---- EventSchemaExport validation (parsed from the JSON string clients send) ----
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
141
|
+
const AnyValiDocumentSchema = av.record(av.unknown());
|
|
142
|
+
const EventExportEntrySchema = objectSchema({
|
|
143
|
+
type: av.enum_(['fire-and-forget', 'returnable', 'broadcast']),
|
|
144
|
+
category: av.enum_([
|
|
137
145
|
'emitEvents', 'onEvents',
|
|
138
146
|
'emitReturnableEvents', 'onReturnableEvents',
|
|
139
147
|
'emitBroadcast', 'onBroadcast',
|
|
140
148
|
]),
|
|
141
|
-
description:
|
|
142
|
-
defaultTimeout:
|
|
143
|
-
inputSchema:
|
|
144
|
-
outputSchema:
|
|
145
|
-
})
|
|
146
|
-
const
|
|
149
|
+
description: av.optional(av.string().maxLength(1000)),
|
|
150
|
+
defaultTimeout: av.optional(av.int32().min(0).max(300)),
|
|
151
|
+
inputSchema: AnyValiDocumentSchema,
|
|
152
|
+
outputSchema: av.nullable(AnyValiDocumentSchema),
|
|
153
|
+
});
|
|
154
|
+
const EventSchemaExportObjectSchema = objectSchema({
|
|
147
155
|
pluginName: safeStringRequired(1, 200),
|
|
148
156
|
version: semverField,
|
|
149
|
-
events:
|
|
150
|
-
capabilities:
|
|
151
|
-
dependencies:
|
|
152
|
-
id:
|
|
157
|
+
events: av.record(EventExportEntrySchema).default({}),
|
|
158
|
+
capabilities: av.optional(av.unknown()),
|
|
159
|
+
dependencies: av.optional(av.array(objectSchema({
|
|
160
|
+
id: av.string().minLength(1).maxLength(200),
|
|
153
161
|
version: safeStringRequired(1, 50),
|
|
154
|
-
})).
|
|
155
|
-
})
|
|
162
|
+
})).maxItems(100)),
|
|
163
|
+
});
|
|
156
164
|
// ---- Publish body schema ----
|
|
157
|
-
const AuthorSchema =
|
|
165
|
+
const AuthorSchema = av.union([
|
|
158
166
|
safeString(200),
|
|
159
|
-
|
|
167
|
+
objectSchema({
|
|
160
168
|
name: safeStringRequired(1, 200),
|
|
161
|
-
email:
|
|
162
|
-
url:
|
|
163
|
-
})
|
|
169
|
+
email: av.optional(av.string().maxLength(200).format('email')),
|
|
170
|
+
url: av.optional(av.string().maxLength(500).format('url')),
|
|
171
|
+
}),
|
|
164
172
|
]);
|
|
165
|
-
const
|
|
166
|
-
org:
|
|
167
|
-
name:
|
|
173
|
+
const PublishBodyObjectSchema = objectSchema({
|
|
174
|
+
org: av.string().minLength(1).maxLength(100).pattern(slugPattern.source),
|
|
175
|
+
name: av.string().minLength(1).maxLength(100).pattern(packageNamePattern.source),
|
|
168
176
|
version: semverField,
|
|
169
177
|
language: languageEnum,
|
|
170
|
-
metadata:
|
|
178
|
+
metadata: objectSchema({
|
|
171
179
|
displayName: safeStringRequired(1, 200),
|
|
172
|
-
description:
|
|
180
|
+
description: av.string().minLength(1).maxLength(1000),
|
|
173
181
|
category: categoryEnum,
|
|
174
|
-
tags:
|
|
175
|
-
author:
|
|
176
|
-
license: safeString(50)
|
|
177
|
-
homepage:
|
|
178
|
-
repository:
|
|
179
|
-
})
|
|
180
|
-
eventSchema:
|
|
181
|
-
capabilities:
|
|
182
|
-
configSchema:
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
java: zod_1.z.string().max(5_000_000).optional(),
|
|
193
|
-
}).strict().optional(),
|
|
194
|
-
documentation: zod_1.z.array(zod_1.z.string().max(1_000_000)).min(1).max(20),
|
|
195
|
-
dependencies: zod_1.z.array(zod_1.z.object({
|
|
196
|
-
id: zod_1.z.string().min(1).max(200).regex(slugPattern, 'Invalid plugin ID'),
|
|
182
|
+
tags: av.array(safeString(50)).maxItems(30),
|
|
183
|
+
author: av.optional(AuthorSchema),
|
|
184
|
+
license: av.optional(safeString(50)),
|
|
185
|
+
homepage: av.optional(av.string().maxLength(500).format('url')),
|
|
186
|
+
repository: av.optional(av.string().maxLength(500).format('url')),
|
|
187
|
+
}),
|
|
188
|
+
eventSchema: EventSchemaExportObjectSchema,
|
|
189
|
+
capabilities: av.optional(av.unknown()),
|
|
190
|
+
configSchema: av.optional(AnyValiDocumentSchema),
|
|
191
|
+
typeDefinitions: av.optional(objectSchema({
|
|
192
|
+
nodejs: av.optional(av.string().maxLength(5_000_000)),
|
|
193
|
+
csharp: av.optional(av.string().maxLength(5_000_000)),
|
|
194
|
+
go: av.optional(av.string().maxLength(5_000_000)),
|
|
195
|
+
java: av.optional(av.string().maxLength(5_000_000)),
|
|
196
|
+
})),
|
|
197
|
+
documentation: av.array(av.string().maxLength(1_000_000)).minItems(1).maxItems(20),
|
|
198
|
+
dependencies: av.optional(av.array(objectSchema({
|
|
199
|
+
id: av.string().minLength(1).maxLength(200).pattern(slugPattern.source),
|
|
197
200
|
version: safeStringRequired(1, 50),
|
|
198
|
-
})
|
|
199
|
-
package:
|
|
200
|
-
nodejs: safeString(200)
|
|
201
|
-
csharp: safeString(200)
|
|
202
|
-
go: safeString(200)
|
|
203
|
-
java: safeString(200)
|
|
204
|
-
python: safeString(200)
|
|
205
|
-
})
|
|
206
|
-
runtime:
|
|
207
|
-
nodejs: safeString(50)
|
|
208
|
-
dotnet: safeString(50)
|
|
209
|
-
go: safeString(50)
|
|
210
|
-
java: safeString(50)
|
|
211
|
-
python: safeString(50)
|
|
212
|
-
})
|
|
213
|
-
visibility:
|
|
214
|
-
})
|
|
215
|
-
|
|
201
|
+
})).maxItems(100)),
|
|
202
|
+
package: av.optional(objectSchema({
|
|
203
|
+
nodejs: av.optional(safeString(200)),
|
|
204
|
+
csharp: av.optional(safeString(200)),
|
|
205
|
+
go: av.optional(safeString(200)),
|
|
206
|
+
java: av.optional(safeString(200)),
|
|
207
|
+
python: av.optional(safeString(200)),
|
|
208
|
+
})),
|
|
209
|
+
runtime: av.optional(objectSchema({
|
|
210
|
+
nodejs: av.optional(safeString(50)),
|
|
211
|
+
dotnet: av.optional(safeString(50)),
|
|
212
|
+
go: av.optional(safeString(50)),
|
|
213
|
+
java: av.optional(safeString(50)),
|
|
214
|
+
python: av.optional(safeString(50)),
|
|
215
|
+
})),
|
|
216
|
+
visibility: av.optional(visibilityEnum),
|
|
217
|
+
});
|
|
218
|
+
function validateAnyValiDocument(value, path) {
|
|
219
|
+
if (!value || typeof value !== 'object') {
|
|
220
|
+
return [{
|
|
221
|
+
code: 'invalid_type',
|
|
222
|
+
message: 'Expected AnyVali document object',
|
|
223
|
+
path,
|
|
224
|
+
expected: 'object',
|
|
225
|
+
received: typeof value,
|
|
226
|
+
}];
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
av.importSchema(value);
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
const issues = error instanceof av.ValidationError
|
|
234
|
+
? error.issues
|
|
235
|
+
: [{
|
|
236
|
+
code: 'invalid_schema',
|
|
237
|
+
message: error instanceof Error ? error.message : 'Invalid AnyVali document',
|
|
238
|
+
path: [],
|
|
239
|
+
}];
|
|
240
|
+
return issues.map((issue) => ({
|
|
241
|
+
...issue,
|
|
242
|
+
path: [...path, ...issue.path],
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function validateEventSchemaExportDocuments(value, path = []) {
|
|
247
|
+
const nestedIssues = [];
|
|
248
|
+
for (const [eventName, eventDef] of Object.entries(value.events)) {
|
|
249
|
+
nestedIssues.push(...validateAnyValiDocument(eventDef.inputSchema, [...path, 'events', eventName, 'inputSchema']));
|
|
250
|
+
if (eventDef.outputSchema !== null) {
|
|
251
|
+
nestedIssues.push(...validateAnyValiDocument(eventDef.outputSchema, [...path, 'events', eventName, 'outputSchema']));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return nestedIssues;
|
|
255
|
+
}
|
|
256
|
+
const PublishBodySchema = createValidator(PublishBodyObjectSchema, {
|
|
257
|
+
extraIssues: (value) => [
|
|
258
|
+
...validateEventSchemaExportDocuments(value.eventSchema, ['eventSchema']),
|
|
259
|
+
...(value.configSchema !== undefined
|
|
260
|
+
? validateAnyValiDocument(value.configSchema, ['configSchema'])
|
|
261
|
+
: []),
|
|
262
|
+
],
|
|
263
|
+
});
|
|
264
|
+
export class RegistryUIServer {
|
|
216
265
|
app;
|
|
217
266
|
port;
|
|
218
267
|
host;
|
|
@@ -225,6 +274,7 @@ class RegistryUIServer {
|
|
|
225
274
|
badgeMap = {};
|
|
226
275
|
registryClient;
|
|
227
276
|
createTrace;
|
|
277
|
+
pluginCwd;
|
|
228
278
|
constructor(port, host, pageSize, uploadDir, badgesFile, maxImageUploadMb) {
|
|
229
279
|
this.port = port;
|
|
230
280
|
this.host = host;
|
|
@@ -233,7 +283,7 @@ class RegistryUIServer {
|
|
|
233
283
|
this.badgesFile = path.resolve(badgesFile);
|
|
234
284
|
this.maxImageUploadBytes = maxImageUploadMb * 1024 * 1024;
|
|
235
285
|
this.imageIndexPath = path.join(this.uploadDir, 'images.json');
|
|
236
|
-
this.app = (
|
|
286
|
+
this.app = Fastify({
|
|
237
287
|
logger: false,
|
|
238
288
|
disableRequestLogging: true,
|
|
239
289
|
bodyLimit: 10 * 1024 * 1024, // 10MB max request body
|
|
@@ -307,7 +357,7 @@ class RegistryUIServer {
|
|
|
307
357
|
registerHandlebarsHelpers() {
|
|
308
358
|
const helpers = this.getHandlebarsHelpers();
|
|
309
359
|
for (const [name, fn] of Object.entries(helpers)) {
|
|
310
|
-
|
|
360
|
+
handlebars.registerHelper(name, fn);
|
|
311
361
|
}
|
|
312
362
|
}
|
|
313
363
|
async init(obs, plugin) {
|
|
@@ -316,9 +366,10 @@ class RegistryUIServer {
|
|
|
316
366
|
// Bind plugin context to this server instance
|
|
317
367
|
this.registryClient = plugin.registryClient;
|
|
318
368
|
this.createTrace = plugin.createTrace.bind(plugin);
|
|
369
|
+
this.pluginCwd = plugin.pluginCwd;
|
|
319
370
|
// Register CORS
|
|
320
371
|
const corsSpan = obs.startSpan('register.cors');
|
|
321
|
-
await this.app.register(
|
|
372
|
+
await this.app.register(fastifyCors, {
|
|
322
373
|
origin: '*',
|
|
323
374
|
methods: ['GET', 'POST', 'OPTIONS'],
|
|
324
375
|
allowedHeaders: ['Content-Type', 'Authorization', 'Accept'],
|
|
@@ -326,7 +377,7 @@ class RegistryUIServer {
|
|
|
326
377
|
corsSpan.end();
|
|
327
378
|
// Register multipart upload handling for plugin images
|
|
328
379
|
const multipartSpan = obs.startSpan('register.multipart');
|
|
329
|
-
await this.app.register(
|
|
380
|
+
await this.app.register(fastifyMultipart, {
|
|
330
381
|
limits: {
|
|
331
382
|
fileSize: this.maxImageUploadBytes,
|
|
332
383
|
files: 1,
|
|
@@ -340,13 +391,13 @@ class RegistryUIServer {
|
|
|
340
391
|
// Register static file serving
|
|
341
392
|
const staticSpan = obs.startSpan('register.static');
|
|
342
393
|
const staticPath = path.join(plugin.pluginCwd, 'static');
|
|
343
|
-
await this.app.register(
|
|
394
|
+
await this.app.register(fastifyStatic, {
|
|
344
395
|
root: staticPath,
|
|
345
396
|
prefix: '/static/',
|
|
346
397
|
index: false,
|
|
347
398
|
});
|
|
348
399
|
await fsp.mkdir(this.uploadDir, { recursive: true });
|
|
349
|
-
await this.app.register(
|
|
400
|
+
await this.app.register(fastifyStatic, {
|
|
350
401
|
root: this.uploadDir,
|
|
351
402
|
prefix: '/images/',
|
|
352
403
|
decorateReply: false,
|
|
@@ -358,9 +409,9 @@ class RegistryUIServer {
|
|
|
358
409
|
const viewSpan = obs.startSpan('register.handlebars');
|
|
359
410
|
const templatesPath = path.join(plugin.pluginCwd, 'templates');
|
|
360
411
|
obs.log.debug('Registering Handlebars templates from {path}', { path: templatesPath });
|
|
361
|
-
await this.app.register(
|
|
412
|
+
await this.app.register(fastifyView, {
|
|
362
413
|
engine: {
|
|
363
|
-
handlebars
|
|
414
|
+
handlebars,
|
|
364
415
|
},
|
|
365
416
|
root: templatesPath,
|
|
366
417
|
layout: 'layouts/main.hbs',
|
|
@@ -426,6 +477,16 @@ class RegistryUIServer {
|
|
|
426
477
|
this.app.get('/', async (request, reply) => {
|
|
427
478
|
return this.handleHome(request, reply);
|
|
428
479
|
});
|
|
480
|
+
// LLM guidance
|
|
481
|
+
this.app.get('/llms.txt', async (_request, reply) => {
|
|
482
|
+
const llmsPath = path.join(this.pluginCwd, 'static', 'llms.txt');
|
|
483
|
+
if (fs.existsSync(llmsPath)) {
|
|
484
|
+
reply.type('text/plain').send(fs.readFileSync(llmsPath, 'utf8'));
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
reply.code(404).type('text/plain').send('Not found');
|
|
488
|
+
}
|
|
489
|
+
});
|
|
429
490
|
// Browse + search plugins (combined list/search)
|
|
430
491
|
this.app.get('/plugins', async (request, reply) => {
|
|
431
492
|
return this.handleBrowse(request, reply);
|
|
@@ -534,24 +595,24 @@ class RegistryUIServer {
|
|
|
534
595
|
return request.headers.accept?.includes('application/json') === true;
|
|
535
596
|
}
|
|
536
597
|
/**
|
|
537
|
-
* Validate input against
|
|
598
|
+
* Validate input against an AnyVali-backed schema. Returns parsed data on success,
|
|
538
599
|
* or sends a 400 response and returns null on failure.
|
|
539
600
|
*/
|
|
540
601
|
validateInput(schema, data, reply) {
|
|
541
602
|
const result = schema.safeParse(data);
|
|
542
|
-
if (
|
|
543
|
-
|
|
544
|
-
path: issue.path.join('.'),
|
|
545
|
-
message: issue.message,
|
|
546
|
-
}));
|
|
547
|
-
reply.code(400).send({
|
|
548
|
-
error: 'Validation Error',
|
|
549
|
-
code: 'INVALID_INPUT',
|
|
550
|
-
details: issues,
|
|
551
|
-
});
|
|
552
|
-
return null;
|
|
603
|
+
if (result.success) {
|
|
604
|
+
return result.data;
|
|
553
605
|
}
|
|
554
|
-
|
|
606
|
+
const issues = result.issues.map((issue) => ({
|
|
607
|
+
path: issue.path.join('.'),
|
|
608
|
+
message: issue.message,
|
|
609
|
+
}));
|
|
610
|
+
reply.code(400).send({
|
|
611
|
+
error: 'Validation Error',
|
|
612
|
+
code: 'INVALID_INPUT',
|
|
613
|
+
details: issues,
|
|
614
|
+
});
|
|
615
|
+
return null;
|
|
555
616
|
}
|
|
556
617
|
/** Render an error page (HTML) or send JSON error depending on Accept header */
|
|
557
618
|
async renderError(request, reply, statusCode, title, message) {
|
|
@@ -969,13 +1030,13 @@ a.s:hover{background:#333;border-color:#FB8C00}
|
|
|
969
1030
|
* External links open in a new tab. Raw HTML in the source is escaped.
|
|
970
1031
|
*/
|
|
971
1032
|
renderMarkdown(md) {
|
|
972
|
-
const renderer = new
|
|
1033
|
+
const renderer = new marked.Renderer();
|
|
973
1034
|
// Open external links in new tab with noopener
|
|
974
1035
|
renderer.link = ({ href, title, text }) => {
|
|
975
1036
|
const titleAttr = title ? ` title="${title}"` : '';
|
|
976
1037
|
return `<a href="${href}"${titleAttr} target="_blank" rel="noopener noreferrer">${text}</a>`;
|
|
977
1038
|
};
|
|
978
|
-
return
|
|
1039
|
+
return marked.parse(md, {
|
|
979
1040
|
renderer,
|
|
980
1041
|
gfm: true,
|
|
981
1042
|
breaks: false,
|
|
@@ -1706,7 +1767,7 @@ a.s:hover{background:#333;border-color:#FB8C00}
|
|
|
1706
1767
|
const safeFileName = `${params.org}__${params.name}${ext}`;
|
|
1707
1768
|
const outputPath = path.join(this.uploadDir, safeFileName);
|
|
1708
1769
|
await fsp.mkdir(this.uploadDir, { recursive: true });
|
|
1709
|
-
await
|
|
1770
|
+
await pipeline(filePart.file, fs.createWriteStream(outputPath));
|
|
1710
1771
|
if (filePart.file.truncated) {
|
|
1711
1772
|
await fsp.unlink(outputPath).catch(() => { });
|
|
1712
1773
|
reply.code(413).send({
|
|
@@ -1821,5 +1882,4 @@ a.s:hover{background:#333;border-color:#FB8C00}
|
|
|
1821
1882
|
this.app.close();
|
|
1822
1883
|
}
|
|
1823
1884
|
}
|
|
1824
|
-
exports.RegistryUIServer = RegistryUIServer;
|
|
1825
1885
|
//# sourceMappingURL=http-server.js.map
|