@bsb/registry 1.1.7 → 9.6.15

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 (27) hide show
  1. package/README.md +4 -2
  2. package/bsb-plugin.json +8 -2
  3. package/lib/.bsb/clients/service-bsb-registry.d.ts +301 -282
  4. package/lib/.bsb/clients/service-bsb-registry.d.ts.map +1 -1
  5. package/lib/.bsb/clients/service-bsb-registry.js +71 -10
  6. package/lib/.bsb/clients/service-bsb-registry.js.map +1 -1
  7. package/lib/plugins/service-bsb-registry/index.d.ts +10 -10
  8. package/lib/plugins/service-bsb-registry/index.d.ts.map +1 -1
  9. package/lib/plugins/service-bsb-registry/index.js +7 -7
  10. package/lib/plugins/service-bsb-registry/index.js.map +1 -1
  11. package/lib/plugins/service-bsb-registry/types.d.ts +158 -158
  12. package/lib/plugins/service-bsb-registry/types.d.ts.map +1 -1
  13. package/lib/plugins/service-bsb-registry-ui/BADGES.json +22 -0
  14. package/lib/plugins/service-bsb-registry-ui/http-server.d.ts +4 -2
  15. package/lib/plugins/service-bsb-registry-ui/http-server.d.ts.map +1 -1
  16. package/lib/plugins/service-bsb-registry-ui/http-server.js +160 -102
  17. package/lib/plugins/service-bsb-registry-ui/http-server.js.map +1 -1
  18. package/lib/plugins/service-bsb-registry-ui/index.d.ts +16 -16
  19. package/lib/plugins/service-bsb-registry-ui/index.d.ts.map +1 -1
  20. package/lib/plugins/service-bsb-registry-ui/index.js +8 -8
  21. package/lib/plugins/service-bsb-registry-ui/index.js.map +1 -1
  22. package/lib/plugins/service-bsb-registry-ui/templates/partials/plugin-card.hbs +1 -1
  23. package/lib/schemas/service-bsb-registry-ui.json +49 -37
  24. package/lib/schemas/service-bsb-registry-ui.plugin.json +49 -37
  25. package/lib/schemas/service-bsb-registry.json +58 -44
  26. package/lib/schemas/service-bsb-registry.plugin.json +40 -26
  27. package/package.json +16 -8
@@ -16,9 +16,9 @@ import fastifyStatic from '@fastify/static';
16
16
  import fastifyView from '@fastify/view';
17
17
  import handlebars from 'handlebars';
18
18
  import { marked } from 'marked';
19
- import * as av from '@anyvali/js';
19
+ import * as av from 'anyvali';
20
20
  function objectSchema(shape) {
21
- return av.object(shape, { unknownKeys: 'strip' });
21
+ return av.object(shape);
22
22
  }
23
23
  function createValidator(schema, options) {
24
24
  return {
@@ -62,37 +62,46 @@ const semverPattern = /^\d{1,5}\.\d{1,5}\.\d{1,5}(-[a-zA-Z0-9.]+)?$/;
62
62
  const majorMinorPattern = /^\d{1,5}\.\d{1,5}$/;
63
63
  // Package name: npm-style scoped or unscoped
64
64
  const packageNamePattern = /^(@[a-zA-Z0-9_-]+\/)?[a-zA-Z0-9._-]+$/;
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);
65
+ const slugField = () => av.string().minLength(1).maxLength(100).pattern(slugPattern.source)
66
+ .describe('Slug containing letters, numbers, dash, underscore, dot, at-sign, or slash');
67
+ const semverField = () => av.string().minLength(5).maxLength(50).pattern(semverPattern.source)
68
+ .describe('Strict semantic version in major.minor.patch format with optional prerelease');
69
+ const languageEnum = () => av.enum_(['nodejs', 'csharp', 'go', 'java', 'python'])
70
+ .describe('Plugin implementation language');
71
+ const categoryEnum = () => av.enum_(['service', 'observable', 'events', 'config'])
72
+ .describe('Plugin category');
73
+ const visibilityEnum = () => av.enum_(['public', 'private'])
74
+ .describe('Plugin visibility level');
75
+ const safeString = (max) => av.string().maxLength(max).pattern(safeAscii.source)
76
+ .describe('Printable ASCII string with no control characters');
77
+ const safeStringRequired = (min, max) => av.string().minLength(min).maxLength(max).pattern(safeAscii.source)
78
+ .describe('Required printable ASCII string with no control characters');
72
79
  // ---- Route param schemas ----
73
80
  const OrgParamsSchema = createValidator(objectSchema({
74
- org: slugField,
75
- }));
81
+ org: slugField().describe('Organization or user namespace'),
82
+ }).describe('Organization route parameters'));
76
83
  const PluginDetailParamsSchema = createValidator(objectSchema({
77
- org: slugField,
78
- name: slugField,
79
- }));
84
+ org: slugField().describe('Organization or user namespace'),
85
+ name: slugField().describe('Plugin name'),
86
+ }).describe('Plugin detail route parameters'));
80
87
  const PluginVersionParamsSchema = createValidator(objectSchema({
81
- org: slugField,
82
- name: slugField,
83
- version: semverField,
84
- }));
88
+ org: slugField().describe('Organization or user namespace'),
89
+ name: slugField().describe('Plugin name'),
90
+ version: semverField().describe('Plugin semantic version'),
91
+ }).describe('Plugin version route parameters'));
85
92
  const PluginTypesParamsSchema = createValidator(objectSchema({
86
- org: slugField,
87
- name: slugField,
88
- version: semverField,
89
- language: languageEnum,
90
- }));
93
+ org: slugField().describe('Organization or user namespace'),
94
+ name: slugField().describe('Plugin name'),
95
+ version: semverField().describe('Plugin semantic version'),
96
+ language: languageEnum().describe('Type definition language'),
97
+ }).describe('Plugin type definition route parameters'));
91
98
  const packageLookupIdPattern = /^[A-Za-z0-9@._\-/:]+$/;
99
+ const staticLookupPattern = /(^|\/)(README|LICENSE|CHANGELOG|CONTRIBUTING)(\..*)?$|[.](md|json|txt|ya?ml|xml|html?|css|js|map|ico|png|jpe?g|svg|webp|gif)$/i;
92
100
  const PackageLookupParamsSchema = createValidator(objectSchema({
93
- language: languageEnum,
94
- '*': av.string().minLength(1).maxLength(300).pattern(packageLookupIdPattern.source),
95
- }), {
101
+ language: languageEnum().describe('Package language to resolve'),
102
+ '*': av.string().minLength(1).maxLength(300).pattern(packageLookupIdPattern.source)
103
+ .describe('Package lookup identifier, including scoped package names and encoded path segments'),
104
+ }).describe('Package lookup route parameters'), {
96
105
  normalize: (input) => mapObjectInput(input, (value) => {
97
106
  const packageId = value['*'];
98
107
  if (typeof packageId !== 'string') {
@@ -114,13 +123,16 @@ const PackageLookupParamsSchema = createValidator(objectSchema({
114
123
  });
115
124
  // ---- Query string schemas ----
116
125
  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
- }), {
126
+ page: av.optional(av.int32().coerce({ from: 'string' }).min(1).max(10000))
127
+ .describe('One-based page number for browse results'),
128
+ query: av.optional(safeString(200)).describe('Search text used to filter browse results'),
129
+ category: av.optional(categoryEnum()).describe('Optional plugin category filter'),
130
+ language: av.optional(languageEnum()).describe('Optional plugin language filter'),
131
+ limit: av.optional(av.int32().coerce({ from: 'string' }).min(1).max(100))
132
+ .describe('Maximum number of results to return'),
133
+ offset: av.optional(av.int32().coerce({ from: 'string' }).min(0).max(100000))
134
+ .describe('Zero-based result offset for pagination'),
135
+ }).describe('Plugin browse query parameters'), {
124
136
  normalize: (input) => mapObjectInput(input, (value) => ({
125
137
  ...value,
126
138
  query: emptyStringToUndefined(value.query),
@@ -129,92 +141,98 @@ const BrowseQuerySchema = createValidator(objectSchema({
129
141
  })),
130
142
  });
131
143
  const VersionsQuerySchema = createValidator(objectSchema({
132
- majorMinor: av.optional(av.string().maxLength(11).pattern(majorMinorPattern.source)),
133
- }));
144
+ majorMinor: av.optional(av.string().maxLength(11).pattern(majorMinorPattern.source))
145
+ .describe('Optional major.minor version filter'),
146
+ }).describe('Plugin versions query parameters'));
134
147
  const MatchQuerySchema = createValidator(objectSchema({
135
- version: av.string().minLength(1).maxLength(20).pattern(safeAscii.source),
136
- }));
148
+ version: av.string().minLength(1).maxLength(20).pattern(safeAscii.source)
149
+ .describe('Version or version range to match'),
150
+ }).describe('Plugin version match query parameters'));
137
151
  const DocsQuerySchema = createValidator(objectSchema({
138
- index: av.optional(av.int32().coerce({ from: 'string' }).min(0).max(100)),
139
- }));
152
+ index: av.optional(av.int32().coerce({ from: 'string' }).min(0).max(100))
153
+ .describe('Zero-based documentation file index'),
154
+ }).describe('Plugin documentation query parameters'));
140
155
  // ---- EventSchemaExport validation (parsed from the JSON string clients send) ----
141
- const AnyValiDocumentSchema = av.record(av.unknown());
156
+ const anyValiDocumentSchema = () => av.record(av.unknown().describe('AnyVali document property value'))
157
+ .describe('AnyVali schema document');
142
158
  const EventExportEntrySchema = objectSchema({
143
- type: av.enum_(['fire-and-forget', 'returnable', 'broadcast']),
159
+ type: av.enum_(['fire-and-forget', 'returnable', 'broadcast'])
160
+ .describe('Event delivery type'),
144
161
  category: av.enum_([
145
162
  'emitEvents', 'onEvents',
146
163
  'emitReturnableEvents', 'onReturnableEvents',
147
164
  'emitBroadcast', 'onBroadcast',
148
- ]),
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
- });
165
+ ]).describe('Event schema registry category'),
166
+ description: av.optional(av.string().maxLength(1000)).describe('Human-readable event description'),
167
+ defaultTimeout: av.optional(av.int32().min(0).max(300)).describe('Default returnable event timeout in seconds'),
168
+ inputSchema: anyValiDocumentSchema().describe('AnyVali document describing the event input payload'),
169
+ outputSchema: av.nullable(anyValiDocumentSchema()).describe('AnyVali document describing the event output payload, or null when there is no output'),
170
+ }).describe('Exported plugin event definition');
154
171
  const EventSchemaExportObjectSchema = objectSchema({
155
- pluginName: safeStringRequired(1, 200),
156
- version: semverField,
157
- events: av.record(EventExportEntrySchema).default({}),
158
- capabilities: av.optional(av.unknown()),
172
+ pluginName: safeStringRequired(1, 200).describe('Plugin name that produced the exported event schema'),
173
+ version: semverField().describe('Plugin schema semantic version'),
174
+ events: av.record(EventExportEntrySchema).default({}).describe('Map of event names to exported event definitions'),
175
+ capabilities: av.optional(av.unknown()).describe('Optional plugin capability metadata'),
159
176
  dependencies: av.optional(av.array(objectSchema({
160
- id: av.string().minLength(1).maxLength(200),
161
- version: safeStringRequired(1, 50),
162
- })).maxItems(100)),
163
- });
177
+ id: av.string().minLength(1).maxLength(200).describe('Dependency plugin ID'),
178
+ version: safeStringRequired(1, 50).describe('Dependency version constraint'),
179
+ }).describe('Generated client dependency')).maxItems(100)).describe('Plugins referenced by generated client imports'),
180
+ }).describe('Exported plugin event schema object');
164
181
  // ---- Publish body schema ----
165
182
  const AuthorSchema = av.union([
166
- safeString(200),
183
+ safeString(200).describe('Author name as a string'),
167
184
  objectSchema({
168
- name: safeStringRequired(1, 200),
169
- email: av.optional(av.string().maxLength(200).format('email')),
170
- url: av.optional(av.string().maxLength(500).format('url')),
171
- }),
172
- ]);
185
+ name: safeStringRequired(1, 200).describe('Author display name'),
186
+ email: av.optional(av.string().maxLength(200).format('email')).describe('Author email address'),
187
+ url: av.optional(av.string().maxLength(500).format('url')).describe('Author homepage URL'),
188
+ }).describe('Author object'),
189
+ ]).describe('Author value, either a string or an object with name, email, and URL fields');
173
190
  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),
176
- version: semverField,
177
- language: languageEnum,
191
+ org: av.string().minLength(1).maxLength(100).pattern(slugPattern.source).describe('Organization or user namespace'),
192
+ name: av.string().minLength(1).maxLength(100).pattern(packageNamePattern.source).describe('Plugin package name'),
193
+ version: semverField().describe('Published plugin semantic version'),
194
+ language: languageEnum().describe('Published plugin implementation language'),
178
195
  metadata: objectSchema({
179
- displayName: safeStringRequired(1, 200),
180
- description: av.string().minLength(1).maxLength(1000),
181
- category: categoryEnum,
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(safeString(500)),
187
- }),
188
- eventSchema: EventSchemaExportObjectSchema,
189
- capabilities: av.optional(av.unknown()),
190
- configSchema: av.optional(AnyValiDocumentSchema),
196
+ displayName: safeStringRequired(1, 200).describe('Human-readable plugin name'),
197
+ description: av.string().minLength(1).maxLength(1000).describe('Short plugin description'),
198
+ category: categoryEnum().describe('Plugin category'),
199
+ tags: av.array(safeString(50)).maxItems(30).describe('Searchable plugin tags'),
200
+ author: av.optional(AuthorSchema).describe('Plugin author, accepting npm-style string or object formats'),
201
+ license: av.optional(safeString(50)).describe('License identifier'),
202
+ homepage: av.optional(av.string().maxLength(500).format('url')).describe('Plugin homepage or documentation URL'),
203
+ repository: av.optional(safeString(500)).describe('Source repository URL or identifier'),
204
+ }).describe('Plugin metadata supplied by the publisher'),
205
+ eventSchema: EventSchemaExportObjectSchema.describe('Exported event schema for the published plugin'),
206
+ capabilities: av.optional(av.unknown()).describe('Optional top-level plugin capability metadata'),
207
+ configSchema: av.optional(anyValiDocumentSchema()).describe('AnyVali document describing plugin configuration'),
191
208
  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),
209
+ nodejs: av.optional(av.string().maxLength(5_000_000)).describe('TypeScript declaration output'),
210
+ csharp: av.optional(av.string().maxLength(5_000_000)).describe('C# type definition output'),
211
+ go: av.optional(av.string().maxLength(5_000_000)).describe('Go type definition output'),
212
+ java: av.optional(av.string().maxLength(5_000_000)).describe('Java type definition output'),
213
+ }).describe('Generated type definitions by language')).describe('Language-specific generated type definitions'),
214
+ documentation: av.array(av.string().maxLength(1_000_000)).minItems(1).maxItems(20)
215
+ .describe('Markdown documentation files published with the plugin'),
198
216
  dependencies: av.optional(av.array(objectSchema({
199
- id: av.string().minLength(1).maxLength(200).pattern(slugPattern.source),
200
- version: safeStringRequired(1, 50),
201
- })).maxItems(100)),
217
+ id: av.string().minLength(1).maxLength(200).pattern(slugPattern.source).describe('Dependency plugin ID'),
218
+ version: safeStringRequired(1, 50).describe('Dependency version constraint'),
219
+ }).describe('Published plugin dependency')).maxItems(100)).describe('Plugins this plugin depends on'),
202
220
  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
- })),
221
+ nodejs: av.optional(safeString(200)).describe('NPM package name'),
222
+ csharp: av.optional(safeString(200)).describe('NuGet package name'),
223
+ go: av.optional(safeString(200)).describe('Go module path'),
224
+ java: av.optional(safeString(200)).describe('Maven coordinates'),
225
+ python: av.optional(safeString(200)).describe('PyPI package name'),
226
+ }).describe('Package identifiers by language')).describe('Language-specific package identifiers'),
209
227
  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
- });
228
+ nodejs: av.optional(safeString(50)).describe('Node.js runtime requirement'),
229
+ dotnet: av.optional(safeString(50)).describe('.NET runtime requirement'),
230
+ go: av.optional(safeString(50)).describe('Go runtime requirement'),
231
+ java: av.optional(safeString(50)).describe('Java runtime requirement'),
232
+ python: av.optional(safeString(50)).describe('Python runtime requirement'),
233
+ }).describe('Runtime requirements by language')).describe('Runtime version requirements'),
234
+ visibility: av.optional(visibilityEnum()).describe('Optional plugin visibility override'),
235
+ }).describe('Plugin publish request body');
218
236
  function validateAnyValiDocument(value, path) {
219
237
  if (!value || typeof value !== 'object') {
220
238
  return [{
@@ -267,6 +285,7 @@ export class RegistryUIServer {
267
285
  host;
268
286
  pageSize;
269
287
  uploadDir;
288
+ badgesFileInput;
270
289
  badgesFile;
271
290
  maxImageUploadBytes;
272
291
  imageIndexPath;
@@ -281,7 +300,7 @@ export class RegistryUIServer {
281
300
  this.host = host;
282
301
  this.pageSize = pageSize;
283
302
  this.uploadDir = path.resolve(uploadDir);
284
- this.badgesFile = path.resolve(badgesFile);
303
+ this.badgesFileInput = badgesFile;
285
304
  this.maxImageUploadBytes = maxImageUploadMb * 1024 * 1024;
286
305
  this.imageIndexPath = path.join(this.uploadDir, 'images.json');
287
306
  this.app = Fastify({
@@ -368,6 +387,7 @@ export class RegistryUIServer {
368
387
  this.registryClient = plugin.registryClient;
369
388
  this.createTrace = plugin.createTrace.bind(plugin);
370
389
  this.pluginCwd = plugin.pluginCwd;
390
+ this.badgesFile = this.resolveBadgesFile(plugin.pluginCwd, plugin.packageCwd);
371
391
  const packageJsonPath = path.join(plugin.packageCwd, 'package.json');
372
392
  if (fs.existsSync(packageJsonPath)) {
373
393
  const packageJson = JSON.parse(await fsp.readFile(packageJsonPath, 'utf-8'));
@@ -651,6 +671,11 @@ export class RegistryUIServer {
651
671
  await fsp.writeFile(this.imageIndexPath, JSON.stringify(this.imageIndex, null, 2), 'utf-8');
652
672
  }
653
673
  async loadBadgeMap(obs) {
674
+ if (!this.badgesFile) {
675
+ this.badgeMap = {};
676
+ obs.log.debug('No badge map file found');
677
+ return;
678
+ }
654
679
  try {
655
680
  const raw = await fsp.readFile(this.badgesFile, 'utf-8');
656
681
  const parsed = JSON.parse(raw);
@@ -658,11 +683,36 @@ export class RegistryUIServer {
658
683
  ? parsed
659
684
  : {};
660
685
  }
661
- catch {
686
+ catch (error) {
662
687
  this.badgeMap = {};
688
+ obs.log.warn('Badge map not loaded from {path}: {error}', {
689
+ path: this.badgesFile,
690
+ error: error.message,
691
+ });
663
692
  }
664
693
  obs.log.debug('Loaded badge map with {count} entries', { count: Object.keys(this.badgeMap).length });
665
694
  }
695
+ resolveBadgesFile(pluginCwd, packageCwd) {
696
+ const defaultCandidates = [
697
+ path.resolve(pluginCwd, 'BADGES.json'),
698
+ path.resolve(packageCwd, 'BADGES.json'),
699
+ path.resolve('BADGES.json'),
700
+ ];
701
+ if (this.badgesFileInput && path.isAbsolute(this.badgesFileInput)) {
702
+ return [
703
+ this.badgesFileInput,
704
+ ...defaultCandidates,
705
+ ].find((candidate) => fs.existsSync(candidate));
706
+ }
707
+ const filename = this.badgesFileInput ?? 'BADGES.json';
708
+ const candidates = [
709
+ path.resolve(pluginCwd, filename),
710
+ path.resolve(packageCwd, filename),
711
+ path.resolve(filename),
712
+ ...defaultCandidates,
713
+ ];
714
+ return candidates.find((candidate) => fs.existsSync(candidate));
715
+ }
666
716
  resolvePluginImageUrl(pluginId) {
667
717
  const filename = this.imageIndex[pluginId];
668
718
  if (!filename)
@@ -1162,6 +1212,10 @@ a.s:hover{background:#333;border-color:#FB8C00}
1162
1212
  if (!params)
1163
1213
  return;
1164
1214
  const packageId = params['*'];
1215
+ if (staticLookupPattern.test(packageId)) {
1216
+ reply.code(404).send({ error: 'Not Found' });
1217
+ return;
1218
+ }
1165
1219
  const trace = this.createTrace('ui.package.lookup', {
1166
1220
  url: request.url,
1167
1221
  method: request.method,
@@ -1358,6 +1412,10 @@ a.s:hover{background:#333;border-color:#FB8C00}
1358
1412
  const params = this.validateInput(PluginDetailParamsSchema, request.params, reply);
1359
1413
  if (!params)
1360
1414
  return;
1415
+ if (staticLookupPattern.test(`${params.org}/${params.name}`)) {
1416
+ reply.code(404).send({ error: 'Not Found' });
1417
+ return;
1418
+ }
1361
1419
  const trace = this.createTrace('ui.plugin.detail', {
1362
1420
  url: request.url,
1363
1421
  method: request.method,