@egen-civitas/esm-platform 0.0.0-stage → 1.1.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +55 -2
  3. package/dist/adapters/web/camera.d.ts +10 -0
  4. package/dist/adapters/web/camera.js +31 -0
  5. package/dist/adapters/web/file-registry.d.ts +7 -0
  6. package/dist/adapters/web/file-registry.js +26 -0
  7. package/dist/adapters/web/files.d.ts +7 -0
  8. package/dist/adapters/web/files.js +22 -0
  9. package/dist/adapters/web/index.d.ts +7 -0
  10. package/dist/adapters/web/index.js +24 -0
  11. package/dist/adapters/web/lifecycle.d.ts +3 -0
  12. package/dist/adapters/web/lifecycle.js +20 -0
  13. package/dist/adapters/web/open-external.d.ts +8 -0
  14. package/dist/adapters/web/open-external.js +32 -0
  15. package/dist/adapters/web/permissions.d.ts +12 -0
  16. package/dist/adapters/web/permissions.js +48 -0
  17. package/dist/adapters/web/pick-files.d.ts +17 -0
  18. package/dist/adapters/web/pick-files.js +49 -0
  19. package/dist/adapters/web/session.d.ts +12 -0
  20. package/dist/adapters/web/session.js +19 -0
  21. package/dist/adapters/web/web-adapter.test.js +1233 -0
  22. package/dist/capabilities.d.ts +26 -0
  23. package/dist/capabilities.js +72 -0
  24. package/dist/detect.d.ts +18 -0
  25. package/dist/detect.js +28 -0
  26. package/dist/detect.test.js +23 -0
  27. package/dist/errors.d.ts +27 -0
  28. package/dist/errors.js +27 -0
  29. package/dist/errors.test.js +31 -0
  30. package/dist/index.d.ts +4 -0
  31. package/dist/index.js +12 -0
  32. package/dist/platform.d.ts +21 -0
  33. package/dist/platform.js +47 -0
  34. package/dist/platform.test.js +699 -0
  35. package/dist/register-capabilities.test.js +188 -0
  36. package/dist/testing/index.d.ts +22 -0
  37. package/dist/testing/index.js +13 -0
  38. package/dist/types.d.ts +86 -0
  39. package/dist/types.js +1 -0
  40. package/package.json +57 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # @egen-civitas/esm-platform
2
+
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 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é.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [d1d2d7d]
12
+ - Updated dependencies [669cbd6]
13
+ - Updated dependencies [5958a56]
14
+ - @egen-civitas/esm-globals@2.1.0
package/README.md CHANGED
@@ -1,3 +1,56 @@
1
- # Temporary Holding Version
1
+ # @egen-civitas/esm-platform
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ ## État d'avancement
46
+ | Élément | Tâche | État |
47
+ |---|---|---|
48
+ | Création du package et câblage (`esm-framework` index/internal/mocks, shell, typedoc) | C1 | fait |
49
+ | Contrats TypeScript des capacités | C2 | fait |
50
+ | Erreurs typées | C2 | fait |
51
+ | Détection de la cible, cohérence du Host (`hasHostBridge`, `ready`) | C3 | fait |
52
+ | `getPlatform()` sûr par défaut (aucune capacité simulée) | C1 | fait |
53
+ | `createMockPlatform` | C9 | partiel (`createMockHost` à faire avec C5) |
54
+ | Adaptateur Web | C4 | fait |
55
+ | Client de pont / protocole v1 | C5 | à faire |
56
+ | Registre et procédure d'ajout d'une capacité | C6 | à faire |
@@ -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,7 @@
1
+ import type { CapabilityOverrides } from '../../capabilities.js';
2
+ /**
3
+ * Capacités fournies par le navigateur. PRINCIPE (doc C4) : ne jamais simuler de la sécurité. Sont donc ABSENTES
4
+ * (`has()` faux, méthodes `UNAVAILABLE`) : `secureStorage` (pas de faux coffre dans `localStorage`), `push` et
5
+ * `biometric` (v1), ainsi que `permissions` quand l'API Permissions n'existe pas.
6
+ */
7
+ export declare function createWebCapabilities(): CapabilityOverrides;
@@ -0,0 +1,24 @@
1
+ import { createWebCamera } from './camera.js';
2
+ import { createWebFiles } from './files.js';
3
+ import { createWebLifecycle } from './lifecycle.js';
4
+ import { createWebOpenExternal } from './open-external.js';
5
+ import { createWebPermissions, isWebPermissionsAvailable } from './permissions.js';
6
+ import { createWebSession } from './session.js';
7
+ /**
8
+ * Capacités fournies par le navigateur. PRINCIPE (doc C4) : ne jamais simuler de la sécurité. Sont donc ABSENTES
9
+ * (`has()` faux, méthodes `UNAVAILABLE`) : `secureStorage` (pas de faux coffre dans `localStorage`), `push` et
10
+ * `biometric` (v1), ainsi que `permissions` quand l'API Permissions n'existe pas.
11
+ */
12
+ export function createWebCapabilities() {
13
+ const capabilities = {
14
+ session: createWebSession(),
15
+ openExternal: createWebOpenExternal(),
16
+ lifecycle: createWebLifecycle(),
17
+ files: createWebFiles(),
18
+ camera: createWebCamera(),
19
+ };
20
+ if (isWebPermissionsAvailable()) {
21
+ capabilities.permissions = createWebPermissions();
22
+ }
23
+ return capabilities;
24
+ }
@@ -0,0 +1,3 @@
1
+ import type { LifecycleCapability } from '../../types.js';
2
+ /** `lifecycle` en Web : `visibilitychange` → `onResume` (page visible) / `onPause` (page masquée). */
3
+ export declare function createWebLifecycle(): LifecycleCapability;
@@ -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
+ }