@springfield/ham-radio-registry 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/catalog/index.d.ts +1 -0
- package/dist/catalog/index.d.ts.map +1 -1
- package/dist/catalog/index.js +1 -0
- package/dist/catalog/index.js.map +1 -1
- package/dist/catalog/module-catalog.d.ts +28 -0
- package/dist/catalog/module-catalog.d.ts.map +1 -0
- package/dist/catalog/module-catalog.js +164 -0
- package/dist/catalog/module-catalog.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types/module-catalog.d.ts +28 -0
- package/dist/types/module-catalog.d.ts.map +1 -0
- package/dist/types/module-catalog.js +2 -0
- package/dist/types/module-catalog.js.map +1 -0
- package/dist/types/radio-config.d.ts +1 -1
- package/dist/types/radio-config.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/catalog/index.ts +8 -0
- package/src/catalog/module-catalog.ts +201 -0
- package/src/index.ts +12 -0
- package/src/types/module-catalog.ts +29 -0
- package/src/types/radio-config.ts +1 -1
package/dist/catalog/index.d.ts
CHANGED
|
@@ -2,4 +2,5 @@ export { hydrateRadioConfig } from './hydrate-radio-config.js';
|
|
|
2
2
|
export { extractCatalogMetadata } from './extract-catalog-metadata.js';
|
|
3
3
|
export { validateConfiguration } from './validate-configuration.js';
|
|
4
4
|
export { computeContentHash, hashRadioConfig } from './content-hash.js';
|
|
5
|
+
export { validateModuleCatalog, parseModuleCatalog, compareSemver, isApiVersionCompatible, normalizeIntegrity, integrityFromSha256Hex, } from './module-catalog.js';
|
|
5
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/catalog/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/catalog/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACxE,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,aAAa,EACb,sBAAsB,EACtB,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,qBAAqB,CAAC"}
|
package/dist/catalog/index.js
CHANGED
|
@@ -2,4 +2,5 @@ export { hydrateRadioConfig } from './hydrate-radio-config.js';
|
|
|
2
2
|
export { extractCatalogMetadata } from './extract-catalog-metadata.js';
|
|
3
3
|
export { validateConfiguration } from './validate-configuration.js';
|
|
4
4
|
export { computeContentHash, hashRadioConfig } from './content-hash.js';
|
|
5
|
+
export { validateModuleCatalog, parseModuleCatalog, compareSemver, isApiVersionCompatible, normalizeIntegrity, integrityFromSha256Hex, } from './module-catalog.js';
|
|
5
6
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/catalog/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/catalog/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACxE,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,aAAa,EACb,sBAAsB,EACtB,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CatalogValidationResult, RadioModuleCatalog } from '../types/module-catalog.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validate and normalize a catalog.json document.
|
|
4
|
+
*/
|
|
5
|
+
export declare function validateModuleCatalog(value: unknown): CatalogValidationResult & {
|
|
6
|
+
catalog?: RadioModuleCatalog;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Parse catalog JSON text and validate it.
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseModuleCatalog(json: string): RadioModuleCatalog;
|
|
12
|
+
/**
|
|
13
|
+
* Compare dotted semver-like versions. Returns negative if left < right, 0 if equal, positive if left > right.
|
|
14
|
+
*/
|
|
15
|
+
export declare function compareSemver(left: string, right: string): number;
|
|
16
|
+
/**
|
|
17
|
+
* True when the installed API version satisfies the module's minimum.
|
|
18
|
+
*/
|
|
19
|
+
export declare function isApiVersionCompatible(installedApiVersion: string, minApiVersion: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Normalize integrity strings for comparison (lowercase hex).
|
|
22
|
+
*/
|
|
23
|
+
export declare function normalizeIntegrity(integrity: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Build a sha256 integrity string from a hex digest.
|
|
26
|
+
*/
|
|
27
|
+
export declare function integrityFromSha256Hex(hex: string): string;
|
|
28
|
+
//# sourceMappingURL=module-catalog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-catalog.d.ts","sourceRoot":"","sources":["../../src/catalog/module-catalog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,kBAAkB,EAA2B,MAAM,4BAA4B,CAAC;AAqFvH;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,uBAAuB,GAAG;IAAE,OAAO,CAAC,EAAE,kBAAkB,CAAA;CAAE,CAiDhH;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,CAiBnE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAejE;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,mBAAmB,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAElG;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE1D"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const INTEGRITY_PATTERN = /^sha256:[a-fA-F0-9]{64}$/;
|
|
2
|
+
const SUPPORTED_SCHEMA_VERSION = 1;
|
|
3
|
+
function isRecord(value) {
|
|
4
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
function isNonEmptyString(value) {
|
|
7
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
8
|
+
}
|
|
9
|
+
function isHttpsUrl(value) {
|
|
10
|
+
try {
|
|
11
|
+
const url = new URL(value);
|
|
12
|
+
return url.protocol === 'https:';
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function validateModuleEntry(entry, index, errors) {
|
|
19
|
+
if (!isRecord(entry)) {
|
|
20
|
+
errors.push(`modules[${index}] must be an object`);
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
const prefix = `modules[${index}]`;
|
|
24
|
+
if (!isNonEmptyString(entry.id)) {
|
|
25
|
+
errors.push(`${prefix}.id is required`);
|
|
26
|
+
}
|
|
27
|
+
if (!isNonEmptyString(entry.package)) {
|
|
28
|
+
errors.push(`${prefix}.package is required`);
|
|
29
|
+
}
|
|
30
|
+
if (!isNonEmptyString(entry.manufacturer)) {
|
|
31
|
+
errors.push(`${prefix}.manufacturer is required`);
|
|
32
|
+
}
|
|
33
|
+
if (!isNonEmptyString(entry.version)) {
|
|
34
|
+
errors.push(`${prefix}.version is required`);
|
|
35
|
+
}
|
|
36
|
+
if (!Array.isArray(entry.supportedRadios) || entry.supportedRadios.length === 0) {
|
|
37
|
+
errors.push(`${prefix}.supportedRadios must be a non-empty array`);
|
|
38
|
+
}
|
|
39
|
+
else if (!entry.supportedRadios.every((radio) => isNonEmptyString(radio))) {
|
|
40
|
+
errors.push(`${prefix}.supportedRadios must contain only non-empty strings`);
|
|
41
|
+
}
|
|
42
|
+
if (!isNonEmptyString(entry.minApiVersion)) {
|
|
43
|
+
errors.push(`${prefix}.minApiVersion is required`);
|
|
44
|
+
}
|
|
45
|
+
if (!isNonEmptyString(entry.downloadUrl) || !isHttpsUrl(entry.downloadUrl)) {
|
|
46
|
+
errors.push(`${prefix}.downloadUrl must be an https URL`);
|
|
47
|
+
}
|
|
48
|
+
if (!isNonEmptyString(entry.integrity) || !INTEGRITY_PATTERN.test(entry.integrity)) {
|
|
49
|
+
errors.push(`${prefix}.integrity must match sha256:<64 hex chars>`);
|
|
50
|
+
}
|
|
51
|
+
if (entry.description !== undefined && typeof entry.description !== 'string') {
|
|
52
|
+
errors.push(`${prefix}.description must be a string when present`);
|
|
53
|
+
}
|
|
54
|
+
if (errors.some((error) => error.startsWith(prefix))) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
id: entry.id,
|
|
59
|
+
package: entry.package,
|
|
60
|
+
manufacturer: entry.manufacturer,
|
|
61
|
+
...(typeof entry.description === 'string' ? { description: entry.description } : {}),
|
|
62
|
+
version: entry.version,
|
|
63
|
+
supportedRadios: entry.supportedRadios,
|
|
64
|
+
minApiVersion: entry.minApiVersion,
|
|
65
|
+
downloadUrl: entry.downloadUrl,
|
|
66
|
+
integrity: entry.integrity.toLowerCase(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Validate and normalize a catalog.json document.
|
|
71
|
+
*/
|
|
72
|
+
export function validateModuleCatalog(value) {
|
|
73
|
+
const errors = [];
|
|
74
|
+
const warnings = [];
|
|
75
|
+
if (!isRecord(value)) {
|
|
76
|
+
return { isValid: false, errors: ['Catalog must be a JSON object'], warnings };
|
|
77
|
+
}
|
|
78
|
+
if (value.schemaVersion !== SUPPORTED_SCHEMA_VERSION) {
|
|
79
|
+
errors.push(`Unsupported schemaVersion: expected ${SUPPORTED_SCHEMA_VERSION}`);
|
|
80
|
+
}
|
|
81
|
+
if (!Array.isArray(value.modules)) {
|
|
82
|
+
errors.push('modules must be an array');
|
|
83
|
+
return { isValid: false, errors, warnings };
|
|
84
|
+
}
|
|
85
|
+
const modules = [];
|
|
86
|
+
const seenIds = new Set();
|
|
87
|
+
for (let index = 0; index < value.modules.length; index += 1) {
|
|
88
|
+
const entry = validateModuleEntry(value.modules[index], index, errors);
|
|
89
|
+
if (!entry) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (seenIds.has(entry.id)) {
|
|
93
|
+
errors.push(`Duplicate module id: ${entry.id}`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
seenIds.add(entry.id);
|
|
97
|
+
modules.push(entry);
|
|
98
|
+
}
|
|
99
|
+
if (errors.length > 0) {
|
|
100
|
+
return { isValid: false, errors, warnings };
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
isValid: true,
|
|
104
|
+
errors,
|
|
105
|
+
warnings,
|
|
106
|
+
catalog: {
|
|
107
|
+
schemaVersion: SUPPORTED_SCHEMA_VERSION,
|
|
108
|
+
modules,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Parse catalog JSON text and validate it.
|
|
114
|
+
*/
|
|
115
|
+
export function parseModuleCatalog(json) {
|
|
116
|
+
let parsed;
|
|
117
|
+
try {
|
|
118
|
+
parsed = JSON.parse(json);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
const message = error instanceof Error ? error.message : 'Invalid JSON';
|
|
122
|
+
throw new Error(`Failed to parse module catalog: ${message}`);
|
|
123
|
+
}
|
|
124
|
+
const result = validateModuleCatalog(parsed);
|
|
125
|
+
if (!result.isValid || !result.catalog) {
|
|
126
|
+
throw new Error(`Invalid module catalog: ${result.errors.join(', ')}`);
|
|
127
|
+
}
|
|
128
|
+
return result.catalog;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Compare dotted semver-like versions. Returns negative if left < right, 0 if equal, positive if left > right.
|
|
132
|
+
*/
|
|
133
|
+
export function compareSemver(left, right) {
|
|
134
|
+
const leftParts = left.split('.').map((part) => Number.parseInt(part, 10) || 0);
|
|
135
|
+
const rightParts = right.split('.').map((part) => Number.parseInt(part, 10) || 0);
|
|
136
|
+
const length = Math.max(leftParts.length, rightParts.length);
|
|
137
|
+
for (let index = 0; index < length; index += 1) {
|
|
138
|
+
const leftValue = leftParts[index] ?? 0;
|
|
139
|
+
const rightValue = rightParts[index] ?? 0;
|
|
140
|
+
if (leftValue !== rightValue) {
|
|
141
|
+
return leftValue - rightValue;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* True when the installed API version satisfies the module's minimum.
|
|
148
|
+
*/
|
|
149
|
+
export function isApiVersionCompatible(installedApiVersion, minApiVersion) {
|
|
150
|
+
return compareSemver(installedApiVersion, minApiVersion) >= 0;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Normalize integrity strings for comparison (lowercase hex).
|
|
154
|
+
*/
|
|
155
|
+
export function normalizeIntegrity(integrity) {
|
|
156
|
+
return integrity.trim().toLowerCase();
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Build a sha256 integrity string from a hex digest.
|
|
160
|
+
*/
|
|
161
|
+
export function integrityFromSha256Hex(hex) {
|
|
162
|
+
return `sha256:${hex.trim().toLowerCase()}`;
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=module-catalog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-catalog.js","sourceRoot":"","sources":["../../src/catalog/module-catalog.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type { RadioConfigMetadata, RadioCapabilities, CodecConfig, RegistryRadio, RadioCatalogEntry, RadioCatalogSource, } from './types/radio-config.js';
|
|
2
|
+
export type { RadioModuleCatalog, RadioModuleCatalogEntry, CatalogValidationResult, } from './types/module-catalog.js';
|
|
2
3
|
export type { ValidationResult } from '@springfield/ham-radio-api';
|
|
3
4
|
export type { PluginModule, PluginCapabilities, SpringfieldPluginConfig, NpmPluginInfo, PluginManifest, PluginManifestConfiguration, PluginSharedComponents, PluginSecurity, } from './types/plugin-module.js';
|
|
4
|
-
export { hydrateRadioConfig, extractCatalogMetadata, validateConfiguration, computeContentHash, hashRadioConfig, } from './catalog/index.js';
|
|
5
|
+
export { hydrateRadioConfig, extractCatalogMetadata, validateConfiguration, computeContentHash, hashRadioConfig, validateModuleCatalog, parseModuleCatalog, compareSemver, isApiVersionCompatible, normalizeIntegrity, integrityFromSha256Hex, } from './catalog/index.js';
|
|
5
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,mBAAmB,EACnB,iBAAiB,EACjB,WAAW,EACX,aAAa,EACb,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,mBAAmB,EACnB,iBAAiB,EACjB,WAAW,EACX,aAAa,EACb,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AAEjC,YAAY,EACV,kBAAkB,EAClB,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAEnE,YAAY,EACV,YAAY,EACZ,kBAAkB,EAClB,uBAAuB,EACvB,aAAa,EACb,cAAc,EACd,2BAA2B,EAC3B,sBAAsB,EACtB,cAAc,GACf,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,EACrB,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,kBAAkB,EAClB,aAAa,EACb,sBAAsB,EACtB,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,oBAAoB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Browser-safe catalog API (no node:fs)
|
|
2
|
-
export { hydrateRadioConfig, extractCatalogMetadata, validateConfiguration, computeContentHash, hashRadioConfig, } from './catalog/index.js';
|
|
2
|
+
export { hydrateRadioConfig, extractCatalogMetadata, validateConfiguration, computeContentHash, hashRadioConfig, validateModuleCatalog, parseModuleCatalog, compareSemver, isApiVersionCompatible, normalizeIntegrity, integrityFromSha256Hex, } from './catalog/index.js';
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA8BA,wCAAwC;AACxC,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,EACrB,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,kBAAkB,EAClB,aAAa,EACb,sBAAsB,EACtB,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Official radio module catalog index (served via GitHub Pages).
|
|
3
|
+
*/
|
|
4
|
+
export interface RadioModuleCatalog {
|
|
5
|
+
schemaVersion: number;
|
|
6
|
+
modules: RadioModuleCatalogEntry[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* One installable official radio module listed in the catalog.
|
|
10
|
+
*/
|
|
11
|
+
export interface RadioModuleCatalogEntry {
|
|
12
|
+
id: string;
|
|
13
|
+
package: string;
|
|
14
|
+
manufacturer: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
version: string;
|
|
17
|
+
supportedRadios: string[];
|
|
18
|
+
minApiVersion: string;
|
|
19
|
+
downloadUrl: string;
|
|
20
|
+
/** SHA-256 of the zip as `sha256:<hex>`. */
|
|
21
|
+
integrity: string;
|
|
22
|
+
}
|
|
23
|
+
export interface CatalogValidationResult {
|
|
24
|
+
isValid: boolean;
|
|
25
|
+
errors: string[];
|
|
26
|
+
warnings: string[];
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=module-catalog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-catalog.d.ts","sourceRoot":"","sources":["../../src/types/module-catalog.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,uBAAuB,EAAE,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-catalog.js","sourceRoot":"","sources":["../../src/types/module-catalog.ts"],"names":[],"mappings":""}
|
|
@@ -39,7 +39,7 @@ export interface RegistryRadio extends Radio {
|
|
|
39
39
|
/**
|
|
40
40
|
* Source of a catalogued radio configuration
|
|
41
41
|
*/
|
|
42
|
-
export type RadioCatalogSource = 'bundled' | 'user';
|
|
42
|
+
export type RadioCatalogSource = 'bundled' | 'installed' | 'user';
|
|
43
43
|
/**
|
|
44
44
|
* Lightweight catalog metadata for listing radios (e.g. import dialog)
|
|
45
45
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"radio-config.d.ts","sourceRoot":"","sources":["../../src/types/radio-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,4BAA4B,CAAC;AAExD;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;IACrB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,WAAW,CAAC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,aAAc,SAAQ,KAAK;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,iBAAiB,CAAC;IAChC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,mBAAmB,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"radio-config.d.ts","sourceRoot":"","sources":["../../src/types/radio-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,4BAA4B,CAAC;AAExD;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;IACrB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,WAAW,CAAC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,aAAc,SAAQ,KAAK;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,iBAAiB,CAAC;IAChC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,mBAAmB,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,WAAW,GAAG,MAAM,CAAC;AAElE;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,iBAAiB,CAAC;IAChC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;CACrB"}
|
package/package.json
CHANGED
package/src/catalog/index.ts
CHANGED
|
@@ -2,3 +2,11 @@ export { hydrateRadioConfig } from './hydrate-radio-config.js';
|
|
|
2
2
|
export { extractCatalogMetadata } from './extract-catalog-metadata.js';
|
|
3
3
|
export { validateConfiguration } from './validate-configuration.js';
|
|
4
4
|
export { computeContentHash, hashRadioConfig } from './content-hash.js';
|
|
5
|
+
export {
|
|
6
|
+
validateModuleCatalog,
|
|
7
|
+
parseModuleCatalog,
|
|
8
|
+
compareSemver,
|
|
9
|
+
isApiVersionCompatible,
|
|
10
|
+
normalizeIntegrity,
|
|
11
|
+
integrityFromSha256Hex,
|
|
12
|
+
} from './module-catalog.js';
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { CatalogValidationResult, RadioModuleCatalog, RadioModuleCatalogEntry } from '../types/module-catalog.js';
|
|
2
|
+
|
|
3
|
+
const INTEGRITY_PATTERN = /^sha256:[a-fA-F0-9]{64}$/;
|
|
4
|
+
const SUPPORTED_SCHEMA_VERSION = 1;
|
|
5
|
+
|
|
6
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
7
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
11
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isHttpsUrl(value: string): boolean {
|
|
15
|
+
try {
|
|
16
|
+
const url = new URL(value);
|
|
17
|
+
return url.protocol === 'https:';
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validateModuleEntry(entry: unknown, index: number, errors: string[]): RadioModuleCatalogEntry | undefined {
|
|
24
|
+
if (!isRecord(entry)) {
|
|
25
|
+
errors.push(`modules[${index}] must be an object`);
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const prefix = `modules[${index}]`;
|
|
30
|
+
|
|
31
|
+
if (!isNonEmptyString(entry.id)) {
|
|
32
|
+
errors.push(`${prefix}.id is required`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!isNonEmptyString(entry.package)) {
|
|
36
|
+
errors.push(`${prefix}.package is required`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!isNonEmptyString(entry.manufacturer)) {
|
|
40
|
+
errors.push(`${prefix}.manufacturer is required`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!isNonEmptyString(entry.version)) {
|
|
44
|
+
errors.push(`${prefix}.version is required`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!Array.isArray(entry.supportedRadios) || entry.supportedRadios.length === 0) {
|
|
48
|
+
errors.push(`${prefix}.supportedRadios must be a non-empty array`);
|
|
49
|
+
} else if (!entry.supportedRadios.every((radio) => isNonEmptyString(radio))) {
|
|
50
|
+
errors.push(`${prefix}.supportedRadios must contain only non-empty strings`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!isNonEmptyString(entry.minApiVersion)) {
|
|
54
|
+
errors.push(`${prefix}.minApiVersion is required`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!isNonEmptyString(entry.downloadUrl) || !isHttpsUrl(entry.downloadUrl)) {
|
|
58
|
+
errors.push(`${prefix}.downloadUrl must be an https URL`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!isNonEmptyString(entry.integrity) || !INTEGRITY_PATTERN.test(entry.integrity)) {
|
|
62
|
+
errors.push(`${prefix}.integrity must match sha256:<64 hex chars>`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (entry.description !== undefined && typeof entry.description !== 'string') {
|
|
66
|
+
errors.push(`${prefix}.description must be a string when present`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (errors.some((error) => error.startsWith(prefix))) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
id: entry.id as string,
|
|
75
|
+
package: entry.package as string,
|
|
76
|
+
manufacturer: entry.manufacturer as string,
|
|
77
|
+
...(typeof entry.description === 'string' ? { description: entry.description } : {}),
|
|
78
|
+
version: entry.version as string,
|
|
79
|
+
supportedRadios: entry.supportedRadios as string[],
|
|
80
|
+
minApiVersion: entry.minApiVersion as string,
|
|
81
|
+
downloadUrl: entry.downloadUrl as string,
|
|
82
|
+
integrity: (entry.integrity as string).toLowerCase(),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Validate and normalize a catalog.json document.
|
|
88
|
+
*/
|
|
89
|
+
export function validateModuleCatalog(value: unknown): CatalogValidationResult & { catalog?: RadioModuleCatalog } {
|
|
90
|
+
const errors: string[] = [];
|
|
91
|
+
const warnings: string[] = [];
|
|
92
|
+
|
|
93
|
+
if (!isRecord(value)) {
|
|
94
|
+
return { isValid: false, errors: ['Catalog must be a JSON object'], warnings };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (value.schemaVersion !== SUPPORTED_SCHEMA_VERSION) {
|
|
98
|
+
errors.push(`Unsupported schemaVersion: expected ${SUPPORTED_SCHEMA_VERSION}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!Array.isArray(value.modules)) {
|
|
102
|
+
errors.push('modules must be an array');
|
|
103
|
+
return { isValid: false, errors, warnings };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const modules: RadioModuleCatalogEntry[] = [];
|
|
107
|
+
const seenIds = new Set<string>();
|
|
108
|
+
|
|
109
|
+
for (let index = 0; index < value.modules.length; index += 1) {
|
|
110
|
+
const entry = validateModuleEntry(value.modules[index], index, errors);
|
|
111
|
+
|
|
112
|
+
if (!entry) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (seenIds.has(entry.id)) {
|
|
117
|
+
errors.push(`Duplicate module id: ${entry.id}`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
seenIds.add(entry.id);
|
|
122
|
+
modules.push(entry);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (errors.length > 0) {
|
|
126
|
+
return { isValid: false, errors, warnings };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
isValid: true,
|
|
131
|
+
errors,
|
|
132
|
+
warnings,
|
|
133
|
+
catalog: {
|
|
134
|
+
schemaVersion: SUPPORTED_SCHEMA_VERSION,
|
|
135
|
+
modules,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Parse catalog JSON text and validate it.
|
|
142
|
+
*/
|
|
143
|
+
export function parseModuleCatalog(json: string): RadioModuleCatalog {
|
|
144
|
+
let parsed: unknown;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
parsed = JSON.parse(json);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
const message = error instanceof Error ? error.message : 'Invalid JSON';
|
|
150
|
+
throw new Error(`Failed to parse module catalog: ${message}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const result = validateModuleCatalog(parsed);
|
|
154
|
+
|
|
155
|
+
if (!result.isValid || !result.catalog) {
|
|
156
|
+
throw new Error(`Invalid module catalog: ${result.errors.join(', ')}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return result.catalog;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Compare dotted semver-like versions. Returns negative if left < right, 0 if equal, positive if left > right.
|
|
164
|
+
*/
|
|
165
|
+
export function compareSemver(left: string, right: string): number {
|
|
166
|
+
const leftParts = left.split('.').map((part) => Number.parseInt(part, 10) || 0);
|
|
167
|
+
const rightParts = right.split('.').map((part) => Number.parseInt(part, 10) || 0);
|
|
168
|
+
const length = Math.max(leftParts.length, rightParts.length);
|
|
169
|
+
|
|
170
|
+
for (let index = 0; index < length; index += 1) {
|
|
171
|
+
const leftValue = leftParts[index] ?? 0;
|
|
172
|
+
const rightValue = rightParts[index] ?? 0;
|
|
173
|
+
|
|
174
|
+
if (leftValue !== rightValue) {
|
|
175
|
+
return leftValue - rightValue;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* True when the installed API version satisfies the module's minimum.
|
|
184
|
+
*/
|
|
185
|
+
export function isApiVersionCompatible(installedApiVersion: string, minApiVersion: string): boolean {
|
|
186
|
+
return compareSemver(installedApiVersion, minApiVersion) >= 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Normalize integrity strings for comparison (lowercase hex).
|
|
191
|
+
*/
|
|
192
|
+
export function normalizeIntegrity(integrity: string): string {
|
|
193
|
+
return integrity.trim().toLowerCase();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Build a sha256 integrity string from a hex digest.
|
|
198
|
+
*/
|
|
199
|
+
export function integrityFromSha256Hex(hex: string): string {
|
|
200
|
+
return `sha256:${hex.trim().toLowerCase()}`;
|
|
201
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,12 @@ export type {
|
|
|
8
8
|
RadioCatalogSource,
|
|
9
9
|
} from './types/radio-config.js';
|
|
10
10
|
|
|
11
|
+
export type {
|
|
12
|
+
RadioModuleCatalog,
|
|
13
|
+
RadioModuleCatalogEntry,
|
|
14
|
+
CatalogValidationResult,
|
|
15
|
+
} from './types/module-catalog.js';
|
|
16
|
+
|
|
11
17
|
// Re-export common types from ham-radio-api
|
|
12
18
|
export type { ValidationResult } from '@springfield/ham-radio-api';
|
|
13
19
|
|
|
@@ -29,4 +35,10 @@ export {
|
|
|
29
35
|
validateConfiguration,
|
|
30
36
|
computeContentHash,
|
|
31
37
|
hashRadioConfig,
|
|
38
|
+
validateModuleCatalog,
|
|
39
|
+
parseModuleCatalog,
|
|
40
|
+
compareSemver,
|
|
41
|
+
isApiVersionCompatible,
|
|
42
|
+
normalizeIntegrity,
|
|
43
|
+
integrityFromSha256Hex,
|
|
32
44
|
} from './catalog/index.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Official radio module catalog index (served via GitHub Pages).
|
|
3
|
+
*/
|
|
4
|
+
export interface RadioModuleCatalog {
|
|
5
|
+
schemaVersion: number;
|
|
6
|
+
modules: RadioModuleCatalogEntry[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* One installable official radio module listed in the catalog.
|
|
11
|
+
*/
|
|
12
|
+
export interface RadioModuleCatalogEntry {
|
|
13
|
+
id: string;
|
|
14
|
+
package: string;
|
|
15
|
+
manufacturer: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
version: string;
|
|
18
|
+
supportedRadios: string[];
|
|
19
|
+
minApiVersion: string;
|
|
20
|
+
downloadUrl: string;
|
|
21
|
+
/** SHA-256 of the zip as `sha256:<hex>`. */
|
|
22
|
+
integrity: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CatalogValidationResult {
|
|
26
|
+
isValid: boolean;
|
|
27
|
+
errors: string[];
|
|
28
|
+
warnings: string[];
|
|
29
|
+
}
|
|
@@ -44,7 +44,7 @@ export interface RegistryRadio extends Radio {
|
|
|
44
44
|
/**
|
|
45
45
|
* Source of a catalogued radio configuration
|
|
46
46
|
*/
|
|
47
|
-
export type RadioCatalogSource = 'bundled' | 'user';
|
|
47
|
+
export type RadioCatalogSource = 'bundled' | 'installed' | 'user';
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* Lightweight catalog metadata for listing radios (e.g. import dialog)
|