@egen-civitas/esm-platform 0.0.0-stage → 1.2.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/CHANGELOG.md +29 -0
- package/README.md +117 -2
- package/dist/adapters/web/camera.d.ts +10 -0
- package/dist/adapters/web/camera.js +31 -0
- package/dist/adapters/web/file-registry.d.ts +7 -0
- package/dist/adapters/web/file-registry.js +26 -0
- package/dist/adapters/web/files.d.ts +7 -0
- package/dist/adapters/web/files.js +22 -0
- package/dist/adapters/web/index.d.ts +9 -0
- package/dist/adapters/web/index.js +9 -0
- package/dist/adapters/web/lifecycle.d.ts +3 -0
- package/dist/adapters/web/lifecycle.js +20 -0
- package/dist/adapters/web/open-external.d.ts +8 -0
- package/dist/adapters/web/open-external.js +32 -0
- package/dist/adapters/web/permissions.d.ts +12 -0
- package/dist/adapters/web/permissions.js +48 -0
- package/dist/adapters/web/pick-files.d.ts +17 -0
- package/dist/adapters/web/pick-files.js +49 -0
- package/dist/adapters/web/session.d.ts +12 -0
- package/dist/adapters/web/session.js +19 -0
- package/dist/adapters/web/web-adapter.test.js +1233 -0
- package/dist/bridge/client.d.ts +48 -0
- package/dist/bridge/client.js +212 -0
- package/dist/bridge/client.test.js +1264 -0
- package/dist/bridge/index.d.ts +4 -0
- package/dist/bridge/index.js +2 -0
- package/dist/bridge/protocol.d.ts +72 -0
- package/dist/bridge/protocol.js +66 -0
- package/dist/bridge/protocol.test.js +215 -0
- package/dist/capabilities.d.ts +34 -0
- package/dist/capabilities.js +48 -0
- package/dist/detect.d.ts +25 -0
- package/dist/detect.js +38 -0
- package/dist/detect.test.js +23 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.js +27 -0
- package/dist/errors.test.js +31 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +14 -0
- package/dist/native-platform.test.js +329 -0
- package/dist/native.d.ts +24 -0
- package/dist/native.js +46 -0
- package/dist/platform.d.ts +24 -0
- package/dist/platform.js +56 -0
- package/dist/platform.test.js +696 -0
- package/dist/register-capabilities.test.js +188 -0
- package/dist/registry.d.ts +52 -0
- package/dist/registry.js +101 -0
- package/dist/registry.test.js +681 -0
- package/dist/testing/index.d.ts +23 -0
- package/dist/testing/index.js +14 -0
- package/dist/testing/mock-host.d.ts +49 -0
- package/dist/testing/mock-host.js +86 -0
- package/dist/types.d.ts +108 -0
- package/dist/types.js +1 -0
- package/package.json +57 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @egen-civitas/esm-platform
|
|
2
|
+
|
|
3
|
+
## 1.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- c5d96c9: Client du protocole Host v1 (C5) : `createBridgeClient` (handshake mémorisé, délais, concurrence, messages invalides ignorés et comptés), `getHostInfo()`, `getHostBridge()`, `createMockHost()` (testing). En natif, `getPlatform().ready` attend désormais le handshake et rejette `INCOMPATIBLE` si le Host est de version incompatible ou annonce une autre plateforme. Aucun changement en Web.
|
|
8
|
+
- 340f5fe: Registre de capacités (C6).
|
|
9
|
+
|
|
10
|
+
- `esm-platform` : `src/registry.ts` devient l'unique description des capacités (version, méthodes, adaptateurs Web et natif) ; le gabarit « indisponible », les adaptateurs, le handshake et la disponibilité en sont dérivés. Une capacité n'est disponible que si le Host l'annonce dans une version supérieure ou égale à celle du registre. `Platform` est dérivé de `CapabilityInterfaces`, extensible par fusion de déclaration. Procédure « ajouter une capacité » dans le README ; comportement des 9 capacités v1 inchangé.
|
|
11
|
+
- `esm-globals` : `EgenCapabilityExtensions` permet d'étendre `CapabilityName` sans modifier ce paquet. Les 9 noms v1 restent une union fermée.
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [340f5fe]
|
|
16
|
+
- @egen-civitas/esm-globals@2.2.0
|
|
17
|
+
|
|
18
|
+
## 1.1.0
|
|
19
|
+
|
|
20
|
+
### Minor Changes
|
|
21
|
+
|
|
22
|
+
- d1d2d7d: Contrat d'app multiplateforme (F2) : `EgenAppRoutes.platforms` et `requiredCapabilities`. `registerApp` écarte (sans rien enregistrer) une app non éligible à la plateforme ou dont une capacité requise manque. `esm-globals` : `setCapabilityResolver` / `hasCapability`. `esm-platform` : `registerPlatformCapabilities()`. Shell : capacités rendues consultables avant l'enregistrement des apps. Sans champs dans `routes.json`, comportement inchangé.
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- Updated dependencies [d1d2d7d]
|
|
27
|
+
- Updated dependencies [669cbd6]
|
|
28
|
+
- Updated dependencies [5958a56]
|
|
29
|
+
- @egen-civitas/esm-globals@2.1.0
|
package/README.md
CHANGED
|
@@ -1,3 +1,118 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @egen-civitas/esm-platform
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Capacités de plateforme EGEN pour **Web, Android et iOS** : contrats TypeScript, détection de la cible, erreurs typées et (à venir) client du protocole Host.
|
|
4
|
+
|
|
5
|
+
> Document de référence : `docs/EGEN-FRAMEWORK-DEV.md` à la racine du dépôt (chantier C).
|
|
6
|
+
|
|
7
|
+
## Règles
|
|
8
|
+
- **Aucun code natif** et aucun import `@capacitor/*` ici (règles R2/N2).
|
|
9
|
+
- La cible est lue **uniquement** via `window.egenTarget` (posé au build). Pas de User-Agent (N4).
|
|
10
|
+
- Les capacités rejettent toujours avec `PlatformError` (`UNAVAILABLE`, `DENIED`, `CANCELLED`, `INVALID_ARGS`, `TIMEOUT`, `INCOMPATIBLE`, `HOST_ERROR`).
|
|
11
|
+
- Une capacité absente existe comme objet ; ses méthodes rejettent `UNAVAILABLE`. Utiliser `platform.has('…')`.
|
|
12
|
+
|
|
13
|
+
## Utilisation
|
|
14
|
+
|
|
15
|
+
Les apps importent **uniquement** via `@egen-civitas/esm-framework` (jamais `esm-platform` directement) :
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { getPlatform, PlatformError } from '@egen-civitas/esm-framework';
|
|
19
|
+
|
|
20
|
+
const platform = getPlatform();
|
|
21
|
+
if (platform.has('secureStorage')) {
|
|
22
|
+
const value = await platform.secureStorage.get('clé');
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Web** : l'adaptateur du navigateur fournit `session` (vide : le cookie reste le mécanisme), `openExternal` (`https:`, `mailto:`, `tel:`),
|
|
27
|
+
`lifecycle` (`visibilitychange`), `files.select`, `camera.capture` et, si l'API existe, `permissions`. `secureStorage`, `push` et `biometric` ne sont **jamais
|
|
28
|
+
simulés** : `has()` est faux et les méthodes rejettent `PlatformError('UNAVAILABLE')`.
|
|
29
|
+
**Natif** : tant que le client de pont (C5) n'est pas branché, **aucune capacité n'est fournie** (même comportement). Les abonnements `onResume`, `onPause`,
|
|
30
|
+
`onMessage`, dont la signature n'est pas une `Promise`, lèvent la même erreur au lieu de rejeter.
|
|
31
|
+
`platform.ready` se résout immédiatement en Web ; en natif il rejette `INCOMPATIBLE` (« Host manquant ») si `window.egenHostBridge` n'a pas été injecté.
|
|
32
|
+
|
|
33
|
+
## Tests d'une app
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { createMockPlatform } from '@egen-civitas/esm-framework/mock';
|
|
37
|
+
|
|
38
|
+
getPlatform.mockReturnValue(createMockPlatform({ capabilities: { openExternal: { open: vi.fn() } } }));
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`createMockPlatform` utilise le même assembleur que `getPlatform()` (`src/capabilities.ts`) : seules les capacités listées sont disponibles,
|
|
42
|
+
le reste reste `UNAVAILABLE`. Le mock et l'implémentation passent les **mêmes** tests de contrat (`src/platform.test.ts`).
|
|
43
|
+
`createMockHost()` (simulation de `window.egenHostBridge`) viendra avec le client de pont (C5/C9).
|
|
44
|
+
|
|
45
|
+
## Capacités v1
|
|
46
|
+
|
|
47
|
+
Tout ce qui énumère les capacités (gabarit « indisponible », handshake avec le Host, adaptateurs Web et natif, mocks) lit **un seul registre** :
|
|
48
|
+
`src/registry.ts`. Il associe à chaque nom une `version`, la liste exhaustive de ses `methods` (`async` ou `subscribe`) et ses adaptateurs `web` / `native`.
|
|
49
|
+
|
|
50
|
+
| Capacité | Version | Web | Natif | Priorité | Remarque |
|
|
51
|
+
|---|---|---|---|---|---|
|
|
52
|
+
| `session` | 1 | fournisseur vide (cookie) | à faire (C7) | P0 | chemin critique du chantier D |
|
|
53
|
+
| `secureStorage` | 1 | **jamais** (pas de faux coffre) | à faire (C7) | P0 | valeurs `string` uniquement |
|
|
54
|
+
| `openExternal` | 1 | `https:`, `mailto:`, `tel:` | à faire (C7) | P0 | allowlist de schémas |
|
|
55
|
+
| `lifecycle` | 1 | `visibilitychange` | à faire (C7) | P0 | abonnements avec désabonnement |
|
|
56
|
+
| `permissions` | 1 | API Permissions si présente | à faire (C7) | P1 | noms normalisés |
|
|
57
|
+
| `files` | 1 | `select` seul | à faire (C7) | P1 | *handle*, jamais de binaire inline |
|
|
58
|
+
| `camera` | 1 | `<input capture>` | à faire (C7) | P1 | *handle* de fichier |
|
|
59
|
+
| `push` | 1 | **jamais** | à faire (C7) | P2 | |
|
|
60
|
+
| `biometric` | 1 | **jamais** | à faire (C7) | P2 | déverrouillage local uniquement |
|
|
61
|
+
|
|
62
|
+
Une capacité n'est disponible (`has()` vrai) que si un adaptateur la fournit **et**, en natif, que le Host l'annonce dans une version **supérieure ou égale** à celle du registre.
|
|
63
|
+
|
|
64
|
+
## Ajouter une capacité
|
|
65
|
+
|
|
66
|
+
Règle (N9) : une capacité P1/P2 n'est implémentée que lorsqu'**une app l'utilise**. La procédure ne modifie **aucun autre fichier du framework**
|
|
67
|
+
que ceux listés ci-dessous : le critère est vérifié par `src/registry.test.ts`, qui ajoute une capacité fictive entièrement depuis un fichier de test.
|
|
68
|
+
|
|
69
|
+
1. **Interface TypeScript** dans son propre fichier (`src/extensions/<nom>.ts`), avec les **deux fusions de déclaration** qui l'enregistrent côté types
|
|
70
|
+
(ni `types.ts` ni `esm-globals` ne sont modifiés) :
|
|
71
|
+
```ts
|
|
72
|
+
export interface MaCapacite {
|
|
73
|
+
faire(entree: string): Promise<string>;
|
|
74
|
+
onEvenement(callback: () => void): Unsubscribe;
|
|
75
|
+
}
|
|
76
|
+
declare module '@egen-civitas/esm-globals' {
|
|
77
|
+
interface EgenCapabilityExtensions {
|
|
78
|
+
maCapacite: true; // la rend valide comme `CapabilityName` (donc dans `requiredCapabilities`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
declare module '../types.js' {
|
|
82
|
+
interface CapabilityInterfaces {
|
|
83
|
+
maCapacite: MaCapacite; // la rend accessible : `platform.maCapacite`
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
2. **Entrée du registre** (`registerCapability('maCapacite', { version, methods, web, native })`). `methods` est vérifié **à la compilation** : un oubli ne compile pas.
|
|
88
|
+
3. **Adaptateur Web** (`src/adapters/web/<nom>.ts`) : une fabrique qui retourne `undefined` si le navigateur ne sait pas faire. Une méthode non fournie reste `UNAVAILABLE`. **Jamais de simulation de sécurité.**
|
|
89
|
+
4. **Adaptateur natif** : un simple *appelant* du pont (`client.call('maCapacite', 'faire', args)`, `client.onEvent(...)`), sans logique métier.
|
|
90
|
+
5. **Mock** : `createMockPlatform({ capabilities: { maCapacite: { … } } })` fonctionne sans rien ajouter ; ajouter ses valeurs par défaut dans `src/testing/` seulement si utile.
|
|
91
|
+
6. **Tests** : ajouter les lignes de la capacité aux tables de `src/platform.test.ts` (elles servent d'oracle indépendant du registre), un test par méthode de l'adaptateur Web, et un test natif avec `createMockHost`.
|
|
92
|
+
7. **Changeset** *minor* (`esm-platform`, et `esm-globals` si une extension de `CapabilityName` est publiée).
|
|
93
|
+
8. **Ligne ajoutée au tableau des capacités** ci-dessus.
|
|
94
|
+
|
|
95
|
+
## État d'avancement
|
|
96
|
+
| Élément | Tâche | État |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| Création du package et câblage (`esm-framework` index/internal/mocks, shell, typedoc) | C1 | fait |
|
|
99
|
+
| Contrats TypeScript des capacités | C2 | fait |
|
|
100
|
+
| Erreurs typées | C2 | fait |
|
|
101
|
+
| Détection de la cible, cohérence du Host (`hasHostBridge`, `ready`) | C3 | fait |
|
|
102
|
+
| `getPlatform()` sûr par défaut (aucune capacité simulée) | C1 | fait |
|
|
103
|
+
| `createMockPlatform` | C9 | partiel (`createMockHost` à faire avec C5) |
|
|
104
|
+
| Adaptateur Web | C4 | fait |
|
|
105
|
+
| Client de pont / protocole v1 (`createBridgeClient`, handshake, `getHostInfo`) | C5 | fait |
|
|
106
|
+
| Registre et procédure d'ajout d'une capacité | C6 | fait |
|
|
107
|
+
|
|
108
|
+
## Tester sans Host
|
|
109
|
+
`createMockHost()` (`@egen-civitas/esm-platform/testing`) simule `window.egenHostBridge` : handshake automatique, méthodes
|
|
110
|
+
programmables (`handlers: { 'secureStorage.get': () => 'valeur' }`), latence, silence (tests de timeout), version de
|
|
111
|
+
protocole incompatible, messages bruts arbitraires. Il rejoue le **contrat** du protocole v1, pas du code natif.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const host = createMockHost({ capabilities: { secureStorage: { v: 1 } }, handlers: { 'secureStorage.get': () => 'v' } });
|
|
115
|
+
host.install(); // window.egenHostBridge
|
|
116
|
+
window.egenTarget = 'android';
|
|
117
|
+
await getPlatform().ready; // handshake
|
|
118
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CameraCapability } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* `camera.capture` en Web : `<input type="file" accept="image/*" capture>`.
|
|
4
|
+
*
|
|
5
|
+
* CHOIX documenté (la doc C4 demande d'en retenir UN) : l'attribut `capture` plutôt que `getUserMedia`. Raisons :
|
|
6
|
+
* pas de flux vidéo à gérer ni de permission préalable à orchestrer, et l'application de caméra du système est
|
|
7
|
+
* employée sur mobile. Contrepartie : sur ordinateur, `capture` est ignoré et l'utilisateur obtient un sélecteur de
|
|
8
|
+
* fichiers image (pas de prévisualisation intégrée).
|
|
9
|
+
*/
|
|
10
|
+
export declare function createWebCamera(): CameraCapability;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { PlatformError } from '../../errors.js';
|
|
2
|
+
import { pickFiles } from './pick-files.js';
|
|
3
|
+
import { registerWebFile } from './file-registry.js';
|
|
4
|
+
/**
|
|
5
|
+
* `camera.capture` en Web : `<input type="file" accept="image/*" capture>`.
|
|
6
|
+
*
|
|
7
|
+
* CHOIX documenté (la doc C4 demande d'en retenir UN) : l'attribut `capture` plutôt que `getUserMedia`. Raisons :
|
|
8
|
+
* pas de flux vidéo à gérer ni de permission préalable à orchestrer, et l'application de caméra du système est
|
|
9
|
+
* employée sur mobile. Contrepartie : sur ordinateur, `capture` est ignoré et l'utilisateur obtient un sélecteur de
|
|
10
|
+
* fichiers image (pas de prévisualisation intégrée).
|
|
11
|
+
*/
|
|
12
|
+
export function createWebCamera() {
|
|
13
|
+
return {
|
|
14
|
+
async capture(options) {
|
|
15
|
+
const facing = options?.facing;
|
|
16
|
+
if (facing !== undefined && facing !== 'front' && facing !== 'back') {
|
|
17
|
+
throw new PlatformError('INVALID_ARGS', '`facing` doit valoir "front" ou "back".', {
|
|
18
|
+
capability: 'camera',
|
|
19
|
+
method: 'capture',
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const [file] = await pickFiles({
|
|
23
|
+
accept: ['image/*'],
|
|
24
|
+
capture: facing === 'front' ? 'user' : 'environment',
|
|
25
|
+
capability: 'camera',
|
|
26
|
+
method: 'capture',
|
|
27
|
+
});
|
|
28
|
+
return registerWebFile(file);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FileHandle } from '../../types.js';
|
|
2
|
+
/** Enregistre un fichier et retourne son handle (sans binaire). */
|
|
3
|
+
export declare function registerWebFile(file: File): FileHandle;
|
|
4
|
+
/** @internal */
|
|
5
|
+
export declare function getWebFile(id: string): File | undefined;
|
|
6
|
+
/** @internal Libère le fichier : à appeler dès qu'il a été lu ou téléversé. */
|
|
7
|
+
export declare function releaseWebFile(id: string): boolean;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registre interne des fichiers choisis par l'utilisateur en Web. Un `FileHandle` ne porte jamais le binaire (contrat
|
|
3
|
+
* v1) : le `File` du navigateur est conservé ici, sous un identifiant opaque, jusqu'à ce que le consommateur
|
|
4
|
+
* (capacités fichiers, tâche C7) le récupère puis le libère. Non exporté par le point d'entrée du package.
|
|
5
|
+
*/
|
|
6
|
+
const files = new Map();
|
|
7
|
+
let counter = 0;
|
|
8
|
+
function newId() {
|
|
9
|
+
const random = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' ? crypto.randomUUID() : null;
|
|
10
|
+
counter += 1;
|
|
11
|
+
return random ?? `web-file-${Date.now().toString(36)}-${counter}-${Math.random().toString(36).slice(2)}`;
|
|
12
|
+
}
|
|
13
|
+
/** Enregistre un fichier et retourne son handle (sans binaire). */
|
|
14
|
+
export function registerWebFile(file) {
|
|
15
|
+
const id = newId();
|
|
16
|
+
files.set(id, file);
|
|
17
|
+
return { id, name: file.name, size: file.size, mime: file.type || 'application/octet-stream' };
|
|
18
|
+
}
|
|
19
|
+
/** @internal */
|
|
20
|
+
export function getWebFile(id) {
|
|
21
|
+
return files.get(id);
|
|
22
|
+
}
|
|
23
|
+
/** @internal Libère le fichier : à appeler dès qu'il a été lu ou téléversé. */
|
|
24
|
+
export function releaseWebFile(id) {
|
|
25
|
+
return files.delete(id);
|
|
26
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FilesCapability } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* `files` en Web : `select` utilise un `<input type="file">` éphémère. `download` n'est PAS fourni en Web en v1 :
|
|
4
|
+
* l'attribut `download` d'un lien est ignoré hors même origine, et la capacité ne doit pas promettre ce qu'elle ne
|
|
5
|
+
* garantit pas (`UNAVAILABLE`, comme les autres méthodes non fournies).
|
|
6
|
+
*/
|
|
7
|
+
export declare function createWebFiles(): Partial<FilesCapability>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { unavailable } from '../../errors.js';
|
|
2
|
+
import { pickFiles } from './pick-files.js';
|
|
3
|
+
import { registerWebFile } from './file-registry.js';
|
|
4
|
+
/**
|
|
5
|
+
* `files` en Web : `select` utilise un `<input type="file">` éphémère. `download` n'est PAS fourni en Web en v1 :
|
|
6
|
+
* l'attribut `download` d'un lien est ignoré hors même origine, et la capacité ne doit pas promettre ce qu'elle ne
|
|
7
|
+
* garantit pas (`UNAVAILABLE`, comme les autres méthodes non fournies).
|
|
8
|
+
*/
|
|
9
|
+
export function createWebFiles() {
|
|
10
|
+
return {
|
|
11
|
+
async select(options) {
|
|
12
|
+
const selected = await pickFiles({
|
|
13
|
+
multiple: options?.multiple,
|
|
14
|
+
accept: options?.accept,
|
|
15
|
+
capability: 'files',
|
|
16
|
+
method: 'select',
|
|
17
|
+
});
|
|
18
|
+
return selected.map(registerWebFile);
|
|
19
|
+
},
|
|
20
|
+
download: () => Promise.reject(unavailable('files', 'download')),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capacités fournies par le navigateur. PRINCIPE (doc C4) : ne jamais simuler de la sécurité.
|
|
3
|
+
*
|
|
4
|
+
* La liste de ce qui est fourni ne vit plus ici : chaque capacité déclare son adaptateur Web dans `registry.ts`
|
|
5
|
+
* (tâche C6), et `createWebCapabilities()` ne fait que lire le registre. Sont ABSENTES (`has()` faux, méthodes
|
|
6
|
+
* `UNAVAILABLE`) : `secureStorage` (pas de faux coffre dans `localStorage`), `push` et `biometric` (v1), ainsi que
|
|
7
|
+
* `permissions` quand l'API Permissions n'existe pas.
|
|
8
|
+
*/
|
|
9
|
+
export { createWebCapabilities } from '../../registry.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capacités fournies par le navigateur. PRINCIPE (doc C4) : ne jamais simuler de la sécurité.
|
|
3
|
+
*
|
|
4
|
+
* La liste de ce qui est fourni ne vit plus ici : chaque capacité déclare son adaptateur Web dans `registry.ts`
|
|
5
|
+
* (tâche C6), et `createWebCapabilities()` ne fait que lire le registre. Sont ABSENTES (`has()` faux, méthodes
|
|
6
|
+
* `UNAVAILABLE`) : `secureStorage` (pas de faux coffre dans `localStorage`), `push` et `biometric` (v1), ainsi que
|
|
7
|
+
* `permissions` quand l'API Permissions n'existe pas.
|
|
8
|
+
*/
|
|
9
|
+
export { createWebCapabilities } from '../../registry.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { PlatformError } from '../../errors.js';
|
|
2
|
+
function subscribe(state, method, callback) {
|
|
3
|
+
if (typeof callback !== 'function') {
|
|
4
|
+
throw new PlatformError('INVALID_ARGS', 'Le callback doit être une fonction.', { capability: 'lifecycle', method });
|
|
5
|
+
}
|
|
6
|
+
const listener = () => {
|
|
7
|
+
if (document.visibilityState === state) {
|
|
8
|
+
callback();
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
document.addEventListener('visibilitychange', listener);
|
|
12
|
+
return () => document.removeEventListener('visibilitychange', listener);
|
|
13
|
+
}
|
|
14
|
+
/** `lifecycle` en Web : `visibilitychange` → `onResume` (page visible) / `onPause` (page masquée). */
|
|
15
|
+
export function createWebLifecycle() {
|
|
16
|
+
return {
|
|
17
|
+
onResume: (callback) => subscribe('visible', 'onResume', callback),
|
|
18
|
+
onPause: (callback) => subscribe('hidden', 'onPause', callback),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { OpenExternalCapability } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Valide une URL à ouvrir hors de l'app : schémas `https:`, `mailto:`, `tel:` UNIQUEMENT (ni `http:`, ni
|
|
4
|
+
* `javascript:`, ni `data:`, ni `file:`…). Le message d'erreur ne reprend jamais l'URL (elle peut contenir un secret).
|
|
5
|
+
*/
|
|
6
|
+
export declare function assertExternalUrl(url: unknown): string;
|
|
7
|
+
/** `openExternal` en Web : `window.open(url, '_blank', 'noopener,noreferrer')` après validation. */
|
|
8
|
+
export declare function createWebOpenExternal(): OpenExternalCapability;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { PlatformError } from '../../errors.js';
|
|
2
|
+
const ALLOWED_PROTOCOLS = new Set(['https:', 'mailto:', 'tel:']);
|
|
3
|
+
/**
|
|
4
|
+
* Valide une URL à ouvrir hors de l'app : schémas `https:`, `mailto:`, `tel:` UNIQUEMENT (ni `http:`, ni
|
|
5
|
+
* `javascript:`, ni `data:`, ni `file:`…). Le message d'erreur ne reprend jamais l'URL (elle peut contenir un secret).
|
|
6
|
+
*/
|
|
7
|
+
export function assertExternalUrl(url) {
|
|
8
|
+
let parsed = null;
|
|
9
|
+
try {
|
|
10
|
+
parsed = typeof url === 'string' ? new URL(url) : null;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
parsed = null;
|
|
14
|
+
}
|
|
15
|
+
if (!parsed || !ALLOWED_PROTOCOLS.has(parsed.protocol)) {
|
|
16
|
+
throw new PlatformError('INVALID_ARGS', 'URL refusée : seuls les schémas https:, mailto: et tel: sont autorisés.', {
|
|
17
|
+
capability: 'openExternal',
|
|
18
|
+
method: 'open',
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return parsed.href;
|
|
22
|
+
}
|
|
23
|
+
/** `openExternal` en Web : `window.open(url, '_blank', 'noopener,noreferrer')` après validation. */
|
|
24
|
+
export function createWebOpenExternal() {
|
|
25
|
+
return {
|
|
26
|
+
async open(url) {
|
|
27
|
+
const href = assertExternalUrl(url);
|
|
28
|
+
// `noopener` : la page ouverte n'a pas accès à `window.opener` ; le retour de `open` est alors toujours `null`.
|
|
29
|
+
window.open(href, '_blank', 'noopener,noreferrer');
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PermissionsCapability } from '../../types.js';
|
|
2
|
+
/** L'API Permissions du navigateur est-elle disponible ? Sinon la capacité n'est pas fournie (`has()` faux). */
|
|
3
|
+
export declare function isWebPermissionsAvailable(): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* `permissions` en Web, via l'API Permissions du navigateur.
|
|
6
|
+
*
|
|
7
|
+
* - `check` : `navigator.permissions.query`. `files` n'a pas d'équivalent Web, et un navigateur qui ne connaît pas un
|
|
8
|
+
* nom (ex. `camera` sous Firefox) lève : dans les deux cas `UNAVAILABLE`, jamais une valeur inventée.
|
|
9
|
+
* - `request` : seule `notifications` a une demande explicite en Web (`Notification.requestPermission`). Pour
|
|
10
|
+
* `camera` et `microphone`, le navigateur demande l'accès au moment de l'usage : `UNAVAILABLE` (rien n'est simulé).
|
|
11
|
+
*/
|
|
12
|
+
export declare function createWebPermissions(): Partial<PermissionsCapability>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { PlatformError, unavailable } from '../../errors.js';
|
|
2
|
+
const NAMES = ['camera', 'microphone', 'notifications', 'files'];
|
|
3
|
+
function assertName(name, method) {
|
|
4
|
+
if (!NAMES.includes(name)) {
|
|
5
|
+
throw new PlatformError('INVALID_ARGS', `Permission inconnue. Valeurs acceptées : ${NAMES.join(', ')}.`, {
|
|
6
|
+
capability: 'permissions',
|
|
7
|
+
method,
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
return name;
|
|
11
|
+
}
|
|
12
|
+
/** L'API Permissions du navigateur est-elle disponible ? Sinon la capacité n'est pas fournie (`has()` faux). */
|
|
13
|
+
export function isWebPermissionsAvailable() {
|
|
14
|
+
return typeof navigator !== 'undefined' && typeof navigator.permissions?.query === 'function';
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* `permissions` en Web, via l'API Permissions du navigateur.
|
|
18
|
+
*
|
|
19
|
+
* - `check` : `navigator.permissions.query`. `files` n'a pas d'équivalent Web, et un navigateur qui ne connaît pas un
|
|
20
|
+
* nom (ex. `camera` sous Firefox) lève : dans les deux cas `UNAVAILABLE`, jamais une valeur inventée.
|
|
21
|
+
* - `request` : seule `notifications` a une demande explicite en Web (`Notification.requestPermission`). Pour
|
|
22
|
+
* `camera` et `microphone`, le navigateur demande l'accès au moment de l'usage : `UNAVAILABLE` (rien n'est simulé).
|
|
23
|
+
*/
|
|
24
|
+
export function createWebPermissions() {
|
|
25
|
+
return {
|
|
26
|
+
async check(name) {
|
|
27
|
+
const permission = assertName(name, 'check');
|
|
28
|
+
if (permission === 'files') {
|
|
29
|
+
throw unavailable('permissions', 'check');
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const status = await navigator.permissions.query({ name: permission });
|
|
33
|
+
return status.state;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw unavailable('permissions', 'check');
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
async request(name) {
|
|
40
|
+
const permission = assertName(name, 'request');
|
|
41
|
+
if (permission !== 'notifications' || typeof Notification === 'undefined') {
|
|
42
|
+
throw unavailable('permissions', 'request');
|
|
43
|
+
}
|
|
44
|
+
// « default » (fenêtre fermée sans choix) n'est pas un octroi.
|
|
45
|
+
return (await Notification.requestPermission()) === 'granted' ? 'granted' : 'denied';
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface PickFilesOptions {
|
|
2
|
+
multiple?: boolean;
|
|
3
|
+
accept?: string[];
|
|
4
|
+
/** Valeur de l'attribut `capture` (`user` = caméra frontale, `environment` = caméra arrière). */
|
|
5
|
+
capture?: 'user' | 'environment';
|
|
6
|
+
capability: string;
|
|
7
|
+
method: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Ouvre le sélecteur de fichiers du navigateur via un `<input type="file">` ÉPHÉMÈRE (créé, utilisé, retiré).
|
|
11
|
+
* Doit être appelé depuis un geste utilisateur, sans quoi le navigateur ignore `click()`.
|
|
12
|
+
*
|
|
13
|
+
* Annulation : l'événement `cancel` (navigateurs récents) rejette `CANCELLED`. Sur un navigateur qui ne l'émet pas,
|
|
14
|
+
* la promesse reste en attente tant que l'utilisateur ne choisit rien : c'est une limite connue, non contournable
|
|
15
|
+
* proprement sans deviner (pas d'heuristique sur le focus).
|
|
16
|
+
*/
|
|
17
|
+
export declare function pickFiles({ multiple, accept, capture, capability, method, }: PickFilesOptions): Promise<File[]>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { PlatformError } from '../../errors.js';
|
|
2
|
+
/** Types MIME ou extensions : `image/*`, `application/pdf`, `.csv`. Rien d'autre n'est écrit dans l'attribut `accept`. */
|
|
3
|
+
const ACCEPT_ENTRY = /^(\.[A-Za-z0-9]+|[A-Za-z0-9!#$&^_.+-]+\/(\*|[A-Za-z0-9!#$&^_.+-]+))$/;
|
|
4
|
+
/**
|
|
5
|
+
* Ouvre le sélecteur de fichiers du navigateur via un `<input type="file">` ÉPHÉMÈRE (créé, utilisé, retiré).
|
|
6
|
+
* Doit être appelé depuis un geste utilisateur, sans quoi le navigateur ignore `click()`.
|
|
7
|
+
*
|
|
8
|
+
* Annulation : l'événement `cancel` (navigateurs récents) rejette `CANCELLED`. Sur un navigateur qui ne l'émet pas,
|
|
9
|
+
* la promesse reste en attente tant que l'utilisateur ne choisit rien : c'est une limite connue, non contournable
|
|
10
|
+
* proprement sans deviner (pas d'heuristique sur le focus).
|
|
11
|
+
*/
|
|
12
|
+
export function pickFiles({ multiple = false, accept, capture, capability, method, }) {
|
|
13
|
+
if (accept !== undefined && (!Array.isArray(accept) || !accept.every((entry) => ACCEPT_ENTRY.test(String(entry))))) {
|
|
14
|
+
return Promise.reject(new PlatformError('INVALID_ARGS', '`accept` doit être une liste de types MIME ou d’extensions (ex. "image/*", ".csv").', {
|
|
15
|
+
capability,
|
|
16
|
+
method,
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const input = document.createElement('input');
|
|
21
|
+
input.type = 'file';
|
|
22
|
+
input.multiple = multiple;
|
|
23
|
+
if (accept && accept.length > 0) {
|
|
24
|
+
input.accept = accept.join(',');
|
|
25
|
+
}
|
|
26
|
+
if (capture) {
|
|
27
|
+
input.setAttribute('capture', capture);
|
|
28
|
+
}
|
|
29
|
+
// Certains navigateurs mobiles exigent l'élément dans le document pour déclencher le sélecteur.
|
|
30
|
+
input.style.display = 'none';
|
|
31
|
+
document.body.appendChild(input);
|
|
32
|
+
const cleanup = () => input.remove();
|
|
33
|
+
input.addEventListener('change', () => {
|
|
34
|
+
const selected = Array.from(input.files ?? []);
|
|
35
|
+
cleanup();
|
|
36
|
+
if (selected.length === 0) {
|
|
37
|
+
reject(new PlatformError('CANCELLED', 'Aucun fichier sélectionné.', { capability, method }));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
resolve(selected);
|
|
41
|
+
}
|
|
42
|
+
}, { once: true });
|
|
43
|
+
input.addEventListener('cancel', () => {
|
|
44
|
+
cleanup();
|
|
45
|
+
reject(new PlatformError('CANCELLED', 'Sélection annulée.', { capability, method }));
|
|
46
|
+
}, { once: true });
|
|
47
|
+
input.click();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SessionCapability } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* `session` en Web : fournisseur d'autorisation VIDE. Le cookie de session reste le mécanisme d'authentification
|
|
4
|
+
* (`egenFetch` n'ajoute aucun en-tête `Authorization`, cf. D1) : il n'y a donc pas de jeton à fournir, et on n'en
|
|
5
|
+
* fabrique pas.
|
|
6
|
+
*
|
|
7
|
+
* - `getAuthorization` → `null` (« pas de jeton géré par le client ») ;
|
|
8
|
+
* - `refresh` → `false` (aucun nouveau jeton) ;
|
|
9
|
+
* - `clear` → no-op (rien n'est stocké côté client ; la déconnexion Web passe par le serveur) ;
|
|
10
|
+
* - `setTokens` → `UNAVAILABLE` : stocker un jeton dans le navigateur serait un faux coffre (jamais simuler de sécurité).
|
|
11
|
+
*/
|
|
12
|
+
export declare function createWebSession(): SessionCapability;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { unavailable } from '../../errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* `session` en Web : fournisseur d'autorisation VIDE. Le cookie de session reste le mécanisme d'authentification
|
|
4
|
+
* (`egenFetch` n'ajoute aucun en-tête `Authorization`, cf. D1) : il n'y a donc pas de jeton à fournir, et on n'en
|
|
5
|
+
* fabrique pas.
|
|
6
|
+
*
|
|
7
|
+
* - `getAuthorization` → `null` (« pas de jeton géré par le client ») ;
|
|
8
|
+
* - `refresh` → `false` (aucun nouveau jeton) ;
|
|
9
|
+
* - `clear` → no-op (rien n'est stocké côté client ; la déconnexion Web passe par le serveur) ;
|
|
10
|
+
* - `setTokens` → `UNAVAILABLE` : stocker un jeton dans le navigateur serait un faux coffre (jamais simuler de sécurité).
|
|
11
|
+
*/
|
|
12
|
+
export function createWebSession() {
|
|
13
|
+
return {
|
|
14
|
+
getAuthorization: () => Promise.resolve(null),
|
|
15
|
+
refresh: () => Promise.resolve(false),
|
|
16
|
+
clear: () => Promise.resolve(),
|
|
17
|
+
setTokens: () => Promise.reject(unavailable('session', 'setTokens')),
|
|
18
|
+
};
|
|
19
|
+
}
|