fhirsmith 0.18.1 → 1.0.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/ACCESSIBILITY.md +119 -0
- package/CHANGELOG.md +190 -9
- package/CONFIGURATION.md +305 -0
- package/LICENSE +29 -0
- package/README.md +29 -122
- package/cluster.js +1220 -0
- package/cluster.md +707 -0
- package/config-template.json +77 -8
- package/extension-tracker/extension-tracker-template.html +5 -8
- package/extension-tracker/extension-tracker.js +0 -2
- package/folder/folder.js +7 -6
- package/ips/analysis.js +560 -0
- package/ips/ips-template.html +5 -8
- package/ips/ips.js +331 -33
- package/ips/jurisdictions.js +65 -0
- package/ips/nationality.js +120 -0
- package/ips/pipeline.js +131 -17
- package/ips/presenter.js +1 -1
- package/ips/profile-editor.js +283 -4
- package/ips/readme.md +170 -20
- package/ips/render.js +769 -109
- package/ips/scripts.js +46 -0
- package/ips/store.js +123 -11
- package/ips/structure.js +247 -0
- package/ips/terminology.js +1 -1
- package/library/body-limit.js +22 -0
- package/library/cache-generation.js +15 -0
- package/library/cgroup-memory.js +133 -1
- package/library/cluster-counts.js +121 -0
- package/library/cluster-events.js +153 -0
- package/library/cluster-metrics.js +193 -0
- package/library/cluster-status.js +468 -0
- package/library/config-reader.js +96 -0
- package/library/daily-report.js +100 -2
- package/library/html-server.js +27 -1
- package/library/i18nsupport.js +23 -1
- package/library/keep-alive.js +18 -0
- package/library/languages.js +17 -3
- package/library/logger.js +31 -0
- package/library/memory-guard.js +428 -0
- package/library/native-memory.js +52 -0
- package/library/npm-audit.js +51 -25
- package/library/openapi-doc.js +7 -5
- package/library/package-manager.js +8 -0
- package/library/role.js +197 -0
- package/library/server-credentials.js +169 -0
- package/library/sqlite-utilities.js +59 -0
- package/library/ssrf-guard.js +100 -0
- package/library/static-response-stats.js +16 -0
- package/library/version-utilities.js +50 -0
- package/library/worker-count.js +192 -0
- package/nginx.md +30 -11
- package/npmprojector/indexer.js +18 -10
- package/package.json +11 -8
- package/packages/feed-check-page.js +117 -0
- package/packages/feed-check.js +279 -0
- package/packages/npm-check.js +57 -3
- package/packages/npm-signing.js +219 -0
- package/packages/openapi.yaml +207 -1
- package/packages/package-crawler.js +322 -41
- package/packages/packages-template.html +6 -8
- package/packages/packages.js +604 -57
- package/packages/readme.md +113 -1
- package/publisher/publisher-template.html +2 -5
- package/publisher/publisher.js +121 -110
- package/publisher/readme.md +75 -0
- package/registry/api.js +179 -1
- package/registry/crawler.js +86 -108
- package/registry/documentation/api.md +68 -2
- package/registry/documentation/configuration.md +34 -0
- package/registry/documentation/internals.md +4 -1
- package/registry/documentation/readme.md +3 -2
- package/registry/model.js +8 -0
- package/registry/openapi.yaml +84 -0
- package/registry/registry-template.html +5 -8
- package/registry/registry.js +58 -6
- package/root-bare-template.html +7 -4
- package/root-template.html +5 -8
- package/server.js +398 -198
- package/shl/shl.js +38 -37
- package/static/a11y/a11y-check.js +832 -0
- package/static/a11y/axe.min.js +12 -0
- package/static/assets/css/bootstrap-fhir.css +15 -15
- package/static/assets/images/flags/au.svg +18 -0
- package/static/assets/images/flags/unknown.svg +11 -0
- package/static/assets/images/flags/us.svg +63 -0
- package/static/assets/images/icon_audit.png +0 -0
- package/static/assets/images/icon_clinical.png +0 -0
- package/static/assets/images/icon_document.png +0 -0
- package/static/assets/images/icon_external.png +0 -0
- package/static/assets/images/icon_medication.png +0 -0
- package/static/assets/images/icon_organization.png +0 -0
- package/static/assets/images/icon_other.png +0 -0
- package/static/assets/images/icon_person.png +0 -0
- package/static/assets/images/icon_section.png +0 -0
- package/stats-db.js +45 -0
- package/stats.js +197 -10
- package/testing/testing-template.html +5 -8
- package/token/nginx_passport_setup.md +1 -1
- package/token/security_guide.md +1 -1
- package/token/token-template.html +2 -5
- package/token/token.js +40 -36
- package/translations/Messages.properties +19 -0
- package/tx/README.md +158 -4
- package/tx/closure/closure-store.js +126 -3
- package/tx/cm/cm-database.js +69 -57
- package/tx/cm/cm-package.js +50 -16
- package/tx/cs/cs-api.js +284 -19
- package/tx/cs/cs-api.md +41 -4
- package/tx/cs/cs-areacode.js +1 -1
- package/tx/cs/cs-country.js +17 -5
- package/tx/cs/cs-cpt.js +33 -16
- package/tx/cs/cs-cs.js +199 -24
- package/tx/cs/cs-currency.js +1 -1
- package/tx/cs/cs-icd11.js +21 -13
- package/tx/cs/cs-icf.js +22 -3
- package/tx/cs/cs-lang.js +2 -2
- package/tx/cs/cs-loinc.js +193 -67
- package/tx/cs/cs-mimetypes.js +12 -2
- package/tx/cs/cs-ndc.js +40 -25
- package/tx/cs/cs-omop.js +54 -29
- package/tx/cs/cs-rxnorm.js +300 -196
- package/tx/cs/cs-snomed.js +861 -95
- package/tx/cs/cs-ucum.js +14 -4
- package/tx/cs/cs-unii.js +18 -14
- package/tx/cs/cs-virtual.js +210 -0
- package/tx/ecosystem-resolver.js +413 -0
- package/tx/html/cluster-dash-metrics.liquid +26 -0
- package/tx/html/cluster-home-metrics.liquid +31 -0
- package/tx/html/cluster-metrics-script.liquid +251 -0
- package/tx/html/codesystem-operations.liquid +3 -3
- package/tx/html/ecl-panel.liquid +1 -1
- package/tx/html/external-metrics.liquid +17 -0
- package/tx/html/operations-form.liquid +17 -17
- package/tx/html/search-form.liquid +11 -11
- package/tx/html/status-line.liquid +58 -0
- package/tx/html/status-panel.liquid +140 -0
- package/tx/html/status-tasks.liquid +25 -0
- package/tx/html/tx-template.html +2 -5
- package/tx/importers/atc-to-fhir.js +17 -16
- package/tx/importers/export-icd11-codesystem.js +3 -2
- package/tx/importers/import-icd11.js +4 -3
- package/tx/importers/import-loinc.module.js +118 -56
- package/tx/importers/import-ndc.module.js +56 -44
- package/tx/importers/import-rxnorm.module.js +118 -79
- package/tx/importers/import-sct.module.js +125 -61
- package/tx/importers/import-unii.module.js +37 -36
- package/tx/importers/subset-loinc.module.js +49 -48
- package/tx/importers/subset-rxnorm.module.js +29 -28
- package/tx/importers/terminal.js +18 -0
- package/tx/importers/tx-import-base.js +13 -12
- package/tx/importers/tx-import-settings.js +5 -4
- package/tx/importers/tx-import.js +53 -52
- package/tx/library/canonical-resource.js +49 -0
- package/tx/library/codesystem.js +94 -5
- package/tx/library/designations.js +45 -2
- package/tx/library/extensions.js +13 -7
- package/tx/library/implicit-valuesets.js +66 -0
- package/tx/library/renderer.js +77 -21
- package/tx/library/sqlite-utilities.js +2 -0
- package/tx/library/tho-precedence.js +40 -0
- package/tx/library/ucum-parsers.js +11 -1
- package/tx/library/wait-for-cache.js +43 -0
- package/tx/library.js +321 -22
- package/tx/openapi-operations.js +6 -4
- package/tx/operation-context.js +460 -99
- package/tx/params.js +25 -0
- package/tx/problems.js +3 -5
- package/tx/provider.js +314 -67
- package/tx/resource-cache-sqlite.js +1050 -0
- package/tx/sct/documentation.md +45 -21
- package/tx/sct/ecl.js +81 -17
- package/tx/sct/editions.js +39 -0
- package/tx/sct/expressions.js +10 -1
- package/tx/sct/load-config.js +107 -0
- package/tx/sct/mmap.js +109 -0
- package/tx/sct/structures.js +175 -15
- package/tx/sct/text-index.js +5 -1
- package/tx/syndication/syndication-feed.js +2 -2
- package/tx/tx-html.js +44 -8
- package/tx/tx.fhir.org.yml +8 -8
- package/tx/tx.js +282 -80
- package/tx/usage-tracker.js +39 -11
- package/tx/vs/vs-api.js +19 -0
- package/tx/vs/vs-database.js +386 -62
- package/tx/vs/vs-package.js +44 -17
- package/tx/vs/vs-vsac.js +649 -101
- package/tx/workers/batch-validate.js +37 -10
- package/tx/workers/batch.js +6 -1
- package/tx/workers/cache-control.js +15 -4
- package/tx/workers/closure.js +156 -13
- package/tx/workers/compare.js +8 -5
- package/tx/workers/expand.js +1173 -289
- package/tx/workers/metadata.js +35 -0
- package/tx/workers/search.js +53 -1
- package/tx/workers/supplement-filters.js +145 -0
- package/tx/workers/translate.js +169 -4
- package/tx/workers/validate.js +223 -119
- package/tx/workers/worker.js +123 -31
- package/tx/xversion/xv-valueset.js +7 -4
- package/utilities/submit-test-report.js +3 -1
- package/utilities/tx-stress-test.js +482 -0
- package/vcl/readme.md +37 -0
- package/vcl/vcl-parser.js +15 -0
- package/xig/readme.md +52 -0
- package/xig/xig-template.html +5 -8
- package/xig/xig.js +238 -422
- package/changelog-template.md +0 -22
- package/registry/test-runner.js +0 -237
- package/release.sh +0 -80
- package/test-scripts/repro-re2-wasm-leak.js +0 -34
- package/tsconfig.eslint.json +0 -9
- package/tx/html/dash-metrics.liquid +0 -147
- package/tx/html/home-metrics.liquid +0 -225
package/packages/packages.js
CHANGED
|
@@ -17,11 +17,16 @@ const Logger = require('../library/logger');
|
|
|
17
17
|
const {validateParameter, Utilities} = require("../library/utilities");
|
|
18
18
|
const {describeCron} = require("../library/cron-utilities");
|
|
19
19
|
const {tokenMatches, tokenConfigured} = require("../library/request-token");
|
|
20
|
+
const { guarded } = require('../library/sqlite-utilities');
|
|
20
21
|
const pckLog = Logger.getInstance().child({ module: 'packages' });
|
|
21
22
|
const packagesOpenApi = require('./openapi');
|
|
22
23
|
const rateLimit = require('express-rate-limit');
|
|
23
24
|
const { runNpmCheck, makeGetter } = require('./npm-check');
|
|
24
25
|
const npmCheckPage = require('./npm-check-page');
|
|
26
|
+
const { FeedCheck } = require('./feed-check');
|
|
27
|
+
const feedCheckPage = require('./feed-check-page');
|
|
28
|
+
const crypto = require('crypto');
|
|
29
|
+
const { NpmSigner, integrityOf } = require('./npm-signing');
|
|
25
30
|
|
|
26
31
|
// Query parameter rules for validateQueryParams. These are also the contract published in
|
|
27
32
|
// openapi.yaml, and tests/packages/openapi.test.js checks that the two agree - so change
|
|
@@ -101,6 +106,25 @@ const NPM_CHECK_DEFAULTS = {
|
|
|
101
106
|
};
|
|
102
107
|
const NPM_CHECK_CONTROL_MAX_AGE = 24 * 60 * 60 * 1000;
|
|
103
108
|
|
|
109
|
+
// GET /feed-check: check a package feed as the crawler reads it, storing nothing (also in openapi.yaml)
|
|
110
|
+
const FEED_CHECK_PARAMS = {
|
|
111
|
+
url: { maxLength: 500, pattern: /^https?:\/\/[^\s<>"'`]+$/ },
|
|
112
|
+
// the feed's signing keys, as JSON: package-feeds.json's signing block ({required, keys}), or a
|
|
113
|
+
// whole feed entry with one. Used instead of what the registry lists for the feed
|
|
114
|
+
signing: { maxLength: 5000, pattern: /^[^<>`]*$/ }
|
|
115
|
+
};
|
|
116
|
+
// feed-check defaults; all can be set under modules.packages.feedCheck in the config. Only one
|
|
117
|
+
// check runs at a time, across all users
|
|
118
|
+
const FEED_CHECK_DEFAULTS = {
|
|
119
|
+
enabled: true,
|
|
120
|
+
maxItems: 50, // items checked per run (each one's package is downloaded)
|
|
121
|
+
maxTotalMB: 500, // downloaded per run; later items are reported but not downloaded
|
|
122
|
+
keepMinutes: 60 // how long a run's report can be fetched
|
|
123
|
+
};
|
|
124
|
+
const FEED_CHECK_MAX_JOBS = 100;
|
|
125
|
+
const FEED_CHECK_JOB_PATTERN = /^[0-9a-f]{24}$/;
|
|
126
|
+
const MASTER_LIST_MAX_AGE = 60 * 60 * 1000;
|
|
127
|
+
|
|
104
128
|
// The query string of a request, for logging (the parameters were validated before the handler runs)
|
|
105
129
|
function queryString(req) {
|
|
106
130
|
const i = req.originalUrl.indexOf('?');
|
|
@@ -115,6 +139,60 @@ const TARBALL_HEADER = 'FHIR-Package-Download';
|
|
|
115
139
|
// Each id#version is stored once (see PackageCrawler.checkVersion)
|
|
116
140
|
const UNIQUE_ID_VERSION_INDEX = 'CREATE UNIQUE INDEX SK_PackageVersions_IdVersion ON PackageVersions (Id, Version)';
|
|
117
141
|
|
|
142
|
+
// Each version's dist.integrity and npm registry signature (see npm-signing.js), kept apart
|
|
143
|
+
// from PackageVersions so reading them doesn't mean reading the tarball (Content)
|
|
144
|
+
const PACKAGE_DIST_TABLE = `CREATE TABLE IF NOT EXISTS PackageDist
|
|
145
|
+
(
|
|
146
|
+
PackageVersionKey INTEGER PRIMARY KEY,
|
|
147
|
+
Integrity TEXT NOT NULL,
|
|
148
|
+
KeyId TEXT,
|
|
149
|
+
Signature TEXT
|
|
150
|
+
)`;
|
|
151
|
+
const DEFAULT_SIGNING_KEY_FILE = 'npm-signing-key.pem';
|
|
152
|
+
|
|
153
|
+
// Publishers' signatures of the packages (fhir:signature in their feeds), kept as they were when
|
|
154
|
+
// the crawler checked them - valid or not - with the outcome (see PackageCrawler.signingFor)
|
|
155
|
+
const PACKAGE_SIGNATURES_TABLE = `CREATE TABLE IF NOT EXISTS PackageSignatures
|
|
156
|
+
(
|
|
157
|
+
PackageVersionKey INTEGER PRIMARY KEY,
|
|
158
|
+
Url TEXT NOT NULL,
|
|
159
|
+
KeyId TEXT,
|
|
160
|
+
Valid INTEGER NOT NULL,
|
|
161
|
+
Message TEXT,
|
|
162
|
+
Checked TEXT NOT NULL,
|
|
163
|
+
Bundle TEXT NOT NULL
|
|
164
|
+
)`;
|
|
165
|
+
|
|
166
|
+
// The signing information a user gives the feed check: package-feeds.json's signing block
|
|
167
|
+
// ({required, keys: [{keyid, key, expires}]}), or a feed entry that has one. Throws if unusable.
|
|
168
|
+
function parseSigningJson(text) {
|
|
169
|
+
let json;
|
|
170
|
+
try {
|
|
171
|
+
json = JSON.parse(text);
|
|
172
|
+
} catch (e) {
|
|
173
|
+
throw new Error(`it isn't JSON (${e.message})`);
|
|
174
|
+
}
|
|
175
|
+
const block = json && typeof json === 'object' && json.signing && typeof json.signing === 'object' ? json.signing : json;
|
|
176
|
+
if (!block || typeof block !== 'object' || !Array.isArray(block.keys) || block.keys.length === 0) {
|
|
177
|
+
throw new Error('it needs a keys array, as in package-feeds.json\'s signing block');
|
|
178
|
+
}
|
|
179
|
+
const keys = block.keys.map((k, i) => {
|
|
180
|
+
if (!k || typeof k.keyid !== 'string' || typeof k.key !== 'string') {
|
|
181
|
+
throw new Error(`keys[${i}] needs a keyid and a key`);
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
crypto.createPublicKey({key: Buffer.from(k.key, 'base64'), format: 'der', type: 'spki'});
|
|
185
|
+
} catch (e) {
|
|
186
|
+
throw new Error(`keys[${i}].key isn't a base64 public key (SubjectPublicKeyInfo)`);
|
|
187
|
+
}
|
|
188
|
+
if (k.expires !== undefined && k.expires !== null && Number.isNaN(Date.parse(k.expires))) {
|
|
189
|
+
throw new Error(`keys[${i}].expires isn't a date`);
|
|
190
|
+
}
|
|
191
|
+
return {keyid: k.keyid, key: k.key, expires: k.expires || null};
|
|
192
|
+
});
|
|
193
|
+
return {required: block.required === true, keys};
|
|
194
|
+
}
|
|
195
|
+
|
|
118
196
|
class PackagesModule {
|
|
119
197
|
constructor(stats) {
|
|
120
198
|
this.router = express.Router();
|
|
@@ -126,6 +204,15 @@ class PackagesModule {
|
|
|
126
204
|
this.totalRuns = 0;
|
|
127
205
|
this.lastCrawlerLog = {};
|
|
128
206
|
this.crawlerRunning = false;
|
|
207
|
+
// npm registry signatures: the key (null when this server doesn't sign), and the
|
|
208
|
+
// background job that fills in PackageDist for versions stored without it
|
|
209
|
+
this.signer = null;
|
|
210
|
+
// feed checks (/feed-check): id -> {id, url, state, report, error, started, ip}
|
|
211
|
+
this.feedCheckJobs = new Map();
|
|
212
|
+
this.feedCheckRunning = false;
|
|
213
|
+
this.distStatus = { state: 'not started', missing: null, done: 0, failed: 0 };
|
|
214
|
+
this.distBackfill = null;
|
|
215
|
+
this.stopping = false;
|
|
129
216
|
this.setupSecurityMiddleware();
|
|
130
217
|
this.setupRoutes();
|
|
131
218
|
this.stats = stats;
|
|
@@ -365,7 +452,7 @@ class PackagesModule {
|
|
|
365
452
|
|
|
366
453
|
const { query, params: queryParams } = this.buildSecureQuery(baseQuery, conditions);
|
|
367
454
|
|
|
368
|
-
this.db.all(query + ' ORDER BY PubDate', queryParams, (err, rows) => {
|
|
455
|
+
this.db.all(query + ' ORDER BY PubDate', queryParams, guarded(reject, (err, rows) => {
|
|
369
456
|
if (err) {
|
|
370
457
|
reject(err);
|
|
371
458
|
return;
|
|
@@ -399,7 +486,7 @@ class PackagesModule {
|
|
|
399
486
|
});
|
|
400
487
|
|
|
401
488
|
resolve(this.applySorting(results, sort));
|
|
402
|
-
});
|
|
489
|
+
}));
|
|
403
490
|
} catch (error) {
|
|
404
491
|
reject(error);
|
|
405
492
|
}
|
|
@@ -503,6 +590,14 @@ class PackagesModule {
|
|
|
503
590
|
}
|
|
504
591
|
|
|
505
592
|
async gatherPackageStatistics() {
|
|
593
|
+
const stats = await this.gatherPackageCounts();
|
|
594
|
+
// the nav bar offers the feed check only where it's turned on
|
|
595
|
+
stats.feedCheckLink = this.feedCheckConfig().enabled
|
|
596
|
+
? '<a href="/packages/feed-check" style="color: gold">Feed Check</a> | ' : '';
|
|
597
|
+
return stats;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async gatherPackageCounts() {
|
|
506
601
|
try {
|
|
507
602
|
// Get database age info
|
|
508
603
|
const dbAge = this.getDatabaseAgeInfo();
|
|
@@ -577,7 +672,7 @@ class PackagesModule {
|
|
|
577
672
|
}
|
|
578
673
|
|
|
579
674
|
async getDatabaseTableCounts() {
|
|
580
|
-
return new Promise((resolve) => {
|
|
675
|
+
return new Promise((resolve, reject) => {
|
|
581
676
|
if (!this.db) {
|
|
582
677
|
resolve({packages: 0, packageVersions: 0});
|
|
583
678
|
return;
|
|
@@ -587,7 +682,7 @@ class PackagesModule {
|
|
|
587
682
|
let completedQueries = 0;
|
|
588
683
|
const totalQueries = 2;
|
|
589
684
|
|
|
590
|
-
this.db.get('SELECT COUNT(*) as count FROM Packages', [], (err, row) => {
|
|
685
|
+
this.db.get('SELECT COUNT(*) as count FROM Packages', [], guarded(reject, (err, row) => {
|
|
591
686
|
if (err) {
|
|
592
687
|
counts.packages = 0;
|
|
593
688
|
} else {
|
|
@@ -598,9 +693,9 @@ class PackagesModule {
|
|
|
598
693
|
if (completedQueries === totalQueries) {
|
|
599
694
|
resolve(counts);
|
|
600
695
|
}
|
|
601
|
-
});
|
|
696
|
+
}));
|
|
602
697
|
|
|
603
|
-
this.db.get('SELECT COUNT(*) as count FROM PackageVersions', [], (err, row) => {
|
|
698
|
+
this.db.get('SELECT COUNT(*) as count FROM PackageVersions', [], guarded(reject, (err, row) => {
|
|
604
699
|
if (err) {
|
|
605
700
|
counts.packageVersions = 0;
|
|
606
701
|
} else {
|
|
@@ -611,7 +706,7 @@ class PackagesModule {
|
|
|
611
706
|
if (completedQueries === totalQueries) {
|
|
612
707
|
resolve(counts);
|
|
613
708
|
}
|
|
614
|
-
});
|
|
709
|
+
}));
|
|
615
710
|
});
|
|
616
711
|
}
|
|
617
712
|
|
|
@@ -673,12 +768,25 @@ class PackagesModule {
|
|
|
673
768
|
|
|
674
769
|
// Initialize database
|
|
675
770
|
await this.initializeDatabase();
|
|
771
|
+
await this.dbRun(PACKAGE_DIST_TABLE);
|
|
772
|
+
await this.dbRun(PACKAGE_SIGNATURES_TABLE);
|
|
676
773
|
|
|
677
774
|
// Ensure mirror directory exists
|
|
678
775
|
await this.ensureMirrorDirectory();
|
|
679
776
|
|
|
777
|
+
// npm registry signatures: on unless turned off
|
|
778
|
+
if (this.config.npmSigning?.enabled !== false) {
|
|
779
|
+
this.signer = NpmSigner.load(this.getSigningKeyPath(), pckLog);
|
|
780
|
+
pckLog.info(`Signing packages with npm key ${this.signer.keyid}`);
|
|
781
|
+
}
|
|
782
|
+
|
|
680
783
|
// Initialize the crawler
|
|
681
784
|
this.crawler = new PackageCrawler(this.config, this.db, this.stats);
|
|
785
|
+
this.crawler.signer = this.signer;
|
|
786
|
+
|
|
787
|
+
// Versions stored without dist.integrity or a signature by this key get them in the
|
|
788
|
+
// background; /-/npm/v1/keys is a 404 until they all have them
|
|
789
|
+
this.startDistBackfill();
|
|
682
790
|
|
|
683
791
|
// Start the hourly web crawler if enabled
|
|
684
792
|
if (config.crawler.enabled) {
|
|
@@ -712,9 +820,137 @@ class PackagesModule {
|
|
|
712
820
|
}
|
|
713
821
|
} finally {
|
|
714
822
|
this.crawlerRunning = false;
|
|
823
|
+
// if the backfill couldn't finish (a crawl's transaction rolled back one of its
|
|
824
|
+
// writes, say), it tries again
|
|
825
|
+
if (this.distStatus.state === 'incomplete') {
|
|
826
|
+
this.startDistBackfill();
|
|
827
|
+
}
|
|
715
828
|
}
|
|
716
829
|
}
|
|
717
830
|
|
|
831
|
+
// The signing key file: npmSigning.keyFile, relative to the database's folder
|
|
832
|
+
getSigningKeyPath() {
|
|
833
|
+
const file = this.config.npmSigning?.keyFile || DEFAULT_SIGNING_KEY_FILE;
|
|
834
|
+
return path.isAbsolute(file) ? file : path.join(path.dirname(this.getDatabasePath()), file);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
dbRun(sql, params = []) {
|
|
838
|
+
return new Promise((resolve, reject) => {
|
|
839
|
+
this.db.run(sql, params, guarded(reject, function (err) {
|
|
840
|
+
if (err) {
|
|
841
|
+
reject(err);
|
|
842
|
+
} else {
|
|
843
|
+
resolve(this);
|
|
844
|
+
}
|
|
845
|
+
}));
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
dbAll(sql, params = []) {
|
|
850
|
+
return new Promise((resolve, reject) => {
|
|
851
|
+
this.db.all(sql, params, guarded(reject, (err, rows) => err ? reject(err) : resolve(rows)));
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
dbGet(sql, params = []) {
|
|
856
|
+
return new Promise((resolve, reject) => {
|
|
857
|
+
this.db.get(sql, params, guarded(reject, (err, row) => err ? reject(err) : resolve(row)));
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Starts the background job that fills in PackageDist, unless it is already running.
|
|
862
|
+
// Returns its promise.
|
|
863
|
+
startDistBackfill() {
|
|
864
|
+
if (!this.distBackfill) {
|
|
865
|
+
this.distBackfill = this.runDistBackfill()
|
|
866
|
+
.catch(e => {
|
|
867
|
+
this.distStatus.state = 'incomplete';
|
|
868
|
+
pckLog.error('Adding npm integrity and signatures failed: ' + e.message);
|
|
869
|
+
})
|
|
870
|
+
.finally(() => {
|
|
871
|
+
this.distBackfill = null;
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
return this.distBackfill;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// The keys of versions with no PackageDist row, or (when signing) one not signed by this key
|
|
878
|
+
async versionsMissingDist() {
|
|
879
|
+
const sql = `SELECT v.PackageVersionKey AS k, d.Integrity AS Integrity
|
|
880
|
+
FROM PackageVersions v LEFT JOIN PackageDist d ON d.PackageVersionKey = v.PackageVersionKey
|
|
881
|
+
WHERE d.PackageVersionKey IS NULL` + (this.signer ? ' OR d.KeyId IS NOT ?' : '') + `
|
|
882
|
+
ORDER BY v.PackageVersionKey`;
|
|
883
|
+
return this.dbAll(sql, this.signer ? [this.signer.keyid] : []);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Gives every version its dist.integrity, and its signature when this server signs: the
|
|
887
|
+
// versions stored before this server did, or signed with an earlier key. A row at a time,
|
|
888
|
+
// in the background, so the server serves meanwhile; a large tarball is hashed in slices.
|
|
889
|
+
async runDistBackfill() {
|
|
890
|
+
// rows for versions that are gone (packages/utilities/dedupe-versions.js deletes versions)
|
|
891
|
+
await this.dbRun('DELETE FROM PackageDist WHERE PackageVersionKey NOT IN (SELECT PackageVersionKey FROM PackageVersions)');
|
|
892
|
+
await this.dbRun('DELETE FROM PackageSignatures WHERE PackageVersionKey NOT IN (SELECT PackageVersionKey FROM PackageVersions)');
|
|
893
|
+
const todo = await this.versionsMissingDist();
|
|
894
|
+
this.distStatus = { state: 'running', missing: todo.length, done: 0, failed: 0 };
|
|
895
|
+
const what = this.signer ? 'npm integrity and signatures' : 'npm integrity';
|
|
896
|
+
if (todo.length > 0) {
|
|
897
|
+
pckLog.info(`Adding ${what} to ${todo.length} package versions, in the background`);
|
|
898
|
+
}
|
|
899
|
+
for (const row of todo) {
|
|
900
|
+
if (this.stopping) {
|
|
901
|
+
this.distStatus.state = 'stopped';
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
try {
|
|
905
|
+
await this.fillDist(row.k, row.Integrity);
|
|
906
|
+
this.distStatus.done++;
|
|
907
|
+
} catch (e) {
|
|
908
|
+
this.distStatus.failed++;
|
|
909
|
+
pckLog.error(`Adding ${what} to package version ${row.k} failed: ${e.message}`);
|
|
910
|
+
}
|
|
911
|
+
if (this.distStatus.done % 1000 === 0 && this.distStatus.done > 0) {
|
|
912
|
+
pckLog.info(`Added ${what} to ${this.distStatus.done} of ${todo.length} package versions`);
|
|
913
|
+
}
|
|
914
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
915
|
+
}
|
|
916
|
+
// Something stored meanwhile is done by the crawler, but a write here can be lost (it
|
|
917
|
+
// joins a crawl's transaction, and that rolls back), so count again
|
|
918
|
+
const left = todo.length === 0 ? 0 : (await this.versionsMissingDist()).length;
|
|
919
|
+
this.distStatus.missing = left;
|
|
920
|
+
this.distStatus.state = left === 0 ? 'complete' : 'incomplete';
|
|
921
|
+
if (todo.length > 0) {
|
|
922
|
+
if (left === 0) {
|
|
923
|
+
pckLog.info(`Added ${what} to every package version`);
|
|
924
|
+
} else {
|
|
925
|
+
pckLog.warn(`${left} package versions still have no ${what}; trying again after the next crawl`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// integrity is the stored one, when only the signature is missing
|
|
931
|
+
async fillDist(key, integrity) {
|
|
932
|
+
let hash = null;
|
|
933
|
+
if (!integrity) {
|
|
934
|
+
const row = await this.dbGet('SELECT Content, Hash FROM PackageVersions WHERE PackageVersionKey = ?', [key]);
|
|
935
|
+
if (!row) {
|
|
936
|
+
return; // deleted meanwhile
|
|
937
|
+
}
|
|
938
|
+
integrity = await integrityOf(row.Content);
|
|
939
|
+
hash = row.Hash;
|
|
940
|
+
}
|
|
941
|
+
const v = await this.dbGet('SELECT Id, Version, Hash FROM PackageVersions WHERE PackageVersionKey = ?', [key]);
|
|
942
|
+
if (!v) {
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
const keyid = this.signer ? this.signer.keyid : null;
|
|
946
|
+
const sig = this.signer ? this.signer.sign(v.Id, v.Version, integrity) : null;
|
|
947
|
+
// Only if the tarball is still the one hashed: the crawler may have replaced it meanwhile
|
|
948
|
+
// (and then it wrote this row itself)
|
|
949
|
+
await this.dbRun(`INSERT OR REPLACE INTO PackageDist (PackageVersionKey, Integrity, KeyId, Signature)
|
|
950
|
+
SELECT ?, ?, ?, ? WHERE EXISTS (SELECT 1 FROM PackageVersions WHERE PackageVersionKey = ? AND Hash = ?)`,
|
|
951
|
+
[key, integrity, keyid, sig, key, hash || v.Hash]);
|
|
952
|
+
}
|
|
953
|
+
|
|
718
954
|
async initializeDatabase() {
|
|
719
955
|
return new Promise((resolve, reject) => {
|
|
720
956
|
// Use config path if absolute, otherwise resolve relative to data dir
|
|
@@ -744,14 +980,14 @@ class PackagesModule {
|
|
|
744
980
|
this.db.run(`CREATE TABLE IF NOT EXISTS FeedPages (
|
|
745
981
|
Url TEXT PRIMARY KEY,
|
|
746
982
|
VisitedAt TEXT NOT NULL
|
|
747
|
-
)`, (err) => {
|
|
983
|
+
)`, guarded(reject, (err) => {
|
|
748
984
|
if (err) pckLog.error('Failed to create FeedPages table:', err.message);
|
|
749
|
-
});
|
|
985
|
+
}));
|
|
750
986
|
// Fails while the database still holds duplicate id#version rows (from before the
|
|
751
987
|
// crawler checked for them): packages/utilities/dedupe-versions.js removes them
|
|
752
|
-
this.db.run(UNIQUE_ID_VERSION_INDEX.replace('CREATE UNIQUE INDEX', 'CREATE UNIQUE INDEX IF NOT EXISTS'), (err) => {
|
|
988
|
+
this.db.run(UNIQUE_ID_VERSION_INDEX.replace('CREATE UNIQUE INDEX', 'CREATE UNIQUE INDEX IF NOT EXISTS'), guarded(reject, (err) => {
|
|
753
989
|
if (err) pckLog.warn('PackageVersions has duplicate id#version rows, so they are not yet unique; run packages/utilities/dedupe-versions.js: ' + err.message);
|
|
754
|
-
});
|
|
990
|
+
}));
|
|
755
991
|
resolve();
|
|
756
992
|
}
|
|
757
993
|
}
|
|
@@ -834,7 +1070,13 @@ class PackagesModule {
|
|
|
834
1070
|
(
|
|
835
1071
|
Url TEXT PRIMARY KEY,
|
|
836
1072
|
VisitedAt TEXT NOT NULL
|
|
837
|
-
)
|
|
1073
|
+
)`,
|
|
1074
|
+
|
|
1075
|
+
// PackageDist table - each version's dist.integrity and npm signature
|
|
1076
|
+
PACKAGE_DIST_TABLE,
|
|
1077
|
+
|
|
1078
|
+
// PackageSignatures table - the publisher's signature of each version, as checked
|
|
1079
|
+
PACKAGE_SIGNATURES_TABLE
|
|
838
1080
|
];
|
|
839
1081
|
|
|
840
1082
|
const indexes = [
|
|
@@ -884,13 +1126,13 @@ class PackagesModule {
|
|
|
884
1126
|
|
|
885
1127
|
// Create indexes
|
|
886
1128
|
indexes.forEach(sql => {
|
|
887
|
-
this.db.run(sql, (err) => {
|
|
1129
|
+
this.db.run(sql, guarded(reject, (err) => {
|
|
888
1130
|
if (err) {
|
|
889
1131
|
handleIndexError(err);
|
|
890
1132
|
} else {
|
|
891
1133
|
checkIndexesComplete();
|
|
892
1134
|
}
|
|
893
|
-
});
|
|
1135
|
+
}));
|
|
894
1136
|
});
|
|
895
1137
|
};
|
|
896
1138
|
|
|
@@ -901,13 +1143,13 @@ class PackagesModule {
|
|
|
901
1143
|
|
|
902
1144
|
// Create tables first
|
|
903
1145
|
tables.forEach(sql => {
|
|
904
|
-
this.db.run(sql, (err) => {
|
|
1146
|
+
this.db.run(sql, guarded(reject, (err) => {
|
|
905
1147
|
if (err) {
|
|
906
1148
|
handleTableError(err);
|
|
907
1149
|
} else {
|
|
908
1150
|
checkTablesComplete();
|
|
909
1151
|
}
|
|
910
|
-
});
|
|
1152
|
+
}));
|
|
911
1153
|
});
|
|
912
1154
|
});
|
|
913
1155
|
}
|
|
@@ -1120,6 +1362,34 @@ class PackagesModule {
|
|
|
1120
1362
|
}
|
|
1121
1363
|
});
|
|
1122
1364
|
|
|
1365
|
+
// GET /packages/feed-check - check a package feed as the crawler reads it, storing nothing.
|
|
1366
|
+
// With url, starts a check and redirects to its report; the report page reloads until done.
|
|
1367
|
+
// One check at a time, across all users.
|
|
1368
|
+
this.router.get('/feed-check', this.validateQueryParams(FEED_CHECK_PARAMS), async (req, res) => {
|
|
1369
|
+
const start = Date.now();
|
|
1370
|
+
try {
|
|
1371
|
+
await this.serveFeedCheck(req, res);
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
pckLog.error('Error in /packages/feed-check:', error);
|
|
1374
|
+
res.status(500).json({error: 'Internal server error'});
|
|
1375
|
+
} finally {
|
|
1376
|
+
this.stats.countRequest('feed-check', Date.now() - start);
|
|
1377
|
+
}
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
// GET /packages/feed-check/{run} - a feed check's report (while it runs, what it has so far)
|
|
1381
|
+
this.router.get('/feed-check/:run', async (req, res) => {
|
|
1382
|
+
const start = Date.now();
|
|
1383
|
+
try {
|
|
1384
|
+
await this.serveFeedCheckRun(req, res);
|
|
1385
|
+
} catch (error) {
|
|
1386
|
+
pckLog.error('Error in /packages/feed-check/{run}:', error);
|
|
1387
|
+
res.status(500).json({error: 'Internal server error'});
|
|
1388
|
+
} finally {
|
|
1389
|
+
this.stats.countRequest('feed-check', Date.now() - start);
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1123
1393
|
// GET /packages/catalog - Search packages or get updates
|
|
1124
1394
|
this.router.get('/catalog', this.validateQueryParams(searchParams), async (req, res) => {
|
|
1125
1395
|
const start = Date.now();
|
|
@@ -1136,6 +1406,56 @@ class PackagesModule {
|
|
|
1136
1406
|
}
|
|
1137
1407
|
});
|
|
1138
1408
|
|
|
1409
|
+
// GET /packages/-/npm/v1/keys - the key the registry signs packages with, as npm's
|
|
1410
|
+
// registry does (npm audit signatures). A 404 says this registry doesn't sign, which is
|
|
1411
|
+
// also the answer while versions are still being signed: once there are keys, npm reports
|
|
1412
|
+
// a version with no signature as an error.
|
|
1413
|
+
this.router.get('/-/npm/v1/keys', (req, res) => {
|
|
1414
|
+
const start = Date.now();
|
|
1415
|
+
try {
|
|
1416
|
+
if (!this.signer) {
|
|
1417
|
+
res.status(404).json({error: 'This registry does not sign packages'});
|
|
1418
|
+
} else if (this.distStatus.state !== 'complete') {
|
|
1419
|
+
res.status(404).json({error: 'This registry is still signing the packages it holds; try again later'});
|
|
1420
|
+
} else {
|
|
1421
|
+
res.json(this.signer.keysDocument());
|
|
1422
|
+
}
|
|
1423
|
+
} finally {
|
|
1424
|
+
this.stats.countRequest('keys', Date.now() - start);
|
|
1425
|
+
}
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
// GET /packages/-/fhir/v1/signatures/{name}@{version} - the publisher's signature of a
|
|
1429
|
+
// package version (its {package}.tgz.sigstore.json), exactly as the crawler fetched and
|
|
1430
|
+
// checked it. dist['fhir-signature'] in the package document links it, with the outcome.
|
|
1431
|
+
this.router.get('/-/fhir/v1/signatures/:spec', async (req, res) => {
|
|
1432
|
+
const start = Date.now();
|
|
1433
|
+
try {
|
|
1434
|
+
const spec = req.params.spec || '';
|
|
1435
|
+
const at = spec.lastIndexOf('@');
|
|
1436
|
+
const id = at > 0 ? spec.substring(0, at) : '';
|
|
1437
|
+
const version = at > 0 ? spec.substring(at + 1) : '';
|
|
1438
|
+
if (!PACKAGE_ID_PATTERN.test(id) || !PACKAGE_VERSION_PATTERN.test(version) ||
|
|
1439
|
+
id.length > PACKAGE_ID_MAX_LENGTH || version.length > PACKAGE_VERSION_MAX_LENGTH) {
|
|
1440
|
+
res.status(400).json({error: 'Expected {name}@{version}'});
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
const row = await this.dbGet(`SELECT s.Bundle FROM PackageSignatures s
|
|
1444
|
+
JOIN PackageVersions v ON v.PackageVersionKey = s.PackageVersionKey
|
|
1445
|
+
WHERE v.Id = ? AND v.Version = ?`, [id, version]);
|
|
1446
|
+
if (!row) {
|
|
1447
|
+
res.status(404).json({error: `No signature is held for ${id}#${version}`});
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
res.type('application/json').send(row.Bundle);
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
pckLog.error('Error in /packages/-/fhir/v1/signatures:', error);
|
|
1453
|
+
res.status(500).json({error: 'Internal server error'});
|
|
1454
|
+
} finally {
|
|
1455
|
+
this.stats.countRequest('signature', Date.now() - start);
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1139
1459
|
// GET /packages/-/v1/search - Search packages (v1 API)
|
|
1140
1460
|
this.router.get('/-/v1/search', this.validateQueryParams(V1_SEARCH_PARAMS), async (req, res) => {
|
|
1141
1461
|
const start = Date.now();
|
|
@@ -1359,7 +1679,7 @@ class PackagesModule {
|
|
|
1359
1679
|
// Don't process routes that are handled elsewhere. These must call next():
|
|
1360
1680
|
// returning without responding leaves the request hanging, which is what
|
|
1361
1681
|
// /status, /stats and /search (registered after this route) used to do.
|
|
1362
|
-
if (['catalog', 'log', 'broken', 'stats', 'status', 'search', 'updates', 'npm-check'].includes(id) ||
|
|
1682
|
+
if (['catalog', 'log', 'broken', 'stats', 'status', 'search', 'updates', 'npm-check', 'feed-check'].includes(id) ||
|
|
1363
1683
|
id.endsWith('.html') || id === '-') {
|
|
1364
1684
|
next();
|
|
1365
1685
|
return;
|
|
@@ -1617,6 +1937,8 @@ class PackagesModule {
|
|
|
1617
1937
|
htmlServer.loadTemplate('packages', templatePath);
|
|
1618
1938
|
}
|
|
1619
1939
|
|
|
1940
|
+
await this.annotateSignatures(updates, u => u.name, u => u.version);
|
|
1941
|
+
|
|
1620
1942
|
// Build template variables
|
|
1621
1943
|
const vars = {
|
|
1622
1944
|
fromDate: fromDate.split('T')[0], // Just the date part
|
|
@@ -1644,6 +1966,37 @@ class PackagesModule {
|
|
|
1644
1966
|
}
|
|
1645
1967
|
}
|
|
1646
1968
|
|
|
1969
|
+
// Sets signatureState on each item of a list for an HTML page: its PackageSignatures row (the
|
|
1970
|
+
// publisher's signature, as the crawler checked it), or null when there is none
|
|
1971
|
+
async annotateSignatures(list, idOf, versionOf) {
|
|
1972
|
+
const ids = [...new Set(list.map(idOf).filter(Boolean))];
|
|
1973
|
+
const found = new Map();
|
|
1974
|
+
for (let i = 0; i < ids.length; i += 500) {
|
|
1975
|
+
const chunk = ids.slice(i, i + 500);
|
|
1976
|
+
const rows = await this.dbAll(`SELECT v.Id, v.Version, s.Valid, s.KeyId, s.Message, s.Checked
|
|
1977
|
+
FROM PackageSignatures s JOIN PackageVersions v ON v.PackageVersionKey = s.PackageVersionKey
|
|
1978
|
+
WHERE v.Id IN (${chunk.map(() => '?').join(',')})`, chunk);
|
|
1979
|
+
for (const r of rows) {
|
|
1980
|
+
found.set(`${r.Id}#${r.Version}`, r);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
for (const item of list) {
|
|
1984
|
+
item.signatureState = found.get(`${idOf(item)}#${versionOf(item)}`) || null;
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// The Signature column: confirmed (the publisher's signature checked out), rejected (it didn't),
|
|
1989
|
+
// or absent (unsigned, or not checked: the feed lists no keys, or signatures are ignored)
|
|
1990
|
+
signatureCell(sig) {
|
|
1991
|
+
if (!sig) {
|
|
1992
|
+
return '<td class="text-muted">absent</td>';
|
|
1993
|
+
}
|
|
1994
|
+
if (sig.Valid) {
|
|
1995
|
+
return `<td title="${escape(`Signed with ${sig.KeyId}; checked ${sig.Checked} UTC`)}"><span class="text-success">confirmed</span></td>`;
|
|
1996
|
+
}
|
|
1997
|
+
return `<td title="${escape(`${sig.Message || 'The signature is not valid'}; checked ${sig.Checked} UTC`)}"><span class="text-danger">rejected</span></td>`;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
1647
2000
|
generateUpdatesTable(updates) {
|
|
1648
2001
|
if (updates.length === 0) {
|
|
1649
2002
|
return '<div class="alert alert-info">No package updates found for the specified time period.</div>';
|
|
@@ -1657,6 +2010,7 @@ class PackagesModule {
|
|
|
1657
2010
|
table += '<th>Type</th>';
|
|
1658
2011
|
table += '<th>Published</th>';
|
|
1659
2012
|
table += '<th>Canonical</th>';
|
|
2013
|
+
table += '<th>Signature</th>';
|
|
1660
2014
|
table += '</tr></thead><tbody>';
|
|
1661
2015
|
|
|
1662
2016
|
for (const pkg of updates) {
|
|
@@ -1667,6 +2021,7 @@ class PackagesModule {
|
|
|
1667
2021
|
table += `<td>${escape(pkg.kind)}</td>`;
|
|
1668
2022
|
table += `<td>${new Date(pkg.date).toLocaleDateString()} ${new Date(pkg.date).toLocaleTimeString()}</td>`;
|
|
1669
2023
|
table += `<td>${escape(pkg.canonical || '')}</td>`;
|
|
2024
|
+
table += this.signatureCell(pkg.signatureState);
|
|
1670
2025
|
table += '</tr>';
|
|
1671
2026
|
}
|
|
1672
2027
|
|
|
@@ -1683,14 +2038,14 @@ class PackagesModule {
|
|
|
1683
2038
|
|
|
1684
2039
|
content += '<form method="GET" action="/packages/updates">';
|
|
1685
2040
|
|
|
1686
|
-
content += `<input type="radio" name="dateType" id="dateType" value="relative" ${formData.dt == 'relative' ? 'checked' : ''}> `;
|
|
1687
|
-
content += '<label for="
|
|
1688
|
-
content += `<input type="number" name="daysValue" value="${formData.days}" min="1" max="365" style="width: 80px; margin: 0 5px;"> `;
|
|
2041
|
+
content += `<input type="radio" aria-label="Is Relative?" name="dateType" id="dateType" value="relative" ${formData.dt == 'relative' ? 'checked' : ''}> `;
|
|
2042
|
+
content += '<label for="daysValue">Last</label> ';
|
|
2043
|
+
content += `<input type="number" id="daysValue" aria-label="Number of Days" name="daysValue" value="${formData.days}" min="1" max="365" style="width: 80px; margin: 0 5px;"> `;
|
|
1689
2044
|
content += '<label>days</label> ';
|
|
1690
2045
|
|
|
1691
|
-
content += `<input type="radio" name="dateType" id="dateType" value="absolute" ${formData.dt != 'relative' ? 'checked' : ''}> `;
|
|
1692
|
-
content += '<label for="
|
|
1693
|
-
content += `<input type="date" name="dateValue" value="${formData.date}" style="margin-left: 5px;"> `;
|
|
2046
|
+
content += `<input type="radio" name="dateType" aria-label="Is Date?" id="dateType" value="absolute" ${formData.dt != 'relative' ? 'checked' : ''}> `;
|
|
2047
|
+
content += '<label for="dateValue">Since date:</label> ';
|
|
2048
|
+
content += `<input type="date" name="dateValue" id="dateValue" aria-label="Date" value="${formData.date}" style="margin-left: 5px;"> `;
|
|
1694
2049
|
content += '<button type="submit" class="btn btn-primary btn-sm" style="margin-left: 10px;">Update Results</button>';
|
|
1695
2050
|
|
|
1696
2051
|
content += '</form>';
|
|
@@ -1718,13 +2073,13 @@ class PackagesModule {
|
|
|
1718
2073
|
WHERE PubDate >= ?
|
|
1719
2074
|
ORDER BY PubDate DESC`;
|
|
1720
2075
|
|
|
1721
|
-
this.db.all(sql, [date], (err, rows) => {
|
|
2076
|
+
this.db.all(sql, [date], guarded(reject, (err, rows) => {
|
|
1722
2077
|
if (err) {
|
|
1723
2078
|
reject(err);
|
|
1724
2079
|
} else {
|
|
1725
2080
|
resolve(rows);
|
|
1726
2081
|
}
|
|
1727
|
-
});
|
|
2082
|
+
}));
|
|
1728
2083
|
});
|
|
1729
2084
|
}
|
|
1730
2085
|
|
|
@@ -1831,6 +2186,140 @@ class PackagesModule {
|
|
|
1831
2186
|
res.json(report);
|
|
1832
2187
|
}
|
|
1833
2188
|
|
|
2189
|
+
feedCheckConfig() {
|
|
2190
|
+
return {...FEED_CHECK_DEFAULTS, ...(this.config?.feedCheck || {})};
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
// The registry's feed list (package-feeds.json), for the feed check; fetched at most hourly
|
|
2194
|
+
async feedCheckMasterList() {
|
|
2195
|
+
if (this.masterListCache && Date.now() - this.masterListCache.at < MASTER_LIST_MAX_AGE) {
|
|
2196
|
+
return this.masterListCache.data;
|
|
2197
|
+
}
|
|
2198
|
+
const reader = Object.create(PackageCrawler.prototype);
|
|
2199
|
+
reader.config = this.config;
|
|
2200
|
+
const data = await reader.fetchJson(this.config.masterUrl);
|
|
2201
|
+
this.masterListCache = {at: Date.now(), data};
|
|
2202
|
+
return data;
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
// Forgets runs older than keepMinutes, and the oldest beyond FEED_CHECK_MAX_JOBS
|
|
2206
|
+
pruneFeedCheckJobs() {
|
|
2207
|
+
const keep = this.feedCheckConfig().keepMinutes * 60 * 1000;
|
|
2208
|
+
for (const [id, job] of this.feedCheckJobs) {
|
|
2209
|
+
if (job.state !== 'running' && Date.now() - job.started > keep) {
|
|
2210
|
+
this.feedCheckJobs.delete(id);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
while (this.feedCheckJobs.size > FEED_CHECK_MAX_JOBS) {
|
|
2214
|
+
this.feedCheckJobs.delete(this.feedCheckJobs.keys().next().value);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
async serveFeedCheck(req, res) {
|
|
2219
|
+
const acceptsHtml = req.headers.accept && req.headers.accept.includes('text/html');
|
|
2220
|
+
const cfg = this.feedCheckConfig();
|
|
2221
|
+
const fail = (status, message) => acceptsHtml
|
|
2222
|
+
? this.sendFeedCheckPage(res, status, `<div class="alert alert-danger">${escape(message)}</div>` + feedCheckPage.renderForm(req.query))
|
|
2223
|
+
: res.status(status).json({error: message});
|
|
2224
|
+
|
|
2225
|
+
if (!cfg.enabled) {
|
|
2226
|
+
return fail(404, 'The feed check is not enabled on this server');
|
|
2227
|
+
}
|
|
2228
|
+
const target = req.query.url;
|
|
2229
|
+
if (!target) {
|
|
2230
|
+
if (acceptsHtml) {
|
|
2231
|
+
return this.sendFeedCheckPage(res, 200, feedCheckPage.renderForm({}));
|
|
2232
|
+
}
|
|
2233
|
+
return res.status(400).json({error: 'Parameter url is required'});
|
|
2234
|
+
}
|
|
2235
|
+
if (typeof target !== 'string') {
|
|
2236
|
+
return fail(400, 'Give the feed URL once');
|
|
2237
|
+
}
|
|
2238
|
+
let url;
|
|
2239
|
+
try {
|
|
2240
|
+
url = new URL(target);
|
|
2241
|
+
} catch (e) {
|
|
2242
|
+
return fail(400, `The URL isn't valid: ${target}`);
|
|
2243
|
+
}
|
|
2244
|
+
// http(s) only: the crawler also reads local files, which a user must never reach
|
|
2245
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) {
|
|
2246
|
+
return fail(400, 'The feed URL must be http(s), with no user name or password');
|
|
2247
|
+
}
|
|
2248
|
+
let signing = null;
|
|
2249
|
+
if (req.query.signing && String(req.query.signing).trim()) {
|
|
2250
|
+
try {
|
|
2251
|
+
signing = parseSigningJson(String(req.query.signing));
|
|
2252
|
+
} catch (e) {
|
|
2253
|
+
return fail(400, `The signing information isn't usable: ${e.message}`);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
if (this.feedCheckRunning) {
|
|
2257
|
+
res.setHeader('Retry-After', '60');
|
|
2258
|
+
return fail(503, 'A feed check is already running (only one runs at a time); try again in a minute');
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
this.pruneFeedCheckJobs();
|
|
2262
|
+
const job = {id: crypto.randomBytes(12).toString('hex'), url: target, state: 'running', report: null, error: null, started: Date.now()};
|
|
2263
|
+
this.feedCheckJobs.set(job.id, job);
|
|
2264
|
+
this.feedCheckRunning = true;
|
|
2265
|
+
const check = new FeedCheck(this.config, {
|
|
2266
|
+
master: () => this.feedCheckMasterList(),
|
|
2267
|
+
held: (id, version) => this.dbGet('SELECT Hash AS hash FROM PackageVersions WHERE Id = ? AND Version = ?', [id, version]),
|
|
2268
|
+
signing,
|
|
2269
|
+
maxItems: cfg.maxItems,
|
|
2270
|
+
maxTotalMB: cfg.maxTotalMB
|
|
2271
|
+
});
|
|
2272
|
+
check.run(target, report => {
|
|
2273
|
+
job.report = report;
|
|
2274
|
+
}).then(report => {
|
|
2275
|
+
job.report = report;
|
|
2276
|
+
job.state = 'done';
|
|
2277
|
+
const errors = report.items.filter(i => i.status === 'error').length;
|
|
2278
|
+
pckLog.info(`feed-check ${target}: ${report.items.length} items, ${errors} with errors, ${report.errors.length} feed errors`);
|
|
2279
|
+
}).catch(e => {
|
|
2280
|
+
job.state = 'failed';
|
|
2281
|
+
job.error = e.message;
|
|
2282
|
+
pckLog.error(`feed-check ${target} failed: ${e.message}`);
|
|
2283
|
+
}).finally(() => {
|
|
2284
|
+
this.feedCheckRunning = false;
|
|
2285
|
+
});
|
|
2286
|
+
|
|
2287
|
+
const where = `/packages/feed-check/${job.id}`;
|
|
2288
|
+
if (acceptsHtml) {
|
|
2289
|
+
return res.redirect(303, where);
|
|
2290
|
+
}
|
|
2291
|
+
res.status(202).setHeader('Location', where);
|
|
2292
|
+
return res.json({id: job.id, state: job.state, report: where});
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
async serveFeedCheckRun(req, res) {
|
|
2296
|
+
const acceptsHtml = req.headers.accept && req.headers.accept.includes('text/html');
|
|
2297
|
+
const id = req.params.run;
|
|
2298
|
+
const job = FEED_CHECK_JOB_PATTERN.test(id || '') ? this.feedCheckJobs.get(id) : null;
|
|
2299
|
+
if (!job) {
|
|
2300
|
+
const message = 'There is no feed check with that id (reports are kept for a while after the check)';
|
|
2301
|
+
return acceptsHtml
|
|
2302
|
+
? this.sendFeedCheckPage(res, 404, `<div class="alert alert-info">${escape(message)}</div>` + feedCheckPage.renderForm({}))
|
|
2303
|
+
: res.status(404).json({error: message});
|
|
2304
|
+
}
|
|
2305
|
+
if (acceptsHtml) {
|
|
2306
|
+
if (job.state === 'running') {
|
|
2307
|
+
res.setHeader('Refresh', '5');
|
|
2308
|
+
}
|
|
2309
|
+
return this.sendFeedCheckPage(res, 200, feedCheckPage.renderJob(job));
|
|
2310
|
+
}
|
|
2311
|
+
return res.json({id: job.id, url: job.url, state: job.state, error: job.error || undefined, report: job.report});
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
async sendFeedCheckPage(res, status, content) {
|
|
2315
|
+
if (!htmlServer.hasTemplate('packages')) {
|
|
2316
|
+
htmlServer.loadTemplate('packages', path.join(__dirname, 'packages-template.html'));
|
|
2317
|
+
}
|
|
2318
|
+
const stats = await this.gatherPackageStatistics();
|
|
2319
|
+
res.status(status).setHeader('Content-Type', 'text/html');
|
|
2320
|
+
res.send(htmlServer.renderPage('packages', 'Package Feed Check', content, stats));
|
|
2321
|
+
}
|
|
2322
|
+
|
|
1834
2323
|
async sendNpmCheckPage(res, status, content) {
|
|
1835
2324
|
if (!htmlServer.hasTemplate('packages')) {
|
|
1836
2325
|
htmlServer.loadTemplate('packages', path.join(__dirname, 'packages-template.html'));
|
|
@@ -1964,13 +2453,13 @@ class PackagesModule {
|
|
|
1964
2453
|
? [id, version]
|
|
1965
2454
|
: [id, `${version}-%`];
|
|
1966
2455
|
|
|
1967
|
-
this.db.get(sql, params, (err, row) => {
|
|
2456
|
+
this.db.get(sql, params, guarded(reject, (err, row) => {
|
|
1968
2457
|
if (err) {
|
|
1969
2458
|
reject(err);
|
|
1970
2459
|
} else {
|
|
1971
2460
|
resolve(row || null);
|
|
1972
2461
|
}
|
|
1973
|
-
});
|
|
2462
|
+
}));
|
|
1974
2463
|
});
|
|
1975
2464
|
}
|
|
1976
2465
|
|
|
@@ -2012,10 +2501,10 @@ class PackagesModule {
|
|
|
2012
2501
|
this.db.run(
|
|
2013
2502
|
'UPDATE PackageVersions SET DownloadCount = DownloadCount + 1 WHERE PackageVersionKey = ?',
|
|
2014
2503
|
[packageVersionKey],
|
|
2015
|
-
(err) => {
|
|
2504
|
+
guarded(reject, (err) => {
|
|
2016
2505
|
if (err) reject(err);
|
|
2017
2506
|
else resolve();
|
|
2018
|
-
}
|
|
2507
|
+
})
|
|
2019
2508
|
);
|
|
2020
2509
|
});
|
|
2021
2510
|
|
|
@@ -2024,10 +2513,10 @@ class PackagesModule {
|
|
|
2024
2513
|
this.db.run(
|
|
2025
2514
|
'UPDATE Packages SET DownloadCount = DownloadCount + 1 WHERE Id = ?',
|
|
2026
2515
|
[packageId],
|
|
2027
|
-
(err) => {
|
|
2516
|
+
guarded(reject, (err) => {
|
|
2028
2517
|
if (err) reject(err);
|
|
2029
2518
|
else resolve();
|
|
2030
|
-
}
|
|
2519
|
+
})
|
|
2031
2520
|
);
|
|
2032
2521
|
});
|
|
2033
2522
|
|
|
@@ -2075,7 +2564,7 @@ class PackagesModule {
|
|
|
2075
2564
|
|
|
2076
2565
|
async getPackageVersions(id) {
|
|
2077
2566
|
return new Promise((resolve, reject) => {
|
|
2078
|
-
const sql = `SELECT PackageVersionKey,
|
|
2567
|
+
const sql = `SELECT v.PackageVersionKey,
|
|
2079
2568
|
Version,
|
|
2080
2569
|
PubDate,
|
|
2081
2570
|
FhirVersions,
|
|
@@ -2087,18 +2576,24 @@ class PackagesModule {
|
|
|
2087
2576
|
License,
|
|
2088
2577
|
Hash,
|
|
2089
2578
|
GUID,
|
|
2090
|
-
Description
|
|
2091
|
-
|
|
2579
|
+
Description,
|
|
2580
|
+
d.Integrity,
|
|
2581
|
+
d.KeyId,
|
|
2582
|
+
d.Signature,
|
|
2583
|
+
s.Valid AS PublisherSigValid,
|
|
2584
|
+
s.KeyId AS PublisherSigKeyId
|
|
2585
|
+
FROM PackageVersions v LEFT JOIN PackageDist d ON d.PackageVersionKey = v.PackageVersionKey
|
|
2586
|
+
LEFT JOIN PackageSignatures s ON s.PackageVersionKey = v.PackageVersionKey
|
|
2092
2587
|
WHERE Id = ?
|
|
2093
2588
|
ORDER BY PubDate DESC`; // Changed from ASC to DESC for most recent first
|
|
2094
2589
|
|
|
2095
|
-
this.db.all(sql, [id], (err, rows) => {
|
|
2590
|
+
this.db.all(sql, [id], guarded(reject, (err, rows) => {
|
|
2096
2591
|
if (err) {
|
|
2097
2592
|
reject(err);
|
|
2098
2593
|
} else {
|
|
2099
2594
|
resolve(rows);
|
|
2100
2595
|
}
|
|
2101
|
-
});
|
|
2596
|
+
}));
|
|
2102
2597
|
});
|
|
2103
2598
|
}
|
|
2104
2599
|
|
|
@@ -2111,7 +2606,7 @@ class PackagesModule {
|
|
|
2111
2606
|
FROM PackageDependencies
|
|
2112
2607
|
WHERE PackageVersionKey IN (${placeholders})`;
|
|
2113
2608
|
|
|
2114
|
-
this.db.all(sql, packageVersionKeys, (err, rows) => {
|
|
2609
|
+
this.db.all(sql, packageVersionKeys, guarded(reject, (err, rows) => {
|
|
2115
2610
|
if (err) {
|
|
2116
2611
|
reject(err);
|
|
2117
2612
|
} else {
|
|
@@ -2132,7 +2627,7 @@ class PackagesModule {
|
|
|
2132
2627
|
}
|
|
2133
2628
|
resolve(deps);
|
|
2134
2629
|
}
|
|
2135
|
-
});
|
|
2630
|
+
}));
|
|
2136
2631
|
});
|
|
2137
2632
|
}
|
|
2138
2633
|
|
|
@@ -2179,6 +2674,23 @@ class PackagesModule {
|
|
|
2179
2674
|
tarball: this.buildTarballUrl(id, pv.Version, secure, req)
|
|
2180
2675
|
}
|
|
2181
2676
|
};
|
|
2677
|
+
if (pv.Integrity) {
|
|
2678
|
+
versionObj.dist.integrity = pv.Integrity;
|
|
2679
|
+
}
|
|
2680
|
+
if (this.signer && pv.Signature && pv.KeyId === this.signer.keyid) {
|
|
2681
|
+
versionObj.dist.signatures = [{keyid: pv.KeyId, sig: pv.Signature}];
|
|
2682
|
+
}
|
|
2683
|
+
// The publisher's signature, as the crawler checked it. Not dist.attestations: npm would
|
|
2684
|
+
// try to verify these as Sigstore attestations, and can't (see readme)
|
|
2685
|
+
if (pv.PublisherSigValid !== null && pv.PublisherSigValid !== undefined) {
|
|
2686
|
+
versionObj.dist['fhir-signature'] = {
|
|
2687
|
+
url: this.buildSignatureUrl(id, pv.Version, secure, req),
|
|
2688
|
+
status: pv.PublisherSigValid ? 'confirmed' : 'rejected'
|
|
2689
|
+
};
|
|
2690
|
+
if (pv.PublisherSigKeyId) {
|
|
2691
|
+
versionObj.dist['fhir-signature'].keyid = pv.PublisherSigKeyId;
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2182
2694
|
|
|
2183
2695
|
// Add optional fields
|
|
2184
2696
|
if (pv.HomePage) {
|
|
@@ -2221,6 +2733,14 @@ class PackagesModule {
|
|
|
2221
2733
|
return registry;
|
|
2222
2734
|
}
|
|
2223
2735
|
|
|
2736
|
+
// Where the publisher's signature of a version is served: npm's form of name@version
|
|
2737
|
+
buildSignatureUrl(id, version, secure, req) {
|
|
2738
|
+
const spec = encodeURIComponent(id).replace(/^%40/, '@') + '@' + encodeURIComponent(version);
|
|
2739
|
+
const base = this.config.baseUrl ? this.config.baseUrl.replace(/\/+$/, '')
|
|
2740
|
+
: `${secure ? 'https' : 'http'}://${(req && req.get && req.get('host')) || 'localhost:3000'}`;
|
|
2741
|
+
return `${base}/packages/-/fhir/v1/signatures/${spec}`;
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2224
2744
|
buildTarballUrl(id, version, secure, req) {
|
|
2225
2745
|
if (this.config.bucketPath) {
|
|
2226
2746
|
let bucketUrl = this.getBucketUrl(secure);
|
|
@@ -2243,6 +2763,8 @@ class PackagesModule {
|
|
|
2243
2763
|
htmlServer.loadTemplate('packages', templatePath);
|
|
2244
2764
|
}
|
|
2245
2765
|
|
|
2766
|
+
await this.annotateSignatures(packageVersions, () => id, pv => pv.Version);
|
|
2767
|
+
|
|
2246
2768
|
// Get package counts
|
|
2247
2769
|
const versionCount = packageVersions.length;
|
|
2248
2770
|
const totalDownloads = packageVersions.reduce((sum, pv) => sum + (pv.DownloadCount || 0), 0);
|
|
@@ -2288,6 +2810,7 @@ class PackagesModule {
|
|
|
2288
2810
|
table += '<th>Type</th>';
|
|
2289
2811
|
table += '<th>Published</th>';
|
|
2290
2812
|
table += '<th>Downloads</th>';
|
|
2813
|
+
table += '<th>Signature</th>';
|
|
2291
2814
|
table += '<th>Actions</th>';
|
|
2292
2815
|
table += '</tr></thead><tbody>';
|
|
2293
2816
|
|
|
@@ -2298,6 +2821,7 @@ class PackagesModule {
|
|
|
2298
2821
|
table += `<td>${escape(this.codeForKind(pv.Kind))}</td>`;
|
|
2299
2822
|
table += `<td>${new Date(pv.PubDate).toLocaleDateString()}</td>`;
|
|
2300
2823
|
table += `<td>${(pv.DownloadCount || 0).toLocaleString()}</td>`;
|
|
2824
|
+
table += this.signatureCell(pv.signatureState);
|
|
2301
2825
|
table += `<td><a href="/packages/${encodeURIComponent(id)}/${escape(pv.Version)}" class="btn btn-sm btn-primary">Download</a></td>`;
|
|
2302
2826
|
table += '</tr>';
|
|
2303
2827
|
}
|
|
@@ -2495,6 +3019,7 @@ class PackagesModule {
|
|
|
2495
3019
|
// Get total package count
|
|
2496
3020
|
const packageCount = await this.getTotalPackageCount();
|
|
2497
3021
|
const downloadCount = await this.getTotalDownloadCount();
|
|
3022
|
+
await this.annotateSignatures(results, r => r.name, r => r.version);
|
|
2498
3023
|
|
|
2499
3024
|
// Build template variables
|
|
2500
3025
|
const vars = {
|
|
@@ -2532,25 +3057,25 @@ class PackagesModule {
|
|
|
2532
3057
|
|
|
2533
3058
|
async getTotalPackageCount() {
|
|
2534
3059
|
return new Promise((resolve, reject) => {
|
|
2535
|
-
this.db.get('SELECT COUNT(*) as count FROM PackageVersions', [], (err, row) => {
|
|
3060
|
+
this.db.get('SELECT COUNT(*) as count FROM PackageVersions', [], guarded(reject, (err, row) => {
|
|
2536
3061
|
if (err) {
|
|
2537
3062
|
reject(err);
|
|
2538
3063
|
} else {
|
|
2539
3064
|
resolve(row ? row.count : 0);
|
|
2540
3065
|
}
|
|
2541
|
-
});
|
|
3066
|
+
}));
|
|
2542
3067
|
});
|
|
2543
3068
|
}
|
|
2544
3069
|
|
|
2545
3070
|
async getTotalDownloadCount() {
|
|
2546
3071
|
return new Promise((resolve, reject) => {
|
|
2547
|
-
this.db.get('SELECT SUM(DownloadCount) as total FROM PackageVersions', [], (err, row) => {
|
|
3072
|
+
this.db.get('SELECT SUM(DownloadCount) as total FROM PackageVersions', [], guarded(reject, (err, row) => {
|
|
2548
3073
|
if (err) {
|
|
2549
3074
|
reject(err);
|
|
2550
3075
|
} else {
|
|
2551
3076
|
resolve(row ? row.total || 0 : 0);
|
|
2552
3077
|
}
|
|
2553
|
-
});
|
|
3078
|
+
}));
|
|
2554
3079
|
});
|
|
2555
3080
|
}
|
|
2556
3081
|
|
|
@@ -2588,6 +3113,7 @@ class PackagesModule {
|
|
|
2588
3113
|
table += `<th>${this.generateSortHeader('Published', 'date', baseUrl, currentSort)}</th>`;
|
|
2589
3114
|
table += `<th>${this.generateSortHeader('Downloads', 'count', baseUrl, currentSort)}</th>`;
|
|
2590
3115
|
table += `<th>${this.generateSortHeader('Canonical', 'canonical', baseUrl, currentSort)}</th>`;
|
|
3116
|
+
table += '<th>Signature</th>';
|
|
2591
3117
|
table += '</tr></thead><tbody>';
|
|
2592
3118
|
|
|
2593
3119
|
for (const pkg of results) {
|
|
@@ -2599,6 +3125,7 @@ class PackagesModule {
|
|
|
2599
3125
|
table += `<td>${pkg.date ? new Date(pkg.date).toLocaleDateString() : 'N/A'}</td>`;
|
|
2600
3126
|
table += `<td>${pkg.count ? pkg.count.toLocaleString() : 'N/A'}</td>`;
|
|
2601
3127
|
table += `<td>${escape(pkg.canonical || '')}</td>`;
|
|
3128
|
+
table += this.signatureCell(pkg.signatureState);
|
|
2602
3129
|
table += '</tr>';
|
|
2603
3130
|
}
|
|
2604
3131
|
|
|
@@ -2652,27 +3179,27 @@ class PackagesModule {
|
|
|
2652
3179
|
|
|
2653
3180
|
content += '<tr>';
|
|
2654
3181
|
content += '<td>Id</td>';
|
|
2655
|
-
content += `<td><input type="text" name="name" value="${escape(vars.name)}"></td>`;
|
|
3182
|
+
content += `<td><input type="text" aria-label="Id" name="name" value="${escape(vars.name)}"></td>`;
|
|
2656
3183
|
content += '</tr>';
|
|
2657
3184
|
|
|
2658
3185
|
content += '<tr>';
|
|
2659
3186
|
content += '<td>Depends On</td>';
|
|
2660
|
-
content += `<td><input type="text" name="dependson" value="${escape(vars.dependson)}"> <i>includes both direct and indirect dependencies</i></td>`;
|
|
3187
|
+
content += `<td><input type="text" aria-label="Depends On" name="dependson" value="${escape(vars.dependson)}"> <i>includes both direct and indirect dependencies</i></td>`;
|
|
2661
3188
|
content += '</tr>';
|
|
2662
3189
|
|
|
2663
3190
|
content += '<tr>';
|
|
2664
3191
|
content += '<td>Canonical (Package)</td>';
|
|
2665
|
-
content += `<td><input type="text" name="pkgcanonical" value="${escape(vars.canonicalPkg)}"></td>`;
|
|
3192
|
+
content += `<td><input type="text" aria-label="Canonical (Package)" name="pkgcanonical" value="${escape(vars.canonicalPkg)}"></td>`;
|
|
2666
3193
|
content += '</tr>';
|
|
2667
3194
|
|
|
2668
3195
|
content += '<tr>';
|
|
2669
3196
|
content += '<td>Canonical (Resource)</td>';
|
|
2670
|
-
content += `<td><input type="text" name="canonical" value="${escape(vars.canonicalUrl)}"></td>`;
|
|
3197
|
+
content += `<td><input type="text" aria-label="Canonical (Resource)" name="canonical" value="${escape(vars.canonicalUrl)}"></td>`;
|
|
2671
3198
|
content += '</tr>';
|
|
2672
3199
|
|
|
2673
3200
|
content += '<tr>';
|
|
2674
3201
|
content += '<td>FHIR Version</td>';
|
|
2675
|
-
content += '<td><select name="fhirversion">';
|
|
3202
|
+
content += '<td><select name="fhirversion" aria-label="FHIR Version">';
|
|
2676
3203
|
content += '<option value=""></option>';
|
|
2677
3204
|
content += `<option value="R2" ${vars.r2selected}>R2</option>`;
|
|
2678
3205
|
content += `<option value="R3" ${vars.r3selected}>R3</option>`;
|
|
@@ -2825,7 +3352,7 @@ class PackagesModule {
|
|
|
2825
3352
|
const row = await new Promise((resolve, reject) => this.db.get(
|
|
2826
3353
|
'SELECT COUNT(*) AS n, COUNT(DISTINCT Id) AS ids FROM PackageVersions WHERE Indexed > ?',
|
|
2827
3354
|
[new Date(since).toISOString().replace('T', ' ').substring(0, 19)],
|
|
2828
|
-
(err, row) => err ? reject(err) : resolve(row)));
|
|
3355
|
+
guarded(reject, (err, row) => err ? reject(err) : resolve(row))));
|
|
2829
3356
|
sentences.push(row.n === 0
|
|
2830
3357
|
? 'No new package versions were found.'
|
|
2831
3358
|
: `${plural(row.n, 'new package version')} ${row.n === 1 ? 'was' : 'were'} found, of ${plural(row.ids, 'package')}.`);
|
|
@@ -2841,17 +3368,22 @@ class PackagesModule {
|
|
|
2841
3368
|
|
|
2842
3369
|
await this.stopCrawlerJob();
|
|
2843
3370
|
|
|
3371
|
+
this.stopping = true;
|
|
3372
|
+
if (this.distBackfill) {
|
|
3373
|
+
await this.distBackfill;
|
|
3374
|
+
}
|
|
3375
|
+
|
|
2844
3376
|
// Close database connection
|
|
2845
3377
|
if (this.db) {
|
|
2846
|
-
return new Promise((resolve) => {
|
|
2847
|
-
this.db.close((err) => {
|
|
3378
|
+
return new Promise((resolve, reject) => {
|
|
3379
|
+
this.db.close(guarded(reject, (err) => {
|
|
2848
3380
|
if (err) {
|
|
2849
3381
|
pckLog.error('Error closing packages database:', err.message);
|
|
2850
3382
|
} else {
|
|
2851
3383
|
pckLog.info('Packages database connection closed');
|
|
2852
3384
|
}
|
|
2853
3385
|
resolve();
|
|
2854
|
-
});
|
|
3386
|
+
}));
|
|
2855
3387
|
});
|
|
2856
3388
|
}
|
|
2857
3389
|
|
|
@@ -2896,7 +3428,7 @@ class PackagesModule {
|
|
|
2896
3428
|
return new Promise((resolve, reject) => {
|
|
2897
3429
|
const sql = 'SELECT Id, Version FROM PackageVersions';
|
|
2898
3430
|
|
|
2899
|
-
this.db.all(sql, [], (err, rows) => {
|
|
3431
|
+
this.db.all(sql, [], guarded(reject, (err, rows) => {
|
|
2900
3432
|
if (err) {
|
|
2901
3433
|
reject(err);
|
|
2902
3434
|
} else {
|
|
@@ -2911,7 +3443,7 @@ class PackagesModule {
|
|
|
2911
3443
|
|
|
2912
3444
|
resolve(validPackages);
|
|
2913
3445
|
}
|
|
2914
|
-
});
|
|
3446
|
+
}));
|
|
2915
3447
|
});
|
|
2916
3448
|
}
|
|
2917
3449
|
|
|
@@ -2923,7 +3455,7 @@ class PackagesModule {
|
|
|
2923
3455
|
PackageVersions
|
|
2924
3456
|
WHERE PackageDependencies.PackageVersionKey = PackageVersions.PackageVersionKey`;
|
|
2925
3457
|
|
|
2926
|
-
this.db.all(sql, [], (err, rows) => {
|
|
3458
|
+
this.db.all(sql, [], guarded(reject, (err, rows) => {
|
|
2927
3459
|
if (err) {
|
|
2928
3460
|
reject(err);
|
|
2929
3461
|
} else {
|
|
@@ -2959,7 +3491,7 @@ class PackagesModule {
|
|
|
2959
3491
|
|
|
2960
3492
|
resolve(brokenDeps);
|
|
2961
3493
|
}
|
|
2962
|
-
});
|
|
3494
|
+
}));
|
|
2963
3495
|
});
|
|
2964
3496
|
}
|
|
2965
3497
|
|
|
@@ -3058,7 +3590,7 @@ class PackagesModule {
|
|
|
3058
3590
|
content += '</table>';
|
|
3059
3591
|
|
|
3060
3592
|
// Help info
|
|
3061
|
-
content += '<
|
|
3593
|
+
content += '<h3>About This Report</h3>';
|
|
3062
3594
|
content += '<p>This report shows packages that have dependencies which cannot be resolved. A dependency is considered broken if:</p>';
|
|
3063
3595
|
content += '<ul>';
|
|
3064
3596
|
content += '<li>The referenced package does not exist</li>';
|
|
@@ -3178,7 +3710,9 @@ class PackagesModule {
|
|
|
3178
3710
|
|
|
3179
3711
|
if (logData.feeds && logData.feeds.length > 0) {
|
|
3180
3712
|
for (const feed of logData.feeds) {
|
|
3181
|
-
if (feed.
|
|
3713
|
+
if (feed.skipped) {
|
|
3714
|
+
output += `Feed: ${feed.url}: skipped - ${feed.skipped}\n`;
|
|
3715
|
+
} else if (feed.exception || feed.rateLimited) {
|
|
3182
3716
|
// Feed itself had an error
|
|
3183
3717
|
const error = feed.rateLimited ? feed.rateLimitMessage : feed.exception;
|
|
3184
3718
|
output += `Feed: ${feed.url}: ${error}\n`;
|
|
@@ -3193,6 +3727,9 @@ class PackagesModule {
|
|
|
3193
3727
|
const guid = item.guid || 'unknown';
|
|
3194
3728
|
output += ` error: ${guid}: ${item.error}\n`;
|
|
3195
3729
|
}
|
|
3730
|
+
if (item.signature && !item.signature.startsWith('valid') && item.signature !== 'unsigned') {
|
|
3731
|
+
output += ` signature: ${item.guid || 'unknown'}: ${item.signature}\n`;
|
|
3732
|
+
}
|
|
3196
3733
|
}
|
|
3197
3734
|
}
|
|
3198
3735
|
}
|
|
@@ -3222,6 +3759,16 @@ class PackagesModule {
|
|
|
3222
3759
|
schedule: this.config.crawler.schedule,
|
|
3223
3760
|
lastRun: this.lastRunTime,
|
|
3224
3761
|
totalRuns: this.totalRuns
|
|
3762
|
+
},
|
|
3763
|
+
npmSigning: {
|
|
3764
|
+
enabled: this.signer ? true : false,
|
|
3765
|
+
keyid: this.signer ? this.signer.keyid : undefined,
|
|
3766
|
+
// complete: every version has its integrity (and signature); running, incomplete
|
|
3767
|
+
// (tried again after the next crawl), stopped
|
|
3768
|
+
state: this.distStatus.state,
|
|
3769
|
+
missing: this.distStatus.missing,
|
|
3770
|
+
done: this.distStatus.done,
|
|
3771
|
+
failed: this.distStatus.failed
|
|
3225
3772
|
}
|
|
3226
3773
|
};
|
|
3227
3774
|
}
|