@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 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 : sans elle la liste est vide, quand bien même
55
- les applications qui l'utilisent seraient publiques. Le paramètre
56
- `privateAccess` la rend visible, mais data-fair répond 401 sans
57
- authentification — il n'est donc envoyé que si `DATAFAIR_API_KEY` est
58
- renseignée. Une clé d'API se crée depuis le compte ou l'organisation sur le
59
- data-fair distant, et se met dans le `.env` de l'application :
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@data-fair/dev-server",
3
- "version": "2.5.1",
3
+ "version": "2.5.2",
4
4
  "description": "A development server for optimal development experience of data-fair applications.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
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
- version = JSON.parse(readFileSync('package.json', 'utf8')).version;
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, and
383
- // without it the listing below finds nothing at all — the applications are then never reached,
384
- // however public they are. privateAccess is what makes it visible, and data-fair answers 401
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
- // told apart from "the base application exists but carries no application": the UI has no
394
- // other way to explain an empty list, and a missing api key is by far the likeliest cause
395
- response.baseAppFound = matchingBaseApps.length > 0;
396
- response.results = [];
397
- for (const baseApp of matchingBaseApps) {
398
- const applications = await remoteFetch('/applications?base-application=' + encodeURIComponent(baseApp.url) + '&size=1000&count=false&select=id,title,owner');
399
- for (const application of applications.results ?? []) {
400
- response.results.push({ id: application.id, title: application.title, owner: application.owner, baseAppVersion: baseApp.version });
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
+ };