@bsb/registry 1.0.4 → 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 +2 -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 +229 -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/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;
|
|
@@ -234,7 +283,7 @@ class RegistryUIServer {
|
|
|
234
283
|
this.badgesFile = path.resolve(badgesFile);
|
|
235
284
|
this.maxImageUploadBytes = maxImageUploadMb * 1024 * 1024;
|
|
236
285
|
this.imageIndexPath = path.join(this.uploadDir, 'images.json');
|
|
237
|
-
this.app = (
|
|
286
|
+
this.app = Fastify({
|
|
238
287
|
logger: false,
|
|
239
288
|
disableRequestLogging: true,
|
|
240
289
|
bodyLimit: 10 * 1024 * 1024, // 10MB max request body
|
|
@@ -308,7 +357,7 @@ class RegistryUIServer {
|
|
|
308
357
|
registerHandlebarsHelpers() {
|
|
309
358
|
const helpers = this.getHandlebarsHelpers();
|
|
310
359
|
for (const [name, fn] of Object.entries(helpers)) {
|
|
311
|
-
|
|
360
|
+
handlebars.registerHelper(name, fn);
|
|
312
361
|
}
|
|
313
362
|
}
|
|
314
363
|
async init(obs, plugin) {
|
|
@@ -320,7 +369,7 @@ class RegistryUIServer {
|
|
|
320
369
|
this.pluginCwd = plugin.pluginCwd;
|
|
321
370
|
// Register CORS
|
|
322
371
|
const corsSpan = obs.startSpan('register.cors');
|
|
323
|
-
await this.app.register(
|
|
372
|
+
await this.app.register(fastifyCors, {
|
|
324
373
|
origin: '*',
|
|
325
374
|
methods: ['GET', 'POST', 'OPTIONS'],
|
|
326
375
|
allowedHeaders: ['Content-Type', 'Authorization', 'Accept'],
|
|
@@ -328,7 +377,7 @@ class RegistryUIServer {
|
|
|
328
377
|
corsSpan.end();
|
|
329
378
|
// Register multipart upload handling for plugin images
|
|
330
379
|
const multipartSpan = obs.startSpan('register.multipart');
|
|
331
|
-
await this.app.register(
|
|
380
|
+
await this.app.register(fastifyMultipart, {
|
|
332
381
|
limits: {
|
|
333
382
|
fileSize: this.maxImageUploadBytes,
|
|
334
383
|
files: 1,
|
|
@@ -342,13 +391,13 @@ class RegistryUIServer {
|
|
|
342
391
|
// Register static file serving
|
|
343
392
|
const staticSpan = obs.startSpan('register.static');
|
|
344
393
|
const staticPath = path.join(plugin.pluginCwd, 'static');
|
|
345
|
-
await this.app.register(
|
|
394
|
+
await this.app.register(fastifyStatic, {
|
|
346
395
|
root: staticPath,
|
|
347
396
|
prefix: '/static/',
|
|
348
397
|
index: false,
|
|
349
398
|
});
|
|
350
399
|
await fsp.mkdir(this.uploadDir, { recursive: true });
|
|
351
|
-
await this.app.register(
|
|
400
|
+
await this.app.register(fastifyStatic, {
|
|
352
401
|
root: this.uploadDir,
|
|
353
402
|
prefix: '/images/',
|
|
354
403
|
decorateReply: false,
|
|
@@ -360,9 +409,9 @@ class RegistryUIServer {
|
|
|
360
409
|
const viewSpan = obs.startSpan('register.handlebars');
|
|
361
410
|
const templatesPath = path.join(plugin.pluginCwd, 'templates');
|
|
362
411
|
obs.log.debug('Registering Handlebars templates from {path}', { path: templatesPath });
|
|
363
|
-
await this.app.register(
|
|
412
|
+
await this.app.register(fastifyView, {
|
|
364
413
|
engine: {
|
|
365
|
-
handlebars
|
|
414
|
+
handlebars,
|
|
366
415
|
},
|
|
367
416
|
root: templatesPath,
|
|
368
417
|
layout: 'layouts/main.hbs',
|
|
@@ -546,24 +595,24 @@ class RegistryUIServer {
|
|
|
546
595
|
return request.headers.accept?.includes('application/json') === true;
|
|
547
596
|
}
|
|
548
597
|
/**
|
|
549
|
-
* Validate input against
|
|
598
|
+
* Validate input against an AnyVali-backed schema. Returns parsed data on success,
|
|
550
599
|
* or sends a 400 response and returns null on failure.
|
|
551
600
|
*/
|
|
552
601
|
validateInput(schema, data, reply) {
|
|
553
602
|
const result = schema.safeParse(data);
|
|
554
|
-
if (
|
|
555
|
-
|
|
556
|
-
path: issue.path.join('.'),
|
|
557
|
-
message: issue.message,
|
|
558
|
-
}));
|
|
559
|
-
reply.code(400).send({
|
|
560
|
-
error: 'Validation Error',
|
|
561
|
-
code: 'INVALID_INPUT',
|
|
562
|
-
details: issues,
|
|
563
|
-
});
|
|
564
|
-
return null;
|
|
603
|
+
if (result.success) {
|
|
604
|
+
return result.data;
|
|
565
605
|
}
|
|
566
|
-
|
|
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;
|
|
567
616
|
}
|
|
568
617
|
/** Render an error page (HTML) or send JSON error depending on Accept header */
|
|
569
618
|
async renderError(request, reply, statusCode, title, message) {
|
|
@@ -981,13 +1030,13 @@ a.s:hover{background:#333;border-color:#FB8C00}
|
|
|
981
1030
|
* External links open in a new tab. Raw HTML in the source is escaped.
|
|
982
1031
|
*/
|
|
983
1032
|
renderMarkdown(md) {
|
|
984
|
-
const renderer = new
|
|
1033
|
+
const renderer = new marked.Renderer();
|
|
985
1034
|
// Open external links in new tab with noopener
|
|
986
1035
|
renderer.link = ({ href, title, text }) => {
|
|
987
1036
|
const titleAttr = title ? ` title="${title}"` : '';
|
|
988
1037
|
return `<a href="${href}"${titleAttr} target="_blank" rel="noopener noreferrer">${text}</a>`;
|
|
989
1038
|
};
|
|
990
|
-
return
|
|
1039
|
+
return marked.parse(md, {
|
|
991
1040
|
renderer,
|
|
992
1041
|
gfm: true,
|
|
993
1042
|
breaks: false,
|
|
@@ -1718,7 +1767,7 @@ a.s:hover{background:#333;border-color:#FB8C00}
|
|
|
1718
1767
|
const safeFileName = `${params.org}__${params.name}${ext}`;
|
|
1719
1768
|
const outputPath = path.join(this.uploadDir, safeFileName);
|
|
1720
1769
|
await fsp.mkdir(this.uploadDir, { recursive: true });
|
|
1721
|
-
await
|
|
1770
|
+
await pipeline(filePart.file, fs.createWriteStream(outputPath));
|
|
1722
1771
|
if (filePart.file.truncated) {
|
|
1723
1772
|
await fsp.unlink(outputPath).catch(() => { });
|
|
1724
1773
|
reply.code(413).send({
|
|
@@ -1833,5 +1882,4 @@ a.s:hover{background:#333;border-color:#FB8C00}
|
|
|
1833
1882
|
this.app.close();
|
|
1834
1883
|
}
|
|
1835
1884
|
}
|
|
1836
|
-
exports.RegistryUIServer = RegistryUIServer;
|
|
1837
1885
|
//# sourceMappingURL=http-server.js.map
|