@nodefony/framework 10.0.0-beta.5 → 10.0.0-beta.7
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/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorateParam.js +7 -0
- package/dist/index.js +14 -10
- package/dist/nodefony/config/config.js +16 -2
- package/dist/nodefony/controller/AdminApiController.js +18 -5
- package/dist/nodefony/controller/BenchController.js +1 -1
- package/dist/nodefony/controller/CspReportController.js +70 -0
- package/dist/nodefony/controller/IssuerMetadataController.js +3 -3
- package/dist/nodefony/controller/LoginPageController.js +5 -7
- package/dist/nodefony/controller/ProtectedResourceMetadataController.js +75 -10
- package/dist/nodefony/controller/SessionAuthController.js +6 -7
- package/dist/nodefony/controller/TokenAuthController.js +3 -3
- package/dist/nodefony/decorators/routerDecorators.js +103 -15
- package/dist/nodefony/grpc/GrpcController.js +138 -0
- package/dist/nodefony/grpc/GrpcService.js +75 -0
- package/dist/nodefony/grpc/declarations.js +33 -0
- package/dist/nodefony/grpc/index.js +3 -0
- package/dist/nodefony/service/AdminBroker.js +1 -1
- package/dist/nodefony/service/IdempotencyStore.js +1 -1
- package/dist/nodefony/service/router.js +15 -12
- package/dist/nodefony/src/Controller.js +330 -115
- package/dist/nodefony/src/PlaygroundAdminApi.js +1 -1
- package/dist/nodefony/src/Resolver.js +85 -51
- package/dist/nodefony/src/Route.js +45 -45
- package/dist/nodefony/src/SecurityApiController.js +30 -7
- package/dist/nodefony/src/conditionalRequest.js +103 -0
- package/dist/nodefony/src/docsReader.js +1 -1
- package/dist/nodefony/src/idempotency.js +1 -1
- package/dist/nodefony/src/loginPage.js +3 -2
- package/dist/nodefony/src/scopeCatalog.js +1 -1
- package/dist/nodefony/src/viewNonce.js +58 -0
- package/dist/types/index.d.ts +4 -2
- package/dist/types/nodefony/config/config.d.ts +20 -0
- package/dist/types/nodefony/controller/AdminApiController.d.ts +4 -2
- package/dist/types/nodefony/controller/CspReportController.d.ts +35 -0
- package/dist/types/nodefony/controller/LoginPageController.d.ts +3 -2
- package/dist/types/nodefony/controller/ProtectedResourceMetadataController.d.ts +21 -1
- package/dist/types/nodefony/controller/SessionAuthController.d.ts +5 -6
- package/dist/types/nodefony/controller/TokenAuthController.d.ts +3 -3
- package/dist/types/nodefony/decorators/routerDecorators.d.ts +2 -1
- package/dist/types/nodefony/grpc/GrpcController.d.ts +75 -0
- package/dist/types/nodefony/grpc/GrpcService.d.ts +51 -0
- package/dist/types/nodefony/grpc/declarations.d.ts +12 -0
- package/dist/types/nodefony/grpc/index.d.ts +13 -0
- package/dist/types/nodefony/interfaces/IController.d.ts +4 -4
- package/dist/types/nodefony/service/router.d.ts +8 -6
- package/dist/types/nodefony/src/Controller.d.ts +124 -14
- package/dist/types/nodefony/src/Resolver.d.ts +35 -8
- package/dist/types/nodefony/src/Route.d.ts +35 -0
- package/dist/types/nodefony/src/SecurityApiController.d.ts +17 -4
- package/dist/types/nodefony/src/conditionalRequest.d.ts +82 -0
- package/dist/types/nodefony/src/loginPage.d.ts +1 -1
- package/dist/types/nodefony/src/viewNonce.d.ts +33 -0
- package/docs/controller.md +234 -35
- package/docs/decorateurs.md +1 -1
- package/docs/grpc.md +690 -0
- package/docs/idempotence.md +13 -8
- package/docs/index.md +16 -10
- package/docs/routing.md +30 -2
- package/docs/templates.md +15 -0
- package/package.json +28 -11
|
@@ -58,12 +58,12 @@ declare class Resolver implements IResolver {
|
|
|
58
58
|
*/
|
|
59
59
|
queryOverride: Record<string, unknown> | null;
|
|
60
60
|
/**
|
|
61
|
-
* Méthode HTTP **logique** d'une invocation par le pont WS-RPC `api.request
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* sur `context.method`.
|
|
61
|
+
* Méthode HTTP **logique** d'une invocation par le pont WS-RPC `api.request`,
|
|
62
|
+
* GET compris. Posée par `Router.resolve(ctx, cleanPath, methodOverride)` →
|
|
63
|
+
* consommée par `match()` pour désambiguïser, sur le transport WEBSOCKET
|
|
64
|
+
* unique, la route logique visée (règle `bridgeServesVerb` du cœur, cf
|
|
65
|
+
* `Route.matchRequirements`). `null` (requête HTTP, ouverture de socket) =
|
|
66
|
+
* match sur `context.method`.
|
|
67
67
|
*/
|
|
68
68
|
methodOverride: string | null;
|
|
69
69
|
/**
|
|
@@ -256,7 +256,7 @@ declare class Resolver implements IResolver {
|
|
|
256
256
|
* (`returnController`) — le chemin du **pont `api.request`** (WS) : la valeur
|
|
257
257
|
* nue est enveloppée `{id, result}` par le peer, jamais rendue sur le
|
|
258
258
|
* transport. La méthode HTTP logique d'une mutation du pont voyage dans
|
|
259
|
-
* {@link methodOverride} ; sans porte ici, un rejeu `socket.
|
|
259
|
+
* {@link methodOverride} ; sans porte ici, un rejeu `socket.post` (socket
|
|
260
260
|
* qui reconnecte) ré-exécuterait la mutation (doublon — vécu au banc duplex).
|
|
261
261
|
*
|
|
262
262
|
* @returns `{ result }` — la valeur retournée par l'action (ou la réponse
|
|
@@ -363,7 +363,15 @@ declare class Resolver implements IResolver {
|
|
|
363
363
|
/**
|
|
364
364
|
* Rend la valeur d'une action sur le transport, selon sa nature : promesse
|
|
365
365
|
* (démêlée puis re-rendue), texte, Buffer, réponse déjà construite, scalaire
|
|
366
|
-
* ou objet (auto-JSON), rien (l'action envoie elle-même)
|
|
366
|
+
* ou objet (auto-JSON), rien (l'action envoie elle-même) — et, en HTTP, une
|
|
367
|
+
* `Response` Web standard (`fetch()`, `new Response(…)`, `Response.json(…)`)
|
|
368
|
+
* ou un flux d'octets nu (`Readable` Node, `ReadableStream` Web), envoyés EN
|
|
369
|
+
* FLUX par `HttpContext.openWebResponse` / `HttpContext.openStream`.
|
|
370
|
+
*
|
|
371
|
+
* Ces deux-là sont reconnus APRÈS l'objet simple et le tableau : l'auto-JSON,
|
|
372
|
+
* le cas courant, ne paie aucun test de plus. Une réponse déjà partie
|
|
373
|
+
* (`sended`) n'est jamais réécrite ; une `Response` renvoyée en plus voit
|
|
374
|
+
* alors son corps annulé, pour ne pas retenir l'amont.
|
|
367
375
|
*
|
|
368
376
|
* Pas `async` : l'envelopper dans une promesse de plus ne changeait rien au
|
|
369
377
|
* rendu, et coûtait une suspension à chaque requête (#505). La valeur n'est
|
|
@@ -375,6 +383,25 @@ declare class Resolver implements IResolver {
|
|
|
375
383
|
* @throws de façon SYNCHRONE ce que lève un envoi synchrone.
|
|
376
384
|
*/
|
|
377
385
|
returnController(result: unknown): MaybePromise<unknown>;
|
|
386
|
+
/**
|
|
387
|
+
* Aiguille une valeur OBJET rendue par l'action — dans l'ordre de fréquence :
|
|
388
|
+
* le tableau et l'objet simple (auto-JSON) d'abord, les instances rares
|
|
389
|
+
* ensuite. Partie de {@link Resolver.returnController}.
|
|
390
|
+
*
|
|
391
|
+
* @param result - un objet non nul.
|
|
392
|
+
* @returns ce que rend l'envoi, ou `undefined` quand il n'y a rien à envoyer.
|
|
393
|
+
*/
|
|
394
|
+
private returnObject;
|
|
395
|
+
/**
|
|
396
|
+
* Ce qui arrive quand l'action ne rend RIEN d'envoyable (`undefined`,
|
|
397
|
+
* `null`, une fonction…) : en HTTP, la redirection posée ou le statut sans
|
|
398
|
+
* corps part ; sinon la requête est marquée non rendue (`waitAsync`). Partie
|
|
399
|
+
* de {@link Resolver.returnController}.
|
|
400
|
+
*
|
|
401
|
+
* @param result - la valeur rendue par l'action.
|
|
402
|
+
* @returns l'envoi d'une redirection ou d'un statut sans corps, sinon `undefined`.
|
|
403
|
+
*/
|
|
404
|
+
private returnNothing;
|
|
378
405
|
/** Statut courant de la réponse (0 si le transport n'en porte pas). */
|
|
379
406
|
private getResponseStatus;
|
|
380
407
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { HTTPMethod, SchemeType, ContextType } from "@nodefony/http";
|
|
2
|
+
import type { IRouteUploadOptions } from "@nodefony/http";
|
|
2
3
|
import type { IRoute } from "../interfaces/index.js";
|
|
3
4
|
import type { RouteActionMeta } from "../decorators/routerDecorators.js";
|
|
4
5
|
import Controller from "./Controller.js";
|
|
@@ -77,6 +78,26 @@ export interface RouteOptions {
|
|
|
77
78
|
* aucun coût par requête. Défaut `false`.
|
|
78
79
|
*/
|
|
79
80
|
fallback?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Délai de réponse de CETTE route (ms), à la place du `responseTimeout` du
|
|
83
|
+
* serveur — dans les deux sens : un export lent le relève sans relever celui
|
|
84
|
+
* des autres routes, une route qui doit répondre vite l'abaisse.
|
|
85
|
+
*
|
|
86
|
+
* Même nature que celui du serveur : un délai d'INACTIVITÉ. Une action qui
|
|
87
|
+
* envoie au fil de l'eau (flux, SSE) n'est pas coupée ; une action muette au
|
|
88
|
+
* delà reçoit 503 et son `ctx.signal` est annulé. Entier de 0 à 2³¹−1 ;
|
|
89
|
+
* `0` désactive le délai pour la route. Absent = celui du serveur.
|
|
90
|
+
*/
|
|
91
|
+
responseTimeout?: number;
|
|
92
|
+
/**
|
|
93
|
+
* Bornes d'envoi de CETTE route, à la place de celles de la configuration
|
|
94
|
+
* http (`upload.*`) — dans les deux sens, clé par clé : un dépôt de vidéos
|
|
95
|
+
* relève `maxFileSize`, un avatar l'abaisse, et le fichier trop gros reçoit
|
|
96
|
+
* 413 PENDANT son flux, avant d'avoir rempli le disque. `hashAlgorithm` y
|
|
97
|
+
* demande l'empreinte de chaque fichier reçu. Entiers ≥ 1 ; une clé absente
|
|
98
|
+
* garde la valeur de la configuration. Exclu avec `bodyStream: true`.
|
|
99
|
+
*/
|
|
100
|
+
upload?: IRouteUploadOptions;
|
|
80
101
|
}
|
|
81
102
|
export interface RouteRequirements {
|
|
82
103
|
domain?: string | string[] | undefined;
|
|
@@ -135,6 +156,13 @@ declare class Route implements IRoute {
|
|
|
135
156
|
areaRoleExempt: boolean;
|
|
136
157
|
/** Route de repli, résolue après toutes les autres. Cf {@link RouteOptions.fallback}. */
|
|
137
158
|
fallback: boolean;
|
|
159
|
+
/**
|
|
160
|
+
* Délai de réponse PROPRE à la route (ms), `null` = celui du serveur.
|
|
161
|
+
* Cf {@link RouteOptions.responseTimeout}.
|
|
162
|
+
*/
|
|
163
|
+
responseTimeout: number | null;
|
|
164
|
+
/** Bornes d'envoi propres à la route. Cf {@link RouteOptions.upload}. */
|
|
165
|
+
upload: IRouteUploadOptions | null;
|
|
138
166
|
/**
|
|
139
167
|
* P2.9 — Cache mémoïsé : l'action attend-elle le **flux brut** du body
|
|
140
168
|
* (`@Body({ stream:true })`) ? `undefined` = pas encore calculé (résolu au 1er
|
|
@@ -142,6 +170,13 @@ declare class Route implements IRoute {
|
|
|
142
170
|
* ensuite). Lu en amont par `handleHttp` pour sauter le parse.
|
|
143
171
|
*/
|
|
144
172
|
bodyStream?: boolean | undefined;
|
|
173
|
+
/**
|
|
174
|
+
* Route servie par un point d'entrée gRPC — posé au démarrage par
|
|
175
|
+
* `markGrpcRoute` (`@nodefony/http/grpc`), jamais à la main.
|
|
176
|
+
*
|
|
177
|
+
* @experimental Surface gRPC, ADR-0026 D11.
|
|
178
|
+
*/
|
|
179
|
+
grpc?: boolean | undefined;
|
|
145
180
|
/**
|
|
146
181
|
* P5 — Metadata d'action figées (`@HttpCode`/`@Header`/`@Redirect`/params/
|
|
147
182
|
* session), memo au 1er hit comme {@link bodyStream} : `undefined` = pas
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import type { OutgoingHttpHeaders } from "node:http";
|
|
2
2
|
import Controller from "./Controller.js";
|
|
3
|
+
/**
|
|
4
|
+
* En-têtes d'un 401 : ceux reçus, plus le défi par défaut
|
|
5
|
+
* ({@link SESSION_AUTH_CHALLENGE}) s'ils n'en portent aucun — RFC 9110 §15.5.2
|
|
6
|
+
* exige un `WWW-Authenticate` sur TOUT 401. Un défi posé par l'appelant (un
|
|
7
|
+
* `Bearer` de ressource protégée, par exemple) garde la main.
|
|
8
|
+
*
|
|
9
|
+
* Chemin froid : n'est appelé que pour un 401.
|
|
10
|
+
*
|
|
11
|
+
* @param headers - les en-têtes déjà prévus pour la réponse.
|
|
12
|
+
* @returns les mêmes en-têtes, défi compris.
|
|
13
|
+
*/
|
|
14
|
+
export declare function withAuthChallenge(headers: Readonly<OutgoingHttpHeaders> | undefined): OutgoingHttpHeaders;
|
|
3
15
|
/**
|
|
4
16
|
* Base des contrôleurs d'authentification (`/nodefony/security/api/*`) : toute
|
|
5
|
-
* réponse JSON part en `Cache-Control: no-store
|
|
17
|
+
* réponse JSON part en `Cache-Control: no-store`, et tout 401 porte un défi.
|
|
6
18
|
*
|
|
7
19
|
* Ces réponses portent une identité, un jeton, un secret affiché une seule
|
|
8
20
|
* fois (clé API, secret TOTP, codes de récupération) ou leur refus. Un cookie
|
|
9
21
|
* ou un en-tête dans la requête n'empêche pas un cache partagé de STOCKER la
|
|
10
|
-
* réponse (RFC 9111 §3) ; RFC 6749 §5.1 exige `no-store` sur un jeton.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
22
|
+
* réponse (RFC 9111 §3) ; RFC 6749 §5.1 exige `no-store` sur un jeton. Un 401
|
|
23
|
+
* reçoit le défi par défaut ({@link withAuthChallenge}, RFC 9110 §15.5.2). Ces
|
|
24
|
+
* règles vivent ici, une fois : une action ajoutée à un contrôleur héritier ne
|
|
25
|
+
* peut pas les oublier. Un en-tête passé par l'action garde la priorité.
|
|
13
26
|
*/
|
|
14
27
|
declare abstract class SecurityApiController extends Controller {
|
|
15
28
|
renderJson(obj: unknown, status?: string | number, headers?: OutgoingHttpHeaders): ReturnType<Controller["renderJson"]>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Requêtes conditionnelles sur une représentation servie par un contrôleur —
|
|
3
|
+
* validateurs (RFC 9110 §8.8) et évaluation des préconditions (§13).
|
|
4
|
+
*
|
|
5
|
+
* Fonctions PURES : l'instant courant est un paramètre, aucune lecture
|
|
6
|
+
* d'horloge ni d'en-tête n'a lieu ici. Le contrôleur rassemble les entrées,
|
|
7
|
+
* ce module rend le verdict.
|
|
8
|
+
*/
|
|
9
|
+
/** Validateurs d'une représentation (RFC 9110 §8.8). */
|
|
10
|
+
export interface IValidators {
|
|
11
|
+
/** Étiquette d'entité, guillemets compris (`"…"` forte, `W/"…"` faible) ; `null` si aucune. */
|
|
12
|
+
readonly etag: string | null;
|
|
13
|
+
/**
|
|
14
|
+
* Dernière modification (ms depuis l'époque), à la seconde — la précision
|
|
15
|
+
* d'une date HTTP — et jamais postérieure à l'instant de la réponse
|
|
16
|
+
* (§8.8.2.1) ; `null` si aucune.
|
|
17
|
+
*/
|
|
18
|
+
readonly lastModified: number | null;
|
|
19
|
+
/**
|
|
20
|
+
* La date est-elle un validateur FORT (§8.8.2.2) : modifiée au moins une
|
|
21
|
+
* seconde avant la réponse, elle ne peut plus changer dans la même seconde.
|
|
22
|
+
* Seul un validateur fort autorise une reprise par `If-Range` (§13.1.5).
|
|
23
|
+
*/
|
|
24
|
+
readonly lastModifiedStrong: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** Issue des préconditions : servir (200), rien n'a changé (304), condition fausse (412). */
|
|
27
|
+
export type PreconditionOutcome = 200 | 304 | 412;
|
|
28
|
+
/** En-têtes de requête lus par l'évaluation (noms en minuscules, comme Node les rend). */
|
|
29
|
+
export interface IConditionalHeaders {
|
|
30
|
+
readonly "if-match"?: string | undefined;
|
|
31
|
+
readonly "if-none-match"?: string | undefined;
|
|
32
|
+
readonly "if-modified-since"?: string | undefined;
|
|
33
|
+
readonly "if-unmodified-since"?: string | undefined;
|
|
34
|
+
readonly "if-range"?: string | undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Validateurs d'un fichier, tirés de ses métadonnées sans le lire.
|
|
38
|
+
*
|
|
39
|
+
* L'`ETag` est FORTE : taille et date de modification à la microseconde, deux
|
|
40
|
+
* grandeurs qu'une écriture change. C'est la forme que posent les serveurs de
|
|
41
|
+
* fichiers courants, et la seule qui autorise une reprise (§13.1.5).
|
|
42
|
+
*
|
|
43
|
+
* @param stats - taille et date de modification du fichier
|
|
44
|
+
* @param now - instant de la réponse (ms)
|
|
45
|
+
* @returns les validateurs du fichier
|
|
46
|
+
*/
|
|
47
|
+
export declare function fileValidators(stats: {
|
|
48
|
+
readonly size: number;
|
|
49
|
+
readonly mtimeMs: number;
|
|
50
|
+
}, now: number): IValidators;
|
|
51
|
+
/**
|
|
52
|
+
* Date HTTP (RFC 9110 §5.6.7) lue en ms, ou `null` si la valeur n'en est pas
|
|
53
|
+
* une — une condition sur une date invalide DOIT être ignorée (§13.1.3, §13.1.4).
|
|
54
|
+
*
|
|
55
|
+
* @param value - valeur brute de l'en-tête
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseHttpDate(value: string | undefined): number | null;
|
|
58
|
+
/**
|
|
59
|
+
* Évalue les préconditions d'une requête sur une représentation qui EXISTE,
|
|
60
|
+
* dans l'ordre de RFC 9110 §13.2.2 — `If-Match`, sinon `If-Unmodified-Since` ;
|
|
61
|
+
* puis `If-None-Match`, sinon `If-Modified-Since`. `Range` et `If-Range`
|
|
62
|
+
* viennent après, s'il faut servir (voir {@link ifRangeHolds}).
|
|
63
|
+
*
|
|
64
|
+
* @param method - méthode de la requête
|
|
65
|
+
* @param headers - en-têtes de la requête
|
|
66
|
+
* @param validators - validateurs de la représentation
|
|
67
|
+
* @returns 200 pour servir, 304 si rien n'a changé (GET et HEAD seulement),
|
|
68
|
+
* 412 si une condition est fausse
|
|
69
|
+
*/
|
|
70
|
+
export declare function evaluatePreconditions(method: string, headers: IConditionalHeaders, validators: IValidators): PreconditionOutcome;
|
|
71
|
+
/**
|
|
72
|
+
* Une condition `If-Range` tient-elle (RFC 9110 §13.1.5) ? Vraie, la plage
|
|
73
|
+
* demandée est servie (206) ; fausse, `Range` est ignoré et la représentation
|
|
74
|
+
* part entière (200) — un client ne recolle jamais deux versions.
|
|
75
|
+
*
|
|
76
|
+
* Une étiquette tient si elle est FORTEMENT égale ; une date, si elle égale
|
|
77
|
+
* exactement `Last-Modified` ET que celle-ci est un validateur fort.
|
|
78
|
+
*
|
|
79
|
+
* @param value - valeur brute de `If-Range`
|
|
80
|
+
* @param validators - validateurs de la représentation
|
|
81
|
+
*/
|
|
82
|
+
export declare function ifRangeHolds(value: string, validators: IValidators): boolean;
|
|
@@ -141,4 +141,4 @@ export declare function buildLoginPageView(page: ILoginPageDescription, request:
|
|
|
141
141
|
* d'empreinte (`i-finger`) reprend `IconFingerprint` de Tabler Icons (MIT),
|
|
142
142
|
* celle du bouton de Studio : les deux écrans montrent le même geste.
|
|
143
143
|
*/
|
|
144
|
-
export declare const LOGIN_PAGE_TEMPLATE = "<!doctype html>\n<html lang=\"fr\"<% if (it.theme) { %> data-theme=\"<%= it.theme %>\"<% } %>>\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <meta name=\"color-scheme\" content=\"dark light\" />\n <meta name=\"robots\" content=\"noindex\" />\n <title><%= it.title %> \u2014 <%= it.brand %></title>\n <link rel=\"icon\" href=\"<%= it.logoHref %>\" type=\"image/svg+xml\" />\n<% if (!it.theme) { %>\n <script nonce=\"<%= it.nonce %>\">try{var t=localStorage.getItem(\"nf-login-theme\");if(t===\"light\"||t===\"dark\")document.documentElement.dataset.theme=t}catch(e){}</script>\n<% } %>\n <link rel=\"stylesheet\" href=\"<%= it.styleHref %>\" />\n<% if (it.skinHref) { %>\n <link rel=\"stylesheet\" href=\"<%= it.skinHref %>\" />\n<% } %>\n<% if (it.stylesheetHref) { %>\n <link rel=\"stylesheet\" href=\"<%= it.stylesheetHref %>\" />\n<% } %>\n <script type=\"module\" src=\"<%= it.scriptHref %>\" nonce=\"<%= it.nonce %>\"></script>\n </head>\n <body data-skin=\"<%= it.skin %>\" data-layout=\"<%= it.layout %>\"<% if (it.backdrop) { %> data-backdrop<% } %><% if (it.ssoFirst) { %> data-sso-first<% } %>>\n <svg width=\"0\" height=\"0\" style=\"position: absolute\" aria-hidden=\"true\">\n <symbol id=\"i-user\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"8\" r=\"3.5\" /><path d=\"M5.5 20a6.5 6.5 0 0 1 13 0\" /></symbol>\n <symbol id=\"i-lock\" viewBox=\"0 0 24 24\"><rect x=\"5\" y=\"10.5\" width=\"14\" height=\"10\" rx=\"2\" /><path d=\"M8.5 10.5V7.5a3.5 3.5 0 0 1 7 0v3\" /></symbol>\n <symbol id=\"i-eye\" viewBox=\"0 0 24 24\"><path d=\"M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z\" /><circle cx=\"12\" cy=\"12\" r=\"2.8\" /></symbol>\n <symbol id=\"i-arrow\" viewBox=\"0 0 24 24\"><path d=\"M5 12h14M13 6l6 6-6 6\" /></symbol>\n <symbol id=\"i-key\" viewBox=\"0 0 24 24\"><circle cx=\"8\" cy=\"15\" r=\"3.8\" /><path d=\"M10.8 12.2 19.5 3.5M16.5 6.5l2.5 2.5M14 9l2 2\" /></symbol>\n <symbol id=\"i-check\" viewBox=\"0 0 24 24\"><path d=\"M5 12.5l4.5 4.5L19 7.5\" /></symbol>\n <symbol id=\"i-finger\" viewBox=\"0 0 24 24\"><path d=\"M18.9 7a8 8 0 0 1 1.1 5v1a6 6 0 0 0 .8 3\" /><path d=\"M8 11a4 4 0 0 1 8 0v1a10 10 0 0 0 2 6\" /><path d=\"M12 11v2a14 14 0 0 0 2.5 8\" /><path d=\"M8 15a18 18 0 0 0 1.8 6\" /><path d=\"M4.9 19a22 22 0 0 1-.9-7v-1a8 8 0 0 1 12-6.95\" /></symbol>\n <symbol id=\"i-sun\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"4\" /><path d=\"M12 2.5v2M12 19.5v2M4.6 4.6l1.4 1.4M18 18l1.4 1.4M2.5 12h2M19.5 12h2M4.6 19.4 6 18M18 6l1.4-1.4\" /></symbol>\n <symbol id=\"i-moon\" viewBox=\"0 0 24 24\"><path d=\"M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5Z\" /></symbol>\n <symbol id=\"i-shield\" viewBox=\"0 0 24 24\"><path d=\"M12 3l7.5 3v5.5c0 4.6-3.2 8-7.5 9.5-4.3-1.5-7.5-4.9-7.5-9.5V6z\" /></symbol>\n <symbol id=\"m-github\" viewBox=\"0 0 16 16\"><path d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z\" /></symbol>\n </svg>\n <header class=\"nf-strip\">\n <div class=\"nf-strip-brand\"><img class=\"nf-logo\" src=\"<%= it.logo %>\" alt=\"\" /><%= it.brand %></div>\n </header>\n <div class=\"nf-layout\">\n <aside class=\"nf-hero\" aria-hidden=\"true\">\n <div class=\"nf-hero-brand\"><img class=\"nf-logo\" src=\"<%= it.logo %>\" alt=\"\" /><%= it.brand %></div>\n<% if (it.hero === null) { %>\n <div class=\"nf-home\">\n <img\n class=\"nf-home-logo\"\n src=\"<%= it.logoHref %>\"\n alt=\"\"\n />\n <h2>Le framework Node.js fullstack</h2>\n <p class=\"sub\">\n Temps r\u00E9el natif \u00B7 d\u00E9veloppement agentic-ready \u00B7 socle TypeScript\n isomorphe\n </p>\n <p class=\"sig\">\n Une action de contr\u00F4leur. Deux transports. La m\u00EAme session, la m\u00EAme\n s\u00E9curit\u00E9, le m\u00EAme code.\n </p>\n <ul class=\"badges\">\n <li class=\"agentic\">Agentic ready</li>\n <li>Node.js \u2265 24.14</li>\n <li>TypeScript strict</li>\n <li>ESM only</li>\n <li>Licence Apache 2.0</li>\n </ul>\n <div\n class=\"install\"\n role=\"img\"\n aria-label=\"Commande d'installation : npx nodefony create app\"\n >\n <span class=\"p\">$</span><span>npx nodefony create app</span>\n </div>\n <div class=\"features\">\n <div class=\"feature\">\n <span class=\"ic\"\n ><svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <polygon\n points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"\n /></svg\n ></span>\n <h3>Temps r\u00E9el natif</h3>\n <p>\n Le WebSocket n'est pas un ajout : m\u00EAme pipeline, m\u00EAme table de\n routes, m\u00EAme s\u00E9curit\u00E9 que le HTTP.\n </p>\n </div>\n <div class=\"feature\">\n <span class=\"ic\"\n ><svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\" />\n <path d=\"m9 12 2 2 4-4\" /></svg\n ></span>\n <h3>S\u00E9curit\u00E9 par conception</h3>\n <p>\n Pare-feu applicatif par zones, Zero Trust, CSRF Fetch Metadata,\n sessions hybrides, WebAuthn.\n </p>\n </div>\n <div class=\"feature\">\n <span class=\"ic\"\n ><svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <rect x=\"3\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\" />\n <rect x=\"14\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\" />\n <rect x=\"3\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\" />\n <rect x=\"14\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\" /></svg\n ></span>\n <h3>DI & modules</h3>\n <p>\n Injection de d\u00E9pendances par d\u00E9corateurs, modules autonomes,\n configuration valid\u00E9e au boot.\n </p>\n </div>\n <div class=\"feature\">\n <span class=\"ic\"\n ><svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <path\n d=\"M3 12h18M12 3c2.5 2.6 4 5.7 4 9s-1.5 6.4-4 9c-2.5-2.6-4-5.7-4-9s1.5-6.4 4-9z\"\n /></svg\n ></span>\n <h3>Socle isomorphe</h3>\n <p>\n Le m\u00EAme paquet c\u00F4t\u00E9 serveur et c\u00F4t\u00E9 navigateur : client temps\n r\u00E9el, r\u00E8gles et types \u00E9crits une fois.\n </p>\n </div>\n </div>\n </div>\n <small>Propuls\u00E9 par Nodefony</small>\n<% } else if (it.hero) { %>\n <div class=\"nf-home\">\n <h2><%= it.hero.heading %></h2>\n<% if (it.hero.text) { %>\n <p class=\"sub\"><%= it.hero.text %></p>\n<% } %>\n </div>\n<% } %>\n </aside>\n <main class=\"nf-stage\" data-nf-login data-from=\"<%= it.from %>\">\n <button type=\"button\" class=\"nf-theme\" data-theme-toggle aria-label=\"Basculer entre th\u00E8me clair et th\u00E8me sombre\"><svg class=\"nf-icon t-sun\" aria-hidden=\"true\"><use href=\"#i-sun\" /></svg><svg class=\"nf-icon t-moon\" aria-hidden=\"true\"><use href=\"#i-moon\" /></svg></button>\n <section class=\"nf-card\" aria-labelledby=\"nf-title\">\n <div class=\"nf-head\">\n <div class=\"nf-brand\"><img class=\"nf-logo\" src=\"<%= it.logo %>\" alt=\"\" /><%= it.brand %></div>\n <h1 id=\"nf-title\"><%= it.title %></h1>\n <p class=\"nf-sub\" id=\"nf-sub\"><%= it.lead %></p>\n </div>\n <div class=\"nf-body\">\n<% if (it.password) { %>\n <ol class=\"nf-steps\" aria-label=\"\u00C9tapes\">\n <li data-tab=\"identifier\" aria-current=\"step\">Identifiant</li>\n <li data-tab=\"password\">Mot de passe</li>\n <li data-tab=\"mfa\" hidden>V\u00E9rification</li>\n </ol>\n <form data-step=\"identifier\">\n <div class=\"nf-field\">\n <label for=\"nf-username\">Identifiant ou adresse e-mail</label>\n <div class=\"nf-input\">\n <svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-user\" /></svg>\n <input id=\"nf-username\" name=\"username\" autocomplete=\"username\" autocapitalize=\"none\" spellcheck=\"false\" required />\n </div>\n </div>\n <button class=\"nf-btn primary\" type=\"submit\">Continuer<svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-arrow\" /></svg></button>\n </form>\n <form data-step=\"password\" hidden>\n <!-- Compte de l'\u00E9tape pr\u00E9c\u00E9dente, masqu\u00E9 : le gestionnaire de mots de passe\n associe le mot de passe \u00E0 CE compte (HTML, jeton \u00AB username \u00BB). -->\n <input name=\"username\" autocomplete=\"username\" hidden readonly tabindex=\"-1\" />\n <div class=\"nf-account\">\n <span class=\"nf-avatar\" aria-hidden=\"true\"></span><span class=\"who\"></span>\n <button type=\"button\" class=\"nf-link\" data-go=\"identifier\">Changer</button>\n </div>\n <div class=\"nf-field\">\n <div class=\"nf-label-row\"><label for=\"nf-password\">Mot de passe</label></div>\n <div class=\"nf-input\">\n <svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-lock\" /></svg>\n <input id=\"nf-password\" name=\"password\" type=\"password\" autocomplete=\"current-password\" required />\n <button type=\"button\" class=\"nf-reveal\" data-reveal aria-label=\"Afficher le mot de passe\" aria-pressed=\"false\"><svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-eye\" /></svg></button>\n </div>\n </div>\n <button class=\"nf-btn primary\" type=\"submit\">Se connecter<svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-arrow\" /></svg></button>\n </form>\n <form data-step=\"mfa\" hidden>\n <div class=\"nf-field\">\n <label id=\"nf-otp-label\">Code \u00E0 6 chiffres</label>\n <div class=\"nf-otp\" role=\"group\" aria-labelledby=\"nf-otp-label\">\n <input inputmode=\"numeric\" maxlength=\"1\" autocomplete=\"one-time-code\" aria-label=\"Chiffre 1\" /><input inputmode=\"numeric\" maxlength=\"1\" aria-label=\"Chiffre 2\" /><input inputmode=\"numeric\" maxlength=\"1\" aria-label=\"Chiffre 3\" /><input inputmode=\"numeric\" maxlength=\"1\" aria-label=\"Chiffre 4\" /><input inputmode=\"numeric\" maxlength=\"1\" aria-label=\"Chiffre 5\" /><input inputmode=\"numeric\" maxlength=\"1\" aria-label=\"Chiffre 6\" />\n </div>\n </div>\n <p class=\"nf-hint\">Le code affich\u00E9 par votre application d'authentification.</p>\n <button class=\"nf-btn primary\" type=\"submit\">V\u00E9rifier</button>\n </form>\n<% } %>\n<% if (it.password || it.passkey) { %>\n <div data-step=\"authenticated\" hidden>\n <div class=\"nf-done\" role=\"status\">\n <span class=\"ring\" aria-hidden=\"true\"><svg class=\"nf-icon\"><use href=\"#i-check\" /></svg></span>\n <strong>Session ouverte</strong><span class=\"dest\"></span>\n <span class=\"nf-progress\" aria-hidden=\"true\"><span></span></span>\n </div>\n </div>\n<% } %>\n<% if (it.providers.length > 0 || it.passkey) { %>\n <div data-alt>\n<% if (it.password) { %>\n <div class=\"nf-or\">ou</div>\n<% } %>\n <div class=\"nf-alt\">\n<% if (it.passkey) { %>\n <button type=\"button\" class=\"nf-btn span\" data-passkey hidden><svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-finger\" /></svg>Passkey / empreinte digitale</button>\n<% } %>\n<% it.providers.forEach(function (provider) { %>\n <a class=\"nf-btn\" href=\"<%= provider.href %>\"><% if (provider.image) { %><img class=\"nf-icon\" src=\"<%= provider.image %>\" alt=\"\" /><% } else { %><svg class=\"<%= provider.icon === 'm-github' ? 'nf-mark' : 'nf-icon' %>\" aria-hidden=\"true\"><use href=\"#<%= provider.icon %>\" /></svg><% } %><% if (provider.caption) { %><span class=\"nf-label\"><%= provider.label %><small><%= provider.caption %></small></span><% } else { %><%= provider.label %><% } %></a>\n<% }) %>\n </div>\n </div>\n<% } %>\n <div class=\"nf-message\" aria-live=\"polite\" data-message><% if (it.empty) { %><p class=\"nf-alert\" role=\"alert\"><span class=\"code\">\u2014</span><span>Aucun moyen de connexion n'est disponible pour le moment. Pr\u00E9venez l'administrateur du site.</span></p><% } else if (it.notice) { %><p class=\"nf-alert\" role=\"alert\"><span class=\"code\">SSO</span><span><%= it.notice %></span></p><% } %></div>\n<% if (it.password || it.passkey) { %>\n <noscript><p class=\"nf-hint\">Cette page a besoin de JavaScript pour vous connecter.</p></noscript>\n<% } %>\n </div>\n </section>\n<% if (it.picker) { %>\n <form class=\"nf-picker\" method=\"get\" data-preview-picker<% if (it.picker.hook) { %> data-preview-hook=\"<%= it.picker.hook %>\"<% } %> aria-label=\"Aper\u00E7u de la page (d\u00E9veloppement)\">\n <input type=\"hidden\" name=\"from\" value=\"<%= it.from %>\" />\n <label>Habillage <select name=\"skin\"><% it.picker.skins.forEach(function (skin) { %><option value=\"<%= skin %>\"<% if (skin === it.skin) { %> selected<% } %>><%= skin %></option><% }) %></select></label>\n <label>Mise en page <select name=\"layout\"><% it.picker.layouts.forEach(function (layout) { %><option value=\"<%= layout %>\"<% if (layout === it.layout) { %> selected<% } %>><%= layout %></option><% }) %></select></label>\n <noscript><button type=\"submit\">Appliquer</button></noscript>\n </form>\n<% } %>\n<% if (it.footer) { %>\n <div class=\"nf-foot\">\n <ul class=\"nf-facts\" aria-label=\"Protection de la session\">\n <li><svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-shield\" /></svg>cookie HttpOnly</li>\n <li><svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-shield\" /></svg>anti-CSRF</li>\n <li><svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-shield\" /></svg>CSP stricte</li>\n </ul>\n <a class=\"nf-powered\" href=\"https://github.com/nodefony/nodefony-core\" rel=\"noopener noreferrer\"><img class=\"nf-logo nf-logo-mono\" src=\"<%= it.logoHref %>\" alt=\"\" />Propuls\u00E9 par Nodefony</a>\n </div>\n<% } %>\n </main>\n </div>\n </body>\n</html>\n";
|
|
144
|
+
export declare const LOGIN_PAGE_TEMPLATE = "<!doctype html>\n<html lang=\"fr\"<% if (it.theme) { %> data-theme=\"<%= it.theme %>\"<% } %>>\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <meta name=\"color-scheme\" content=\"dark light\" />\n <meta name=\"robots\" content=\"noindex\" />\n <% if (it.nonce) { %><meta property=\"csp-nonce\" nonce=\"<%= it.nonce %>\"><% } %>\n <title><%= it.title %> \u2014 <%= it.brand %></title>\n <link rel=\"icon\" href=\"<%= it.logoHref %>\" type=\"image/svg+xml\" />\n<% if (!it.theme) { %>\n <script nonce=\"<%= it.nonce %>\">try{var t=localStorage.getItem(\"nf-login-theme\");if(t===\"light\"||t===\"dark\")document.documentElement.dataset.theme=t}catch(e){}</script>\n<% } %>\n <link rel=\"stylesheet\" href=\"<%= it.styleHref %>\" />\n<% if (it.skinHref) { %>\n <link rel=\"stylesheet\" href=\"<%= it.skinHref %>\" />\n<% } %>\n<% if (it.stylesheetHref) { %>\n <link rel=\"stylesheet\" href=\"<%= it.stylesheetHref %>\" />\n<% } %>\n <script type=\"module\" src=\"<%= it.scriptHref %>\" nonce=\"<%= it.nonce %>\"></script>\n </head>\n <body data-skin=\"<%= it.skin %>\" data-layout=\"<%= it.layout %>\"<% if (it.backdrop) { %> data-backdrop<% } %><% if (it.ssoFirst) { %> data-sso-first<% } %>>\n <svg width=\"0\" height=\"0\" style=\"position: absolute\" aria-hidden=\"true\">\n <symbol id=\"i-user\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"8\" r=\"3.5\" /><path d=\"M5.5 20a6.5 6.5 0 0 1 13 0\" /></symbol>\n <symbol id=\"i-lock\" viewBox=\"0 0 24 24\"><rect x=\"5\" y=\"10.5\" width=\"14\" height=\"10\" rx=\"2\" /><path d=\"M8.5 10.5V7.5a3.5 3.5 0 0 1 7 0v3\" /></symbol>\n <symbol id=\"i-eye\" viewBox=\"0 0 24 24\"><path d=\"M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z\" /><circle cx=\"12\" cy=\"12\" r=\"2.8\" /></symbol>\n <symbol id=\"i-arrow\" viewBox=\"0 0 24 24\"><path d=\"M5 12h14M13 6l6 6-6 6\" /></symbol>\n <symbol id=\"i-key\" viewBox=\"0 0 24 24\"><circle cx=\"8\" cy=\"15\" r=\"3.8\" /><path d=\"M10.8 12.2 19.5 3.5M16.5 6.5l2.5 2.5M14 9l2 2\" /></symbol>\n <symbol id=\"i-check\" viewBox=\"0 0 24 24\"><path d=\"M5 12.5l4.5 4.5L19 7.5\" /></symbol>\n <symbol id=\"i-finger\" viewBox=\"0 0 24 24\"><path d=\"M18.9 7a8 8 0 0 1 1.1 5v1a6 6 0 0 0 .8 3\" /><path d=\"M8 11a4 4 0 0 1 8 0v1a10 10 0 0 0 2 6\" /><path d=\"M12 11v2a14 14 0 0 0 2.5 8\" /><path d=\"M8 15a18 18 0 0 0 1.8 6\" /><path d=\"M4.9 19a22 22 0 0 1-.9-7v-1a8 8 0 0 1 12-6.95\" /></symbol>\n <symbol id=\"i-sun\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"4\" /><path d=\"M12 2.5v2M12 19.5v2M4.6 4.6l1.4 1.4M18 18l1.4 1.4M2.5 12h2M19.5 12h2M4.6 19.4 6 18M18 6l1.4-1.4\" /></symbol>\n <symbol id=\"i-moon\" viewBox=\"0 0 24 24\"><path d=\"M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5Z\" /></symbol>\n <symbol id=\"i-shield\" viewBox=\"0 0 24 24\"><path d=\"M12 3l7.5 3v5.5c0 4.6-3.2 8-7.5 9.5-4.3-1.5-7.5-4.9-7.5-9.5V6z\" /></symbol>\n <symbol id=\"m-github\" viewBox=\"0 0 16 16\"><path d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z\" /></symbol>\n </svg>\n <header class=\"nf-strip\">\n <div class=\"nf-strip-brand\"><img class=\"nf-logo\" src=\"<%= it.logo %>\" alt=\"\" /><%= it.brand %></div>\n </header>\n <div class=\"nf-layout\">\n <aside class=\"nf-hero\" aria-hidden=\"true\">\n <div class=\"nf-hero-brand\"><img class=\"nf-logo\" src=\"<%= it.logo %>\" alt=\"\" /><%= it.brand %></div>\n<% if (it.hero === null) { %>\n <div class=\"nf-home\">\n <img\n class=\"nf-home-logo\"\n src=\"<%= it.logoHref %>\"\n alt=\"\"\n />\n <h2>Le framework Node.js fullstack</h2>\n <p class=\"sub\">\n Temps r\u00E9el natif \u00B7 d\u00E9veloppement agentic-ready \u00B7 socle TypeScript\n isomorphe\n </p>\n <p class=\"sig\">\n Une action de contr\u00F4leur. Deux transports. La m\u00EAme session, la m\u00EAme\n s\u00E9curit\u00E9, le m\u00EAme code.\n </p>\n <ul class=\"badges\">\n <li class=\"agentic\">Agentic ready</li>\n <li>Node.js 24.16+ ou 26+</li>\n <li>TypeScript strict</li>\n <li>ESM only</li>\n <li>Licence Apache 2.0</li>\n </ul>\n <div\n class=\"install\"\n role=\"img\"\n aria-label=\"Commande d'installation : npx nodefony create app\"\n >\n <span class=\"p\">$</span><span>npx nodefony create app</span>\n </div>\n <div class=\"features\">\n <div class=\"feature\">\n <span class=\"ic\"\n ><svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <polygon\n points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"\n /></svg\n ></span>\n <h3>Temps r\u00E9el natif</h3>\n <p>\n Le WebSocket n'est pas un ajout : m\u00EAme pipeline, m\u00EAme table de\n routes, m\u00EAme s\u00E9curit\u00E9 que le HTTP.\n </p>\n </div>\n <div class=\"feature\">\n <span class=\"ic\"\n ><svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\" />\n <path d=\"m9 12 2 2 4-4\" /></svg\n ></span>\n <h3>S\u00E9curit\u00E9 par conception</h3>\n <p>\n Pare-feu applicatif par zones, Zero Trust, CSRF Fetch Metadata,\n sessions hybrides, WebAuthn.\n </p>\n </div>\n <div class=\"feature\">\n <span class=\"ic\"\n ><svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <rect x=\"3\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\" />\n <rect x=\"14\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\" />\n <rect x=\"3\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\" />\n <rect x=\"14\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\" /></svg\n ></span>\n <h3>DI & modules</h3>\n <p>\n Injection de d\u00E9pendances par d\u00E9corateurs, modules autonomes,\n configuration valid\u00E9e au boot.\n </p>\n </div>\n <div class=\"feature\">\n <span class=\"ic\"\n ><svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <path\n d=\"M3 12h18M12 3c2.5 2.6 4 5.7 4 9s-1.5 6.4-4 9c-2.5-2.6-4-5.7-4-9s1.5-6.4 4-9z\"\n /></svg\n ></span>\n <h3>Socle isomorphe</h3>\n <p>\n Le m\u00EAme paquet c\u00F4t\u00E9 serveur et c\u00F4t\u00E9 navigateur : client temps\n r\u00E9el, r\u00E8gles et types \u00E9crits une fois.\n </p>\n </div>\n </div>\n </div>\n <small>Propuls\u00E9 par Nodefony</small>\n<% } else if (it.hero) { %>\n <div class=\"nf-home\">\n <h2><%= it.hero.heading %></h2>\n<% if (it.hero.text) { %>\n <p class=\"sub\"><%= it.hero.text %></p>\n<% } %>\n </div>\n<% } %>\n </aside>\n <main class=\"nf-stage\" data-nf-login data-from=\"<%= it.from %>\">\n <button type=\"button\" class=\"nf-theme\" data-theme-toggle aria-label=\"Basculer entre th\u00E8me clair et th\u00E8me sombre\"><svg class=\"nf-icon t-sun\" aria-hidden=\"true\"><use href=\"#i-sun\" /></svg><svg class=\"nf-icon t-moon\" aria-hidden=\"true\"><use href=\"#i-moon\" /></svg></button>\n <section class=\"nf-card\" aria-labelledby=\"nf-title\">\n <div class=\"nf-head\">\n <div class=\"nf-brand\"><img class=\"nf-logo\" src=\"<%= it.logo %>\" alt=\"\" /><%= it.brand %></div>\n <h1 id=\"nf-title\"><%= it.title %></h1>\n <p class=\"nf-sub\" id=\"nf-sub\"><%= it.lead %></p>\n </div>\n <div class=\"nf-body\">\n<% if (it.password) { %>\n <ol class=\"nf-steps\" aria-label=\"\u00C9tapes\">\n <li data-tab=\"identifier\" aria-current=\"step\">Identifiant</li>\n <li data-tab=\"password\">Mot de passe</li>\n <li data-tab=\"mfa\" hidden>V\u00E9rification</li>\n </ol>\n <form data-step=\"identifier\">\n <div class=\"nf-field\">\n <label for=\"nf-username\">Identifiant ou adresse e-mail</label>\n <div class=\"nf-input\">\n <svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-user\" /></svg>\n <input id=\"nf-username\" name=\"username\" autocomplete=\"username\" autocapitalize=\"none\" spellcheck=\"false\" required />\n </div>\n </div>\n <button class=\"nf-btn primary\" type=\"submit\">Continuer<svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-arrow\" /></svg></button>\n </form>\n <form data-step=\"password\" hidden>\n <!-- Compte de l'\u00E9tape pr\u00E9c\u00E9dente, masqu\u00E9 : le gestionnaire de mots de passe\n associe le mot de passe \u00E0 CE compte (HTML, jeton \u00AB username \u00BB). -->\n <input name=\"username\" autocomplete=\"username\" hidden readonly tabindex=\"-1\" />\n <div class=\"nf-account\">\n <span class=\"nf-avatar\" aria-hidden=\"true\"></span><span class=\"who\"></span>\n <button type=\"button\" class=\"nf-link\" data-go=\"identifier\">Changer</button>\n </div>\n <div class=\"nf-field\">\n <div class=\"nf-label-row\"><label for=\"nf-password\">Mot de passe</label></div>\n <div class=\"nf-input\">\n <svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-lock\" /></svg>\n <input id=\"nf-password\" name=\"password\" type=\"password\" autocomplete=\"current-password\" required />\n <button type=\"button\" class=\"nf-reveal\" data-reveal aria-label=\"Afficher le mot de passe\" aria-pressed=\"false\"><svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-eye\" /></svg></button>\n </div>\n </div>\n <button class=\"nf-btn primary\" type=\"submit\">Se connecter<svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-arrow\" /></svg></button>\n </form>\n <form data-step=\"mfa\" hidden>\n <div class=\"nf-field\">\n <label id=\"nf-otp-label\">Code \u00E0 6 chiffres</label>\n <div class=\"nf-otp\" role=\"group\" aria-labelledby=\"nf-otp-label\">\n <input inputmode=\"numeric\" maxlength=\"1\" autocomplete=\"one-time-code\" aria-label=\"Chiffre 1\" /><input inputmode=\"numeric\" maxlength=\"1\" aria-label=\"Chiffre 2\" /><input inputmode=\"numeric\" maxlength=\"1\" aria-label=\"Chiffre 3\" /><input inputmode=\"numeric\" maxlength=\"1\" aria-label=\"Chiffre 4\" /><input inputmode=\"numeric\" maxlength=\"1\" aria-label=\"Chiffre 5\" /><input inputmode=\"numeric\" maxlength=\"1\" aria-label=\"Chiffre 6\" />\n </div>\n </div>\n <p class=\"nf-hint\">Le code affich\u00E9 par votre application d'authentification.</p>\n <button class=\"nf-btn primary\" type=\"submit\">V\u00E9rifier</button>\n </form>\n<% } %>\n<% if (it.password || it.passkey) { %>\n <div data-step=\"authenticated\" hidden>\n <div class=\"nf-done\" role=\"status\">\n <span class=\"ring\" aria-hidden=\"true\"><svg class=\"nf-icon\"><use href=\"#i-check\" /></svg></span>\n <strong>Session ouverte</strong><span class=\"dest\"></span>\n <span class=\"nf-progress\" aria-hidden=\"true\"><span></span></span>\n </div>\n </div>\n<% } %>\n<% if (it.providers.length > 0 || it.passkey) { %>\n <div data-alt>\n<% if (it.password) { %>\n <div class=\"nf-or\">ou</div>\n<% } %>\n <div class=\"nf-alt\">\n<% if (it.passkey) { %>\n <button type=\"button\" class=\"nf-btn span\" data-passkey hidden><svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-finger\" /></svg>Passkey / empreinte digitale</button>\n<% } %>\n<% it.providers.forEach(function (provider) { %>\n <a class=\"nf-btn\" href=\"<%= provider.href %>\"><% if (provider.image) { %><img class=\"nf-icon\" src=\"<%= provider.image %>\" alt=\"\" /><% } else { %><svg class=\"<%= provider.icon === 'm-github' ? 'nf-mark' : 'nf-icon' %>\" aria-hidden=\"true\"><use href=\"#<%= provider.icon %>\" /></svg><% } %><% if (provider.caption) { %><span class=\"nf-label\"><%= provider.label %><small><%= provider.caption %></small></span><% } else { %><%= provider.label %><% } %></a>\n<% }) %>\n </div>\n </div>\n<% } %>\n <div class=\"nf-message\" aria-live=\"polite\" data-message><% if (it.empty) { %><p class=\"nf-alert\" role=\"alert\"><span class=\"code\">\u2014</span><span>Aucun moyen de connexion n'est disponible pour le moment. Pr\u00E9venez l'administrateur du site.</span></p><% } else if (it.notice) { %><p class=\"nf-alert\" role=\"alert\"><span class=\"code\">SSO</span><span><%= it.notice %></span></p><% } %></div>\n<% if (it.password || it.passkey) { %>\n <noscript><p class=\"nf-hint\">Cette page a besoin de JavaScript pour vous connecter.</p></noscript>\n<% } %>\n </div>\n </section>\n<% if (it.picker) { %>\n <form class=\"nf-picker\" method=\"get\" data-preview-picker<% if (it.picker.hook) { %> data-preview-hook=\"<%= it.picker.hook %>\"<% } %> aria-label=\"Aper\u00E7u de la page (d\u00E9veloppement)\">\n <input type=\"hidden\" name=\"from\" value=\"<%= it.from %>\" />\n <label>Habillage <select name=\"skin\"><% it.picker.skins.forEach(function (skin) { %><option value=\"<%= skin %>\"<% if (skin === it.skin) { %> selected<% } %>><%= skin %></option><% }) %></select></label>\n <label>Mise en page <select name=\"layout\"><% it.picker.layouts.forEach(function (layout) { %><option value=\"<%= layout %>\"<% if (layout === it.layout) { %> selected<% } %>><%= layout %></option><% }) %></select></label>\n <noscript><button type=\"submit\">Appliquer</button></noscript>\n </form>\n<% } %>\n<% if (it.footer) { %>\n <div class=\"nf-foot\">\n <ul class=\"nf-facts\" aria-label=\"Protection de la session\">\n <li><svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-shield\" /></svg>cookie HttpOnly</li>\n <li><svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-shield\" /></svg>anti-CSRF</li>\n <li><svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-shield\" /></svg>CSP stricte</li>\n </ul>\n <a class=\"nf-powered\" href=\"https://github.com/nodefony/nodefony-core\" rel=\"noopener noreferrer\"><img class=\"nf-logo nf-logo-mono\" src=\"<%= it.logoHref %>\" alt=\"\" />Propuls\u00E9 par Nodefony</a>\n </div>\n<% } %>\n </main>\n </div>\n </body>\n</html>\n";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signature CSP des vues : chaque `<script>` et `<style>` ÉCRIT dans un
|
|
3
|
+
* gabarit reçoit le nonce de la requête, sans geste de l'auteur.
|
|
4
|
+
*
|
|
5
|
+
* La politique par défaut exige un nonce sur les scripts (`'strict-dynamic'`)
|
|
6
|
+
* et sur les styles (ADR-0024 §C1, §C2). Une page écrite vite, par quelqu'un
|
|
7
|
+
* qui ignore tout de la CSP, ne doit pas être bloquée (§C0) : la signature se
|
|
8
|
+
* pose donc au rendu, par le framework.
|
|
9
|
+
*
|
|
10
|
+
* Elle porte sur le TEXTE DU GABARIT, avant que les données y soient insérées :
|
|
11
|
+
* une balise que les données apporteraient (une valeur insérée brute par
|
|
12
|
+
* `<%~ %>`) n'est jamais signée — c'est ce qui garde au nonce sa valeur contre
|
|
13
|
+
* l'injection. Même principe que l'`index.html` d'un front bâti
|
|
14
|
+
* (`prebuiltUi.ts#splitAroundNonce`).
|
|
15
|
+
*/
|
|
16
|
+
/** Variable de vue qui porte le nonce CSP de la requête (`<%= cspNonce %>`). */
|
|
17
|
+
export declare const VIEW_NONCE_LOCAL = "cspNonce";
|
|
18
|
+
/**
|
|
19
|
+
* Ajoute le nonce de la requête à chaque `<script>` et `<style>` littéral du
|
|
20
|
+
* gabarit qui n'en porte pas.
|
|
21
|
+
*
|
|
22
|
+
* Une balise déjà signée par l'auteur est laissée telle quelle ; un `<script`
|
|
23
|
+
* écrit À L'INTÉRIEUR d'une balise Eta (une chaîne de code) n'est jamais
|
|
24
|
+
* touché. Un `<script>` dont un attribut vient des données
|
|
25
|
+
* (`src="<%= it.url %>"`) n'est pas signé non plus : le signer exécuterait
|
|
26
|
+
* l'URL qu'elles apportent. L'auteur qui la tient pour sûre signe lui-même
|
|
27
|
+
* (`nonce="<%= it.cspNonce %>"`). Le rendu doit fournir la variable
|
|
28
|
+
* {@link VIEW_NONCE_LOCAL}.
|
|
29
|
+
*
|
|
30
|
+
* @param source - texte du gabarit Eta, tel que lu sur le disque.
|
|
31
|
+
* @returns le gabarit signé — la MÊME chaîne quand il n'y a rien à signer.
|
|
32
|
+
*/
|
|
33
|
+
export declare function signTemplateTags(source: string): string;
|