@data-fair/dev-server 2.5.1 → 2.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -6
- package/package.json +1 -1
- package/src/app.js +25 -17
- package/src/base-app-urls.d.ts +1 -0
- package/src/base-app-urls.js +37 -0
- package/ui/dist/assets/{index-C93GrQ2l.js → index-DIjFauc6.js} +1 -1
- package/ui/dist/index.html +1 -1
package/README.md
CHANGED
|
@@ -51,12 +51,23 @@ balise `<meta name="application-name">` de l'`index.html` local et de la version
|
|
|
51
51
|
du `package.json`.
|
|
52
52
|
|
|
53
53
|
Une application de base réservée à une organisation (`privateAccess`) est
|
|
54
|
-
invisible à une requête anonyme
|
|
55
|
-
|
|
56
|
-
`
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
invisible à une requête anonyme. Le paramètre `privateAccess` la rend visible,
|
|
55
|
+
mais data-fair répond 401 sans authentification — il n'est donc envoyé que si
|
|
56
|
+
`DATAFAIR_API_KEY` est renseignée.
|
|
57
|
+
|
|
58
|
+
Ne pas la trouver n'arrête pas la recherche pour autant : les applications qui
|
|
59
|
+
tournent dessus sont souvent publiques, et il suffit de connaître son URL pour
|
|
60
|
+
les lister. Les URL des applications de base publiées suivent deux conventions
|
|
61
|
+
(`<data-fair>/apps/<slug>/<mineure>/` et
|
|
62
|
+
`https://cdn.jsdelivr.net/npm/@scope/<paquet>@<mineure>/dist/`), et le filtre
|
|
63
|
+
de `/applications` est une égalité stricte : une URL devinée ne peut que ne
|
|
64
|
+
rien renvoyer, jamais renvoyer autre chose. Les candidates sont donc essayées
|
|
65
|
+
en une requête, ce qui suffit à lister les configurations publiques d'une
|
|
66
|
+
application de base privée, sans aucune clé.
|
|
67
|
+
|
|
68
|
+
Une clé reste nécessaire pour une application, elle, privée. Elle se crée
|
|
69
|
+
depuis le compte ou l'organisation sur le data-fair distant, et se met dans le
|
|
70
|
+
`.env` de l'application :
|
|
60
71
|
|
|
61
72
|
```
|
|
62
73
|
DATAFAIR_API_KEY=xxx
|
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -3,6 +3,7 @@ import config from './config.js';
|
|
|
3
3
|
import uiConfig from './ui-config.js';
|
|
4
4
|
import { localizeConfig } from './localize.js';
|
|
5
5
|
import { ATTACHMENTS_DIR, attachmentPath, copyAttachments, listAttachments } from './attachments.js';
|
|
6
|
+
import { candidateBaseAppUrls } from './base-app-urls.js';
|
|
6
7
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
7
8
|
import { createServer } from 'node:http';
|
|
8
9
|
import express from 'express';
|
|
@@ -150,13 +151,16 @@ const localAppInfo = async () => {
|
|
|
150
151
|
debug('failed to fetch local app index.html', err);
|
|
151
152
|
}
|
|
152
153
|
let version;
|
|
154
|
+
let packageName;
|
|
153
155
|
try {
|
|
154
|
-
|
|
156
|
+
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
|
|
157
|
+
version = pkg.version;
|
|
158
|
+
packageName = pkg.name;
|
|
155
159
|
}
|
|
156
160
|
catch (err) {
|
|
157
161
|
debug('failed to read local package.json', err);
|
|
158
162
|
}
|
|
159
|
-
return { name, version };
|
|
163
|
+
return { name, version, packageName };
|
|
160
164
|
};
|
|
161
165
|
// The owner the dev-server works on behalf of, in the "type:id[:department]" form the
|
|
162
166
|
// data-fair api expects for its owner and privateAccess filters.
|
|
@@ -368,7 +372,7 @@ const minorVersion = (version) => version.split('.').slice(0, 2).join('.');
|
|
|
368
372
|
// list configurations of the app under development that exist on the remote data-fair
|
|
369
373
|
// with the same minor version, useful to test quickly for regressions
|
|
370
374
|
app.get('/configurations', async (req, res) => {
|
|
371
|
-
const { name, version } = await localAppInfo();
|
|
375
|
+
const { name, version, packageName } = await localAppInfo();
|
|
372
376
|
const response = { appName: name, localVersion: version, remoteUrl: config.dataFair.url };
|
|
373
377
|
if (!name) {
|
|
374
378
|
res.status(400).send({ ...response, error: 'The "application-name" meta tag was not found in the local app index.html' });
|
|
@@ -379,10 +383,9 @@ app.get('/configurations', async (req, res) => {
|
|
|
379
383
|
return;
|
|
380
384
|
}
|
|
381
385
|
response.minorVersion = minorVersion(version);
|
|
382
|
-
// a base application restricted to an organization is invisible to an anonymous request
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
// to it without credentials, so it is only sent when an api key is configured.
|
|
386
|
+
// a base application restricted to an organization is invisible to an anonymous request.
|
|
387
|
+
// privateAccess is what makes it visible, and data-fair answers 401 to it without
|
|
388
|
+
// credentials, so it is only sent when an api key is configured.
|
|
386
389
|
response.authenticated = !!config.dataFair.apiKey;
|
|
387
390
|
try {
|
|
388
391
|
let baseAppsQuery = '/base-applications?applicationName=' + encodeURIComponent(name) + '&size=1000&count=false';
|
|
@@ -390,16 +393,21 @@ app.get('/configurations', async (req, res) => {
|
|
|
390
393
|
baseAppsQuery += '&privateAccess=' + encodeURIComponent(ownerFilter());
|
|
391
394
|
const baseApps = await remoteFetch(baseAppsQuery);
|
|
392
395
|
const matchingBaseApps = (baseApps.results ?? []).filter((b) => typeof b.version === 'string' && minorVersion(b.version) === response.minorVersion);
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
396
|
+
// Not finding the base application is not the end of the search: it may simply be private
|
|
397
|
+
// while the applications running on it are public, and those are reachable as soon as its
|
|
398
|
+
// url is known. Guessing the url costs nothing — the filter below is an exact match.
|
|
399
|
+
const versionByUrl = new Map(matchingBaseApps.map((b) => [b.url, b.version]));
|
|
400
|
+
const baseAppUrls = matchingBaseApps.length
|
|
401
|
+
? [...versionByUrl.keys()]
|
|
402
|
+
: candidateBaseAppUrls(config.dataFair.url, name, packageName, response.minorVersion);
|
|
403
|
+
// one request for the whole set: the data-fair list filters accept comma separated values
|
|
404
|
+
const applications = await remoteFetch('/applications?base-application=' + encodeURIComponent(baseAppUrls.join(',')) + '&size=1000&count=false&select=id,title,owner,url');
|
|
405
|
+
response.results = (applications.results ?? []).map((application) => ({
|
|
406
|
+
id: application.id,
|
|
407
|
+
title: application.title,
|
|
408
|
+
owner: application.owner,
|
|
409
|
+
baseAppVersion: versionByUrl.get(application.url) ?? response.minorVersion
|
|
410
|
+
}));
|
|
403
411
|
response.results.sort((a, b) => (a.title || '').localeCompare(b.title || ''));
|
|
404
412
|
res.send(response);
|
|
405
413
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const candidateBaseAppUrls: (dataFairUrl: string, appName: string, packageName: string | undefined, minorVersion: string) => string[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Candidate urls of the base application under development.
|
|
2
|
+
//
|
|
3
|
+
// Discovering it by name is the authoritative path, but it only works for a base application
|
|
4
|
+
// the request is allowed to see: one restricted to an organization is invisible to an anonymous
|
|
5
|
+
// request, while the applications running on it are often public — which is precisely the case
|
|
6
|
+
// of an application developed for a single customer. The /applications listing filters on an
|
|
7
|
+
// exact url, so a wrong candidate returns nothing and a right one returns the applications: a
|
|
8
|
+
// guess can never produce a false match, and they all fit in a single request.
|
|
9
|
+
//
|
|
10
|
+
// The two conventions published base applications follow, as of today:
|
|
11
|
+
// https://koumoul.com/apps/<slug>/<minor>/
|
|
12
|
+
// https://cdn.jsdelivr.net/npm/@scope/<package>@<minor>/dist/
|
|
13
|
+
// The slug is the repository name, which is not always the application name — "Liste et fiches"
|
|
14
|
+
// is served under /apps/list-details/ — so both are tried, along with the package name stripped
|
|
15
|
+
// of the prefixes repositories use.
|
|
16
|
+
const slugs = (appName, packageName) => {
|
|
17
|
+
const candidates = [appName];
|
|
18
|
+
if (packageName) {
|
|
19
|
+
const unscoped = packageName.startsWith('@') ? packageName.slice(packageName.indexOf('/') + 1) : packageName;
|
|
20
|
+
candidates.push(unscoped, unscoped.replace(/^(app|data-fair)-/, ''));
|
|
21
|
+
}
|
|
22
|
+
return [...new Set(candidates.filter(Boolean))];
|
|
23
|
+
};
|
|
24
|
+
export const candidateBaseAppUrls = (dataFairUrl, appName, packageName, minorVersion) => {
|
|
25
|
+
const urls = [];
|
|
26
|
+
// the same origin as the data-fair the dev-server talks to: a base application is served
|
|
27
|
+
// beside it, never on some unrelated host
|
|
28
|
+
const origin = new URL(dataFairUrl).origin;
|
|
29
|
+
for (const slug of slugs(appName, packageName)) {
|
|
30
|
+
urls.push(`${origin}/apps/${slug}/${minorVersion}/`);
|
|
31
|
+
}
|
|
32
|
+
if (packageName) {
|
|
33
|
+
const scoped = packageName.startsWith('@') ? packageName : '@data-fair/' + packageName;
|
|
34
|
+
urls.push(`https://cdn.jsdelivr.net/npm/${scoped}@${minorVersion}/dist/`);
|
|
35
|
+
}
|
|
36
|
+
return urls;
|
|
37
|
+
};
|