@pulse-si/next 0.0.0-stage → 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Charles Désiré Bally
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,114 @@
1
- # Temporary Holding Version
1
+ # @pulse-si/next
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
+ Ton app Next.js surveillée par [Pulse](https://pulse-app.si), sur ton iPhone et ton Mac : signal de vie, base de données, Redis, files, routes de cron et journaux du serveur, depuis `instrumentation.ts`.
4
+
5
+ ```
6
+ ton serveur Next.js ton iPhone, ton Mac
7
+ ┌────────────────────────────┐ HTTPS ┌──────────┐ ┌──────────────┐
8
+ │ instrumentation.ts │ ────────────────▶ │ Pulse │ ────▶ │ app Pulse │
9
+ │ └ startPulse({ watch }) │ état, crons, │ (API) │ push │ alertes, │
10
+ │ ├ Prisma SELECT 1 │ journaux │ │ ◀──── │ journaux, │
11
+ │ ├ Redis PING │ (chiffrés si tu │ │ SSE │ en direct │
12
+ │ └ crons trackCron │ le veux) └──────────┘ └──────────────┘
13
+ └────────────────────────────┘
14
+ ```
15
+
16
+ ## En deux minutes
17
+
18
+ À la racine du projet, avec la clé que l'app Pulse t'affiche :
19
+
20
+ ```bash
21
+ npx @pulse-si/init pk_live_…
22
+ ```
23
+
24
+ La commande installe `@pulse-si/next`, écrit la clé dans `.env.local` (lisible par toi seul, couvert par le `.gitignore`), crée `instrumentation.ts` au bon endroit (`src/` si ton projet l'utilise), puis te demande ce que tu veux envoyer. Relance ton serveur.
25
+
26
+ ### Ou à la main
27
+
28
+ ```ts
29
+ // instrumentation.ts (ou src/instrumentation.ts)
30
+ export async function register() {
31
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
32
+ const { startPulse } = await import('@pulse-si/next');
33
+ const { prisma } = await import('./lib/prisma');
34
+ startPulse({ watch: [prisma] });
35
+ }
36
+ }
37
+ ```
38
+
39
+ `watch` reçoit tes clients : Prisma, TypeORM, Mongoose, ioredis, node-redis, files BullMQ. Pulse les reconnaît à leur forme et les vérifie en lecture seule (`SELECT 1`, `PING`), 5 secondes au plus. Next.js 14 demande en plus `experimental.instrumentationHook: true` dans `next.config`.
40
+
41
+ ## Les routes de cron
42
+
43
+ Une route appelée par Vercel Cron, un planificateur externe ou un `curl` sur le serveur remonte ses échecs :
44
+
45
+ ```ts
46
+ import { trackCron } from '@pulse-si/next';
47
+
48
+ export async function GET() {
49
+ await trackCron('relance-abonnements', () => relancer());
50
+ return Response.json({ ok: true });
51
+ }
52
+ ```
53
+
54
+ L'erreur est nettoyée, notée dans Pulse, puis relancée : ta route répond comme avant.
55
+
56
+ ## Où ça marche
57
+
58
+ `@pulse-si/next` vit dans le processus Node de ton serveur. Il est fait pour `next start` sur un VPS, dans Docker ou chez un hébergeur qui garde le processus en vie. Sur une plateforme sans serveur (Vercel, Netlify), une instance peut dormir entre deux requêtes : surveille plutôt l'app par son URL dans Pulse, et garde `trackCron` pour tes routes de cron. Le runtime Edge est ignoré sans erreur.
59
+
60
+ ## Choisir ce qui part
61
+
62
+ Le signal de vie part toujours. Le reste se règle par variables ou dans `startPulse` :
63
+
64
+ ```bash
65
+ PULSE_SEND=checks,crons # pas de journaux ni de versions
66
+ PULSE_EXCLUDE=File emails # ignorer une instance (* permis)
67
+ PULSE_LOG_LEVEL=WARN # journaux : avertissements et erreurs seulement
68
+ ```
69
+
70
+ ```ts
71
+ startPulse({ watch: [prisma, redis], send: ['checks', 'logs'], logLevel: 'WARN' });
72
+ ```
73
+
74
+ ## Journaux et mode direct (Pro et Team)
75
+
76
+ Pulse recopie ce que ton serveur écrit (`console`, pino, messages de Next.js) sans rien changer à ton terminal. Les niveaux sont reconnus (`⚠` et `⨯` de Next.js compris), les couleurs retirées, les secrets masqués avant de partir. Quand tu ouvres l'app dans Pulse, l'état part toutes les 5 secondes et les journaux chaque seconde. Gardés 3 jours en Pro, 7 jours en Team.
77
+
78
+ ## Mode sécurisé : Pulse ne peut rien lire
79
+
80
+ Ton iPhone ou ton Mac fabrique une paire de clés ; la clé publique va dans `.env.local`, la clé privée reste dans ton trousseau.
81
+
82
+ ```bash
83
+ PULSE_PUBLIC_KEY=pulse_pub_…
84
+ PULSE_CLEAR=status # par défaut : « ok » ou « N soucis », pour garder les alertes
85
+ ```
86
+
87
+ Rapports et journaux partent chiffrés (HPKE, X25519 et AES-256-GCM) et Pulse ne garde qu'un paquet qu'il ne sait pas ouvrir. Avec `PULSE_CLEAR=none`, Pulse ne sait plus que si ton app est en vie.
88
+
89
+ ## Sécurité, en clair
90
+
91
+ - Le package ne lève jamais d'erreur et ne bloque jamais une requête : tout se passe à côté, sur des minuteurs qui ne retiennent pas le processus.
92
+ - Rien n'est écrit chez toi, rien de métier ne part : état, temps de réponse, messages d'erreur nettoyés.
93
+ - La clé ne voyage qu'en HTTPS, dans un en-tête. Elle n'ouvre pas ton compte et se remplace depuis l'app.
94
+ - En développement, Next.js recharge `instrumentation.ts` à chaque modification : un seul rapporteur tourne par processus.
95
+
96
+ ## Variables
97
+
98
+ | Variable | Défaut | Rôle |
99
+ |---|---|---|
100
+ | `PULSE_KEY` | (obligatoire) | clé de l'app, `pk_live_…` ou `pk_test_…` |
101
+ | `PULSE_SEND` | `checks,crons,logs,versions` | ce qui part en plus du signal de vie |
102
+ | `PULSE_INCLUDE`, `PULSE_EXCLUDE` | (tout), (rien) | instances et tâches suivies, `*` permis |
103
+ | `PULSE_LOG_LEVEL` | `DEBUG` | niveau minimal des journaux |
104
+ | `PULSE_LOGS` | `true` | `false` coupe les journaux |
105
+ | `PULSE_PUBLIC_KEY` | (aucune) | active le mode sécurisé |
106
+ | `PULSE_CLEAR` | `status` | en mode sécurisé, ce qui reste lisible par Pulse |
107
+ | `PULSE_ENDPOINT` | `https://api.pulse-app.si` | ton Pulse Relay en mode VPS perso (Team), par exemple `https://relais.kalao.ci` |
108
+ | `PULSE_RELAY_FINGERPRINT` | (aucune) | empreinte du certificat d'un relais sans domaine : rien ne part si elle ne correspond pas |
109
+ | `PULSE_APP_VERSION` | version du `package.json` | version affichée dans Pulse |
110
+ | `PULSE_DISABLED` | `false` | `true` coupe tout |
111
+
112
+ ## Licence
113
+
114
+ MIT
@@ -0,0 +1,2 @@
1
+ export { httpProbe, type Probe } from '@pulse-si/core';
2
+ export { pulse, startPulse, stopPulse, trackCron, type PulseNextOptions } from './start';
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.trackCron = exports.stopPulse = exports.startPulse = exports.pulse = exports.httpProbe = void 0;
4
+ var core_1 = require("@pulse-si/core");
5
+ Object.defineProperty(exports, "httpProbe", { enumerable: true, get: function () { return core_1.httpProbe; } });
6
+ var start_1 = require("./start");
7
+ Object.defineProperty(exports, "pulse", { enumerable: true, get: function () { return start_1.pulse; } });
8
+ Object.defineProperty(exports, "startPulse", { enumerable: true, get: function () { return start_1.startPulse; } });
9
+ Object.defineProperty(exports, "stopPulse", { enumerable: true, get: function () { return start_1.stopPulse; } });
10
+ Object.defineProperty(exports, "trackCron", { enumerable: true, get: function () { return start_1.trackCron; } });
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,uCAAuD;AAA9C,iGAAA,SAAS,OAAA;AAClB,iCAAyF;AAAhF,8FAAA,KAAK,OAAA;AAAE,mGAAA,UAAU,OAAA;AAAE,kGAAA,SAAS,OAAA;AAAE,kGAAA,SAAS,OAAA"}
@@ -0,0 +1,9 @@
1
+ import { PulseReporter, type Probe, type PulseOptions } from '@pulse-si/core';
2
+ export type PulseNextOptions = PulseOptions & {
3
+ watch?: unknown[];
4
+ probes?: Probe[];
5
+ };
6
+ export declare function startPulse(options?: PulseNextOptions): PulseReporter | null;
7
+ export declare function pulse(): PulseReporter | null;
8
+ export declare function trackCron<T>(name: string, task: () => Promise<T>): Promise<T>;
9
+ export declare function stopPulse(): void;
package/dist/start.js ADDED
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.startPulse = startPulse;
4
+ exports.pulse = pulse;
5
+ exports.trackCron = trackCron;
6
+ exports.stopPulse = stopPulse;
7
+ const node_path_1 = require("node:path");
8
+ const core_1 = require("@pulse-si/core");
9
+ const GLOBAL_KEY = Symbol.for('pulse-next.reporter');
10
+ function startPulse(options = {}) {
11
+ if (process.env.NEXT_RUNTIME && process.env.NEXT_RUNTIME !== 'nodejs')
12
+ return null;
13
+ const store = globalThis;
14
+ const existing = store[GLOBAL_KEY];
15
+ if (existing)
16
+ return existing;
17
+ const reporter = new core_1.PulseReporter({
18
+ packageName: 'pulse-next',
19
+ packageVersion: (0, core_1.ownVersion)((0, node_path_1.join)(__dirname, '..')),
20
+ framework: 'nextjs',
21
+ frameworkVersion: (0, core_1.installedVersion)('next'),
22
+ }, options);
23
+ if (!reporter.enabled)
24
+ return null;
25
+ for (const probe of [...(options.probes ?? []), ...(0, core_1.detectProbes)(options.watch ?? [])]) {
26
+ reporter.addProbe(probe);
27
+ }
28
+ reporter.start();
29
+ store[GLOBAL_KEY] = reporter;
30
+ return reporter;
31
+ }
32
+ function pulse() {
33
+ return globalThis[GLOBAL_KEY] ?? null;
34
+ }
35
+ async function trackCron(name, task) {
36
+ try {
37
+ const result = await task();
38
+ pulse()?.recordCronRun(name, true);
39
+ return result;
40
+ }
41
+ catch (error) {
42
+ pulse()?.recordCronRun(name, false, error);
43
+ throw error;
44
+ }
45
+ }
46
+ function stopPulse() {
47
+ const store = globalThis;
48
+ store[GLOBAL_KEY]?.stop();
49
+ delete store[GLOBAL_KEY];
50
+ }
51
+ //# sourceMappingURL=start.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"start.js","sourceRoot":"","sources":["../src/start.ts"],"names":[],"mappings":";;AAoBA,gCAqBC;AAED,sBAEC;AAGD,8BASC;AAED,8BAIC;AA/DD,yCAAiC;AACjC,yCAOwB;AAOxB,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;AAKrD,SAAgB,UAAU,CAAC,UAA4B,EAAE;IACvD,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACnF,MAAM,KAAK,GAAG,UAAyB,CAAC;IACxC,MAAM,QAAQ,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC;IACnC,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC9B,MAAM,QAAQ,GAAG,IAAI,oBAAa,CAChC;QACE,WAAW,EAAE,YAAY;QACzB,cAAc,EAAE,IAAA,iBAAU,EAAC,IAAA,gBAAI,EAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QACjD,SAAS,EAAE,QAAQ;QACnB,gBAAgB,EAAE,IAAA,uBAAgB,EAAC,MAAM,CAAC;KAC3C,EACD,OAAO,CACR,CAAC;IACF,IAAI,CAAC,QAAQ,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IACnC,KAAK,MAAM,KAAK,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,GAAG,IAAA,mBAAY,EAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;QACtF,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC;IACD,QAAQ,CAAC,KAAK,EAAE,CAAC;IACjB,KAAK,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAC;IAC7B,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAgB,KAAK;IACnB,OAAQ,UAA0B,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC;AACzD,CAAC;AAGM,KAAK,UAAU,SAAS,CAAI,IAAY,EAAE,IAAsB;IACrE,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC;QAC5B,KAAK,EAAE,EAAE,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACnC,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,KAAK,EAAE,EAAE,aAAa,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;QAC3C,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAgB,SAAS;IACvB,MAAM,KAAK,GAAG,UAAyB,CAAC;IACxC,KAAK,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,CAAC;IAC1B,OAAO,KAAK,CAAC,UAAU,CAAC,CAAC;AAC3B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "@pulse-si/next",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.1",
4
+ "description": "Surveillance Pulse pour Next.js : signal de vie et dépendances depuis instrumentation.ts.",
5
+ "license": "MIT",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "engines": {
12
+ "node": ">=18.17"
13
+ },
14
+ "homepage": "https://pulse-app.si",
15
+ "jest": {
16
+ "rootDir": "src",
17
+ "testRegex": ".*\\.spec\\.ts$",
18
+ "transform": {
19
+ "^.+\\.ts$": "ts-jest"
20
+ },
21
+ "testEnvironment": "node"
22
+ },
23
+ "devDependencies": {
24
+ "@eslint/js": "^9.39.5",
25
+ "@types/jest": "^30.0.0",
26
+ "@types/node": "^22.20.5",
27
+ "eslint": "^9.39.5",
28
+ "eslint-config-prettier": "^10.1.8",
29
+ "globals": "^17.13.0",
30
+ "jest": "^30.5.2",
31
+ "ts-jest": "^29.4.15",
32
+ "typescript": "~5.9.3",
33
+ "typescript-eslint": "^8.71.2"
34
+ },
35
+ "dependencies": {
36
+ "@pulse-si/core": "^0.0.1"
37
+ },
38
+ "peerDependencies": {
39
+ "next": ">=14"
40
+ },
41
+ "scripts": {
42
+ "build": "tsc -p tsconfig.build.json",
43
+ "typecheck": "tsc --noEmit",
44
+ "lint": "eslint \"src/**/*.ts\"",
45
+ "test": "jest"
46
+ }
6
47
  }